From fc33a99151fb7f205fcf6320014ee8ee1415ccab Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:42:40 -0400 Subject: [PATCH 1/7] Update Why section --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ad9cda3..0740be6 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,22 @@ touches the frontend — and to make the web a faster and more secure place. Rus compiled to WASM gives you memory safety and near-native speed; wasm_zero removes the friction of getting that power to the browser. You annotate a Rust function, and wasm_zero emits a JavaScript/TypeScript shim you can drop straight -into a bare `.html` file — no bundler, no glue runtime, no `std` assumption. The -same shim works on a server or inside a [Spin](https://www.fermyon.com/spin) -Fermyon edge container, so one model spans the browser, the edge, and the -backend. The less machinery between Rust and the host, the smaller, faster, and -easier to audit the result — which is the whole point. +into a bare `.html` file — no bundler, no glue runtime, no `std` assumption. The +same emitted wasm can be run on the server with wasmtime or a self hosted VM. +To allow wasi targets set `--target wasm32-wasi` as target. +`wasm-bindgen` is excellent but pulls in a JS glue. -`wasm-bindgen` is excellent but pulls in a JS glue runtime and assumes `std`. For small `no_std` wasm modules that just need to hand structured data to a JavaScript host, that's a lot of machinery. wasm_zero takes a different tack: - The Rust side serializes return values with rkyv into a flat byte buffer. - The JS side reads that buffer field-by-field straight from wasm memory, using readers generated from your Rust types — no hand-maintained schema files and - no runtime decode library. + no runtime decode library. Strings still use TextDecoder which has a runtime + cost along with branching for SSO. - The only ABI surface is a handful of integer-in/integer-out functions plus - linear memory. + linear memory. In the future we want to provide a way to provide a validated + abi at compile time for custom ABI needs ## At Dusk Network: exu From 55d6b89a4cb03c31c7742280978dea3b2b9f7b4d Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:08:20 -0400 Subject: [PATCH 2/7] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0740be6..b93075c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ function, and wasm_zero emits a JavaScript/TypeScript shim you can drop straight into a bare `.html` file — no bundler, no glue runtime, no `std` assumption. The same emitted wasm can be run on the server with wasmtime or a self hosted VM. To allow wasi targets set `--target wasm32-wasi` as target. -`wasm-bindgen` is excellent but pulls in a JS glue. +`wasm-bindgen` is excellent but pulls in a JS glue. This library is meant for +rust to js communication and not the other way. For small `no_std` wasm modules that just need to hand structured data to a JavaScript host, that's a lot of machinery. wasm_zero takes a different tack: From c887262c2869b9fab925f2a138334cd12fab6400 Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:11:04 -0400 Subject: [PATCH 3/7] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b93075c..27f3198 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ web browser or the Node.js runtime to run your WASM with: delete the WASM memory and drop the worker entirely, so nothing leaks between calls. +Note there are caveats with running thread pool like rayon in wasm and rust +version doesn't map to directly to web. At least until the shared everything +proposal. [Read more about this in wasm-bindgen documentation](https://wasm-bindgen.github.io/wasm-bindgen/examples/raytrace.html?highlight=threading#caveats) + We maintain a **fork of exu** (vendored in [`exu/`](exu)) that extends upstream with a [**rayon**](https://github.com/rayon-rs/rayon) thread pool running over *shared* wasm memory — no `wasm-bindgen` required. The fork keeps exu's From 467a2334e1181aa9fc5860096ffa3943593396bb Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Sat, 25 Jul 2026 05:05:38 +0900 Subject: [PATCH 4/7] migrate to rkyv-js v0.2 release --- Cargo.lock | 7 +- README.md | 20 +++--- benchmark/Cargo.lock | 7 +- benchmark/README.md | 5 +- benchmark/build.sh | 10 ++- benchmark/web/index.html | 6 +- crates/wasm_zero/Cargo.toml | 2 +- crates/wasm_zero/src/mem.rs | 12 ++-- crates/wasm_zero_build/src/lib.rs | 97 ++++++++++++++++++++++---- crates/wasm_zero_test_nostd/index.html | 9 +-- 10 files changed, 130 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84fa82e..481b1b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,12 @@ dependencies = [ [[package]] name = "rkyv-js-codegen" -version = "0.0.1" -source = "git+https://github.com/cometkim/rkyv-js#2c3fc14860535eb9b8075f1db7547a45dbd8fa6e" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe07dd63d23d11001d8ceb9c46bbf57b1dfd413b229594d94028abf50ccced2" dependencies = [ + "proc-macro2", + "quote", "syn", "walkdir", ] diff --git a/README.md b/README.md index 27f3198..396ae4d 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,9 @@ For each `#[wasm_zero] fn foo(args...) -> T`: 2. Arguments that are scalar primitives (`i8`…`u64`, `f32`, `f64`, `bool`) are passed **directly as wasm function parameters** — no encoding, no input buffer (the fast path). If any argument is non-scalar (`String`, a struct, - `Vec`, …), all args are instead rkyv-encoded (`r.encode`) into an input - buffer (`[len][bytes]`) and `in_ptr` is passed. + `Vec`, …), all args are instead rkyv-archived **directly into** the input + buffer (`[len][bytes]`) — rkyv-js writes through a fixed `RkyvWriter` bound + to that region of wasm memory, so nothing is copied — and `in_ptr` is passed. 3. If the return type is a scalar primitive or `()`, the shim **returns it directly** as the wasm function's return value — no output buffer, no rkyv, no error code (it can't fail). Otherwise it writes @@ -270,8 +271,9 @@ Two files are emitted from one model: in a browser (no bundler, no CDN). `rkyv-js` is imported **only** if a function takes a non-scalar argument -(`String`/struct/…) — it's used to `r.encode` the argument into the input buffer -(hand-rolling the rkyv *writer* is out of scope). The read path never needs it. +(`String`/struct/…) — and then only its encoder-only `rkyv-js/encode` entry, +which archives the argument straight into the input buffer (hand-rolling the +rkyv *writer* is out of scope). The read path never needs it. ### 4. Call it @@ -282,9 +284,10 @@ Two files are emitted from one model: console.log(wasmzero.get_adult_person()); ``` @@ -365,7 +368,7 @@ wasm_zero picks the cheapest way to pass arguments based on their types: |-----------|----------------|------| | none | nullary shim | — | | all scalar primitives (`i8`…`u64`, `f32`, `f64`, `bool`) | passed **directly as wasm params** | none — like a bare call | -| any non-scalar (`String`, struct, `Vec`, …) | rkyv-encoded (`r.encode`) into the input buffer | one encode + copy | +| any non-scalar (`String`, struct, `Vec`, …) | rkyv-archived **in place** into the input buffer via a fixed `RkyvWriter` | one archive, no copy | (i64/u64 args are passed as JS `BigInt`.) @@ -400,7 +403,8 @@ a touch faster, while wasm_zero wins when you read only some fields. - Arguments must be **owned** rkyv types (e.g. `i32`, `String`, a `#[derive(Archive)]` struct) — borrowed parameters like `&str` aren't decodable from the input - buffer. Non-scalar arguments require `rkyv-js` (for encoding). + buffer. Non-scalar arguments require `rkyv-js` >= 0.2.0 (its encoder-only + `rkyv-js/encode` entry, for the external-buffer `RkyvWriter`). - The generated readers cover scalars, `String`, `Option`, `Vec`, `Box`/`Rc`/`Arc`, and `#[derive(Archive)]` structs. Enums, maps, and tuples aren't read yet (the build fails with a clear message). diff --git a/benchmark/Cargo.lock b/benchmark/Cargo.lock index 47f4423..241a13e 100644 --- a/benchmark/Cargo.lock +++ b/benchmark/Cargo.lock @@ -221,9 +221,12 @@ dependencies = [ [[package]] name = "rkyv-js-codegen" -version = "0.0.1" -source = "git+https://github.com/cometkim/rkyv-js#2c3fc14860535eb9b8075f1db7547a45dbd8fa6e" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe07dd63d23d11001d8ceb9c46bbf57b1dfd413b229594d94028abf50ccced2" dependencies = [ + "proc-macro2", + "quote", "syn", "walkdir", ] diff --git a/benchmark/README.md b/benchmark/README.md index 4c5baa9..9e34030 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -44,7 +44,7 @@ cargo run -p wasm_zero_serve # serve the repo root (from the parent workspace) ``` Open and click **Run -benchmarks**. (rkyv-js is loaded from GitHub via esm.sh, so the page needs +benchmarks**. (rkyv-js resolves to its npm package via esm.sh, so the page needs network access the first time.) ## Bundle size @@ -71,7 +71,8 @@ Caveats (also printed by the script): - wasm_bindgen ships the `.wasm` plus a **per-module** JS glue file. - wasm_zero's `bindings.js` is **self-contained**: it decodes straight from wasm memory with no runtime dependency. `rkyv-js` is imported *only* by modules that - take a **non-scalar argument** (to `r.encode` it); the benchmark has none, so + take a **non-scalar argument** (to archive it into the input buffer), and then + only its encoder-only `rkyv-js/encode` entry; the benchmark has none, so nothing extra ships. ## Notes diff --git a/benchmark/build.sh b/benchmark/build.sh index 739505a..c42882b 100755 --- a/benchmark/build.sh +++ b/benchmark/build.sh @@ -12,13 +12,19 @@ cd "$(dirname "$0")" mkdir -p web/pkg/wasm-bindgen web/pkg/wasm-zero echo ">> building wasm-bindgen suite (wasm-pack)" -wasm-pack build wasm-bindgen \ +# RUSTFLAGS (even empty) replaces all config-file rustflags: +# the repo root .cargo/config.toml sets +atomics,+bulk-memory for the rayon demos, +# which would inflate this module and skew the size comparison. +RUSTFLAGS="" wasm-pack build wasm-bindgen \ --target web --release \ --out-dir ../web/pkg/wasm-bindgen \ --out-name bench_wasm_bindgen echo ">> building wasm-zero suite (cargo, then bindings from the wasm metadata)" -( cd wasm-zero && cargo build --release ) +# RUSTFLAGS replaces all config-file rustflags: the repo root .cargo/config.toml +# sets +atomics,+bulk-memory for the rayon demos, which would make this module +# import a shared memory and break the (non-threaded) generated loader. +( cd wasm-zero && RUSTFLAGS="-C panic=abort" cargo build --release ) # Generate bindings from the __wasm_zero custom section of the compiled wasm, # then ship a copy with that section stripped (it's build-time-only data). diff --git a/benchmark/web/index.html b/benchmark/web/index.html index 41e79b9..be17a53 100644 --- a/benchmark/web/index.html +++ b/benchmark/web/index.html @@ -24,11 +24,13 @@ window.Foo = class { bar() {} }; - + diff --git a/crates/wasm_zero/Cargo.toml b/crates/wasm_zero/Cargo.toml index 6f8403f..32df1f5 100644 --- a/crates/wasm_zero/Cargo.toml +++ b/crates/wasm_zero/Cargo.toml @@ -9,4 +9,4 @@ rkyv.workspace = true bytecheck = "0.8.2" [build-dependencies] -rkyv-js-codegen = { git = "https://github.com/cometkim/rkyv-js" } +rkyv-js-codegen = "0.2.0" diff --git a/crates/wasm_zero/src/mem.rs b/crates/wasm_zero/src/mem.rs index 6644f78..a0df500 100644 --- a/crates/wasm_zero/src/mem.rs +++ b/crates/wasm_zero/src/mem.rs @@ -10,7 +10,6 @@ use core::slice; use bytecheck::CheckBytes; use rkyv::api::high::{HighDeserializer, HighValidator}; use rkyv::rancor; -use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize}; use crate::error::ErrorCode; @@ -71,18 +70,17 @@ pub unsafe fn read_buffer<'a>(ptr: *const u8) -> &'a [u8] { /// Parse the buffer /// # SAFETY -/// the pointer +/// `bytes` must start at a 16-aligned address. +/// Buffers produced by [`read_buffer`] satisfy this by construction: +/// `malloc` returns 16-aligned pointers and [`HEADER`] is 16, +/// so the archive payload is 16-aligned in place — no copy into an aligned scratch is needed. pub unsafe fn parse_buffer(bytes: &[u8]) -> Result where T: Archive, T::Archived: for<'a> CheckBytes> + Deserialize>, { - // AlignedVec guarantees 16-byte alignment — enough for all rkyv types - let mut aligned = AlignedVec::<16>::new(); - aligned.extend_from_slice(bytes); - - rkyv::from_bytes::(&aligned).or(Err(ErrorCode::UnarchivingError)) + rkyv::from_bytes::(bytes).or(Err(ErrorCode::UnarchivingError)) } /// Checks and deserializes a value from the given po diff --git a/crates/wasm_zero_build/src/lib.rs b/crates/wasm_zero_build/src/lib.rs index 248a36e..354d1dc 100644 --- a/crates/wasm_zero_build/src/lib.rs +++ b/crates/wasm_zero_build/src/lib.rs @@ -12,7 +12,7 @@ //! runtime is needed to decode. //! //! rkyv-js is imported only when a function takes a **non-scalar argument** -//! (`String`/struct/…), to `r.encode` it into the input buffer (hand-rolling +//! (`String`/struct/…), to codec-encode it into the input buffer (hand-rolling //! the rkyv *writer* is out of scope). Scalar args pass directly as wasm params, //! and scalar/unit returns come back as the wasm function's value. //! @@ -510,7 +510,9 @@ fn render_module(typed: bool, structs: &StructMap, funcs: &[FnMeta]) -> String { let mut out = String::new(); out.push_str("// Auto-generated by wasm_zero. Do not edit by hand.\n"); if needs_rkyv { - out.push_str("import * as r from 'rkyv-js';\n"); + // Encoder-only entry: the read path is hand-rolled, so the generated + // module never needs rkyv-js's decode/access machinery. + out.push_str("import * as r from 'rkyv-js/encode';\n"); } out.push('\n'); @@ -559,6 +561,20 @@ fn render_module(typed: bool, structs: &StructMap, funcs: &[FnMeta]) -> String { } out.push_str("});\n\n"); } + + // One hoisted codec per rkyv-encoded function: containers are + // constructed once at module setup instead of on every call. + out.push_str("// ---- per-function argument codecs ----\n"); + for f in funcs { + if !f.args.is_empty() && !f.args.iter().all(|(_, ty)| is_scalar(ty)) { + out.push_str(&format!( + "const __argCodec_{} = {};\n", + f.name, + args_codec(&f.args, structs) + )); + } + } + out.push('\n'); } // One decoder per struct: reads each field at its archived offset. @@ -591,25 +607,39 @@ fn render_module(typed: bool, structs: &StructMap, funcs: &[FnMeta]) -> String { } // ---- client ---- + // The argument writer archives in place into the input buffer, so it is + // bound to that region of wasm memory and goes stale — along with the + // views — whenever the memory grows. + let writer_decl = if needs_rkyv { + format!( + "\x20 let argWriter{} = null; // fixed rkyv writer over the input buffer\n", + t(": r.RkyvWriter | null"), + ) + } else { + String::new() + }; + let writer_reset = if needs_rkyv { " argWriter = null;" } else { "" }; out.push_str(&format!( "export function bindWasmZero(wasm{}) {{\n\ \x20 const outPtr = wasm.malloc(HEADER + MAX_BUFFER_SIZE);\n\ \x20 let inPtr = 0; // lazily allocated for non-scalar args\n\ + {}\ \x20 let buf = wasm.memory.buffer, dv = new DataView(buf), u8 = new Uint8Array(buf);\n\ - \x20 function views() {{ if (buf !== wasm.memory.buffer) {{ buf = wasm.memory.buffer; dv = new DataView(buf); u8 = new Uint8Array(buf); }} }}\n\n\ + \x20 function views() {{ if (buf !== wasm.memory.buffer) {{ buf = wasm.memory.buffer; dv = new DataView(buf); u8 = new Uint8Array(buf);{} }} }}\n\n\ \x20 function invoke(shim{}, argCodec{}, argValue{}, directArgs{}) {{\n\ \x20 if (directArgs !== null) return wasm[shim](...directArgs, outPtr);\n\ \x20 if (argCodec === null) return wasm[shim](outPtr);\n", - t(": any"), t(": string"), t(": any"), t(": any"), t(": any"), + t(": any"), writer_decl, writer_reset, + t(": string"), t(": any"), t(": any"), t(": any"), )); if needs_rkyv { out.push_str( - " \x20 const inBytes = r.encode(argCodec, argValue);\n\ - \x20 if (inBytes.length > MAX_BUFFER_SIZE) throw new RangeError('wasm_zero: input too large');\n\ - \x20 if (inPtr === 0) inPtr = wasm.malloc(HEADER + MAX_BUFFER_SIZE);\n\ + " \x20 if (inPtr === 0) inPtr = wasm.malloc(HEADER + MAX_BUFFER_SIZE);\n\ \x20 views();\n\ - \x20 dv.setUint32(inPtr, inBytes.length, true);\n\ - \x20 u8.set(inBytes, inPtr + HEADER);\n\ + \x20 if (argWriter === null) argWriter = new r.RkyvWriter({ buffer: u8.subarray(inPtr + HEADER, inPtr + HEADER + MAX_BUFFER_SIZE) });\n\ + \x20 argWriter.reset();\n\ + \x20 argCodec.encodeInto(argWriter, argValue); // archived in place; overflow throws RangeError\n\ + \x20 dv.setUint32(inPtr, argWriter.pos, true);\n\ \x20 return wasm[shim](inPtr, outPtr);\n", ); } else { @@ -648,7 +678,8 @@ fn render_module(typed: bool, structs: &StructMap, funcs: &[FnMeta]) -> String { \x20 const {{ instance }} = await WebAssembly.instantiateStreaming(fetch(wasmUrl), importObject);\n\ \x20 return bindWasmZero(instance.exports);\n\ }}\n", - t(": string | URL"), t("?: WebAssembly.Imports"), + t(": string | URL"), + t("?: WebAssembly.Imports"), )); out @@ -697,7 +728,8 @@ fn render_method(f: &FnMeta, structs: &StructMap, typed: bool) -> String { return format!(" {name}({params}){ann} {{ return {expr}; }},\n"); } - // Arg mode: nullary / all-scalar (direct) / non-scalar (rkyv-encoded). + // Arg mode: nullary / all-scalar (direct) / non-scalar (rkyv-encoded, + // via the module-level hoisted codec). let (arg_codec, arg_value, direct_args) = if args.is_empty() { ("null".into(), "null".into(), "null".into()) } else if args.iter().all(|(_, t)| is_scalar(t)) { @@ -707,7 +739,7 @@ fn render_method(f: &FnMeta, structs: &StructMap, typed: bool) -> String { [(n, _)] => n.clone(), _ => format!("[{}]", names_csv()), }; - (args_codec(args, structs), value, "null".into()) + (format!("__argCodec_{name}"), value, "null".into()) }; let size = archived(ret, structs).0; @@ -840,11 +872,46 @@ mod tests { blob.extend(record(&["1", "struct", "P", &pair("x", "u32")])); blob.extend(record(&["1", "fn", "shout", "String", &pair("msg", "String")])); let g = generate_bindings(&fake_wasm(&blob)).unwrap(); - assert!(g.js.contains("import * as r from 'rkyv-js'")); + assert!(g.js.contains("import * as r from 'rkyv-js/encode'")); + // Arg codec built once at module setup, not per call. + assert!(g.js.contains("const __argCodec_shout = r.string;")); assert!(g.js.contains( - "shout(msg) { return call(\"__wasm_zero_shout\", 8, (dv, u8, p) => rdStr(dv, u8, p), r.string, msg, null); }" + "shout(msg) { return call(\"__wasm_zero_shout\", 8, (dv, u8, p) => rdStr(dv, u8, p), __argCodec_shout, msg, null); }" )); - assert!(g.js.contains("r.encode(argCodec, argValue)")); + // Arguments archive in place through a fixed writer over the wasm + // input buffer — no intermediate encode buffer, no copy. + assert!(g.js.contains("argCodec.encodeInto(argWriter, argValue)")); + assert!(g.js.contains("argWriter = new r.RkyvWriter({ buffer: u8.subarray(inPtr + HEADER, inPtr + HEADER + MAX_BUFFER_SIZE) })")); + assert!(g.js.contains("dv.setUint32(inPtr, argWriter.pos, true)")); + } + + #[test] + fn multi_arg_codec_is_a_hoisted_tuple() { + let mut blob = Vec::new(); + blob.extend(record(&[ + "1", + "fn", + "send", + "u32", + &pair("tags", "Vec"), + &pair("note", "String"), + ])); + let g = generate_bindings(&fake_wasm(&blob)).unwrap(); + assert!(g.js.contains("const __argCodec_send = r.tuple(r.vec(r.u32), r.string);")); + + // Scalar-only modules never import rkyv-js and keep the writer-free path. + let mut scalar_blob = Vec::new(); + scalar_blob.extend(record(&[ + "1", + "fn", + "add", + "i32", + &pair("a", "i32"), + &pair("b", "i32"), + ])); + let scalar = generate_bindings(&fake_wasm(&scalar_blob)).unwrap(); + assert!(!scalar.js.contains("rkyv-js")); + assert!(!scalar.js.contains("argWriter")); } #[test] diff --git a/crates/wasm_zero_test_nostd/index.html b/crates/wasm_zero_test_nostd/index.html index 1070354..cfc9da5 100644 --- a/crates/wasm_zero_test_nostd/index.html +++ b/crates/wasm_zero_test_nostd/index.html @@ -13,13 +13,14 @@ .ok { color: #0a7d2c; } - + From a1f8b68db7bbaf07f04b462bb74e52f8fff8b1cc Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Fri, 21 Aug 2026 05:17:43 +0900 Subject: [PATCH 5/7] zero copy only where it's safe --- crates/wasm_zero/src/mem.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/wasm_zero/src/mem.rs b/crates/wasm_zero/src/mem.rs index a0df500..c399c70 100644 --- a/crates/wasm_zero/src/mem.rs +++ b/crates/wasm_zero/src/mem.rs @@ -10,6 +10,7 @@ use core::slice; use bytecheck::CheckBytes; use rkyv::api::high::{HighDeserializer, HighValidator}; use rkyv::rancor; +use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize}; use crate::error::ErrorCode; @@ -69,18 +70,33 @@ pub unsafe fn read_buffer<'a>(ptr: *const u8) -> &'a [u8] { } /// Parse the buffer +/// +/// rkyv reads the archive **in place**, so `bytes` must satisfy +/// the archived type's alignment. +/// +/// Buffers from [`read_buffer`] are always 16-aligned, +/// which covers every rkyv archived type, so the generated bindings unarchive with no copy at all. +/// +/// Any other caller is made correct by copying into aligned scratch first. +/// /// # SAFETY -/// `bytes` must start at a 16-aligned address. -/// Buffers produced by [`read_buffer`] satisfy this by construction: -/// `malloc` returns 16-aligned pointers and [`HEADER`] is 16, -/// so the archive payload is 16-aligned in place — no copy into an aligned scratch is needed. +/// the pointer pub unsafe fn parse_buffer(bytes: &[u8]) -> Result where T: Archive, T::Archived: for<'a> CheckBytes> + Deserialize>, { - rkyv::from_bytes::(bytes).or(Err(ErrorCode::UnarchivingError)) + if (bytes.as_ptr() as usize).is_multiple_of(ALIGNMENT) { + return rkyv::from_bytes::(bytes) + .or(Err(ErrorCode::UnarchivingError)); + } + + // AlignedVec guarantees 16-byte alignment — enough for all rkyv types + let mut aligned = AlignedVec::<16>::new(); + aligned.extend_from_slice(bytes); + + rkyv::from_bytes::(&aligned).or(Err(ErrorCode::UnarchivingError)) } /// Checks and deserializes a value from the given po From f5cf83db9f1c76465cf7b9fd4804f043ebd4ee63 Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Fri, 21 Aug 2026 05:30:50 +0900 Subject: [PATCH 6/7] bump rkyv-js --- Cargo.lock | 4 ++-- benchmark/web/index.html | 4 ++-- crates/wasm_zero/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 481b1b6..5e375d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "rkyv-js-codegen" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe07dd63d23d11001d8ceb9c46bbf57b1dfd413b229594d94028abf50ccced2" +checksum = "6cdbd1432d9f87be505bfaa3cea041083270068caf0508631120e57387164245" dependencies = [ "proc-macro2", "quote", diff --git a/benchmark/web/index.html b/benchmark/web/index.html index be17a53..2bc2a4e 100644 --- a/benchmark/web/index.html +++ b/benchmark/web/index.html @@ -29,8 +29,8 @@ diff --git a/crates/wasm_zero/Cargo.toml b/crates/wasm_zero/Cargo.toml index 32df1f5..31eea6d 100644 --- a/crates/wasm_zero/Cargo.toml +++ b/crates/wasm_zero/Cargo.toml @@ -9,4 +9,4 @@ rkyv.workspace = true bytecheck = "0.8.2" [build-dependencies] -rkyv-js-codegen = "0.2.0" +rkyv-js-codegen = "0.2.1" From 42dbb453bfb36b41128bf5bf1ec85a4ad50091a1 Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Fri, 21 Aug 2026 14:49:20 +0900 Subject: [PATCH 7/7] bump rkyv-js --- Cargo.lock | 4 ++-- README.md | 2 +- benchmark/web/index.html | 4 ++-- crates/wasm_zero/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e375d7..cc309db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "rkyv-js-codegen" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdbd1432d9f87be505bfaa3cea041083270068caf0508631120e57387164245" +checksum = "916262f9d28bc154ab6bd81caa9720851ff883f393c41a1e67ddc3025e95c7b8" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 396ae4d..4ff969f 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ rkyv *writer* is out of scope). The read path never needs it. rkyv-js/encode (needs rkyv-js >= 0.2.0 for the external-buffer RkyvWriter); add an import map then: --> ``` diff --git a/benchmark/web/index.html b/benchmark/web/index.html index 2bc2a4e..9f38017 100644 --- a/benchmark/web/index.html +++ b/benchmark/web/index.html @@ -29,8 +29,8 @@ diff --git a/crates/wasm_zero/Cargo.toml b/crates/wasm_zero/Cargo.toml index 31eea6d..8209e97 100644 --- a/crates/wasm_zero/Cargo.toml +++ b/crates/wasm_zero/Cargo.toml @@ -9,4 +9,4 @@ rkyv.workspace = true bytecheck = "0.8.2" [build-dependencies] -rkyv-js-codegen = "0.2.1" +rkyv-js-codegen = "0.3.0"