diff --git a/ext/src/ruby_api/convert.rs b/ext/src/ruby_api/convert.rs index 7d4f577b..36795dea 100644 --- a/ext/src/ruby_api/convert.rs +++ b/ext/src/ruby_api/convert.rs @@ -1,9 +1,17 @@ use crate::{define_rb_intern, err, error, helpers::SymbolEnum}; use lazy_static::lazy_static; -use magnus::{prelude::*, Error, IntoValue, RArray, Ruby, Symbol, TryConvert, TypedData, Value}; +use magnus::{ + prelude::*, try_convert, Error, IntoValue, RArray, Ruby, Symbol, TryConvert, TypedData, Value, +}; use wasmtime::{ExternRef, RefType, Val, ValType}; -use super::{func::Func, global::Global, memory::Memory, store::StoreContextValue, table::Table}; +use super::{ + func::{Func, FuncType}, + global::{Global, GlobalType}, + memory::{Memory, MemoryType}, + store::StoreContextValue, + table::{Table, TableType}, +}; define_rb_intern!( I32 => "i32", @@ -206,3 +214,10 @@ where { fn wrap_wasmtime_type(&self, store: StoreContextValue<'a>) -> Result; } + +pub trait WrapWasmtimeExternType +where + T: TypedData, +{ + fn wrap_wasmtime_type(&self) -> Result; +} diff --git a/ext/src/ruby_api/externals.rs b/ext/src/ruby_api/externals.rs index e3c4416c..46e38c47 100644 --- a/ext/src/ruby_api/externals.rs +++ b/ext/src/ruby_api/externals.rs @@ -1,6 +1,11 @@ use super::{ - convert::WrapWasmtimeType, func::Func, global::Global, memory::Memory, root, - store::StoreContextValue, table::Table, + convert::{WrapWasmtimeExternType, WrapWasmtimeType}, + func::{Func, FuncType}, + global::{Global, GlobalType}, + memory::{Memory, MemoryType}, + root, + store::StoreContextValue, + table::{Table, TableType}, }; use crate::{conversion_err, not_implemented}; use magnus::{ @@ -8,6 +13,86 @@ use magnus::{ Error, Module, RClass, Ruby, TypedData, Value, }; +#[derive(TypedData)] +#[magnus( + class = "Wasmtime::ExternType", + size, + mark, + free_immediately, + unsafe_generics +)] +pub enum ExternType { + Func(Obj), + Global(Obj), + Memory(Obj), + Table(Obj), +} + +impl DataTypeFunctions for ExternType { + fn mark(&self, marker: &Marker) { + match self { + ExternType::Func(f) => marker.mark(*f), + ExternType::Global(g) => marker.mark(*g), + ExternType::Memory(m) => marker.mark(*m), + ExternType::Table(t) => marker.mark(*t), + } + } +} +unsafe impl Send for ExternType {} + +impl ExternType { + /// @yard + /// Returns the exported function's FuncType or raises a `{ConversionError}` when the export is not a + /// function. + /// @return [FuncType] The exported function's type. + pub fn to_func_type(ruby: &Ruby, rb_self: Obj) -> Result { + match *rb_self { + ExternType::Func(f) => Ok(f.as_value()), + _ => conversion_err!(Self::inner_class(rb_self), Func::class(ruby)), + } + } + + /// @yard + /// Returns the exported global's GlobalType or raises a `{ConversionError}` when the export is not a global. + /// @return [GlobalType] The exported global's type. + pub fn to_global_type(ruby: &Ruby, rb_self: Obj) -> Result { + match *rb_self { + ExternType::Global(g) => Ok(g.as_value()), + _ => conversion_err!(Self::inner_class(rb_self), Global::class(ruby)), + } + } + + /// @yard + /// Returns the exported memory's MemoryType or raises a `{ConversionError}` when the export is not a + /// memory. + /// @return [MemoryType] The exported memory's type. + pub fn to_memory_type(ruby: &Ruby, rb_self: Obj) -> Result { + match *rb_self { + ExternType::Memory(m) => Ok(m.as_value()), + _ => conversion_err!(Self::inner_class(rb_self), Memory::class(ruby)), + } + } + + /// @yard + /// Returns the exported table's TableType or raises a `{ConversionError}` when the export is not a table. + /// @return [TableType] The exported table's type. + pub fn to_table_type(ruby: &Ruby, rb_self: Obj) -> Result { + match *rb_self { + ExternType::Table(t) => Ok(t.as_value()), + _ => conversion_err!(Self::inner_class(rb_self), Table::class(ruby)), + } + } + + fn inner_class(rb_self: Obj) -> RClass { + match *rb_self { + ExternType::Func(f) => f.class(), + ExternType::Global(g) => g.class(), + ExternType::Memory(m) => m.class(), + ExternType::Table(t) => t.class(), + } + } +} + /// @yard /// @rename Wasmtime::Extern /// An external item to a WebAssembly module, or a list of what can possibly be exported from a Wasm module. @@ -127,7 +212,32 @@ impl<'a> WrapWasmtimeType<'a, Extern<'a>> for wasmtime::Extern { } } +impl WrapWasmtimeExternType for wasmtime::ExternType { + fn wrap_wasmtime_type(&self) -> Result { + match self { + wasmtime::ExternType::Func(ft) => Ok(ExternType::Func(Obj::wrap( + FuncType::from_inner(ft.clone()), + ))), + wasmtime::ExternType::Global(gt) => Ok(ExternType::Global(Obj::wrap( + GlobalType::from_inner(gt.clone()), + ))), + wasmtime::ExternType::Memory(mt) => Ok(ExternType::Memory(Obj::wrap( + MemoryType::from_inner(mt.clone()), + ))), + wasmtime::ExternType::Table(tt) => Ok(ExternType::Table(Obj::wrap( + TableType::from_inner(tt.clone()), + ))), + } + } +} + pub fn init() -> Result<(), Error> { + let extern_type = root().define_class("ExternType", class::object())?; + extern_type.define_method("to_func_type", method!(ExternType::to_func_type, 0))?; + extern_type.define_method("to_global_type", method!(ExternType::to_global_type, 0))?; + extern_type.define_method("to_memory_type", method!(ExternType::to_memory_type, 0))?; + extern_type.define_method("to_table_type", method!(ExternType::to_table_type, 0))?; + let class = root().define_class("Extern", class::object())?; class.define_method("to_func", method!(Extern::to_func, 0))?; diff --git a/ext/src/ruby_api/func.rs b/ext/src/ruby_api/func.rs index b82d4eb9..eb7637c3 100644 --- a/ext/src/ruby_api/func.rs +++ b/ext/src/ruby_api/func.rs @@ -1,5 +1,6 @@ use super::{ convert::{ToRubyValue, ToSym, ToValTypeVec, ToWasmVal}, + engine, errors::result_error, params::Params, root, @@ -13,6 +14,58 @@ use magnus::{ }; use wasmtime::{Caller as CallerImpl, Func as FuncImpl, Val}; +/// @yard +/// @rename Wasmtime::FuncType +/// Represents a WebAssembly Function Type +/// @see https://docs.rs/wasmtime/latest/wasmtime/struct.FuncType.html Wasmtime's Rust doc +#[derive(TypedData)] +#[magnus( + class = "Wasmtime::FuncType", + size, + mark, + free_immediately, + unsafe_generics +)] +pub struct FuncType { + inner: wasmtime::FuncType, +} + +impl DataTypeFunctions for FuncType {} + +impl FuncType { + pub fn from_inner(inner: wasmtime::FuncType) -> Self { + Self { inner } + } + + /// @yard + /// @return [Array] The function's parameter types. + pub fn params(&self) -> Result { + let len = self.inner.params().len(); + let mut params = self.inner.params(); + params.try_fold(RArray::with_capacity(len), |array, p| { + array.push(p.to_sym()?)?; + Ok(array) + }) + } + + /// @yard + /// @return [Array] The function's result types. + pub fn results(&self) -> Result { + let len = self.inner.results().len(); + let mut results = self.inner.results(); + results.try_fold(RArray::with_capacity(len), |array, r| { + array.push(r.to_sym()?)?; + Ok(array) + }) + } +} + +impl From<&FuncType> for wasmtime::ExternType { + fn from(func: &FuncType) -> Self { + Self::Func(func.inner.clone()) + } +} + /// @yard /// @rename Wasmtime::Func /// Represents a WebAssembly Function @@ -287,6 +340,10 @@ pub fn make_func_closure( } pub fn init() -> Result<(), Error> { + let func_type = root().define_class("FuncType", class::object())?; + func_type.define_method("params", method!(FuncType::params, 0))?; + func_type.define_method("results", method!(FuncType::results, 0))?; + let func = root().define_class("Func", class::object())?; func.define_singleton_method("new", function!(Func::new, -1))?; func.define_method("call", method!(Func::call, -1))?; diff --git a/ext/src/ruby_api/global.rs b/ext/src/ruby_api/global.rs index d5f7cd09..6dac81c2 100644 --- a/ext/src/ruby_api/global.rs +++ b/ext/src/ruby_api/global.rs @@ -8,7 +8,55 @@ use magnus::{ class, function, gc::Marker, method, prelude::*, typed_data::Obj, DataTypeFunctions, Error, Object, Symbol, TypedData, Value, }; -use wasmtime::{Extern, Global as GlobalImpl, GlobalType, Mutability}; +use wasmtime::{Extern, Global as GlobalImpl, Mutability}; + +#[derive(TypedData)] +#[magnus( + class = "Wasmtime::GlobalType", + free_immediately, + mark, + unsafe_generics +)] +pub struct GlobalType { + inner: wasmtime::GlobalType, +} + +impl DataTypeFunctions for GlobalType { + fn mark(&self, _marker: &Marker) {} +} + +impl GlobalType { + pub fn from_inner(inner: wasmtime::GlobalType) -> Self { + Self { inner } + } + + /// @yard + /// @def const? + /// @return [Boolean] + pub fn is_const(&self) -> bool { + self.inner.mutability() == Mutability::Const + } + + /// @yard + /// @def var? + /// @return [Boolean] + pub fn is_var(&self) -> bool { + self.inner.mutability() == Mutability::Var + } + + /// @yard + /// @def type + /// @return [Symbol] The Wasm type of the global‘s content. + pub fn type_(&self) -> Result { + self.inner.content().to_sym() + } +} + +impl From<&GlobalType> for wasmtime::ExternType { + fn from(global_type: &GlobalType) -> Self { + Self::Global(global_type.inner.clone()) + } +} /// @yard /// @rename Wasmtime::Global @@ -58,7 +106,7 @@ impl<'a> Global<'a> { let wasm_default = default.to_wasm_val(&store.into(), wasm_type.clone())?; let inner = GlobalImpl::new( store.context_mut(), - GlobalType::new(wasm_type, mutability), + wasmtime::GlobalType::new(wasm_type, mutability), wasm_default, ) .map_err(|e| error!("{}", e))?; @@ -124,7 +172,7 @@ impl<'a> Global<'a> { }) } - fn ty(&self) -> Result { + fn ty(&self) -> Result { Ok(self.inner.ty(self.store.context()?)) } @@ -151,6 +199,11 @@ impl From<&Global<'_>> for Extern { } pub fn init() -> Result<(), Error> { + let type_class = root().define_class("GlobalType", class::object())?; + type_class.define_method("const?", method!(GlobalType::is_const, 0))?; + type_class.define_method("var?", method!(GlobalType::is_var, 0))?; + type_class.define_method("type", method!(GlobalType::type_, 0))?; + let class = root().define_class("Global", class::object())?; class.define_singleton_method("var", function!(Global::var, 3))?; class.define_singleton_method("const", function!(Global::const_, 3))?; diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index dc0a101e..b92d06c6 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -21,6 +21,45 @@ define_rb_intern!( MAX_SIZE => "max_size", ); +#[derive(TypedData)] +#[magnus( + class = "Wasmtime::MemoryType", + free_immediately, + mark, + unsafe_generics +)] +pub struct MemoryType { + inner: wasmtime::MemoryType, +} + +impl DataTypeFunctions for MemoryType { + fn mark(&self, _marker: &Marker) {} +} + +impl MemoryType { + pub fn from_inner(inner: wasmtime::MemoryType) -> Self { + Self { inner } + } + + /// @yard + /// @return [Integer] The minimum number of memory pages. + pub fn min_size(&self) -> u64 { + self.inner.minimum() + } + + /// @yard + /// @return [Integer, nil] The maximum number of memory pages. + pub fn max_size(&self) -> Option { + self.inner.maximum() + } +} + +impl From<&MemoryType> for wasmtime::ExternType { + fn from(memory_type: &MemoryType) -> Self { + Self::Memory(memory_type.inner.clone()) + } +} + /// @yard /// @rename Wasmtime::Memory /// Represents a WebAssembly memory. @@ -224,6 +263,10 @@ impl From<&Memory<'_>> for Extern { } pub fn init(ruby: &Ruby) -> Result<(), Error> { + let type_class = root().define_class("MemoryType", class::object())?; + type_class.define_method("min_size", method!(MemoryType::min_size, 0))?; + type_class.define_method("max_size", method!(MemoryType::max_size, 0))?; + let class = root().define_class("Memory", class::object())?; class.define_singleton_method("new", function!(Memory::new, -1))?; class.define_method("min_size", method!(Memory::min_size, 0))?; diff --git a/ext/src/ruby_api/module.rs b/ext/src/ruby_api/module.rs index ea1575ad..44be02fd 100644 --- a/ext/src/ruby_api/module.rs +++ b/ext/src/ruby_api/module.rs @@ -4,16 +4,23 @@ use std::{ os::raw::c_void, }; -use super::{engine::Engine, root}; +use super::{ + convert::{WrapWasmtimeExternType, WrapWasmtimeType}, + engine::Engine, + root, +}; use crate::{ error, helpers::{nogvl, Tmplock}, }; -use magnus::{class, function, method, rb_sys::AsRawValue, Error, Module as _, Object, RString}; +use magnus::{ + class, function, method, rb_sys::AsRawValue, Error, Module as _, Object, RArray, RHash, + RString, Ruby, +}; use rb_sys::{ rb_str_locktmp, rb_str_unlocktmp, tracking_allocator::ManuallyTracked, RSTRING_LEN, RSTRING_PTR, }; -use wasmtime::Module as ModuleImpl; +use wasmtime::{ImportType, Module as ModuleImpl}; /// @yard /// Represents a WebAssembly module. @@ -106,6 +113,24 @@ impl Module { pub fn get(&self) -> &ModuleImpl { &self.inner } + + /// @yard + /// Returns the list of imports that this Module has and must be satisfied. + /// @return [Array] An array of hashes containing import information + pub fn imports(&self) -> Result { + let module = self.get(); + let imports = module.imports(); + + let result = RArray::with_capacity(imports.len()); + for import in imports { + let hash = RHash::new(); + hash.aset("module", import.module())?; + hash.aset("name", import.name())?; + hash.aset("type", import.ty().wrap_wasmtime_type()?)?; + result.push(hash)?; + } + Ok(result) + } } impl From for Module { @@ -137,6 +162,7 @@ pub fn init() -> Result<(), Error> { class.define_singleton_method("deserialize", function!(Module::deserialize, 2))?; class.define_singleton_method("deserialize_file", function!(Module::deserialize_file, 2))?; class.define_method("serialize", method!(Module::serialize, 0))?; + class.define_method("imports", method!(Module::imports, 0))?; Ok(()) } diff --git a/ext/src/ruby_api/table.rs b/ext/src/ruby_api/table.rs index 8ddaa9c5..7a0f8526 100644 --- a/ext/src/ruby_api/table.rs +++ b/ext/src/ruby_api/table.rs @@ -11,13 +11,54 @@ use magnus::{ class, function, gc::Marker, method, prelude::*, scan_args, typed_data::Obj, DataTypeFunctions, Error, IntoValue, Object, Symbol, TypedData, Value, }; -use wasmtime::{Extern, Table as TableImpl, TableType, Val}; +use wasmtime::{Extern, Table as TableImpl, Val}; define_rb_intern!( MIN_SIZE => "min_size", MAX_SIZE => "max_size", ); +#[derive(TypedData)] +#[magnus(class = "Wasmtime::TableType", free_immediately, mark, unsafe_generics)] +pub struct TableType { + inner: wasmtime::TableType, +} + +impl DataTypeFunctions for TableType { + fn mark(&self, _marker: &Marker) {} +} + +impl TableType { + pub fn from_inner(inner: wasmtime::TableType) -> Self { + Self { inner } + } + + /// @yard + /// @def type + /// @return [Symbol] The Wasm type of the elements of this table. + pub fn type_(&self) -> Result { + self.inner.element().to_sym() + } + + /// @yard + /// @return [Integer] The minimum size of this table. + pub fn min_size(&self) -> u32 { + self.inner.minimum() + } + + /// @yard + /// @return [Integer, nil] The maximum size of this table. + pub fn max_size(&self) -> Option { + self.inner.maximum() + } +} + +impl From<&TableType> for wasmtime::ExternType { + fn from(table_type: &TableType) -> Self { + Self::Table(table_type.inner.clone()) + } +} + /// @yard /// @rename Wasmtime::Table /// Represents a WebAssembly table. @@ -66,7 +107,7 @@ impl<'a> Table<'a> { let inner = TableImpl::new( store.context_mut(), - TableType::new(table_type, min, max), + wasmtime::TableType::new(table_type, min, max), ref_, ) .map_err(|e| error!("{}", e))?; @@ -172,7 +213,7 @@ impl<'a> Table<'a> { Ok(self.inner.size(self.store.context()?)) } - fn ty(&self) -> Result { + fn ty(&self) -> Result { Ok(self.inner.ty(self.store.context()?)) } @@ -202,6 +243,11 @@ impl From<&Table<'_>> for Extern { } pub fn init() -> Result<(), Error> { + let type_class = root().define_class("TableType", class::object())?; + type_class.define_method("type", method!(TableType::type_, 0))?; + type_class.define_method("min_size", method!(TableType::min_size, 0))?; + type_class.define_method("max_size", method!(TableType::max_size, 0))?; + let class = root().define_class("Table", class::object())?; class.define_singleton_method("new", function!(Table::new, -1))?; diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 57068e1d..5d1490c9 100644 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -86,5 +86,75 @@ def create_tmpfile(content) expect(mod).to be_a(Wasmtime::Module) end end + + describe "#imports" do + cases = { + f: [:to_func_type, FuncType, {params: [:i32], results: [:i32]}], + m: [:to_memory_type, MemoryType, {min_size: 1, max_size: nil}], + t: [:to_table_type, TableType, {type: :funcref, min_size: 1, max_size: nil}], + g: [:to_global_type, GlobalType, {const?: false, var?: true, type: :i32}] + } + + cases.each do |name, (meth, klass, calls)| + describe "##{meth}" do + it "returns a type that is an instance of #{klass}" do + import = get_import_by_name(name) + expect(import["type"].public_send(meth)).to be_instance_of(klass) + end + + it "raises an error when extern type is not a #{klass}" do + import = get_import_by_name(name) + invalid_methods = cases.values.map(&:first) - [meth] + + invalid_methods.each do |invalid_method| + expect { import["type"].public_send(invalid_method) }.to raise_error(Wasmtime::ConversionError) + end + end + + it "has a type that responds to the expected methods for #{klass}" do + import = get_import_by_name(name) + extern_type = import["type"].public_send(meth) + + calls.each do |(meth_name, expected_return)| + expect(extern_type.public_send(meth_name)).to eq(expected_return) + end + end + end + end + + it "has a module name" do + mod_with_imports = new_import_module + imports = mod_with_imports.imports + + imports.each do |import| + expect(import["module"]).to eq("env") + end + end + + it "returns an empty array for a module with no imports" do + mod = Module.new(engine, "(module)") + + expect(mod.imports).to be_an(Array) + expect(mod.imports).to be_empty + end + + def get_import_by_name(name) + mod_with_imports = new_import_module + imports = mod_with_imports.imports + imports.find { _1["name"] == name.to_s } + end + + def new_import_module + Module.new(engine, <<~WAT + (module + (import "env" "f" (func (param i32) (result i32))) + (import "env" "m" (memory 1)) + (import "env" "t" (table 1 funcref)) + (global (import "env" "g") (mut i32)) + ) + WAT + ) + end + end end end