From 9c1c15237644ba1339ded813e08d510373cb4bd3 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 23 Mar 2026 16:51:19 +0100 Subject: [PATCH 1/4] feat: add std::io::Write impl for BufferMut Enables bincode::serialize_into to write directly into WASM linear memory buffers, eliminating the intermediate Vec allocation that was previously needed when serializing data across the FFI boundary. --- rust/src/memory/buf.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/rust/src/memory/buf.rs b/rust/src/memory/buf.rs index c637bb7..a63c26c 100644 --- a/rust/src/memory/buf.rs +++ b/rust/src/memory/buf.rs @@ -184,6 +184,27 @@ impl<'instance> BufferMut<'instance> { } } +impl std::io::Write for BufferMut<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let last_write = (*self.write_ptr) as usize; + let free = self.buffer.len() - last_write; + let n = buf.len().min(free); + if n == 0 && !buf.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "buffer full", + )); + } + self.buffer[last_write..last_write + n].copy_from_slice(&buf[..n]); + *self.write_ptr = (last_write + n) as u32; + Ok(n) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + #[inline(always)] pub(crate) fn compute_ptr(ptr: *mut T, linear_mem_space: &WasmLinearMem) -> *mut T { let mem_start_ptr = linear_mem_space.start_ptr; From 5f80cf367c84967f8b20caa962f3df9d6d5aabcd Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 23 Mar 2026 17:12:52 +0100 Subject: [PATCH 2/4] test: add unit tests for BufferMut std::io::Write impl Tests cover basic write, exact fill, partial write when near full, error on full buffer, empty slice, write_all, write_all overflow, and bincode::serialize_into integration. Uses host-memory buffers (no WASM runtime needed). --- rust/src/memory/buf.rs | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/rust/src/memory/buf.rs b/rust/src/memory/buf.rs index a63c26c..76d59b4 100644 --- a/rust/src/memory/buf.rs +++ b/rust/src/memory/buf.rs @@ -361,6 +361,94 @@ fn __frnt__initiate_buffer(capacity: u32) -> i64 { buffer as i64 } +#[cfg(test)] +mod test_io_write { + use super::*; + use std::io::Write; + + /// Create a BufferMut backed by host memory (no WASM runtime needed). + /// Uses `__frnt__initiate_buffer` which allocates in host memory during tests, + /// and a null-base WasmLinearMem so compute_ptr is a no-op. + unsafe fn host_buffer_mut(capacity: u32) -> BufferMut<'static> { + let builder_ptr = __frnt__initiate_buffer(capacity) as *mut BufferBuilder; + let linear_mem = WasmLinearMem { + start_ptr: std::ptr::null(), + size: 0, + }; + BufferMut::from_ptr(builder_ptr, linear_mem) + } + + /// Call std::io::Write::write (not BufferMut::write which has different signature) + fn io_write(buf: &mut BufferMut<'_>, data: &[u8]) -> std::io::Result { + Write::write(buf, data) + } + + #[test] + fn write_trait_basic() { + let mut buf = unsafe { host_buffer_mut(32) }; + let n = io_write(&mut buf, b"hello").unwrap(); + assert_eq!(n, 5); + assert_eq!(buf.read_bytes(5), b"hello"); + } + + #[test] + fn write_trait_fills_exactly() { + let mut buf = unsafe { host_buffer_mut(4) }; + let n = io_write(&mut buf, b"abcd").unwrap(); + assert_eq!(n, 4); + assert_eq!(buf.read_bytes(4), b"abcd"); + } + + #[test] + fn write_trait_partial_when_near_full() { + let mut buf = unsafe { host_buffer_mut(4) }; + io_write(&mut buf, b"ab").unwrap(); + // Only 2 bytes free, writing 3 should write 2 + let n = io_write(&mut buf, b"xyz").unwrap(); + assert_eq!(n, 2); + assert_eq!(buf.read_bytes(4), b"abxy"); + } + + #[test] + fn write_trait_error_when_full() { + let mut buf = unsafe { host_buffer_mut(2) }; + io_write(&mut buf, b"ab").unwrap(); + let err = io_write(&mut buf, b"c").unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::WriteZero); + } + + #[test] + fn write_trait_empty_slice_ok() { + let mut buf = unsafe { host_buffer_mut(4) }; + let n = io_write(&mut buf, b"").unwrap(); + assert_eq!(n, 0); + } + + #[test] + fn write_all_trait() { + let mut buf = unsafe { host_buffer_mut(16) }; + buf.write_all(b"hello world").unwrap(); + assert_eq!(buf.read_bytes(11), b"hello world"); + } + + #[test] + fn write_all_insufficient_space() { + let mut buf = unsafe { host_buffer_mut(4) }; + let err = buf.write_all(b"hello").unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::WriteZero); + } + + #[test] + fn bincode_serialize_into() { + let data: Vec = vec![1, 2, 3, 4, 5]; + let size = bincode::serialized_size(&data).unwrap() as usize; + let mut buf = unsafe { host_buffer_mut(size as u32) }; + bincode::serialize_into(&mut buf, &data).unwrap(); + let result: Vec = bincode::deserialize(buf.read_bytes(size)).unwrap(); + assert_eq!(result, data); + } +} + #[cfg(all(test, any(unix, windows), feature = "wasmer-tests"))] mod test { use super::*; From 1ecab568a4e3a194cdb1ff4bab3a9bf8fd64242a Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 23 Mar 2026 17:25:28 +0100 Subject: [PATCH 3/4] fix: guard trace code against null WasmLinearMem in tests The from_raw_parts call in from_raw_builder's trace block panics when start_ptr is null (even with size=0) on newer Rust versions. Skip the trace logging when the linear memory pointer is null, which happens in host-memory buffer tests. --- rust/src/memory/buf.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rust/src/memory/buf.rs b/rust/src/memory/buf.rs index 76d59b4..46d9e4a 100644 --- a/rust/src/memory/buf.rs +++ b/rust/src/memory/buf.rs @@ -221,13 +221,15 @@ fn from_raw_builder<'a>(builder_ptr: *mut BufferBuilder, mem: WasmLinearMem) -> unsafe { #[cfg(feature = "trace")] { - let contract_mem = std::slice::from_raw_parts(mem.start_ptr, mem.size as usize); - tracing::trace!( - "*mut BufferBuilder <- offset: {}; in mem: {:?}", - builder_ptr as usize, - &contract_mem[builder_ptr as usize - ..builder_ptr as usize + std::mem::size_of::()] - ); + if !mem.start_ptr.is_null() && mem.size > 0 { + let contract_mem = std::slice::from_raw_parts(mem.start_ptr, mem.size as usize); + tracing::trace!( + "*mut BufferBuilder <- offset: {}; in mem: {:?}", + builder_ptr as usize, + &contract_mem[builder_ptr as usize + ..builder_ptr as usize + std::mem::size_of::()] + ); + } // use std::{fs::File, io::Write}; // let mut f = File::create(std::env::temp_dir().join("dump.mem")).unwrap(); // f.write_all(contract_mem).unwrap(); @@ -368,7 +370,7 @@ mod test_io_write { /// Create a BufferMut backed by host memory (no WASM runtime needed). /// Uses `__frnt__initiate_buffer` which allocates in host memory during tests, - /// and a null-base WasmLinearMem so compute_ptr is a no-op. + /// and a null-base WasmLinearMem so compute_ptr is a no-op on absolute pointers. unsafe fn host_buffer_mut(capacity: u32) -> BufferMut<'static> { let builder_ptr = __frnt__initiate_buffer(capacity) as *mut BufferBuilder; let linear_mem = WasmLinearMem { From 54056e101e3dd4f34193f953820dad9cef9313e4 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 23 Mar 2026 17:30:05 +0100 Subject: [PATCH 4/4] build: bump version to 0.3.3 Includes std::io::Write impl for BufferMut, enabling direct serialization into WASM linear memory buffers. --- rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ed14de2..dd08d57 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freenet-stdlib" -version = "0.3.2" +version = "0.3.3" edition = "2021" rust-version = "1.80" publish = true