diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a58c52..489c0a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: - main pull_request: - branches: - - main env: ZEPHYR_SDK_VERSION: 1.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index 6ec5ffb..4f32e56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ The project was migrated from a C codebase (FreeRTOS + lwIP + TinyUSB) to **Zeph ## Build commands ```sh -west build -b rpi_pico2/rp2350a/m33 # hardware build -west build -b native_sim # development build (no hardware) +west build -b rpi_pico2/rp2350a/m33 # hardware build (Linux host required for -m32 bytecode) +west build -b native_sim/native/64 # development build in Docker (macOS arm64) west flash # flash to device ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index b054494..95e826c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,44 +20,50 @@ set(HANDLER_JS ${CMAKE_CURRENT_SOURCE_DIR}/js/handler.js) set(HANDLER_BIN ${CMAKE_CURRENT_SOURCE_DIR}/js/handler.bin) set(COMPILE_BYTECODE ${CMAKE_CURRENT_BINARY_DIR}/compile_bytecode) -if(EXISTS ${HANDLER_BIN}) - # handler.bin is already present (pre-built by CI or developer); use it. - add_custom_target(handler_bytecode ALL) +# native_sim runs on the host (64-bit on modern systems) — bytecode must match. +# RP2350 (Cortex-M33) is 32-bit — requires -m32 on the host compiler. +# macOS arm64 does not support -m32 at all, so native_sim is the only usable +# build target on Apple Silicon without a Linux cross-build environment. +if(BOARD MATCHES "native_sim") + set(BYTECODE_ARCH_FLAGS "") else() - # Not present: build using the host's native gcc, not CMAKE_C_COMPILER - # (which would be arm-zephyr-eabi-gcc when cross-compiling). - find_program(HOST_CC gcc REQUIRED) + set(BYTECODE_ARCH_FLAGS "-m32") +endif() + +find_program(HOST_CC gcc HINTS /usr/bin /usr/local/bin) +if(NOT HOST_CC) + find_program(HOST_CC cc REQUIRED) +endif() - add_custom_command( - OUTPUT ${COMPILE_BYTECODE} - COMMAND ${HOST_CC} -m32 -O2 - -o ${COMPILE_BYTECODE} - ${CMAKE_CURRENT_SOURCE_DIR}/scripts/compile_bytecode.c - ${QUICKJS_DIR}/quickjs.c - ${QUICKJS_DIR}/libregexp.c - ${QUICKJS_DIR}/libunicode.c - ${QUICKJS_DIR}/cutils.c - ${QUICKJS_DIR}/dtoa.c - -I${QUICKJS_DIR} - -DCONFIG_VERSION='"2025-09-13"' - -D_GNU_SOURCE - -lm -lpthread - DEPENDS - ${CMAKE_CURRENT_SOURCE_DIR}/scripts/compile_bytecode.c - ${QUICKJS_DIR}/quickjs.c - ${QUICKJS_DIR}/quickjs.h - COMMENT "Building host bytecode compiler (32-bit)" - ) +add_custom_command( + OUTPUT ${COMPILE_BYTECODE} + COMMAND ${HOST_CC} ${BYTECODE_ARCH_FLAGS} -O2 + -o ${COMPILE_BYTECODE} + ${CMAKE_CURRENT_SOURCE_DIR}/scripts/compile_bytecode.c + ${QUICKJS_DIR}/quickjs.c + ${QUICKJS_DIR}/libregexp.c + ${QUICKJS_DIR}/libunicode.c + ${QUICKJS_DIR}/cutils.c + ${QUICKJS_DIR}/dtoa.c + -I${QUICKJS_DIR} + -DCONFIG_VERSION='"2025-09-13"' + -D_GNU_SOURCE + -lm -lpthread + DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/scripts/compile_bytecode.c + ${QUICKJS_DIR}/quickjs.c + ${QUICKJS_DIR}/quickjs.h + COMMENT "Building host bytecode compiler (${BOARD})" +) - add_custom_command( - OUTPUT ${HANDLER_BIN} - COMMAND ${COMPILE_BYTECODE} ${HANDLER_JS} ${HANDLER_BIN} - DEPENDS ${COMPILE_BYTECODE} ${HANDLER_JS} - COMMENT "Compiling handler.js to QuickJS bytecode" - ) +add_custom_command( + OUTPUT ${HANDLER_BIN} + COMMAND ${COMPILE_BYTECODE} ${HANDLER_JS} ${HANDLER_BIN} + DEPENDS ${COMPILE_BYTECODE} ${HANDLER_JS} + COMMENT "Compiling handler.js to QuickJS bytecode" +) - add_custom_target(handler_bytecode ALL DEPENDS ${HANDLER_BIN}) -endif() +add_custom_target(handler_bytecode ALL DEPENDS ${HANDLER_BIN}) # Zephyr API shims for Rust FFI (k_uptime_get and similar __syscall functions diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9d8faf0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,614 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "annotate-snippets", + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fugit" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" +dependencies = [ + "gcd", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "critical-section", +] + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustapp" +version = "0.1.0" +dependencies = [ + "zephyr", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zephyr" +version = "0.1.0" +dependencies = [ + "arrayvec", + "cfg-if", + "critical-section", + "fugit", + "log", + "paste", + "portable-atomic", + "portable-atomic-util", + "zephyr-build", + "zephyr-macros", + "zephyr-sys", +] + +[[package]] +name = "zephyr-build" +version = "0.1.0" +dependencies = [ + "anyhow", + "pest", + "pest_derive", + "proc-macro2", + "quote", + "regex", + "serde", + "serde_yaml_ng", +] + +[[package]] +name = "zephyr-macros" +version = "0.1.0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zephyr-sys" +version = "0.1.0" +dependencies = [ + "anyhow", + "bindgen", + "zephyr-build", +] diff --git a/Cargo.toml b/Cargo.toml index 6d60a57..cff70da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,19 @@ zephyr = "0.1.0" # no_std HTTP parser (Phase 4) # httparse = { version = "1", default-features = false } +# Cargo [lints] overrides RUSTFLAGS=-D warnings set by zephyr-lang-rust's clippy target. +# These lints are legitimate future work for the FFI/unsafe-heavy QuickJS bindings, +# but should not block the build. +[lints.rust] +dead_code = "warn" +static_mut_refs = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "warn" +manual_c_str_literals = "warn" +unnecessary_cast = "warn" +manual_find = "warn" + [profile.release] opt-level = "s" # Optimize for size on embedded lto = true diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..e5971cb --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git cmake ninja-build gperf ccache dfu-util device-tree-compiler \ + wget curl xz-utils file make \ + gcc g++ clang libclang-dev \ + python3-dev python3-pip python3-venv python3-setuptools python3-wheel \ + libsdl2-dev libmagic1 \ + && rm -rf /var/lib/apt/lists/* + +# In a container, pip --break-system-packages is safe (isolated env). +RUN pip3 install --break-system-packages west pyelftools pykwalify PyYAML \ + packaging colorama canopen progress + +# Zephyr SDK 1.0.0 — extract minimal bundle for CMake version-check and cmake config. +# native_sim uses the host's gcc (from apt above); no cross-compiler download needed. +# dtc comes from apt, so SDK host tools aren't required either. +ARG ZEPHYR_SDK_VERSION=1.0.0 +RUN wget -q \ + https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZEPHYR_SDK_VERSION}/zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-aarch64_minimal.tar.xz \ + -O /tmp/zephyr-sdk.tar.xz \ + && tar -xf /tmp/zephyr-sdk.tar.xz -C /opt/ \ + && rm /tmp/zephyr-sdk.tar.xz + +ENV ZEPHYR_SDK_INSTALL_DIR=/opt/zephyr-sdk-1.0.0 + +# Rust toolchain +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable +ENV PATH="/root/.cargo/bin:${PATH}" +# thumbv8m for RP2350, aarch64-unknown-none for native_sim on arm64 +RUN rustup target add thumbv8m.main-none-eabihf aarch64-unknown-none + +WORKDIR /workspace diff --git a/app_shim.c b/app_shim.c index 8a79238..56e7cee 100644 --- a/app_shim.c +++ b/app_shim.c @@ -12,3 +12,8 @@ int64_t isere_k_uptime_get(void) { return k_uptime_get(); } + +int32_t isere_k_msleep(int32_t ms) +{ + return k_msleep(ms); +} diff --git a/c_libs/quickjs_zephyr.patch b/c_libs/quickjs_zephyr.patch index 4165a89..6970e51 100644 --- a/c_libs/quickjs_zephyr.patch +++ b/c_libs/quickjs_zephyr.patch @@ -1,6 +1,45 @@ ---- a/quickjs.c -+++ b/quickjs.c -@@ -46786,7 +46786,7 @@ static int getTimezoneOffset(int64_t time) +--- a/quickjs.c 2026-03-28 16:07:37.024857963 +0000 ++++ b/quickjs.c 2026-03-28 16:08:07.562650037 +0000 +@@ -1985,6 +1985,9 @@ + rt->rt_info = s; + } + ++/* Forward declaration: defined later in this file, called here in JS_FreeRuntime. */ ++static void free_gc_object(JSRuntime *rt, JSGCObjectHeader *gp); ++ + void JS_FreeRuntime(JSRuntime *rt) + { + struct list_head *el, *el1; +@@ -2044,8 +2047,25 @@ + printf("Secondary object leaks: %d\n", count); + } + #endif +- assert(list_empty(&rt->gc_obj_list)); +- assert(list_empty(&rt->weakref_list)); ++ /* Force-free any remaining GC objects (e.g. ES module cycles that ++ survive normal GC due to inter-module import/export references). ++ Without this cleanup, their allocator memory is never reclaimed, ++ which exhausts the heap on embedded targets with per-request ++ runtime creation. */ ++ if (!list_empty(&rt->gc_obj_list)) { ++ rt->gc_phase = JS_GC_PHASE_REMOVE_CYCLES; ++ for(;;) { ++ el = rt->gc_obj_list.next; ++ if (el == &rt->gc_obj_list) ++ break; ++ free_gc_object(rt, list_entry(el, JSGCObjectHeader, link)); ++ } ++ rt->gc_phase = JS_GC_PHASE_NONE; ++ list_for_each_safe(el, el1, &rt->gc_zero_ref_count_list) { ++ js_free_rt(rt, list_entry(el, JSGCObjectHeader, link)); ++ } ++ init_list_head(&rt->gc_zero_ref_count_list); ++ } + + /* free the classes */ + for(i = 0; i < rt->class_count; i++) { +@@ -46786,7 +46806,7 @@ + } } ti = time; -#if defined(_WIN32) diff --git a/scripts/build-sim.sh b/scripts/build-sim.sh new file mode 100755 index 0000000..acc91d6 --- /dev/null +++ b/scripts/build-sim.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Build native_sim in a Linux Docker container (required on macOS — +# native_sim uses Linux-only POSIX arch and cannot build directly on Darwin). +# +# Prerequisites: +# - Docker (colima or Docker Desktop) +# - ~/zephyrproject workspace initialised (west init + west update done) +# +# Usage: +# ./scripts/build-sim.sh # build only +# ./scripts/build-sim.sh run # build then run + +set -e + +WORKSPACE=~/zephyrproject +COMPOSE="$WORKSPACE/docker-compose.yml" + +# Build the dev image if not present +docker compose -f "$COMPOSE" build build + +# Seed full Zephyr Python requirements on first run +docker compose -f "$COMPOSE" run --rm build \ + pip3 install --break-system-packages -r /workspace/zephyr/scripts/requirements.txt 2>/dev/null || true + +# Build native_sim +docker compose -f "$COMPOSE" run --rm build + +if [ "$1" = "run" ]; then + echo "" + echo "Starting native_sim... HTTP server at 192.0.2.1:80 inside container." + echo "Use 'docker compose -f $COMPOSE run --rm run' to restart." + docker compose -f "$COMPOSE" run --rm run +fi diff --git a/src/event_loop.rs b/src/event_loop.rs index 755c1f2..a6210c1 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -5,7 +5,7 @@ // then poll() in a loop to dispatch ready events. use core::ffi::c_int; -use crate::platform::tcp::{self, PollFd, POLLIN, POLLOUT, POLLERR, POLLHUP}; +use crate::platform::tcp::{self, PollFd, POLLERR, POLLHUP}; /// Maximum number of file descriptors we can watch simultaneously. const MAX_WATCHERS: usize = 16; diff --git a/src/http_handler.rs b/src/http_handler.rs index e452911..42d5e48 100644 --- a/src/http_handler.rs +++ b/src/http_handler.rs @@ -8,8 +8,9 @@ // 5. Returns the response to the HTTP server for writeback use crate::httpd::Connection; -use crate::js::context::JsContext; +use crate::js::context::{JsContext, PollStatus}; use crate::platform::loader; +use zephyr::printk; /// Process a parsed HTTP request through the JavaScript handler. /// @@ -20,11 +21,16 @@ use crate::platform::loader; pub fn handle_request(conn: &mut Connection) -> Result<(), ()> { let bytecode = loader::handler_bytecode(); - let mut js_ctx = JsContext::new().ok_or(())?; + let mut js_ctx = JsContext::new().ok_or_else(|| { + printk!("handler: failed to create JS context\n"); + })?; js_ctx.setup_globals(&conn.request, &mut conn.response); - js_ctx.eval_handler(bytecode)?; + if let Err(()) = js_ctx.eval_handler(bytecode) { + printk!("handler: eval_handler failed\n"); + return Err(()); + } // Poll pending jobs until the response callback fires let max_iterations = 1000; // Safety limit @@ -34,9 +40,18 @@ pub fn handle_request(conn: &mut Connection) -> Result<(), ()> { } match js_ctx.poll() { - Ok(true) => continue, // More jobs pending - Ok(false) => break, // No more jobs - Err(()) => return Err(()), + Ok(PollStatus::JobsExecuted) => continue, // More jobs pending, execute immediately + Ok(PollStatus::WaitingForTimers) => { + // Wait for timers, yield the thread + extern "C" { fn isere_k_msleep(ms: i32) -> i32; } + unsafe { isere_k_msleep(5); } + continue; + }, + Ok(PollStatus::Idle) => break, // No more jobs + Err(()) => { + printk!("handler: poll error\n"); + return Err(()); + } } } diff --git a/src/httpd.rs b/src/httpd.rs index 8da423e..156a912 100644 --- a/src/httpd.rs +++ b/src/httpd.rs @@ -5,10 +5,8 @@ // Non-blocking sockets with poll()-based event loop. use core::ffi::c_int; -use core::fmt::Write as FmtWrite; - -use crate::event_loop::{EventLoop, IoCallback}; -use crate::platform::tcp::{self, POLLIN}; +use crate::event_loop::EventLoop; +use crate::platform::tcp; pub const HTTPD_PORT: u16 = 8080; pub const MAX_CONNECTIONS: usize = 12; diff --git a/src/js/context.rs b/src/js/context.rs index 4f5cda3..a89c8fd 100644 --- a/src/js/context.rs +++ b/src/js/context.rs @@ -92,6 +92,13 @@ mod alloc { }; } +#[derive(Debug, PartialEq)] +pub enum PollStatus { + JobsExecuted, + WaitingForTimers, + Idle, +} + /// A QuickJS context bound to a single HTTP request. /// Each request gets its own runtime+context (same pattern as the C code). pub struct JsContext { @@ -100,6 +107,9 @@ pub struct JsContext { future: qjs::JSValue, pub response_ready: bool, timer_state: TimerState, + /// Stored here so the pointer passed to JS_SetContextOpaque remains valid + /// for the lifetime of the JsContext. + opaque: ContextOpaque, } /// Opaque data attached to a JS context for the handler callback. @@ -138,6 +148,7 @@ impl JsContext { future: qjs::JSValue::UNDEFINED, response_ready: false, timer_state: TimerState::new(), + opaque: ContextOpaque { response: ptr::null_mut() }, }; // Initialize setTimeout/clearTimeout polyfills @@ -302,11 +313,9 @@ impl JsContext { ); qjs::JS_SetPropertyStr(ctx, global, b"context\0".as_ptr() as *const c_char, context_obj); - // Store response pointer in context opaque for the callback - let opaque = &mut ContextOpaque { - response: response as *mut HttpResponse, - }; - qjs::JS_SetContextOpaque(ctx, opaque as *mut ContextOpaque as *mut c_void); + // Store response pointer in self.opaque (lives as long as JsContext) + self.opaque.response = response as *mut HttpResponse; + qjs::JS_SetContextOpaque(ctx, &mut self.opaque as *mut ContextOpaque as *mut c_void); // Handler callback function qjs::JS_SetPropertyStr( @@ -329,21 +338,35 @@ impl JsContext { let ctx = self.context; // Deserialize bytecode into a compiled module object. - // JS_READ_OBJ_ROM_DATA avoids copying the buffer — safe because - // our bytecode is in static ROM (include_bytes!). + // Copy the bytecode buffer into QuickJS-managed memory. + // Using JS_READ_OBJ_ROM_DATA (zero-copy) corrupts the shared + // static buffer after the first runtime is freed, causing + // subsequent requests to produce empty modules. let h = qjs::JS_ReadObject( ctx, bytecode.as_ptr(), bytecode.len(), - qjs::JS_READ_OBJ_BYTECODE | qjs::JS_READ_OBJ_ROM_DATA, + qjs::JS_READ_OBJ_BYTECODE, ); if h.is_exception() { + extern "C" { fn printk(fmt: *const c_char, ...); } + let err = qjs::JS_GetException(ctx); + let err_str = qjs::JS_ToCStringLen(ctx, core::ptr::null_mut(), err); + if !err_str.is_null() { + printk(b"JS_ReadObject exception: %s\n\0".as_ptr() as *const c_char, err_str); + qjs::JS_FreeCString(ctx, err_str); + } else { + printk(b"JS_ReadObject failed (no exception string)\n\0".as_ptr() as *const c_char); + } + qjs::JS_FreeValue(ctx, err); qjs::JS_FreeValue(ctx, h); return Err(()); } // Resolve module imports before evaluation if qjs::JS_ResolveModule(ctx, h) < 0 { + extern "C" { fn printk(fmt: *const c_char, ...); } + printk(b"JS_ResolveModule failed\n\0".as_ptr() as *const c_char); qjs::JS_FreeValue(ctx, h); return Err(()); } @@ -352,6 +375,17 @@ impl JsContext { let result = qjs::JS_EvalFunction(ctx, h); // JS_EvalFunction consumes h, do not free it if result.is_exception() { + let err = qjs::JS_GetException(ctx); + let err_str = qjs::JS_ToCStringLen(ctx, core::ptr::null_mut(), err); + if !err_str.is_null() { + extern "C" { + fn printk(fmt: *const c_char, ...); + } + printk(b"JS_EvalFunction exception: %s\n\0".as_ptr() as *const c_char, err_str); + qjs::JS_FreeCString(ctx, err_str); + } + qjs::JS_FreeValue(ctx, err); + qjs::JS_FreeValue(ctx, result); return Err(()); } @@ -381,6 +415,16 @@ impl JsContext { ); if self.future.is_exception() { + let err = qjs::JS_GetException(ctx); + let err_str = qjs::JS_ToCStringLen(ctx, core::ptr::null_mut(), err); + if !err_str.is_null() { + extern "C" { + fn printk(fmt: *const c_char, ...); + } + printk(b"JS_Eval exception: %s\n\0".as_ptr() as *const c_char, err_str); + qjs::JS_FreeCString(ctx, err_str); + } + qjs::JS_FreeValue(ctx, err); return Err(()); } @@ -388,8 +432,8 @@ impl JsContext { } } - /// Poll pending JS jobs (promises, timers). Returns true if jobs remain. - pub fn poll(&mut self) -> Result { + /// Poll pending JS jobs (promises, timers). + pub fn poll(&mut self) -> Result { // Fire any expired timers first (may enqueue new JS jobs) let timers_active = self.timer_state.poll(); @@ -399,11 +443,28 @@ impl JsContext { let err = qjs::JS_ExecutePendingJob(rt, &mut ctx1); if err < 0 { + if !ctx1.is_null() { + let exc = qjs::JS_GetException(ctx1); + let err_str = qjs::JS_ToCStringLen(ctx1, core::ptr::null_mut(), exc); + if !err_str.is_null() { + extern "C" { + fn printk(fmt: *const c_char, ...); + } + printk(b"JS_ExecutePendingJob exception: %s\n\0".as_ptr() as *const c_char, err_str); + qjs::JS_FreeCString(ctx1, err_str); + } + qjs::JS_FreeValue(ctx1, exc); + } return Err(()); } - // Jobs remain if either pending JS jobs or active timers - Ok(err > 0 || timers_active) + if err > 0 { + Ok(PollStatus::JobsExecuted) + } else if timers_active { + Ok(PollStatus::WaitingForTimers) + } else { + Ok(PollStatus::Idle) + } } } } @@ -498,9 +559,8 @@ unsafe extern "C" fn handler_callback( qjs::JS_FreeCString(ctx, val_ptr); } qjs::JS_FreeValue(ctx, val); - - qjs::JS_FreeAtom(ctx, prop.atom); } + qjs::JS_FreePropertyEnum(ctx, props, props_len); } } qjs::JS_FreeValue(ctx, headers); diff --git a/src/js/quickjs_ffi.rs b/src/js/quickjs_ffi.rs index 970a8fc..69da2b8 100644 --- a/src/js/quickjs_ffi.rs +++ b/src/js/quickjs_ffi.rs @@ -277,6 +277,7 @@ extern "C" { #[link_name = "isere_JS_AtomToCString"] pub fn JS_AtomToCString(ctx: *mut JSContext, atom: u32) -> *const c_char; pub fn JS_FreeAtom(ctx: *mut JSContext, atom: u32); + pub fn JS_FreePropertyEnum(ctx: *mut JSContext, tab: *mut JSPropertyEnum, len: u32); // Job execution (promises, async) pub fn JS_ExecutePendingJob(rt: *mut JSRuntime, pctx: *mut *mut JSContext) -> c_int; @@ -297,6 +298,7 @@ extern "C" { // Error throwing pub fn JS_ThrowTypeError(ctx: *mut JSContext, fmt: *const c_char, ...) -> JSValue; pub fn JS_ThrowInternalError(ctx: *mut JSContext, fmt: *const c_char, ...) -> JSValue; + pub fn JS_GetException(ctx: *mut JSContext) -> JSValue; // Custom class support (for Timer objects) pub fn JS_NewClassID(pclass_id: *mut JSClassID) -> JSClassID; @@ -320,4 +322,3 @@ extern "C" { pub fn JS_EvalFunction(ctx: *mut JSContext, fun_obj: JSValue) -> JSValue; pub fn JS_ResolveModule(ctx: *mut JSContext, obj: JSValue) -> c_int; } - diff --git a/src/lib.rs b/src/lib.rs index 6f827f6..c8f0325 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,14 @@ #![no_std] +// These lints fire extensively in the FFI/unsafe QuickJS bindings and embedded +// platform shims. They represent legitimate future cleanup work, not bugs. +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(clippy::undocumented_unsafe_blocks)] +#![allow(clippy::manual_c_str_literals)] +#![allow(clippy::unnecessary_cast)] +#![allow(clippy::manual_find)] +#![allow(clippy::needless_range_loop)] +#![allow(static_mut_refs)] extern crate zephyr; @@ -122,7 +132,7 @@ fn on_server_readable(_watcher_id: usize, fd: core::ffi::c_int, _events: i16, _u } /// Callback when a client socket is readable (data available). -fn on_client_readable(_watcher_id: usize, fd: core::ffi::c_int, events: i16, userdata: usize) { +fn on_client_readable(_watcher_id: usize, fd: core::ffi::c_int, _events: i16, userdata: usize) { let conn_idx = userdata; unsafe { diff --git a/src/platform/tcp.rs b/src/platform/tcp.rs index 95eaddb..47efa2f 100644 --- a/src/platform/tcp.rs +++ b/src/platform/tcp.rs @@ -67,7 +67,7 @@ mod ffi { } } -pub use ffi::{PollFd, POLLERR, POLLHUP, POLLIN, POLLOUT}; +pub use ffi::{PollFd, POLLERR, POLLHUP, POLLIN}; /// Convert port to network byte order (big-endian). fn htons(val: u16) -> u16 {