From 013728ca2228932666f55ebb1408f500c08e223a Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 06:43:40 +0800 Subject: [PATCH 1/8] runtime: support no_std and Zephyr targets --- .github/workflows/ci.yml | 4 ++ Cargo.toml | 22 +++++++- README.md | 25 +++++++++ core/Cargo.toml | 21 +++++--- core/src/context/async.rs | 6 +-- core/src/context/owner.rs | 17 +++++-- core/src/runtime.rs | 2 + core/src/runtime/async.rs | 53 +++++++++++++------- core/src/runtime/base.rs | 27 ++++++++++ core/src/runtime/raw.rs | 5 ++ core/src/safe_ref.rs | 28 +++++++++-- core/src/value/convert/from.rs | 4 ++ examples/zephyr/CMakeLists.txt | 6 +++ examples/zephyr/Cargo.toml | 12 +++++ examples/zephyr/README.md | 18 +++++++ examples/zephyr/app.overlay | 13 +++++ examples/zephyr/prj.conf | 14 ++++++ examples/zephyr/src/lib.rs | 92 ++++++++++++++++++++++++++++++++++ sys/Cargo.toml | 1 + sys/build.rs | 72 +++++++++++++++++++++++--- 20 files changed, 398 insertions(+), 44 deletions(-) create mode 100644 examples/zephyr/CMakeLists.txt create mode 100644 examples/zephyr/Cargo.toml create mode 100644 examples/zephyr/README.md create mode 100644 examples/zephyr/app.overlay create mode 100644 examples/zephyr/prj.conf create mode 100644 examples/zephyr/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 034cab04..78b32fab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,10 @@ jobs: components: clippy - name: Cargo clippy run: cargo clippy --all --all-targets --features full-async,bindgen + - name: Check no_std feature sets + run: | + cargo check -p rquickjs --no-default-features + cargo check -p rquickjs --no-default-features --features full-nostd msrv: # Check to see if rquickjs builds on minimal supported Rust version. diff --git a/Cargo.toml b/Cargo.toml index c0c83d2d..6422ffb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ repository = "https://github.com/DelSkayn/rquickjs.git" [workspace] +exclude = ["examples/zephyr"] members = [ "sys", "core", @@ -33,8 +34,8 @@ rquickjs-sys = { version = "0.11.0", path = "sys", default-features = false } rquickjs = { version = "0.11.0", path = "./", default-features = false } [dependencies] -indexmap-rs = { package = "indexmap", version = "2", optional = true } -either-rs = { package = "either", version = "1", optional = true } +indexmap-rs = { package = "indexmap", version = "2", optional = true, default-features = false } +either-rs = { package = "either", version = "1", optional = true, default-features = false } rquickjs-core = { workspace = true } rquickjs-macro = { workspace = true, optional = true } @@ -60,6 +61,23 @@ full-wasi = ["std", "chrono", "loader", "either", "indexmap", "macro", "phf"] # A version of full designed for wasm32-unknown-unknown and similar hosts full-wasm = ["full-wasi"] +# Portable feature set for no_std hosts with a global allocator. +full-nostd = [ + "chrono", + "loader", + "either", + "indexmap", + "macro", + "phf", + "half", + "futures", + "parallel", + "rust-alloc", +] + +# Build QuickJS against Zephyr's C headers and configuration. +zephyr = ["full-nostd", "bindgen", "rquickjs-core/zephyr"] + # Almost all features excluding "parallel" full-async = ["full", "futures"] diff --git a/README.md b/README.md index 852fc257..1d737d20 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,35 @@ See below for a list of supported platforms. | wasm32-wasip1 | ✅ | ✅ | ✅ | | wasm32-wasip2 | ✅ | ✅ | ✅ | | wasm32-unknown-unknown | ✅ | ✅* | ✅ | +| thumbv7m-none-eabi (Zephyr) | ❌ | ✅** | ✅ | | other | ❌ | ❌ | Unknown | \* `wasm32-unknown-unknown` is validated with workspace builds and a dedicated smoke example compile. Runtime embedding still depends on the host providing the generated wasm imports. +\** Zephyr is validated on the `mps2/an385` QEMU board. Bindings are generated during the Zephyr build. + +## `no_std` and Zephyr + +Disable default features to use rquickjs with `no_std + alloc`. The `full-nostd` feature enables the +portable high-level APIs, including async/futures support, macros, in-memory module loaders, +`parallel`, and the Rust global allocator. The default feature set continues to use `std`. + +```toml +rquickjs = { version = "0.11", default-features = false, features = ["full-nostd"] } +``` + +For Zephyr, use the `zephyr` feature. It also enables bindgen and the C integration required to use +Zephyr's generated configuration, libc, threading primitives, and compiler toolchain. + +```toml +rquickjs = { path = "../rquickjs", default-features = false, features = ["zephyr"] } +``` + +The [Zephyr example](examples/zephyr/README.md) runs two Zephyr threads against one runtime and +checks promises, the pending-job queue, SharedArrayBuffer, and 8/16/32/64-bit Atomics on QEMU. +Filesystem and dynamic-library module loading remain host facilities; use in-memory or custom +resolvers and loaders on embedded targets. + ## WebAssembly notes For browser-style or freestanding wasm hosts, prefer the `full-wasm` or `full-async-wasm` feature sets: diff --git a/core/Cargo.toml b/core/Cargo.toml index ef49c867..a5502852 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,16 +15,17 @@ features = ["full-async", "doc-cfg"] [dependencies] rquickjs-sys = { workspace = true } -hashbrown = { version = "0.17" } -phf = { version = "0.13", optional = true } -indexmap = { version = "2", optional = true } -either = { version = "1", optional = true } +hashbrown = { version = "0.17", default-features = false, features = ["default-hasher"] } +phf = { version = "0.13", optional = true, default-features = false } +indexmap = { version = "2", optional = true, default-features = false } +either = { version = "1", optional = true, default-features = false } async-lock = { version = "3", optional = true, default-features = false } -chrono = { version = "0.4", optional = true } +chrono = { version = "0.4", optional = true, default-features = false, features = ["alloc"] } relative-path = { version = "2.0", optional = true, default-features = false, features = [ "alloc", ] } -half = { version = "2.7.1", optional = true } +half = { version = "2.7.1", optional = true, default-features = false } +spin = { version = "0.10", optional = true, default-features = false, features = ["mutex", "spin_mutex"] } [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] dlopen2 = { version = "0.8", optional = true } @@ -44,7 +45,7 @@ trybuild = "1" [features] default = ["std"] -std = ["relative-path?/std"] +std = ["relative-path?/std", "chrono?/clock"] # Almost all features excluding "parallel" and support for async runtimes full = ["std", "chrono", "loader", "dyn-load", "either", "indexmap", "half"] @@ -52,6 +53,9 @@ full = ["std", "chrono", "loader", "dyn-load", "either", "indexmap", "half"] # A version of full designed for wasm hosts without a native filesystem or dynamic loading. full-wasm = ["std", "chrono", "loader", "either", "indexmap", "half"] +# Portable feature set for no_std hosts with a global allocator. +full-nostd = ["chrono", "loader", "either", "indexmap", "half", "futures", "parallel", "rust-alloc"] + # Almost all features excluding "parallel" full-async = ["full", "futures"] @@ -64,9 +68,10 @@ chrono = ["dep:chrono"] # Use bindgen to generate bindings at compile-type # otherwise bundled bindings will be used bindgen = ["rquickjs-sys/bindgen"] +zephyr = ["rquickjs-sys/zephyr"] # Enable support of parallel execution -parallel = ["std", "tokio/rt-multi-thread"] +parallel = ["dep:spin", "tokio/rt-multi-thread"] # Enable user-defined module loader support loader = ["relative-path"] diff --git a/core/src/context/async.rs b/core/src/context/async.rs index fa5dd5f9..32bbdc2e 100644 --- a/core/src/context/async.rs +++ b/core/src/context/async.rs @@ -3,6 +3,8 @@ use super::{ owner::{ContextOwner, DropContext}, ContextBuilder, Intrinsic, }; +#[cfg(feature = "parallel")] +use crate::runtime::DropContextPtr; use crate::{markers::ParallelSend, qjs, runtime::AsyncRuntime, Ctx, Error, Result}; use core::{mem, ptr::NonNull}; @@ -100,9 +102,7 @@ impl DropContext for AsyncRuntime { } #[cfg(feature = "parallel")] { - self.drop_send - .send(ctx) - .expect("runtime should be alive while contexts life"); + self.drop_queue.lock().push(DropContextPtr(ctx)); return; } } diff --git a/core/src/context/owner.rs b/core/src/context/owner.rs index 25517996..7e5c3126 100644 --- a/core/src/context/owner.rs +++ b/core/src/context/owner.rs @@ -3,7 +3,16 @@ use core::ptr::NonNull; use crate::qjs; #[cfg(feature = "parallel")] -use std::sync::Arc; +use alloc::sync::Arc; + +#[cfg(feature = "parallel")] +struct ParallelContext(NonNull); + +// Access to the context is serialized by the owning runtime lock. +#[cfg(feature = "parallel")] +unsafe impl Send for ParallelContext {} +#[cfg(feature = "parallel")] +unsafe impl Sync for ParallelContext {} /// Trait to specify how to drop a context once it goes out of scope. /// Implemented on Runtime and AsyncRuntime. @@ -19,7 +28,7 @@ pub(crate) struct ContextOwner { #[cfg(not(feature = "parallel"))] ctx: NonNull, #[cfg(feature = "parallel")] - pub(crate) ctx: Arc>, + ctx: Arc, pub(crate) rt: R, } @@ -31,7 +40,7 @@ impl ContextOwner { #[cfg(feature = "parallel")] pub(crate) unsafe fn new(ctx: NonNull, rt: R) -> Self { Self { - ctx: Arc::new(ctx), + ctx: Arc::new(ParallelContext(ctx)), rt, } } @@ -43,7 +52,7 @@ impl ContextOwner { #[cfg(feature = "parallel")] pub(crate) fn ctx(&self) -> NonNull { - *self.ctx + self.ctx.0 } pub(crate) fn rt(&self) -> &R { diff --git a/core/src/runtime.rs b/core/src/runtime.rs index dd09a061..d5991b6a 100644 --- a/core/src/runtime.rs +++ b/core/src/runtime.rs @@ -19,6 +19,8 @@ use alloc::boxed::Box; pub use base::{Runtime, WeakRuntime}; pub use userdata::{UserDataError, UserDataGuard}; +#[cfg(all(feature = "futures", feature = "parallel"))] +pub(crate) use r#async::DropContextPtr; #[cfg(feature = "futures")] pub(crate) use r#async::InnerRuntime; #[cfg(feature = "futures")] diff --git a/core/src/runtime/async.rs b/core/src/runtime/async.rs index 52949cea..4639c5e4 100644 --- a/core/src/runtime/async.rs +++ b/core/src/runtime/async.rs @@ -7,9 +7,6 @@ use core::{ptr::NonNull, result::Result as StdResult, task::Poll}; #[cfg(feature = "std")] use std::println; -#[cfg(feature = "parallel")] -use std::sync::mpsc::{self, Receiver, Sender}; - use async_lock::Mutex; use super::{ @@ -22,21 +19,33 @@ use crate::loader::{Loader, Resolver}; #[cfg(feature = "parallel")] use crate::util::{AssertSendFuture, AssertSyncFuture}; use crate::{ - context::AsyncContext, qjs, result::AsyncJobException, util::ManualPoll, Ctx, Exception, Result, + context::AsyncContext, qjs, result::AsyncJobException, util::ManualPoll, Ctx, Exception, Mut, + Result, }; +#[cfg(feature = "parallel")] +#[derive(Debug)] +pub(crate) struct DropContextPtr(pub(crate) NonNull); + +// Context destruction is deferred until the runtime lock is held. +#[cfg(feature = "parallel")] +unsafe impl Send for DropContextPtr {} + +#[cfg(feature = "parallel")] +type DropQueue = Arc>>; + #[derive(Debug)] pub(crate) struct InnerRuntime { pub runtime: RawRuntime, #[cfg(feature = "parallel")] - pub drop_recv: Receiver>, + pub drop_queue: DropQueue, } impl InnerRuntime { pub fn drop_pending(&self) { #[cfg(feature = "parallel")] - while let Ok(x) = self.drop_recv.try_recv() { - unsafe { qjs::JS_FreeContext(x.as_ptr()) } + for ptr in self.drop_queue.lock().drain(..) { + unsafe { qjs::JS_FreeContext(ptr.0.as_ptr()) } } } } @@ -58,7 +67,7 @@ unsafe impl Send for InnerRuntime {} pub struct AsyncWeakRuntime { inner: Weak>, #[cfg(feature = "parallel")] - drop_send: Sender>, + drop_queue: DropQueue, } impl AsyncWeakRuntime { @@ -66,7 +75,7 @@ impl AsyncWeakRuntime { self.inner.upgrade().map(|inner| AsyncRuntime { inner, #[cfg(feature = "parallel")] - drop_send: self.drop_send.clone(), + drop_queue: self.drop_queue.clone(), }) } } @@ -78,7 +87,7 @@ pub struct AsyncRuntime { // use Arc instead of Ref so we can use OwnedLock pub(crate) inner: Arc>, #[cfg(feature = "parallel")] - pub(crate) drop_send: Sender>, + pub(crate) drop_queue: DropQueue, } // Since all functions which use runtime are behind a mutex @@ -110,16 +119,16 @@ impl AsyncRuntime { let runtime = unsafe { RawRuntime::new(opaque) }?; #[cfg(feature = "parallel")] - let (drop_send, drop_recv) = mpsc::channel(); + let drop_queue = Arc::new(Mut::new(Vec::new())); Ok(Self { inner: Arc::new(Mutex::new(InnerRuntime { runtime, #[cfg(feature = "parallel")] - drop_recv, + drop_queue: drop_queue.clone(), })), #[cfg(feature = "parallel")] - drop_send, + drop_queue, }) } @@ -136,16 +145,16 @@ impl AsyncRuntime { let runtime = unsafe { RawRuntime::new_with_allocator(opaque, allocator) }?; #[cfg(feature = "parallel")] - let (drop_send, drop_recv) = mpsc::channel(); + let drop_queue = Arc::new(Mut::new(Vec::new())); Ok(Self { inner: Arc::new(Mutex::new(InnerRuntime { runtime, #[cfg(feature = "parallel")] - drop_recv, + drop_queue: drop_queue.clone(), })), #[cfg(feature = "parallel")] - drop_send, + drop_queue, }) } @@ -154,7 +163,7 @@ impl AsyncRuntime { AsyncWeakRuntime { inner: Arc::downgrade(&self.inner), #[cfg(feature = "parallel")] - drop_send: self.drop_send.clone(), + drop_queue: self.drop_queue.clone(), } } @@ -192,6 +201,16 @@ impl AsyncRuntime { } } + /// Configure whether JavaScript Atomics operations may block this thread. + /// + /// This must be enabled before using `Atomics.wait`. + #[inline] + pub async fn set_can_block(&self, can_block: bool) { + unsafe { + self.inner.lock().await.runtime.set_can_block(can_block); + } + } + /// Set the module loader #[cfg(feature = "loader")] #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))] diff --git a/core/src/runtime/base.rs b/core/src/runtime/base.rs index b4b72e76..20fcf44b 100644 --- a/core/src/runtime/base.rs +++ b/core/src/runtime/base.rs @@ -92,6 +92,16 @@ impl Runtime { } } + /// Configure whether JavaScript Atomics operations may block this thread. + /// + /// This must be enabled before using `Atomics.wait`. + #[inline] + pub fn set_can_block(&self, can_block: bool) { + unsafe { + self.inner.lock().set_can_block(can_block); + } + } + /// Set the module loader #[cfg(feature = "loader")] #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))] @@ -218,6 +228,23 @@ mod test { rt.run_gc(); } + #[test] + fn atomics_wait_can_be_enabled() { + let rt = Runtime::new().unwrap(); + rt.set_can_block(true); + let ctx = crate::Context::full(&rt).unwrap(); + ctx.with(|ctx| { + let result: String = ctx + .eval( + r#"Atomics.wait( + new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1 + )"#, + ) + .unwrap(); + assert_eq!(result, "timed-out"); + }); + } + #[test] fn set_max_stack_size_large_values() { let rt = Runtime::new().unwrap(); diff --git a/core/src/runtime/raw.rs b/core/src/runtime/raw.rs index 2b0cb42d..ad24ad3b 100644 --- a/core/src/runtime/raw.rs +++ b/core/src/runtime/raw.rs @@ -422,6 +422,11 @@ impl RawRuntime { self.get_opaque().set_interrupt_handler(handler); } + /// Allow or reject blocking Atomics operations in this runtime. + pub unsafe fn set_can_block(&mut self, can_block: bool) { + qjs::JS_SetCanBlock(self.rt.as_ptr(), can_block); + } + fn add_dump_flags(rt: *mut rquickjs_sys::JSRuntime) { unsafe { qjs::JS_SetDumpFlags(rt, build_dump_flags()); diff --git a/core/src/safe_ref.rs b/core/src/safe_ref.rs index fdd5fc95..a4d88019 100644 --- a/core/src/safe_ref.rs +++ b/core/src/safe_ref.rs @@ -1,9 +1,12 @@ #[cfg(not(feature = "parallel"))] use core::cell::RefCell as Cell; -#[cfg(feature = "parallel")] +#[cfg(all(feature = "parallel", feature = "std"))] use std::sync::Mutex as Cell; +#[cfg(all(feature = "parallel", not(feature = "std")))] +use spin::Mutex as Cell; + #[cfg(not(feature = "parallel"))] pub use core::cell::RefMut as Lock; @@ -11,9 +14,16 @@ pub use core::cell::RefMut as Lock; pub use alloc::rc::{Rc as Ref, Weak}; #[cfg(feature = "parallel")] -pub use std::sync::{Arc as Ref, MutexGuard as Lock, Weak}; +pub use alloc::sync::{Arc as Ref, Weak}; + +#[cfg(all(feature = "parallel", feature = "std"))] +pub use std::sync::MutexGuard as Lock; + +#[cfg(all(feature = "parallel", not(feature = "std")))] +pub use spin::MutexGuard as Lock; #[repr(transparent)] +#[derive(Debug)] pub struct Mut(Cell); impl Mut { @@ -35,10 +45,15 @@ impl Mut { self.0.borrow_mut() } - #[cfg(feature = "parallel")] + #[cfg(all(feature = "parallel", feature = "std"))] { self.0.lock().unwrap() } + + #[cfg(all(feature = "parallel", not(feature = "std")))] + { + self.0.lock() + } } pub fn try_lock(&self) -> Option> { @@ -47,9 +62,14 @@ impl Mut { self.0.try_borrow_mut().ok() } - #[cfg(feature = "parallel")] + #[cfg(all(feature = "parallel", feature = "std"))] { self.0.lock().ok() } + + #[cfg(all(feature = "parallel", not(feature = "std")))] + { + self.0.try_lock() + } } } diff --git a/core/src/value/convert/from.rs b/core/src/value/convert/from.rs index 0848c848..b59683a8 100644 --- a/core/src/value/convert/from.rs +++ b/core/src/value/convert/from.rs @@ -429,6 +429,10 @@ macro_rules! chrono_from_js_impls { chrono_from_js_impls! { Utc; +} + +#[cfg(feature = "std")] +chrono_from_js_impls! { Local; } diff --git a/examples/zephyr/CMakeLists.txt b/examples/zephyr/CMakeLists.txt new file mode 100644 index 00000000..9fd59e77 --- /dev/null +++ b/examples/zephyr/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(rquickjs_zephyr) + +rust_cargo_application() diff --git a/examples/zephyr/Cargo.toml b/examples/zephyr/Cargo.toml new file mode 100644 index 00000000..037c42ef --- /dev/null +++ b/examples/zephyr/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rustapp" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["staticlib"] + +[dependencies] +rquickjs = { path = "../..", default-features = false, features = ["zephyr"] } +zephyr = "0.1.0" diff --git a/examples/zephyr/README.md b/examples/zephyr/README.md new file mode 100644 index 00000000..46380dda --- /dev/null +++ b/examples/zephyr/README.md @@ -0,0 +1,18 @@ +# Zephyr example + +This firmware validates rquickjs and QuickJS on a `no_std + alloc` Zephyr host. It exercises two +Zephyr threads sharing a `parallel` runtime, promises and the job queue, SharedArrayBuffer, 8/16/32 +bit Atomics, 64-bit BigInt Atomics, and timed `Atomics.wait`. + +Zephyr 4.4.2 and SDK 1.0.1 were used for the initial port. The `mps2/an385` QEMU board provides the +same Cortex-M3 Rust target as `qemu_cortex_m3`, with enough RAM and flash for the engine. + +```sh +source /Users/yorkie/zephyrproject/env.sh +cd /Users/yorkie/zephyrproject +west build -b mps2/an385 /Users/yorkie/workspace/rquickjs/examples/zephyr \ + -d build/rquickjs-zephyr +west build -d build/rquickjs-zephyr -t run +``` + +Exit QEMU with Ctrl+A, then X. diff --git a/examples/zephyr/app.overlay b/examples/zephyr/app.overlay new file mode 100644 index 00000000..08449b10 --- /dev/null +++ b/examples/zephyr/app.overlay @@ -0,0 +1,13 @@ +/ { + aliases { + /delete-property/ led0; + /delete-property/ led1; + /delete-property/ sw0; + /delete-property/ sw1; + }; +}; + +/delete-node/ &led_0; +/delete-node/ &led_1; +/delete-node/ &user_button_0; +/delete-node/ &user_button_1; diff --git a/examples/zephyr/prj.conf b/examples/zephyr/prj.conf new file mode 100644 index 00000000..6a2b03fe --- /dev/null +++ b/examples/zephyr/prj.conf @@ -0,0 +1,14 @@ +CONFIG_RUST=y +CONFIG_RUST_ALLOC=y + +CONFIG_MAIN_STACK_SIZE=65536 +CONFIG_HEAP_MEM_POOL_SIZE=3145728 + +CONFIG_POSIX_API=y +CONFIG_POSIX_THREAD_THREADS_MAX=8 +CONFIG_MAX_PTHREAD_MUTEX_COUNT=16 +CONFIG_MAX_PTHREAD_COND_COUNT=16 +CONFIG_POSIX_THREAD_ATTR_STACKSIZE=y + +CONFIG_LOG=y +CONFIG_LOG_BACKEND_RTT=n diff --git a/examples/zephyr/src/lib.rs b/examples/zephyr/src/lib.rs new file mode 100644 index 00000000..b74aa60f --- /dev/null +++ b/examples/zephyr/src/lib.rs @@ -0,0 +1,92 @@ +#![no_std] + +extern crate alloc; + +use rquickjs::{Context, Runtime}; +use zephyr::printkln; + +const WORKER_STACK_SIZE: usize = 65536; + +fn run_engine(rt: &Runtime) -> rquickjs::Result { + printkln!("rquickjs: create context"); + rt.set_can_block(true); + let ctx = Context::full(rt)?; + + ctx.with(|ctx| { + ctx.eval::<(), _>( + r#" + globalThis.promiseResult = 0; + Promise.resolve(40).then(value => { globalThis.promiseResult = value + 2; }); + "#, + ) + })?; + printkln!("rquickjs: promise queued"); + + while rt.is_job_pending() { + rt.execute_pending_job() + .map_err(|_| rquickjs::Error::Exception)?; + } + printkln!("rquickjs: jobs drained"); + + ctx.with(|ctx| { + let result = ctx.eval( + r#" + (() => { + const sab = new SharedArrayBuffer(32); + const u8 = new Uint8Array(sab); + const i16 = new Int16Array(sab); + const i32 = new Int32Array(sab); + const i64 = new BigInt64Array(sab); + + if (Atomics.add(i32, 0, 40) !== 0) return -1; + if (Atomics.compareExchange(i32, 0, 40, 42) !== 40) return -2; + if (Atomics.load(i32, 0) !== 42) return -3; + if (Atomics.wait(i32, 1, 0, 1) !== "timed-out") return -4; + Atomics.store(i64, 1, 7n); + if (Atomics.load(i64, 1) !== 7n) return -5; + if (Atomics.exchange(u8, 16, 5) !== 0) return -6; + if (Atomics.or(u8, 16, 2) !== 5) return -7; + if (Atomics.and(u8, 16, 6) !== 7) return -8; + if (Atomics.store(i16, 9, 9) !== 9) return -9; + if (Atomics.sub(i16, 9, 2) !== 9) return -10; + if (Atomics.xor(i16, 9, 5) !== 7) return -11; + if (Atomics.add(i64, 1, 2n) !== 7n) return -12; + if (Atomics.load(i64, 1) !== 9n) return -13; + if (Atomics.notify(i32, 1, 1) !== 0) return -14; + Atomics.pause(); + return globalThis.promiseResult; + })() + "#, + ); + if result.is_err() { + printkln!("rquickjs: JavaScript exception: {:?}", ctx.catch()); + } + result + }) +} + +#[zephyr::thread(stack_size = WORKER_STACK_SIZE, pool_size = 2)] +fn worker(id: usize, runtime: Runtime) { + printkln!("rquickjs worker {}: start", id); + match run_engine(&runtime) { + Ok(42) => printkln!("rquickjs worker {}: PASS", id), + Ok(value) => panic!("rquickjs worker {} returned {}", id, value), + Err(error) => panic!("rquickjs worker {} failed: {:?}", id, error), + } +} + +#[no_mangle] +extern "C" fn rust_main() { + printkln!("rquickjs: create runtime"); + let runtime = Runtime::new().expect("create QuickJS runtime"); + printkln!("rquickjs: runtime ready"); + + let first = worker(1, runtime.clone()).start(); + printkln!("rquickjs: first worker started"); + let second = worker(2, runtime).start(); + printkln!("rquickjs: second worker started"); + first.join().expect("join first rquickjs worker"); + second.join().expect("join second rquickjs worker"); + + printkln!("rquickjs Zephyr no_std + alloc: PASS"); +} diff --git a/sys/Cargo.toml b/sys/Cargo.toml index 1b992fac..c30b1e39 100644 --- a/sys/Cargo.toml +++ b/sys/Cargo.toml @@ -24,6 +24,7 @@ optional = true [features] bindgen = ["bindgen-rs"] +zephyr = ["bindgen"] # Debug logging logging = ["pretty_env_logger"] diff --git a/sys/build.rs b/sys/build.rs index 22e6f19a..1c72cd38 100644 --- a/sys/build.rs +++ b/sys/build.rs @@ -111,12 +111,17 @@ fn main() { "dump-promise", "dump-read-object", "disable-assertions", + "zephyr", ]; for feature in &features { println!("cargo:rerun-if-env-changed={}", feature_to_cargo(feature)); } println!("cargo:rerun-if-env-changed=CARGO_CFG_SANITIZE"); + println!("cargo:rerun-if-env-changed=INCLUDE_DIRS"); + println!("cargo:rerun-if-env-changed=INCLUDE_DEFINES"); + println!("cargo:rerun-if-env-changed=BINARY_DIR_INCLUDE_GENERATED"); + println!("cargo:rerun-if-env-changed=ZEPHYR_SDK_INSTALL_DIR"); let src_dir = Path::new("quickjs"); @@ -160,14 +165,13 @@ fn main() { let is_wasi = target_os == "wasi"; let is_wasm_unknown = target_arch == "wasm32" && target_os == "unknown" && target_family == "wasm"; + let is_zephyr = env::var_os("CARGO_FEATURE_ZEPHYR").is_some(); let mut builder = cc::Build::new(); - builder - .extra_warnings(false) - .flag_if_supported("-Wno-implicit-const-int-float-conversion") - //.flag("-Wno-array-bounds") - //.flag("-Wno-format-truncation") - ; + builder.extra_warnings(false); + if !is_zephyr { + builder.flag_if_supported("-Wno-implicit-const-int-float-conversion"); + } match env::var("CARGO_CFG_SANITIZE").as_deref() { Ok("address") => { @@ -194,6 +198,62 @@ fn main() { let mut bindgen_cflags = vec![]; + if is_zephyr { + defines.push(("__ZEPHYR__".into(), Some("1"))); + + let generated_include = PathBuf::from( + env::var("BINARY_DIR_INCLUDE_GENERATED").expect( + "the zephyr feature requires BINARY_DIR_INCLUDE_GENERATED from rust_cargo_application()", + ), + ); + let autoconf = generated_include.join("autoconf.h"); + builder.flag("-imacros").flag(&autoconf); + bindgen_cflags.push("-imacros".into()); + bindgen_cflags.push(autoconf.display().to_string()); + + let include_dirs = env::var("INCLUDE_DIRS") + .expect("the zephyr feature requires INCLUDE_DIRS from rust_cargo_application()") + .split([' ', ';']) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .collect::>(); + for include_dir in include_dirs { + builder.include(&include_dir); + bindgen_cflags.push(format!("-I{}", include_dir.display())); + } + + let include_defines = env::var("INCLUDE_DEFINES") + .expect("the zephyr feature requires INCLUDE_DEFINES from rust_cargo_application()"); + for definition in include_defines + .split([' ', ';']) + .filter(|value| !value.is_empty()) + { + let (name, value) = definition + .split_once('=') + .map_or((definition, None), |(name, value)| (name, Some(value))); + builder.define(name, value); + bindgen_cflags.push(match value { + Some(value) => format!("-D{name}={value}"), + None => format!("-D{name}"), + }); + } + + if target_arch == "arm" { + if let Some(sdk_dir) = env::var_os("ZEPHYR_SDK_INSTALL_DIR") { + let toolchain = PathBuf::from(sdk_dir).join("gnu/arm-zephyr-eabi/bin"); + builder.compiler(toolchain.join("arm-zephyr-eabi-gcc")); + builder.archiver(toolchain.join("arm-zephyr-eabi-ar")); + + let libc_include = toolchain + .parent() + .expect("Zephyr ARM toolchain bin directory has no parent") + .join("arm-zephyr-eabi/include"); + builder.include(&libc_include); + bindgen_cflags.push(format!("-I{}", libc_include.display())); + } + } + } + if target_os == "windows" { if target_env == "msvc" { env::set_var( From e0b891e1063d01d59723e54a4afda256e7a959dc Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 07:36:03 +0800 Subject: [PATCH 2/8] sys: update QuickJS for Zephyr support Point the submodule at the matching QuickJS portability changes so clean checkouts include the Zephyr threading and atomics implementation. --- sys/quickjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sys/quickjs b/sys/quickjs index f23aa4db..0f549bb7 160000 --- a/sys/quickjs +++ b/sys/quickjs @@ -1 +1 @@ -Subproject commit f23aa4db6749ddb36867bd05f982bd57d286d346 +Subproject commit 0f549bb7afc203e842661e372c96bba4d384617c From f17ea21230856ed2ac132ed583c91c931d63e617 Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 07:36:03 +0800 Subject: [PATCH 3/8] runtime: gate parallel-only Mut import --- core/src/runtime/async.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/runtime/async.rs b/core/src/runtime/async.rs index 4639c5e4..95daf5b5 100644 --- a/core/src/runtime/async.rs +++ b/core/src/runtime/async.rs @@ -18,9 +18,10 @@ use crate::allocator::Allocator; use crate::loader::{Loader, Resolver}; #[cfg(feature = "parallel")] use crate::util::{AssertSendFuture, AssertSyncFuture}; +#[cfg(feature = "parallel")] +use crate::Mut; use crate::{ - context::AsyncContext, qjs, result::AsyncJobException, util::ManualPoll, Ctx, Exception, Mut, - Result, + context::AsyncContext, qjs, result::AsyncJobException, util::ManualPoll, Ctx, Exception, Result, }; #[cfg(feature = "parallel")] From 6b7d26a8ec1480d3e7628f6d97fac2f261d52585 Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 08:22:57 +0800 Subject: [PATCH 4/8] runtime: skip blocking atomics test on WASI WASI builds expose the Atomics API but cannot block the Wasmtime test thread. Keep compiling the test while skipping its runtime assertion on WASI targets. --- core/src/runtime/base.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/runtime/base.rs b/core/src/runtime/base.rs index 20fcf44b..9bcb44ee 100644 --- a/core/src/runtime/base.rs +++ b/core/src/runtime/base.rs @@ -229,6 +229,7 @@ mod test { } #[test] + #[cfg_attr(target_os = "wasi", ignore = "Atomics.wait cannot block on WASI")] fn atomics_wait_can_be_enabled() { let rt = Runtime::new().unwrap(); rt.set_can_block(true); From e9ee2cc9c6e3378662b23116599fd904523c402b Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 13:46:08 +0800 Subject: [PATCH 5/8] sys: update QuickJS submodule Pick up the QuickJS cleanup that removes committed macOS metadata files and ignores future .DS_Store files. --- sys/quickjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sys/quickjs b/sys/quickjs index 0f549bb7..33f9bfff 160000 --- a/sys/quickjs +++ b/sys/quickjs @@ -1 +1 @@ -Subproject commit 0f549bb7afc203e842661e372c96bba4d384617c +Subproject commit 33f9bfffba40e7f89c2d2e763095bb79a0440f2e From d1d57c98a6e75575d32437e8f24157a6aaa8a548 Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 13:52:21 +0800 Subject: [PATCH 6/8] sys: update QuickJS Zephyr example Pick up the QuickJS example output that prints the evaluated JavaScript result before reporting success. --- sys/quickjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sys/quickjs b/sys/quickjs index 33f9bfff..602edbf7 160000 --- a/sys/quickjs +++ b/sys/quickjs @@ -1 +1 @@ -Subproject commit 33f9bfffba40e7f89c2d2e763095bb79a0440f2e +Subproject commit 602edbf71a054b3303219bb959a9ab74aec6c3d0 From 712d38a75e518dd6af61071b705bf6f035a79b73 Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 14:22:28 +0800 Subject: [PATCH 7/8] sys: track merged QuickJS Zephyr support Point the submodule at the QuickJS master commit created when the Zephyr support PR was merged, replacing the feature-branch commit. --- sys/quickjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sys/quickjs b/sys/quickjs index 602edbf7..836f07d0 160000 --- a/sys/quickjs +++ b/sys/quickjs @@ -1 +1 @@ -Subproject commit 602edbf71a054b3303219bb959a9ab74aec6c3d0 +Subproject commit 836f07d01f997b45cd2ed146fb88ffc5709348c6 From 12aed28de1a7b364abe05465ef8b96a32b9555f5 Mon Sep 17 00:00:00 2001 From: Yorkie Makoto Date: Mon, 7 Sep 2026 16:05:25 +0800 Subject: [PATCH 8/8] runtime: keep try_lock non-blocking Use the standard mutex try_lock operation so context drop paths cannot block while the runtime is already locked. Export the Zephyr entry point explicitly and make the example build commands portable. --- core/src/safe_ref.rs | 15 ++++++++++++++- examples/zephyr/README.md | 11 ++++++----- examples/zephyr/src/lib.rs | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/core/src/safe_ref.rs b/core/src/safe_ref.rs index a4d88019..aa69a178 100644 --- a/core/src/safe_ref.rs +++ b/core/src/safe_ref.rs @@ -64,7 +64,7 @@ impl Mut { #[cfg(all(feature = "parallel", feature = "std"))] { - self.0.lock().ok() + self.0.try_lock().ok() } #[cfg(all(feature = "parallel", not(feature = "std")))] @@ -73,3 +73,16 @@ impl Mut { } } } + +#[cfg(test)] +mod tests { + use super::Mut; + + #[test] + fn try_lock_returns_none_while_locked() { + let value = Mut::new(0); + let _guard = value.lock(); + + assert!(value.try_lock().is_none()); + } +} diff --git a/examples/zephyr/README.md b/examples/zephyr/README.md index 46380dda..f8f42ab0 100644 --- a/examples/zephyr/README.md +++ b/examples/zephyr/README.md @@ -8,11 +8,12 @@ Zephyr 4.4.2 and SDK 1.0.1 were used for the initial port. The `mps2/an385` QEMU same Cortex-M3 Rust target as `qemu_cortex_m3`, with enough RAM and flash for the engine. ```sh -source /Users/yorkie/zephyrproject/env.sh -cd /Users/yorkie/zephyrproject -west build -b mps2/an385 /Users/yorkie/workspace/rquickjs/examples/zephyr \ - -d build/rquickjs-zephyr -west build -d build/rquickjs-zephyr -t run +export RQUICKJS_DIR="$(git rev-parse --show-toplevel)" +export ZEPHYR_WORKSPACE=/path/to/zephyrproject +source "$ZEPHYR_WORKSPACE/env.sh" +west build -b mps2/an385 "$RQUICKJS_DIR/examples/zephyr" \ + -d "$ZEPHYR_WORKSPACE/build/rquickjs-zephyr" +west build -d "$ZEPHYR_WORKSPACE/build/rquickjs-zephyr" -t run ``` Exit QEMU with Ctrl+A, then X. diff --git a/examples/zephyr/src/lib.rs b/examples/zephyr/src/lib.rs index b74aa60f..ae0eeb3e 100644 --- a/examples/zephyr/src/lib.rs +++ b/examples/zephyr/src/lib.rs @@ -76,7 +76,7 @@ fn worker(id: usize, runtime: Runtime) { } #[no_mangle] -extern "C" fn rust_main() { +pub extern "C" fn rust_main() { printkln!("rquickjs: create runtime"); let runtime = Runtime::new().expect("create QuickJS runtime"); printkln!("rquickjs: runtime ready");