diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index 3050d3d0..14dd4103 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -226,6 +226,132 @@ impl<'a> Memory<'a> { .map_err(|e| error!("{}", e)) } + /// @yard + /// Read a little-endian +i32+ starting at +offset+. + /// + /// @def read_i32(offset) + /// @param offset [Integer] + /// @return [Integer] + pub fn read_i32(&self, offset: usize) -> Result { + Ok(i32::from_le_bytes(self.read_fixed::<4>(offset)?)) + } + + /// @yard + /// Read a little-endian +u32+ starting at +offset+. + /// + /// @def read_u32(offset) + /// @param offset [Integer] + /// @return [Integer] + pub fn read_u32(&self, offset: usize) -> Result { + Ok(u32::from_le_bytes(self.read_fixed::<4>(offset)?)) + } + + /// @yard + /// Read a little-endian +i64+ starting at +offset+. + /// + /// @def read_i64(offset) + /// @param offset [Integer] + /// @return [Integer] + pub fn read_i64(&self, offset: usize) -> Result { + Ok(i64::from_le_bytes(self.read_fixed::<8>(offset)?)) + } + + /// @yard + /// Read a little-endian +u64+ starting at +offset+. + /// + /// @def read_u64(offset) + /// @param offset [Integer] + /// @return [Integer] + pub fn read_u64(&self, offset: usize) -> Result { + Ok(u64::from_le_bytes(self.read_fixed::<8>(offset)?)) + } + + /// @yard + /// Read a little-endian +f32+ starting at +offset+. + /// + /// @def read_f32(offset) + /// @param offset [Integer] + /// @return [Float] + pub fn read_f32(&self, offset: usize) -> Result { + Ok(f32::from_le_bytes(self.read_fixed::<4>(offset)?)) + } + + /// @yard + /// Read a little-endian +f64+ starting at +offset+. + /// + /// @def read_f64(offset) + /// @param offset [Integer] + /// @return [Float] + pub fn read_f64(&self, offset: usize) -> Result { + Ok(f64::from_le_bytes(self.read_fixed::<8>(offset)?)) + } + + /// @yard + /// Write a little-endian +i32+ starting at +offset+. + /// + /// @def write_i32(offset, value) + /// @param offset [Integer] + /// @param value [Integer] + /// @return [void] + pub fn write_i32(&self, offset: usize, value: i32) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + + /// @yard + /// Write a little-endian +u32+ starting at +offset+. + /// + /// @def write_u32(offset, value) + /// @param offset [Integer] + /// @param value [Integer] + /// @return [void] + pub fn write_u32(&self, offset: usize, value: u32) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + + /// @yard + /// Write a little-endian +i64+ starting at +offset+. + /// + /// @def write_i64(offset, value) + /// @param offset [Integer] + /// @param value [Integer] + /// @return [void] + pub fn write_i64(&self, offset: usize, value: i64) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + + /// @yard + /// Write a little-endian +u64+ starting at +offset+. + /// + /// @def write_u64(offset, value) + /// @param offset [Integer] + /// @param value [Integer] + /// @return [void] + pub fn write_u64(&self, offset: usize, value: u64) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + + /// @yard + /// Write a little-endian +f32+ starting at +offset+. + /// + /// @def write_f32(offset, value) + /// @param offset [Integer] + /// @param value [Float] + /// @return [void] + pub fn write_f32(&self, offset: usize, value: f32) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + + /// @yard + /// Write a little-endian +f64+ starting at +offset+. + /// + /// @def write_f64(offset, value) + /// @param offset [Integer] + /// @param value [Float] + /// @return [void] + pub fn write_f64(&self, offset: usize, value: f64) -> Result<(), Error> { + self.write_fixed(offset, value.to_le_bytes()) + } + /// @yard /// Grows a memory by +delta+ pages. /// Raises if the memory grows beyond its limit. @@ -264,6 +390,23 @@ impl<'a> Memory<'a> { fn data(&self) -> Result<&[u8], Error> { Ok(self.get_wasmtime_memory().data(self.store.context()?)) } + + fn read_fixed(&self, offset: usize) -> Result<[u8; N], Error> { + let context = self.store.context()?; + + self.get_wasmtime_memory() + .data(context) + .get(offset..) + .and_then(|s| s.get(..N)) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| error!("out of bounds memory access")) + } + + fn write_fixed(&self, offset: usize, bytes: [u8; N]) -> Result<(), Error> { + self.get_wasmtime_memory() + .write(self.store.context_mut()?, offset, &bytes) + .map_err(|e| error!("{}", e)) + } } impl From<&Memory<'_>> for Extern { @@ -284,6 +427,18 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { class.define_method("read", method!(Memory::read, 2))?; class.define_method("read_utf8", method!(Memory::read_utf8, 2))?; class.define_method("write", method!(Memory::write, 2))?; + class.define_method("read_i32", method!(Memory::read_i32, 1))?; + class.define_method("read_u32", method!(Memory::read_u32, 1))?; + class.define_method("read_i64", method!(Memory::read_i64, 1))?; + class.define_method("read_u64", method!(Memory::read_u64, 1))?; + class.define_method("read_f32", method!(Memory::read_f32, 1))?; + class.define_method("read_f64", method!(Memory::read_f64, 1))?; + class.define_method("write_i32", method!(Memory::write_i32, 2))?; + class.define_method("write_u32", method!(Memory::write_u32, 2))?; + class.define_method("write_i64", method!(Memory::write_i64, 2))?; + class.define_method("write_u64", method!(Memory::write_u64, 2))?; + class.define_method("write_f32", method!(Memory::write_f32, 2))?; + class.define_method("write_f64", method!(Memory::write_f64, 2))?; class.define_method("grow", method!(Memory::grow, 1))?; class.define_method("size", method!(Memory::size, 0))?; class.define_method("data_size", method!(Memory::data_size, 0))?; diff --git a/spec/unit/func_spec.rb b/spec/unit/func_spec.rb index 54165374..269a3d06 100644 --- a/spec/unit/func_spec.rb +++ b/spec/unit/func_spec.rb @@ -157,6 +157,7 @@ module Wasmtime message = "Caller outlived its Func execution" expect { caller.export("f1_export") }.to raise_error(Wasmtime::Error, message) expect { mem.read(0, 3) }.to raise_error(Wasmtime::Error, message) + expect { mem.read_i32(0) }.to raise_error(Wasmtime::Error, message) expect { f1_export.call }.to raise_error(Wasmtime::Error, message) end end diff --git a/spec/unit/memory_spec.rb b/spec/unit/memory_spec.rb index 0530cdf7..bffde274 100644 --- a/spec/unit/memory_spec.rb +++ b/spec/unit/memory_spec.rb @@ -77,6 +77,126 @@ module Wasmtime end end + describe "#read_i32, #write_i32" do + it "round-trips a signed 32-bit integer" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_i32(0, -42)).to be_nil + expect(mem.read_i32(0)).to eq(-42) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_i32(mem.data_size - 3) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_i32(mem.data_size - 3, -42) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + + describe "#read_u32, #write_u32" do + it "round-trips an unsigned 32-bit integer" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_u32(0, (2**32) - 1)).to be_nil + expect(mem.read_u32(0)).to eq((2**32) - 1) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_u32(mem.data_size - 3) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_u32(mem.data_size - 3, (2**32) - 1) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + + describe "#read_i64, #write_i64" do + it "round-trips a signed 64-bit integer" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_i64(0, -1)).to be_nil + expect(mem.read_i64(0)).to eq(-1) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_i64(mem.data_size - 7) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_i64(mem.data_size - 7, -1) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + + describe "#read_u64, #write_u64" do + it "round-trips an unsigned 64-bit integer" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_u64(0, (2**64) - 1)).to be_nil + expect(mem.read_u64(0)).to eq((2**64) - 1) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_u64(mem.data_size - 7) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_u64(mem.data_size - 7, (2**64) - 1) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + + describe "#read_f32, #write_f32" do + it "round-trips a 32-bit float" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_f32(0, 5.5)).to be_nil + expect(mem.read_f32(0)).to eq(5.5) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_f32(mem.data_size - 3) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_f32(mem.data_size - 3, 5.5) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + + describe "#read_f64, #write_f64" do + it "round-trips a 64-bit float" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_f64(0, -5.5)).to be_nil + expect(mem.read_f64(0)).to eq(-5.5) + end + + it "raises when reading past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.read_f64(mem.data_size - 7) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_f64(mem.data_size - 7, -5.5) } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + end + describe "#read_utf8" do it "reads a UTF-8 string" do mem = Memory.new(store, min_size: 1)