From 47c351276164710a1ecc64435e11ec6b80cc4eef Mon Sep 17 00:00:00 2001 From: william-stacken Date: Tue, 28 Apr 2026 17:48:13 +0200 Subject: [PATCH 1/4] Component host functions --- Cargo.toml | 1 + ext/src/ruby_api/component.rs | 2 + ext/src/ruby_api/component/convert.rs | 248 +++++- ext/src/ruby_api/component/func.rs | 4 +- ext/src/ruby_api/component/linker.rs | 381 ++++++++- ext/src/ruby_api/component/types.rs | 720 ++++++++++++++++++ .../host-func-imports/.cargo/config.toml | 2 + spec/fixtures/host-func-imports/.gitignore | 2 + spec/fixtures/host-func-imports/Cargo.toml | 22 + spec/fixtures/host-func-imports/README.md | 15 + spec/fixtures/host-func-imports/src/lib.rs | 42 + spec/fixtures/host-func-imports/wit/world.wit | 34 + spec/fixtures/host_func_imports.wasm | Bin 0 -> 12774 bytes spec/unit/component/linker_spec.rb | 434 +++++++++++ 14 files changed, 1891 insertions(+), 16 deletions(-) create mode 100644 ext/src/ruby_api/component/types.rs create mode 100644 spec/fixtures/host-func-imports/.cargo/config.toml create mode 100644 spec/fixtures/host-func-imports/.gitignore create mode 100644 spec/fixtures/host-func-imports/Cargo.toml create mode 100644 spec/fixtures/host-func-imports/README.md create mode 100644 spec/fixtures/host-func-imports/src/lib.rs create mode 100644 spec/fixtures/host-func-imports/wit/world.wit create mode 100644 spec/fixtures/host_func_imports.wasm diff --git a/Cargo.toml b/Cargo.toml index b4e32295..983d28b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ exclude = [ "spec/fixtures/component-types", "spec/fixtures/wasi-debug", "spec/fixtures/wasi-deterministic", + "spec/fixtures/host-func-imports", ] [profile.release] diff --git a/ext/src/ruby_api/component.rs b/ext/src/ruby_api/component.rs index 1ceb68ff..a4dc72d5 100644 --- a/ext/src/ruby_api/component.rs +++ b/ext/src/ruby_api/component.rs @@ -2,6 +2,7 @@ mod convert; mod func; mod instance; mod linker; +mod types; mod wasi_command; use super::root; @@ -169,6 +170,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { linker::init(ruby, &namespace)?; instance::init(ruby, &namespace)?; func::init(ruby, &namespace)?; + types::init(ruby, &namespace)?; convert::init(ruby)?; wasi_command::init(ruby, &namespace)?; diff --git a/ext/src/ruby_api/component/convert.rs b/ext/src/ruby_api/component/convert.rs index 5ecd7984..589115dd 100644 --- a/ext/src/ruby_api/component/convert.rs +++ b/ext/src/ruby_api/component/convert.rs @@ -9,6 +9,8 @@ use magnus::{ }; use wasmtime::component::{Type, Val}; +use super::types::ComponentType; + define_rb_intern!( // For Component::Result OK => "ok", @@ -25,7 +27,7 @@ define_rb_intern!( pub(crate) fn component_val_to_rb( ruby: &Ruby, val: Val, - _store: &StoreContextValue, + _store: Option<&StoreContextValue>, ) -> Result { match val { Val::Bool(bool) => Ok(bool.into_value_with(ruby)), @@ -305,6 +307,250 @@ fn variant_class(ruby: &Ruby) -> RClass { ruby.get_inner(&VARIANT_CLASS) } +/// Validate a Ruby value against a ComponentType and convert to Val +/// This is used for host functions where we define types standalone +pub(super) fn validate_and_convert( + value: Value, + _store: Option<&StoreContextValue>, + ty: &ComponentType, +) -> Result { + let ruby = Ruby::get_with(value); + match ty { + ComponentType::Bool => { + if value.as_raw() == ruby.qtrue().as_raw() { + Ok(Val::Bool(true)) + } else if value.as_raw() == ruby.qfalse().as_raw() { + Ok(Val::Bool(false)) + } else { + Err(Error::new( + ruby.exception_type_error(), + format!("expected bool, got {}", unsafe { value.classname() }), + )) + } + } + ComponentType::S8 => i8::try_convert(value) + .map(Val::S8) + .map_err(|_| error!("expected s8, got {}", value.inspect())), + ComponentType::U8 => u8::try_convert(value) + .map(Val::U8) + .map_err(|_| error!("expected u8, got {}", value.inspect())), + ComponentType::S16 => i16::try_convert(value) + .map(Val::S16) + .map_err(|_| error!("expected s16, got {}", value.inspect())), + ComponentType::U16 => u16::try_convert(value) + .map(Val::U16) + .map_err(|_| error!("expected u16, got {}", value.inspect())), + ComponentType::S32 => i32::try_convert(value) + .map(Val::S32) + .map_err(|_| error!("expected s32, got {}", value.inspect())), + ComponentType::U32 => u32::try_convert(value) + .map(Val::U32) + .map_err(|_| error!("expected u32, got {}", value.inspect())), + ComponentType::S64 => i64::try_convert(value) + .map(Val::S64) + .map_err(|_| error!("expected s64, got {}", value.inspect())), + ComponentType::U64 => u64::try_convert(value) + .map(Val::U64) + .map_err(|_| error!("expected u64, got {}", value.inspect())), + ComponentType::Float32 => f32::try_convert(value) + .map(Val::Float32) + .map_err(|_| error!("expected float32, got {}", value.inspect())), + ComponentType::Float64 => f64::try_convert(value) + .map(Val::Float64) + .map_err(|_| error!("expected float64, got {}", value.inspect())), + ComponentType::Char => value + .to_r_string() + .and_then(|s| s.to_char()) + .map(Val::Char) + .map_err(|_| error!("expected char, got {}", value.inspect())), + ComponentType::String => RString::try_convert(value) + .and_then(|s| s.to_string()) + .map(Val::String) + .map_err(|_| error!("expected string, got {}", value.inspect())), + ComponentType::List(element_ty) => { + let rarray = RArray::try_convert(value) + .map_err(|_| error!("expected list (array), got {}", value.inspect()))?; + + let mut vals: Vec = Vec::with_capacity(rarray.len()); + for (i, item_value) in unsafe { rarray.as_slice() }.iter().enumerate() { + let component_val = validate_and_convert(*item_value, _store, element_ty) + .map_err(|e| e.append(format!(" (list item at index {i})")))?; + vals.push(component_val); + } + Ok(Val::List(vals)) + } + ComponentType::Record(fields) => { + let hash = RHash::try_convert(value) + .map_err(|_| error!("expected record (hash), got {}", value.inspect()))?; + + let mut kv = Vec::with_capacity(fields.len()); + for field in fields { + let field_value = hash + .get(field.name.as_str()) + .ok_or_else(|| error!("record field missing: {}", field.name)) + .and_then(|v| { + validate_and_convert(v, _store, &field.ty) + .map_err(|e| e.append(format!(" (record field \"{}\")", field.name))) + })?; + + kv.push((field.name.clone(), field_value)) + } + Ok(Val::Record(kv)) + } + ComponentType::Tuple(types) => { + let rarray = RArray::try_convert(value) + .map_err(|_| error!("expected tuple (array), got {}", value.inspect()))?; + + if types.len() != rarray.len() { + return Err(error!( + "expected tuple with {} elements, got {}", + types.len(), + rarray.len() + )); + } + + let mut vals: Vec = Vec::with_capacity(rarray.len()); + for (i, (ty, item_value)) in types + .iter() + .zip(unsafe { rarray.as_slice() }.iter()) + .enumerate() + { + let component_val = validate_and_convert(*item_value, _store, ty) + .map_err(|e| e.append(format!(" (tuple element at index {i})")))?; + vals.push(component_val); + } + + Ok(Val::Tuple(vals)) + } + ComponentType::Variant(cases) => { + let name: RString = value + .funcall(NAME.into_id_with(&ruby), ()) + .map_err(|_| error!("expected variant, got {}", value.inspect()))?; + let name = name.to_string()?; + + let case = cases + .iter() + .find(|c| c.name == name.as_str()) + .ok_or_else(|| { + error!( + "invalid variant case \"{}\", valid cases: [{}]", + name, + cases + .iter() + .map(|c| format!("\"{}\"", c.name)) + .collect::>() + .join(", ") + ) + })?; + + let payload_rb: Value = value.funcall(VALUE.into_id_with(&ruby), ())?; + let payload_val = match (&case.ty, payload_rb.is_nil()) { + (Some(ty), _) => validate_and_convert(payload_rb, _store, ty) + .map(|val| Some(Box::new(val))) + .map_err(|e| e.append(format!(" (variant value for \"{}\")", &name))), + + (None, true) => Ok(None), + + (None, false) => err!( + "expected no value for variant case \"{}\", got {}", + &name, + payload_rb.inspect() + ), + }?; + + Ok(Val::Variant(name, payload_val)) + } + ComponentType::Enum(cases) => { + let rstring = RString::try_convert(value) + .map_err(|_| error!("expected enum (string), got {}", value.inspect()))?; + let case_name = rstring.to_string()?; + + if !cases.contains(&case_name) { + return Err(error!( + "invalid enum case \"{}\", valid cases: [{}]", + case_name, + cases + .iter() + .map(|c| format!("\"{}\"", c)) + .collect::>() + .join(", ") + )); + } + + Ok(Val::Enum(case_name)) + } + ComponentType::Option(inner_ty) => { + if value.is_nil() { + Ok(Val::Option(None)) + } else { + validate_and_convert(value, _store, inner_ty) + .map(|v| Val::Option(Some(Box::new(v)))) + } + } + ComponentType::Result { ok, err } => { + let is_ok = value + .funcall::<_, (), bool>(IS_OK.into_id_with(&ruby), ()) + .map_err(|_| error!("expected result, got {}", value.inspect()))?; + + if is_ok { + let ok_value = value.funcall::<_, (), Value>(OK.into_id_with(&ruby), ())?; + match ok { + Some(ty) => validate_and_convert(ok_value, _store, ty) + .map(|val| Val::Result(Result::Ok(Some(Box::new(val))))), + None => { + if ok_value.is_nil() { + Ok(Val::Result(Ok(None))) + } else { + err!( + "expected nil for result<_, E> ok branch, got {}", + ok_value.inspect() + ) + } + } + } + } else { + let err_value = value.funcall::<_, (), Value>(ERROR.into_id_with(&ruby), ())?; + match err { + Some(ty) => validate_and_convert(err_value, _store, ty) + .map(|val| Val::Result(Result::Err(Some(Box::new(val))))), + None => { + if err_value.is_nil() { + Ok(Val::Result(Err(None))) + } else { + err!( + "expected nil for result error branch, got {}", + err_value.inspect() + ) + } + } + } + } + } + ComponentType::Flags(flag_names) => { + let flags_vec = Vec::::try_convert(value).map_err(|_| { + error!("expected flags (array of strings), got {}", value.inspect()) + })?; + + // Validate that all flags are valid + for flag in &flags_vec { + if !flag_names.contains(flag) { + return Err(error!( + "invalid flag \"{}\", valid flags: [{}]", + flag, + flag_names + .iter() + .map(|f| format!("\"{}\"", f)) + .collect::>() + .join(", ") + )); + } + } + + Ok(Val::Flags(flags_vec)) + } + } +} + pub fn init(ruby: &Ruby) -> Result<(), Error> { // Warm up let _ = result_class(ruby); diff --git a/ext/src/ruby_api/component/func.rs b/ext/src/ruby_api/component/func.rs index 8b9ee3e1..437f391b 100644 --- a/ext/src/ruby_api/component/func.rs +++ b/ext/src/ruby_api/component/func.rs @@ -115,12 +115,12 @@ impl Func { 1 => component_val_to_rb( ruby, results.into_iter().next().unwrap(), - &store_context_value, + Some(&store_context_value), ), _ => { let ary = ruby.ary_new_capa(results_ty.len()); for result in results { - let val = component_val_to_rb(ruby, result, &store_context_value)?; + let val = component_val_to_rb(ruby, result, Some(&store_context_value))?; ary.push(val)?; } Ok(ary.into_value_with(ruby)) diff --git a/ext/src/ruby_api/component/linker.rs b/ext/src/ruby_api/component/linker.rs index b0fc9a36..fdf9afa8 100644 --- a/ext/src/ruby_api/component/linker.rs +++ b/ext/src/ruby_api/component/linker.rs @@ -1,3 +1,5 @@ +use super::convert; +use super::types::{self, ComponentType}; use super::{Component, Instance}; use crate::{ err, @@ -10,16 +12,34 @@ use crate::{ use std::{ borrow::BorrowMut, cell::{RefCell, RefMut}, + collections::HashMap, }; use crate::error; use magnus::{ - class, function, gc::Marker, method, r_string::RString, scan_args, typed_data::Obj, - DataTypeFunctions, Error, Module as _, Object, RModule, Ruby, TryConvert, TypedData, Value, + block::Proc, + class, function, + gc::Marker, + method, + r_string::RString, + scan_args::scan_args, + typed_data::Obj, + value::{Opaque, ReprValue}, + DataTypeFunctions, Error, Module as _, Object, RArray, RModule, Ruby, TryConvert, TypedData, + Value, }; -use wasmtime::component::{Linker as LinkerImpl, LinkerInstance as LinkerInstanceImpl}; +use wasmtime::component::{Linker as LinkerImpl, LinkerInstance as LinkerInstanceImpl, Val}; use wasmtime_wasi::{ResourceTable, WasiCtx}; +/// Stores type information for a registered host function +#[derive(Clone, Debug)] +struct FuncTypeInfo { + #[allow(dead_code)] + path: String, // e.g., "" for root, "math" for nested instance + param_types: Vec, + result_types: Vec, +} + /// @yard /// @rename Wasmtime::Component::Linker /// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.Linker.html Wasmtime's Rust doc @@ -29,6 +49,7 @@ pub struct Linker { inner: RefCell>, refs: RefCell>, has_wasi: RefCell, + func_types: RefCell>, } unsafe impl Send for Linker {} @@ -50,6 +71,7 @@ impl Linker { inner: RefCell::new(linker), refs: RefCell::new(Vec::new()), has_wasi: RefCell::new(false), + func_types: RefCell::new(HashMap::new()), }) } @@ -72,7 +94,11 @@ impl Linker { let Ok(mut inner) = rb_self.inner.try_borrow_mut() else { return err!("Linker is not reentrant"); }; - let instance = ruby.obj_wrap(LinkerInstance::from_inner(inner.root())); + let instance = ruby.obj_wrap(LinkerInstance::from_inner( + inner.root(), + String::new(), // root path is empty + rb_self.as_value(), + )); let block_result: Result = ruby.yield_value(instance); instance.take_inner(); @@ -93,11 +119,14 @@ impl Linker { /// @return [Linker] +self+ pub fn instance(ruby: &Ruby, rb_self: Obj, name: RString) -> Result, Error> { let mut inner = rb_self.inner.borrow_mut(); - let instance = inner - .instance(unsafe { name.as_str() }?) - .map_err(|e| error!("{}", e))?; + let name_str = unsafe { name.as_str() }?; + let instance = inner.instance(name_str).map_err(|e| error!("{}", e))?; - let instance = ruby.obj_wrap(LinkerInstance::from_inner(instance)); + let instance = ruby.obj_wrap(LinkerInstance::from_inner( + instance, + name_str.to_string(), + rb_self.as_value(), + )); let block_result: Result = ruby.yield_value(instance); @@ -125,6 +154,8 @@ impl Linker { return err!("{}", errors::missing_wasi_ctx_error("linker.instantiate")); } + Self::validate_host_function_signatures(&rb_self, component)?; + let inner = rb_self.inner.borrow(); inner .instantiate(store.context_mut(), component.get()) @@ -140,6 +171,141 @@ impl Linker { .map_err(|e| error!("{}", e)) } + /// Validate that host functions defined via func_new match the component's expected import signatures + fn validate_host_function_signatures( + rb_self: &Obj, + component: &Component, + ) -> Result<(), Error> { + // Get component type information + let component_ty = component.get().component_type(); + let func_types = rb_self.func_types.borrow(); + let inner = rb_self.inner.borrow(); + let engine = inner.engine(); + + // Helper to validate a single import + fn validate_import( + path: &str, + name: &str, + expected_func: &wasmtime::component::types::ComponentFunc, + func_types: &HashMap, + ) -> Result<(), String> { + let full_key = if path.is_empty() { + name.to_string() + } else { + format!("{}/{}", path, name) + }; + + // Check if this function was defined in the linker + if let Some(declared_info) = func_types.get(&full_key) { + // Extract expected types from component + let (expected_params, expected_results) = types::extract_func_types(expected_func) + .map_err(|e| format!("failed to extract function types: {}", e))?; + + // Validate parameter count + if declared_info.param_types.len() != expected_params.len() { + return Err(format!( + "host function \"{}\" parameter count mismatch: declared has {} parameters, expected has {}", + full_key, + declared_info.param_types.len(), + expected_params.len() + )); + } + + // Validate each parameter type + for (i, (declared_ty, expected_ty)) in declared_info + .param_types + .iter() + .zip(expected_params.iter()) + .enumerate() + { + if let Err(e) = types::types_match(declared_ty, expected_ty) { + return Err(format!( + "host function \"{}\" parameter {} has incompatible type: {}", + full_key, i, e + )); + } + } + + // Validate result count + if declared_info.result_types.len() != expected_results.len() { + return Err(format!( + "host function \"{}\" result count mismatch: declared has {} results, expected has {}", + full_key, + declared_info.result_types.len(), + expected_results.len() + )); + } + + // Validate each result type + for (i, (declared_ty, expected_ty)) in declared_info + .result_types + .iter() + .zip(expected_results.iter()) + .enumerate() + { + if let Err(e) = types::types_match(declared_ty, expected_ty) { + return Err(format!( + "host function \"{}\" result {} has incompatible type: {}", + full_key, i, e + )); + } + } + } + + Ok(()) + } + + // Helper to recursively validate imports + fn validate_imports_recursive( + path: &str, + item: &wasmtime::component::types::ComponentItem, + func_types: &HashMap, + engine: &wasmtime::Engine, + ) -> Result<(), String> { + use wasmtime::component::types::ComponentItem; + + match item { + ComponentItem::ComponentFunc(func) => { + // Extract just the function name from the full path + let name = if let Some(idx) = path.rfind('/') { + &path[idx + 1..] + } else { + path + }; + let parent_path = if let Some(idx) = path.rfind('/') { + &path[..idx] + } else { + "" + }; + validate_import(parent_path, name, func, func_types)?; + } + ComponentItem::ComponentInstance(instance) => { + // Recursively validate nested exports + for (export_name, export_item) in instance.exports(engine) { + let nested_path = if path.is_empty() { + export_name.to_string() + } else { + format!("{}/{}", path, export_name) + }; + validate_imports_recursive(&nested_path, &export_item, func_types, engine)?; + } + } + _ => { + // Other types (Module, CoreFunc, Type, etc.) are not validated here + } + } + Ok(()) + } + + // Validate all imports + for (import_name, import_item) in component_ty.imports(engine) { + validate_imports_recursive(import_name, &import_item, &func_types, engine) + .map_err(|e| error!("{}", e))?; + } + + Ok(()) + } + pub(crate) fn add_wasi_p2(&self) -> Result<(), Error> { *self.has_wasi.borrow_mut() = true; let mut inner = self.inner.borrow_mut(); @@ -164,6 +330,8 @@ impl Linker { pub struct LinkerInstance<'a> { inner: RefCell>, refs: RefCell>, + path: String, // namespace path, e.g., "" for root, "math" for nested + parent_linker: Value, // Reference to parent Linker object for accessing func_types } unsafe impl Send for LinkerInstance<'_> {} @@ -171,6 +339,7 @@ unsafe impl Send for LinkerInstance<'_> {} impl DataTypeFunctions for LinkerInstance<'_> { fn mark(&self, marker: &Marker) { marker.mark_slice(self.refs.borrow().as_slice()); + marker.mark(self.parent_linker); } } @@ -193,10 +362,16 @@ impl<'a> MaybeInstanceImpl<'a> { } impl<'a> LinkerInstance<'a> { - fn from_inner(inner: LinkerInstanceImpl<'a, StoreData>) -> Self { + fn from_inner( + inner: LinkerInstanceImpl<'a, StoreData>, + path: String, + parent_linker: Value, + ) -> Self { Self { inner: RefCell::new(MaybeInstanceImpl::new(inner)), refs: RefCell::new(Vec::new()), + path, + parent_linker, } } @@ -230,12 +405,22 @@ impl<'a> LinkerInstance<'a> { return err!("LinkerInstance is not reentrant"); }; + let name_str = unsafe { name.as_str() }?; let inner = maybe_instance.get_mut()?; - let nested_inner = inner - .instance(unsafe { name.as_str()? }) - .map_err(|e| error!("{}", e))?; + let nested_inner = inner.instance(name_str).map_err(|e| error!("{}", e))?; + + // Build nested path: if parent is "", use name; otherwise "parent/name" + let nested_path = if rb_self.path.is_empty() { + name_str.to_string() + } else { + format!("{}/{}", rb_self.path, name_str) + }; - let nested_instance = ruby.obj_wrap(LinkerInstance::from_inner(nested_inner)); + let nested_instance = ruby.obj_wrap(LinkerInstance::from_inner( + nested_inner, + nested_path, + rb_self.parent_linker, + )); let block_result: Result = ruby.yield_value(nested_instance); nested_instance.take_inner(); @@ -245,6 +430,73 @@ impl<'a> LinkerInstance<'a> { } } + /// @yard + /// Define a host function in this linker instance. + /// + /// @def func_new(name, params, results, &block) + /// @param name [String] The function name + /// @param params [Array] The function parameter types + /// @param results [Array] The function result types + /// @yield [caller, *args] The block implementing the host function + /// @yieldparam caller [Caller] The caller context (not yet fully implemented) + /// @yieldparam args [Array] The function arguments, converted from component values + /// @yieldreturn [Object, Array] The function result(s), will be validated and converted + /// @return [LinkerInstance] +self+ + fn func_new(_ruby: &Ruby, rb_self: Obj, args: &[Value]) -> Result, Error> { + let args = scan_args::<(RString, RArray, RArray), (), (), (), (), Proc>(args)?; + let (name, params_array, results_array) = args.required; + let callable = args.block; + + // Extract ComponentType from Type values + let mut param_types = Vec::with_capacity(params_array.len()); + for param_value in unsafe { params_array.as_slice() } { + param_types.push(types::extract_component_type(*param_value)?); + } + + let mut result_types = Vec::with_capacity(results_array.len()); + for result_value in unsafe { results_array.as_slice() } { + result_types.push(types::extract_component_type(*result_value)?); + } + + let name_str = unsafe { name.as_str() }?; + + // Store type metadata for later validation + let full_key = if rb_self.path.is_empty() { + name_str.to_string() + } else { + format!("{}/{}", rb_self.path, name_str) + }; + + // Get parent Linker - we'll store the callable there to prevent GC + // (rb_self is ephemeral and won't keep references alive after the block ends) + let parent_linker: Obj = Obj::try_convert(rb_self.parent_linker)?; + + parent_linker.refs.borrow_mut().push(callable.as_value()); + parent_linker.func_types.borrow_mut().insert( + full_key, + FuncTypeInfo { + path: rb_self.path.clone(), + param_types: param_types.clone(), + result_types: result_types.clone(), + }, + ); + + // Create the closure that will be called from Wasm + let func_closure = + make_component_func_closure(param_types.clone(), result_types.clone(), callable.into()); + + let Ok(mut maybe_instance) = rb_self.inner.try_borrow_mut() else { + return err!("LinkerInstance is not reentrant"); + }; + + let inner = maybe_instance.get_mut()?; + inner + .func_new(name_str, func_closure) + .map_err(|e| error!("failed to define host function: {}", e))?; + + Ok(rb_self) + } + fn take_inner(&self) { let Ok(mut maybe_instance) = self.inner.try_borrow_mut() else { panic!("Linker instance is already borrowed, can't expire.") @@ -254,6 +506,108 @@ impl<'a> LinkerInstance<'a> { } } +/// Create a closure that wraps a Ruby Proc for use as a component host function +fn make_component_func_closure( + param_types: Vec, + result_types: Vec, + callable: Opaque, +) -> impl Fn( + wasmtime::StoreContextMut<'_, StoreData>, + wasmtime::component::types::ComponentFunc, + &[Val], + &mut [Val], +) -> wasmtime::Result<()> + + Send + + Sync + + 'static { + move |mut store_context: wasmtime::StoreContextMut<'_, StoreData>, + _func: wasmtime::component::types::ComponentFunc, + params: &[Val], + results: &mut [Val]| { + let ruby = Ruby::get().unwrap(); + + // Convert Wasm params to Ruby values + let rparams = ruby.ary_new_capa(params.len()); + for (i, (param, _param_ty)) in params.iter().zip(param_types.iter()).enumerate() { + let rb_value = + convert::component_val_to_rb(&ruby, param.clone(), None).map_err(|e| { + wasmtime::Error::msg(format!("failed to convert parameter at index {i}: {e}")) + })?; + rparams.push(rb_value).map_err(|e| { + wasmtime::Error::msg(format!("failed to push parameter at index {i}: {e}")) + })?; + } + + // Call the Ruby Proc + let callable = ruby.get_inner(callable); + let proc_result = callable.call::<_, Value>(rparams).map_err(|e| { + // Store the Ruby error on StoreData so it can be properly raised later + store_context.data_mut().set_error(e); + // Return a generic error that will be replaced with the Ruby error + wasmtime::Error::msg("") + })?; + + // Handle result conversion based on arity + match result_types.len() { + 0 => { + // No return value expected + Ok(()) + } + 1 => { + // Single return value - accept either the value directly or in an array + let result_value = if let Ok(result_array) = RArray::to_ary(proc_result) { + if result_array.len() != 1 { + return Err(wasmtime::Error::msg(format!( + "expected 1 result, got {}", + result_array.len() + ))); + } + unsafe { result_array.as_slice()[0] } + } else { + proc_result + }; + + let converted = convert::validate_and_convert(result_value, None, &result_types[0]) + .map_err(|e| { + // Store type errors on StoreData as well + store_context.data_mut().set_error(e); + wasmtime::Error::msg("") + })?; + results[0] = converted; + Ok(()) + } + n => { + // Multiple return values - expect an array + let result_array = RArray::to_ary(proc_result) + .map_err(|_| wasmtime::Error::msg("expected array of results"))?; + + if result_array.len() != n { + return Err(wasmtime::Error::msg(format!( + "expected {} results, got {}", + n, + result_array.len() + ))); + } + + for (i, (result_value, result_ty)) in unsafe { result_array.as_slice() } + .iter() + .zip(result_types.iter()) + .enumerate() + { + let converted = convert::validate_and_convert(*result_value, None, result_ty) + .map_err(|e| { + // Store type errors on StoreData as well + store_context.data_mut().set_error(e); + wasmtime::Error::msg("") + })?; + results[i] = converted; + } + Ok(()) + } + } + } +} + pub fn init(ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { let linker = namespace.define_class("Linker", ruby.class_object())?; linker.define_singleton_method("new", function!(Linker::new, 1))?; @@ -264,6 +618,7 @@ pub fn init(ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { let linker_instance = namespace.define_class("LinkerInstance", ruby.class_object())?; linker_instance.define_method("module", method!(LinkerInstance::module, 2))?; linker_instance.define_method("instance", method!(LinkerInstance::instance, 1))?; + linker_instance.define_method("func_new", method!(LinkerInstance::func_new, -1))?; Ok(()) } diff --git a/ext/src/ruby_api/component/types.rs b/ext/src/ruby_api/component/types.rs new file mode 100644 index 00000000..c6ffe99c --- /dev/null +++ b/ext/src/ruby_api/component/types.rs @@ -0,0 +1,720 @@ +use crate::error; +use magnus::{ + class, function, prelude::*, r_hash::ForEach, Error, Module as _, RArray, RHash, RString, Ruby, + Symbol, TryConvert, TypedData, Value, +}; +use std::fmt; + +/// Standalone component type system that can be constructed independently +/// of a component instance. Used for defining host function signatures. +#[derive(Clone, Debug)] +pub enum ComponentType { + Bool, + S8, + U8, + S16, + U16, + S32, + U32, + S64, + U64, + Float32, + Float64, + Char, + String, + List(Box), + Record(Vec), + Tuple(Vec), + Variant(Vec), + Enum(Vec), + Option(Box), + Result { + ok: Option>, + err: Option>, + }, + Flags(Vec), +} + +#[derive(Clone, Debug)] +pub struct RecordField { + pub name: String, + pub ty: ComponentType, +} + +#[derive(Clone, Debug)] +pub struct VariantCase { + pub name: String, + pub ty: Option, +} + +impl fmt::Display for ComponentType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ComponentType::Bool => write!(f, "bool"), + ComponentType::S8 => write!(f, "s8"), + ComponentType::U8 => write!(f, "u8"), + ComponentType::S16 => write!(f, "s16"), + ComponentType::U16 => write!(f, "u16"), + ComponentType::S32 => write!(f, "s32"), + ComponentType::U32 => write!(f, "u32"), + ComponentType::S64 => write!(f, "s64"), + ComponentType::U64 => write!(f, "u64"), + ComponentType::Float32 => write!(f, "float32"), + ComponentType::Float64 => write!(f, "float64"), + ComponentType::Char => write!(f, "char"), + ComponentType::String => write!(f, "string"), + ComponentType::List(inner) => write!(f, "list<{}>", inner), + ComponentType::Record(fields) => { + write!(f, "record {{")?; + for (i, field) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", field.name, field.ty)?; + } + write!(f, "}}") + } + ComponentType::Tuple(types) => { + write!(f, "tuple<")?; + for (i, ty) in types.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", ty)?; + } + write!(f, ">") + } + ComponentType::Variant(cases) => { + write!(f, "variant {{")?; + for (i, case) in cases.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", case.name)?; + if let Some(ty) = &case.ty { + write!(f, "({})", ty)?; + } + } + write!(f, "}}") + } + ComponentType::Enum(cases) => { + write!(f, "enum {{")?; + for (i, case) in cases.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", case)?; + } + write!(f, "}}") + } + ComponentType::Option(inner) => write!(f, "option<{}>", inner), + ComponentType::Result { ok, err } => { + write!(f, "result<")?; + if let Some(ok) = ok { + write!(f, "{}", ok)?; + } else { + write!(f, "_")?; + } + write!(f, ", ")?; + if let Some(err) = err { + write!(f, "{}", err)?; + } else { + write!(f, "_")?; + } + write!(f, ">") + } + ComponentType::Flags(flags) => { + write!(f, "flags {{")?; + for (i, flag) in flags.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", flag)?; + } + write!(f, "}}") + } + } + } +} + +/// @yard +/// @rename Wasmtime::Component::Type +/// Ruby wrapper for ComponentType - stored as opaque Rust data +/// Factory methods for creating component types +/// @see https://docs.wasmtime.dev/api/wasmtime/component/enum.Val.html +/// +/// @!method self.bool +/// @return [Type] A boolean type +/// @!method self.s8 +/// @return [Type] A signed 8-bit integer type +/// @!method self.u8 +/// @return [Type] An unsigned 8-bit integer type +/// @!method self.s16 +/// @return [Type] A signed 16-bit integer type +/// @!method self.u16 +/// @return [Type] An unsigned 16-bit integer type +/// @!method self.s32 +/// @return [Type] A signed 32-bit integer type +/// @!method self.u32 +/// @return [Type] An unsigned 32-bit integer type +/// @!method self.s64 +/// @return [Type] A signed 64-bit integer type +/// @!method self.u64 +/// @return [Type] An unsigned 64-bit integer type +/// @!method self.float32 +/// @return [Type] A 32-bit floating point type +/// @!method self.float64 +/// @return [Type] A 64-bit floating point type +/// @!method self.char +/// @return [Type] A Unicode character type +/// @!method self.string +/// @return [Type] A UTF-8 string type +/// @!method self.list(element_type) +/// @param element_type [Type] The type of list elements +/// @return [Type] A list type +/// @!method self.record(fields) +/// @param fields [Hash] A hash of field names to types +/// @return [Type] A record (struct) type +/// @!method self.tuple(types) +/// @param types [Array] The types in the tuple +/// @return [Type] A tuple type +/// @!method self.variant(cases) +/// @param cases [Hash] A hash of case names to optional types +/// @return [Type] A variant type +/// @!method self.enum(cases) +/// @param cases [Array] The enum case names +/// @return [Type] An enum type +/// @!method self.option(inner_type) +/// @param inner_type [Type] The type of the optional value +/// @return [Type] An option type +/// @!method self.result(ok_type, err_type) +/// @param ok_type [Type, nil] The type of the ok variant (nil for result<_, E>) +/// @param err_type [Type, nil] The type of the error variant (nil for result) +/// @return [Type] A result type +/// @!method self.flags(flag_names) +/// @param flag_names [Array] The flag names +/// @return [Type] A flags type +#[derive(Clone, TypedData)] +#[magnus(class = "Wasmtime::Component::Type", free_immediately)] +pub struct RbComponentType { + inner: ComponentType, +} + +impl magnus::DataTypeFunctions for RbComponentType {} + +impl RbComponentType { + pub fn new(inner: ComponentType) -> Self { + Self { inner } + } +} + +pub struct TypeFactory; + +impl TypeFactory { + pub fn bool(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::Bool) + } + + pub fn s8(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::S8) + } + + pub fn u8(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::U8) + } + + pub fn s16(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::S16) + } + + pub fn u16(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::U16) + } + + pub fn s32(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::S32) + } + + pub fn u32(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::U32) + } + + pub fn s64(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::S64) + } + + pub fn u64(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::U64) + } + + pub fn float32(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::Float32) + } + + pub fn float64(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::Float64) + } + + pub fn char(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::Char) + } + + pub fn string(_ruby: &Ruby) -> RbComponentType { + RbComponentType::new(ComponentType::String) + } + + pub fn list(_ruby: &Ruby, element_type: &RbComponentType) -> RbComponentType { + RbComponentType::new(ComponentType::List(Box::new(element_type.inner.clone()))) + } + + pub fn record(_ruby: &Ruby, fields: RHash) -> Result { + let mut record_fields = Vec::new(); + + // Use foreach to iterate over hash + fields.foreach(|key: Value, ty_value: Value| { + let name = RString::try_convert(key)?.to_string()?; + let ty_ref: &RbComponentType = TryConvert::try_convert(ty_value)?; + record_fields.push(RecordField { + name, + ty: ty_ref.inner.clone(), + }); + Ok(ForEach::Continue) + })?; + + Ok(RbComponentType::new(ComponentType::Record(record_fields))) + } + + pub fn tuple(_ruby: &Ruby, types: RArray) -> Result { + let mut tuple_types = Vec::with_capacity(types.len()); + + for ty_value in unsafe { types.as_slice() } { + let ty_ref: &RbComponentType = TryConvert::try_convert(*ty_value)?; + tuple_types.push(ty_ref.inner.clone()); + } + + Ok(RbComponentType::new(ComponentType::Tuple(tuple_types))) + } + + pub fn variant(_ruby: &Ruby, cases: RHash) -> Result { + let mut variant_cases = Vec::new(); + + // Use foreach to iterate over hash + cases.foreach(|key: Value, ty_value: Value| { + let name = RString::try_convert(key)?.to_string()?; + let ty = if ty_value.is_nil() { + None + } else { + let ty_ref: &RbComponentType = TryConvert::try_convert(ty_value)?; + Some(ty_ref.inner.clone()) + }; + variant_cases.push(VariantCase { name, ty }); + Ok(ForEach::Continue) + })?; + + Ok(RbComponentType::new(ComponentType::Variant(variant_cases))) + } + + pub fn enum_type(_ruby: &Ruby, cases: RArray) -> Result { + let mut enum_cases = Vec::with_capacity(cases.len()); + + for case_value in unsafe { cases.as_slice() } { + let case_name = RString::try_convert(*case_value)?.to_string()?; + enum_cases.push(case_name); + } + + Ok(RbComponentType::new(ComponentType::Enum(enum_cases))) + } + + pub fn option(_ruby: &Ruby, inner_type: &RbComponentType) -> RbComponentType { + RbComponentType::new(ComponentType::Option(Box::new(inner_type.inner.clone()))) + } + + pub fn result( + _ruby: &Ruby, + ok_type: Option<&RbComponentType>, + err_type: Option<&RbComponentType>, + ) -> RbComponentType { + RbComponentType::new(ComponentType::Result { + ok: ok_type.map(|t| Box::new(t.inner.clone())), + err: err_type.map(|t| Box::new(t.inner.clone())), + }) + } + + pub fn flags(_ruby: &Ruby, flag_names: RArray) -> Result { + let mut flags = Vec::with_capacity(flag_names.len()); + + for flag_value in unsafe { flag_names.as_slice() } { + let flag_name = RString::try_convert(*flag_value)?.to_string()?; + flags.push(flag_name); + } + + Ok(RbComponentType::new(ComponentType::Flags(flags))) + } +} + +pub fn init(ruby: &Ruby, namespace: &magnus::RModule) -> Result<(), Error> { + let type_class = namespace.define_class("Type", ruby.class_object())?; + + // Factory methods + type_class.define_singleton_method("bool", function!(TypeFactory::bool, 0))?; + type_class.define_singleton_method("s8", function!(TypeFactory::s8, 0))?; + type_class.define_singleton_method("u8", function!(TypeFactory::u8, 0))?; + type_class.define_singleton_method("s16", function!(TypeFactory::s16, 0))?; + type_class.define_singleton_method("u16", function!(TypeFactory::u16, 0))?; + type_class.define_singleton_method("s32", function!(TypeFactory::s32, 0))?; + type_class.define_singleton_method("u32", function!(TypeFactory::u32, 0))?; + type_class.define_singleton_method("s64", function!(TypeFactory::s64, 0))?; + type_class.define_singleton_method("u64", function!(TypeFactory::u64, 0))?; + type_class.define_singleton_method("float32", function!(TypeFactory::float32, 0))?; + type_class.define_singleton_method("float64", function!(TypeFactory::float64, 0))?; + type_class.define_singleton_method("char", function!(TypeFactory::char, 0))?; + type_class.define_singleton_method("string", function!(TypeFactory::string, 0))?; + type_class.define_singleton_method("list", function!(TypeFactory::list, 1))?; + type_class.define_singleton_method("record", function!(TypeFactory::record, 1))?; + type_class.define_singleton_method("tuple", function!(TypeFactory::tuple, 1))?; + type_class.define_singleton_method("variant", function!(TypeFactory::variant, 1))?; + type_class.define_singleton_method("enum", function!(TypeFactory::enum_type, 1))?; + type_class.define_singleton_method("option", function!(TypeFactory::option, 1))?; + type_class.define_singleton_method("result", function!(TypeFactory::result, 2))?; + type_class.define_singleton_method("flags", function!(TypeFactory::flags, 1))?; + + Ok(()) +} + +// Make ComponentType accessible from other component modules +pub(super) fn extract_component_type(value: Value) -> Result { + let rb_ty: &RbComponentType = TryConvert::try_convert(value)?; + Ok(rb_ty.inner.clone()) +} + +/// Convert wasmtime's component Type to our ComponentType +/// This is used for validating host function signatures against component imports +pub(super) fn wasmtime_type_to_component_type( + ty: &wasmtime::component::Type, +) -> Result { + use wasmtime::component::types::ComponentItem; + + match ty { + wasmtime::component::Type::Bool => Ok(ComponentType::Bool), + wasmtime::component::Type::S8 => Ok(ComponentType::S8), + wasmtime::component::Type::U8 => Ok(ComponentType::U8), + wasmtime::component::Type::S16 => Ok(ComponentType::S16), + wasmtime::component::Type::U16 => Ok(ComponentType::U16), + wasmtime::component::Type::S32 => Ok(ComponentType::S32), + wasmtime::component::Type::U32 => Ok(ComponentType::U32), + wasmtime::component::Type::S64 => Ok(ComponentType::S64), + wasmtime::component::Type::U64 => Ok(ComponentType::U64), + wasmtime::component::Type::Float32 => Ok(ComponentType::Float32), + wasmtime::component::Type::Float64 => Ok(ComponentType::Float64), + wasmtime::component::Type::Char => Ok(ComponentType::Char), + wasmtime::component::Type::String => Ok(ComponentType::String), + wasmtime::component::Type::List(inner) => { + let inner_ty = wasmtime_type_to_component_type(&inner.ty())?; + Ok(ComponentType::List(Box::new(inner_ty))) + } + wasmtime::component::Type::Record(record) => { + let mut fields = Vec::new(); + for field in record.fields() { + let field_ty = wasmtime_type_to_component_type(&field.ty)?; + fields.push(RecordField { + name: field.name.to_string(), + ty: field_ty, + }); + } + Ok(ComponentType::Record(fields)) + } + wasmtime::component::Type::Tuple(tuple) => { + let mut types = Vec::new(); + for ty in tuple.types() { + types.push(wasmtime_type_to_component_type(&ty)?); + } + Ok(ComponentType::Tuple(types)) + } + wasmtime::component::Type::Variant(variant) => { + let mut cases = Vec::new(); + for case in variant.cases() { + let case_ty = case + .ty + .as_ref() + .map(wasmtime_type_to_component_type) + .transpose()?; + cases.push(VariantCase { + name: case.name.to_string(), + ty: case_ty, + }); + } + Ok(ComponentType::Variant(cases)) + } + wasmtime::component::Type::Enum(enum_ty) => { + let cases: Vec = enum_ty.names().map(|s| s.to_string()).collect(); + Ok(ComponentType::Enum(cases)) + } + wasmtime::component::Type::Option(opt) => { + let inner_ty = wasmtime_type_to_component_type(&opt.ty())?; + Ok(ComponentType::Option(Box::new(inner_ty))) + } + wasmtime::component::Type::Result(result) => { + let ok = result + .ok() + .map(|ty| wasmtime_type_to_component_type(&ty).map(Box::new)) + .transpose()?; + let err = result + .err() + .map(|ty| wasmtime_type_to_component_type(&ty).map(Box::new)) + .transpose()?; + Ok(ComponentType::Result { ok, err }) + } + wasmtime::component::Type::Flags(flags) => { + let names: Vec = flags.names().map(|s| s.to_string()).collect(); + Ok(ComponentType::Flags(names)) + } + wasmtime::component::Type::Own(_) | wasmtime::component::Type::Borrow(_) => Err( + "resource types (own/borrow) are not yet supported for host function validation" + .to_string(), + ), + wasmtime::component::Type::Map(_) => { + Err("map types are not yet supported for host function validation".to_string()) + } + wasmtime::component::Type::Future(_) => { + Err("future types are not yet supported for host function validation".to_string()) + } + wasmtime::component::Type::Stream(_) => { + Err("stream types are not yet supported for host function validation".to_string()) + } + wasmtime::component::Type::ErrorContext => Err( + "error-context types are not yet supported for host function validation".to_string(), + ), + } +} + +/// Extract function parameter and result types from a ComponentFunc +pub(super) fn extract_func_types( + func: &wasmtime::component::types::ComponentFunc, +) -> Result<(Vec, Vec), String> { + let mut param_types = Vec::new(); + for (_name, ty) in func.params() { + param_types.push(wasmtime_type_to_component_type(&ty)?); + } + + let mut result_types = Vec::new(); + // Results is an iterator of Type directly (not tuples) + for ty in func.results() { + result_types.push(wasmtime_type_to_component_type(&ty)?); + } + + Ok((param_types, result_types)) +} + +/// Compare two ComponentTypes for compatibility +/// Returns Ok(()) if types match, Err with descriptive message if they don't +pub(super) fn types_match( + declared: &ComponentType, + expected: &ComponentType, +) -> Result<(), String> { + match (declared, expected) { + // Primitive types must match exactly + (ComponentType::Bool, ComponentType::Bool) + | (ComponentType::S8, ComponentType::S8) + | (ComponentType::U8, ComponentType::U8) + | (ComponentType::S16, ComponentType::S16) + | (ComponentType::U16, ComponentType::U16) + | (ComponentType::S32, ComponentType::S32) + | (ComponentType::U32, ComponentType::U32) + | (ComponentType::S64, ComponentType::S64) + | (ComponentType::U64, ComponentType::U64) + | (ComponentType::Float32, ComponentType::Float32) + | (ComponentType::Float64, ComponentType::Float64) + | (ComponentType::Char, ComponentType::Char) + | (ComponentType::String, ComponentType::String) => Ok(()), + + // List types: element types must match + (ComponentType::List(d_inner), ComponentType::List(e_inner)) => { + types_match(d_inner, e_inner).map_err(|e| format!("list element type mismatch: {}", e)) + } + + // Option types: inner types must match + (ComponentType::Option(d_inner), ComponentType::Option(e_inner)) => { + types_match(d_inner, e_inner).map_err(|e| format!("option type mismatch: {}", e)) + } + + // Record types: must have same fields with matching types + (ComponentType::Record(d_fields), ComponentType::Record(e_fields)) => { + if d_fields.len() != e_fields.len() { + return Err(format!( + "record field count mismatch: declared has {} fields, expected has {}", + d_fields.len(), + e_fields.len() + )); + } + + for (d_field, e_field) in d_fields.iter().zip(e_fields.iter()) { + if d_field.name != e_field.name { + return Err(format!( + "record field name mismatch: declared has '{}', expected has '{}'", + d_field.name, e_field.name + )); + } + types_match(&d_field.ty, &e_field.ty) + .map_err(|e| format!("record field '{}' type mismatch: {}", d_field.name, e))?; + } + Ok(()) + } + + // Tuple types: must have same number of elements with matching types + (ComponentType::Tuple(d_types), ComponentType::Tuple(e_types)) => { + if d_types.len() != e_types.len() { + return Err(format!( + "tuple length mismatch: declared has {} elements, expected has {}", + d_types.len(), + e_types.len() + )); + } + + for (i, (d_ty, e_ty)) in d_types.iter().zip(e_types.iter()).enumerate() { + types_match(d_ty, e_ty) + .map_err(|e| format!("tuple element {} mismatch: {}", i, e))?; + } + Ok(()) + } + + // Variant types: must have same cases with matching types + (ComponentType::Variant(d_cases), ComponentType::Variant(e_cases)) => { + if d_cases.len() != e_cases.len() { + return Err(format!( + "variant case count mismatch: declared has {} cases, expected has {}", + d_cases.len(), + e_cases.len() + )); + } + + for (d_case, e_case) in d_cases.iter().zip(e_cases.iter()) { + if d_case.name != e_case.name { + return Err(format!( + "variant case name mismatch: declared has '{}', expected has '{}'", + d_case.name, e_case.name + )); + } + + match (&d_case.ty, &e_case.ty) { + (Some(d_ty), Some(e_ty)) => { + types_match(d_ty, e_ty).map_err(|e| { + format!("variant case '{}' type mismatch: {}", d_case.name, e) + })?; + } + (None, None) => {} + (Some(_), None) => { + return Err(format!( + "variant case '{}': declared has payload, expected has none", + d_case.name + )); + } + (None, Some(_)) => { + return Err(format!( + "variant case '{}': declared has no payload, expected has payload", + d_case.name + )); + } + } + } + Ok(()) + } + + // Enum types: must have same cases in same order + (ComponentType::Enum(d_cases), ComponentType::Enum(e_cases)) => { + if d_cases.len() != e_cases.len() { + return Err(format!( + "enum case count mismatch: declared has {} cases, expected has {}", + d_cases.len(), + e_cases.len() + )); + } + + for (d_case, e_case) in d_cases.iter().zip(e_cases.iter()) { + if d_case != e_case { + return Err(format!( + "enum case mismatch: declared has '{}', expected has '{}'", + d_case, e_case + )); + } + } + Ok(()) + } + + // Result types: ok and err types must match + ( + ComponentType::Result { + ok: d_ok, + err: d_err, + }, + ComponentType::Result { + ok: e_ok, + err: e_err, + }, + ) => { + match (d_ok, e_ok) { + (Some(d_ty), Some(e_ty)) => { + types_match(d_ty, e_ty) + .map_err(|e| format!("result ok type mismatch: {}", e))?; + } + (None, None) => {} + (Some(_), None) => { + return Err( + "result ok type mismatch: declared has ok type, expected has none" + .to_string(), + ); + } + (None, Some(_)) => { + return Err( + "result ok type mismatch: declared has no ok type, expected has ok type" + .to_string(), + ); + } + } + + match (d_err, e_err) { + (Some(d_ty), Some(e_ty)) => { + types_match(d_ty, e_ty) + .map_err(|e| format!("result err type mismatch: {}", e))?; + } + (None, None) => {} + (Some(_), None) => { + return Err( + "result err type mismatch: declared has err type, expected has none" + .to_string(), + ); + } + (None, Some(_)) => { + return Err( + "result err type mismatch: declared has no err type, expected has err type" + .to_string(), + ); + } + } + Ok(()) + } + + // Flags types: must have same flags in same order + (ComponentType::Flags(d_flags), ComponentType::Flags(e_flags)) => { + if d_flags.len() != e_flags.len() { + return Err(format!( + "flags count mismatch: declared has {} flags, expected has {}", + d_flags.len(), + e_flags.len() + )); + } + + for (d_flag, e_flag) in d_flags.iter().zip(e_flags.iter()) { + if d_flag != e_flag { + return Err(format!( + "flag mismatch: declared has '{}', expected has '{}'", + d_flag, e_flag + )); + } + } + Ok(()) + } + + // Type mismatch + _ => Err(format!("expected {}, got {}", expected, declared)), + } +} diff --git a/spec/fixtures/host-func-imports/.cargo/config.toml b/spec/fixtures/host-func-imports/.cargo/config.toml new file mode 100644 index 00000000..f4e8c002 --- /dev/null +++ b/spec/fixtures/host-func-imports/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/spec/fixtures/host-func-imports/.gitignore b/spec/fixtures/host-func-imports/.gitignore new file mode 100644 index 00000000..201d8caa --- /dev/null +++ b/spec/fixtures/host-func-imports/.gitignore @@ -0,0 +1,2 @@ +src/bindings.rs +Cargo.lock diff --git a/spec/fixtures/host-func-imports/Cargo.toml b/spec/fixtures/host-func-imports/Cargo.toml new file mode 100644 index 00000000..77b865b7 --- /dev/null +++ b/spec/fixtures/host-func-imports/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "host-func-imports" +version = "0.1.0" +edition = "2021" + +[dependencies] +wit-bindgen-rt = { version = "0.33.0", features = ["bitflags"] } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = false +strip = true +lto = true + +[package.metadata.component] +package = "fixtures:host-func-imports" + +[package.metadata.component.dependencies] diff --git a/spec/fixtures/host-func-imports/README.md b/spec/fixtures/host-func-imports/README.md new file mode 100644 index 00000000..ee1c0507 --- /dev/null +++ b/spec/fixtures/host-func-imports/README.md @@ -0,0 +1,15 @@ +Wasm component fixture to test host function imports via `func_new`. + +This component imports various host functions with different type signatures +and provides guest functions that call them. + +Prerequisite: `cargo install cargo-component` + +To rebuild, run the following from the wasmtime-rb's root: +``` +( + cd spec/fixtures/host-func-imports && \ + cargo component build --release && \ + cp target/wasm32-unknown-unknown/release/host_func_imports.wasm ../ +) +``` diff --git a/spec/fixtures/host-func-imports/src/lib.rs b/spec/fixtures/host-func-imports/src/lib.rs new file mode 100644 index 00000000..31685caf --- /dev/null +++ b/spec/fixtures/host-func-imports/src/lib.rs @@ -0,0 +1,42 @@ +#[allow(warnings)] +mod bindings; + +use bindings::{math, Guest, Point}; + +struct Component; + +impl Guest for Component { + fn test_greet(name: String) -> String { + bindings::greet(&name) + } + + fn test_add(a: u32, b: u32) -> u32 { + bindings::add(a, b) + } + + fn test_constant() -> u32 { + bindings::get_constant() + } + + fn test_point(x: i32, y: i32) -> Point { + bindings::make_point(x, y) + } + + fn test_sum(numbers: Vec) -> i32 { + bindings::sum_list(&numbers) + } + + fn test_maybe(n: Option) -> Option { + bindings::maybe_double(n) + } + + fn test_divide(a: u32, b: u32) -> Result { + bindings::safe_divide(a, b) + } + + fn test_multiply(a: u32, b: u32) -> u32 { + math::multiply(a, b) + } +} + +bindings::export!(Component with_types_in bindings); diff --git a/spec/fixtures/host-func-imports/wit/world.wit b/spec/fixtures/host-func-imports/wit/world.wit new file mode 100644 index 00000000..03c334b0 --- /dev/null +++ b/spec/fixtures/host-func-imports/wit/world.wit @@ -0,0 +1,34 @@ +package fixtures:host-func-imports; + +world host-imports { + // Simple primitives + import greet: func(name: string) -> string; + import add: func(a: u32, b: u32) -> u32; + import get-constant: func() -> u32; + + // Complex types + record point { + x: s32, + y: s32, + } + + import make-point: func(x: s32, y: s32) -> point; + import sum-list: func(numbers: list) -> s32; + import maybe-double: func(n: option) -> option; + import safe-divide: func(a: u32, b: u32) -> result; + + // Nested instance + import math: interface { + multiply: func(a: u32, b: u32) -> u32; + } + + // Guest functions that call the imports + export test-greet: func(name: string) -> string; + export test-add: func(a: u32, b: u32) -> u32; + export test-constant: func() -> u32; + export test-point: func(x: s32, y: s32) -> point; + export test-sum: func(numbers: list) -> s32; + export test-maybe: func(n: option) -> option; + export test-divide: func(a: u32, b: u32) -> result; + export test-multiply: func(a: u32, b: u32) -> u32; +} diff --git a/spec/fixtures/host_func_imports.wasm b/spec/fixtures/host_func_imports.wasm new file mode 100644 index 0000000000000000000000000000000000000000..34ccadafe507a64310b7c1b5d1f9558fff2b0596 GIT binary patch literal 12774 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 BU2FgV literal 0 HcmV?d00001 diff --git a/spec/unit/component/linker_spec.rb b/spec/unit/component/linker_spec.rb index 01473c2b..dc1a8800 100644 --- a/spec/unit/component/linker_spec.rb +++ b/spec/unit/component/linker_spec.rb @@ -35,6 +35,440 @@ module Component .to be_instance_of(Wasmtime::Component::Instance) end end + + describe "LinkerInstance#func_new" do + let(:t) { Type } + + context "simple host functions" do + it "defines a function with primitives" do + linker.root do |root| + root.func_new("greet", [t.string], [t.string]) do |name| + "Hello, #{name}!" + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with multiple params" do + linker.root do |root| + root.func_new("add", [t.u32, t.u32], [t.u32]) do |a, b| + a + b + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with no params" do + linker.root do |root| + root.func_new("get-constant", [], [t.u32]) do + 42 + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with no results" do + linker.root do |root| + root.func_new("log", [t.string], []) do |_msg| + nil + end + end + + expect(linker).to be_a(Linker) + end + end + + context "complex types" do + it "defines a function with record types" do + point_type = t.record("x" => t.s32, "y" => t.s32) + + linker.root do |root| + root.func_new("make-point", [t.s32, t.s32], [point_type]) do |x, y| + {"x" => x, "y" => y} + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with list types" do + linker.root do |root| + root.func_new("sum-list", [t.list(t.s32)], [t.s32]) do |numbers| + numbers.sum + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with option types" do + linker.root do |root| + root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) do |n| + n.nil? ? nil : n * 2 + end + end + + expect(linker).to be_a(Linker) + end + + it "defines a function with result types" do + linker.root do |root| + root.func_new( + "safe-divide", + [t.u32, t.u32], + [t.result(t.u32, t.string)] + ) do |a, b| + if b == 0 + Result.error("division by zero") + else + Result.ok(a / b) + end + end + end + + expect(linker).to be_a(Linker) + end + end + + context "nested instances" do + it "defines functions in nested instances" do + linker.instance("math") do |math| + math.func_new("add", [t.u32, t.u32], [t.u32]) do |a, b| + a + b + end + end + + expect(linker).to be_a(Linker) + end + end + + context "error cases" do + it "requires a block" do + expect { + linker.root do |root| + root.func_new("no-block", [], [t.u32]) + end + }.to raise_error(ArgumentError, /no block given/) + end + end + end + + describe "LinkerInstance#func_new integration" do + before(:all) do + @host_imports_component = Component.from_file( + GLOBAL_ENGINE, + "spec/fixtures/host_func_imports.wasm" + ) + end + + let(:t) { Type } + + # Helper to stub all required imports except the specified one(s) + # @param except [Symbol, Array, nil] Import(s) to skip stubbing (nil = stub all) + def stub_imports_except(linker, except: nil) + skip_funcs = Array(except).map(&:to_s).to_set + + # Stub root functions + linker.root do |root| + root.func_new("greet", [t.string], [t.string]) { |name| name } unless skip_funcs.include?("greet") + root.func_new("add", [t.u32, t.u32], [t.u32]) { |a, b| a + b } unless skip_funcs.include?("add") + root.func_new("get-constant", [], [t.u32]) { 0 } unless skip_funcs.include?("get-constant") + unless skip_funcs.include?("make-point") + root.func_new("make-point", [t.s32, t.s32], [t.record("x" => t.s32, "y" => t.s32)]) do |x, y| + {"x" => x, "y" => y} + end + end + root.func_new("sum-list", [t.list(t.s32)], [t.s32]) { |nums| nums.sum } unless skip_funcs.include?("sum-list") + root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) { |n| n } unless skip_funcs.include?("maybe-double") + unless skip_funcs.include?("safe-divide") + root.func_new("safe-divide", [t.u32, t.u32], [t.result(t.u32, t.string)]) do |a, b| + Result.ok(a) + end + end + end + + # Stub math instance unless skipped + unless skip_funcs.include?("math") + linker.instance("math") do |math| + math.func_new("multiply", [t.u32, t.u32], [t.u32]) { |a, b| a * b } + end + end + end + + context "with primitive types" do + it "provides a string function" do + stub_imports_except(linker, except: :greet) + + linker.root do |root| + root.func_new("greet", [t.string], [t.string]) do |name| + "Hello, #{name}!" + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-greet").call("World") + + expect(result).to eq("Hello, World!") + end + + it "provides a function with multiple params" do + stub_imports_except(linker, except: :add) + + linker.root do |root| + root.func_new("add", [t.u32, t.u32], [t.u32]) do |a, b| + a + b + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-add").call(42, 8) + + expect(result).to eq(50) + end + + it "provides a function with no params" do + stub_imports_except(linker, except: :"get-constant") + + linker.root do |root| + root.func_new("get-constant", [], [t.u32]) do + 1234 + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-constant").call + + expect(result).to eq(1234) + end + end + + context "with complex types" do + it "provides a function returning a record" do + stub_imports_except(linker, except: :"make-point") + + point_type = t.record("x" => t.s32, "y" => t.s32) + + linker.root do |root| + root.func_new("make-point", [t.s32, t.s32], [point_type]) do |x, y| + {"x" => x, "y" => y} + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-point").call(10, 20) + + expect(result).to eq({"x" => 10, "y" => 20}) + end + + it "provides a function accepting a list" do + stub_imports_except(linker, except: :"sum-list") + + linker.root do |root| + root.func_new("sum-list", [t.list(t.s32)], [t.s32]) do |numbers| + numbers.sum + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-sum").call([1, 2, 3, 4, 5]) + + expect(result).to eq(15) + end + + it "provides a function with option type" do + stub_imports_except(linker, except: :"maybe-double") + + linker.root do |root| + root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) do |n| + n.nil? ? nil : n * 2 + end + end + + instance = linker.instantiate(store, @host_imports_component) + + expect(instance.get_func("test-maybe").call(21)).to eq(42) + expect(instance.get_func("test-maybe").call(nil)).to be_nil + end + + it "provides a function with result type" do + stub_imports_except(linker, except: :"safe-divide") + + linker.root do |root| + root.func_new( + "safe-divide", + [t.u32, t.u32], + [t.result(t.u32, t.string)] + ) do |a, b| + if b == 0 + Result.error("division by zero") + else + Result.ok(a / b) + end + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-divide") + + expect(func.call(10, 2)).to eq(Result.ok(5)) + expect(func.call(10, 0)).to eq(Result.error("division by zero")) + end + end + + context "with nested instances" do + it "provides functions in nested instances" do + stub_imports_except(linker, except: :math) + + linker.instance("math") do |math| + math.func_new("multiply", [t.u32, t.u32], [t.u32]) do |a, b| + a * b + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-multiply").call(6, 7) + + expect(result).to eq(42) + end + end + + context "with stateful closures" do + it "maintains state across calls" do + counter = 0 + + stub_imports_except(linker, except: :"get-constant") + + linker.root do |root| + root.func_new("get-constant", [], [t.u32]) do + counter += 1 + counter + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-constant") + + expect(func.call).to eq(1) + expect(func.call).to eq(2) + expect(func.call).to eq(3) + end + + it "allows accessing Ruby objects" do + log = [] + + stub_imports_except(linker, except: :greet) + + linker.root do |root| + root.func_new("greet", [t.string], [t.string]) do |name| + log << name + "Hello, #{name}!" + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-greet") + + func.call("Alice") + func.call("Bob") + + expect(log).to eq(["Alice", "Bob"]) + end + end + + context "error handling" do + it "propagates Ruby exceptions" do + stub_imports_except(linker, except: :"get-constant") + + linker.root do |root| + root.func_new("get-constant", [], [t.u32]) do + raise "Something went wrong" + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-constant") + + expect { func.call }.to raise_error(RuntimeError, /Something went wrong/) + end + + it "validates return values match declared types" do + stub_imports_except(linker, except: :add) + + linker.root do |root| + root.func_new("add", [t.u32, t.u32], [t.u32]) do |_a, _b| + "not a number" + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-add") + + expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /expected u32, got "not a number"/) + end + + it "validates host function signatures" do + stub_imports_except(linker, except: :add) + + linker.root do |root| + root.func_new("add", [t.s32, t.u32], [t.u32]) do |a, b| + a + b + end + end + + expect { + linker.instantiate(store, @host_imports_component) + }.to raise_error(Wasmtime::Error, /host function "add" parameter 0 has incompatible type/) + end + + it "validates nested instance function signatures" do + stub_imports_except(linker, except: :math) + + linker.instance("math") do |math| + math.func_new("multiply", [t.s32, t.s32], [t.u32]) do |a, b| + a * b + end + end + + expect { + linker.instantiate(store, @host_imports_component) + }.to raise_error(Wasmtime::Error, /host function "math\/multiply" parameter 0 has incompatible type/) + end + + it "validates result type mismatches" do + stub_imports_except(linker, except: :add) + + linker.root do |root| + root.func_new("add", [t.u32, t.u32], [t.s32]) do |a, b| + a + b + end + end + + expect { + linker.instantiate(store, @host_imports_component) + }.to raise_error(Wasmtime::Error, /host function "add" result 0 has incompatible type/) + end + + it "validates complex type mismatches" do + stub_imports_except(linker, except: :"make-point") + + # Define point with wrong field types (x should be s32, not u32) + wrong_point_type = t.record("x" => t.u32, "y" => t.s32) + + linker.root do |root| + root.func_new("make-point", [t.s32, t.s32], [wrong_point_type]) do |x, y| + {"x" => x, "y" => y} + end + end + + expect { + linker.instantiate(store, @host_imports_component) + }.to raise_error(Wasmtime::Error, /host function "make-point" result 0 has incompatible type.*record field 'x' type mismatch/) + end + end + end end end end From 70ed74e2dba8cc4fae632f08817eb9e76dfdea79 Mon Sep 17 00:00:00 2001 From: william-stacken Date: Tue, 19 May 2026 12:54:59 +0200 Subject: [PATCH 2/4] Remove instantiation-time type validation --- ext/src/ruby_api/component/convert.rs | 25 +- ext/src/ruby_api/component/linker.rs | 277 +++---------- ext/src/ruby_api/component/types.rs | 392 +++--------------- spec/fixtures/host-func-imports/src/lib.rs | 12 + spec/fixtures/host-func-imports/wit/world.wit | 6 + spec/fixtures/host_func_imports.wasm | Bin 12774 -> 14466 bytes spec/unit/component/linker_spec.rb | 316 +++++++------- 7 files changed, 318 insertions(+), 710 deletions(-) diff --git a/ext/src/ruby_api/component/convert.rs b/ext/src/ruby_api/component/convert.rs index 589115dd..cb1c1dc1 100644 --- a/ext/src/ruby_api/component/convert.rs +++ b/ext/src/ruby_api/component/convert.rs @@ -9,7 +9,7 @@ use magnus::{ }; use wasmtime::component::{Type, Val}; -use super::types::ComponentType; +use super::types::{ComponentType, WrappedValue}; define_rb_intern!( // For Component::Result @@ -307,6 +307,29 @@ fn variant_class(ruby: &Ruby) -> RClass { ruby.get_inner(&VARIANT_CLASS) } +/// Extract type and value from a wrapped value and convert to Val +/// This is the primary conversion path for host function return values +pub(super) fn wrapped_to_component_val( + value: Value, + _store: Option<&StoreContextValue>, +) -> Result { + let ruby = Ruby::get_with(value); + + // Try to convert to WrappedValue + let wrapped: &WrappedValue = magnus::TryConvert::try_convert(value).map_err(|_| { + Error::new( + ruby.exception_type_error(), + format!( + "host function must return wrapped value (e.g., Type::U32.wrap(value)), got {}", + unsafe { value.classname() } + ), + ) + })?; + + // Extract the inner value and type, then validate and convert + validate_and_convert(wrapped.value(&ruby), _store, wrapped.component_type()) +} + /// Validate a Ruby value against a ComponentType and convert to Val /// This is used for host functions where we define types standalone pub(super) fn validate_and_convert( diff --git a/ext/src/ruby_api/component/linker.rs b/ext/src/ruby_api/component/linker.rs index fdf9afa8..e5672e9a 100644 --- a/ext/src/ruby_api/component/linker.rs +++ b/ext/src/ruby_api/component/linker.rs @@ -1,10 +1,10 @@ use super::convert; -use super::types::{self, ComponentType}; +use super::types; use super::{Component, Instance}; use crate::{ err, ruby_api::{ - errors, + errors::{self, ExceptionMessage}, store::{StoreContextValue, StoreData}, Engine, Module, Store, }, @@ -12,7 +12,6 @@ use crate::{ use std::{ borrow::BorrowMut, cell::{RefCell, RefMut}, - collections::HashMap, }; use crate::error; @@ -31,15 +30,6 @@ use magnus::{ use wasmtime::component::{Linker as LinkerImpl, LinkerInstance as LinkerInstanceImpl, Val}; use wasmtime_wasi::{ResourceTable, WasiCtx}; -/// Stores type information for a registered host function -#[derive(Clone, Debug)] -struct FuncTypeInfo { - #[allow(dead_code)] - path: String, // e.g., "" for root, "math" for nested instance - param_types: Vec, - result_types: Vec, -} - /// @yard /// @rename Wasmtime::Component::Linker /// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.Linker.html Wasmtime's Rust doc @@ -49,7 +39,6 @@ pub struct Linker { inner: RefCell>, refs: RefCell>, has_wasi: RefCell, - func_types: RefCell>, } unsafe impl Send for Linker {} @@ -71,7 +60,6 @@ impl Linker { inner: RefCell::new(linker), refs: RefCell::new(Vec::new()), has_wasi: RefCell::new(false), - func_types: RefCell::new(HashMap::new()), }) } @@ -154,8 +142,6 @@ impl Linker { return err!("{}", errors::missing_wasi_ctx_error("linker.instantiate")); } - Self::validate_host_function_signatures(&rb_self, component)?; - let inner = rb_self.inner.borrow(); inner .instantiate(store.context_mut(), component.get()) @@ -171,141 +157,6 @@ impl Linker { .map_err(|e| error!("{}", e)) } - /// Validate that host functions defined via func_new match the component's expected import signatures - fn validate_host_function_signatures( - rb_self: &Obj, - component: &Component, - ) -> Result<(), Error> { - // Get component type information - let component_ty = component.get().component_type(); - let func_types = rb_self.func_types.borrow(); - let inner = rb_self.inner.borrow(); - let engine = inner.engine(); - - // Helper to validate a single import - fn validate_import( - path: &str, - name: &str, - expected_func: &wasmtime::component::types::ComponentFunc, - func_types: &HashMap, - ) -> Result<(), String> { - let full_key = if path.is_empty() { - name.to_string() - } else { - format!("{}/{}", path, name) - }; - - // Check if this function was defined in the linker - if let Some(declared_info) = func_types.get(&full_key) { - // Extract expected types from component - let (expected_params, expected_results) = types::extract_func_types(expected_func) - .map_err(|e| format!("failed to extract function types: {}", e))?; - - // Validate parameter count - if declared_info.param_types.len() != expected_params.len() { - return Err(format!( - "host function \"{}\" parameter count mismatch: declared has {} parameters, expected has {}", - full_key, - declared_info.param_types.len(), - expected_params.len() - )); - } - - // Validate each parameter type - for (i, (declared_ty, expected_ty)) in declared_info - .param_types - .iter() - .zip(expected_params.iter()) - .enumerate() - { - if let Err(e) = types::types_match(declared_ty, expected_ty) { - return Err(format!( - "host function \"{}\" parameter {} has incompatible type: {}", - full_key, i, e - )); - } - } - - // Validate result count - if declared_info.result_types.len() != expected_results.len() { - return Err(format!( - "host function \"{}\" result count mismatch: declared has {} results, expected has {}", - full_key, - declared_info.result_types.len(), - expected_results.len() - )); - } - - // Validate each result type - for (i, (declared_ty, expected_ty)) in declared_info - .result_types - .iter() - .zip(expected_results.iter()) - .enumerate() - { - if let Err(e) = types::types_match(declared_ty, expected_ty) { - return Err(format!( - "host function \"{}\" result {} has incompatible type: {}", - full_key, i, e - )); - } - } - } - - Ok(()) - } - - // Helper to recursively validate imports - fn validate_imports_recursive( - path: &str, - item: &wasmtime::component::types::ComponentItem, - func_types: &HashMap, - engine: &wasmtime::Engine, - ) -> Result<(), String> { - use wasmtime::component::types::ComponentItem; - - match item { - ComponentItem::ComponentFunc(func) => { - // Extract just the function name from the full path - let name = if let Some(idx) = path.rfind('/') { - &path[idx + 1..] - } else { - path - }; - let parent_path = if let Some(idx) = path.rfind('/') { - &path[..idx] - } else { - "" - }; - validate_import(parent_path, name, func, func_types)?; - } - ComponentItem::ComponentInstance(instance) => { - // Recursively validate nested exports - for (export_name, export_item) in instance.exports(engine) { - let nested_path = if path.is_empty() { - export_name.to_string() - } else { - format!("{}/{}", path, export_name) - }; - validate_imports_recursive(&nested_path, &export_item, func_types, engine)?; - } - } - _ => { - // Other types (Module, CoreFunc, Type, etc.) are not validated here - } - } - Ok(()) - } - - // Validate all imports - for (import_name, import_item) in component_ty.imports(engine) { - validate_imports_recursive(import_name, &import_item, &func_types, engine) - .map_err(|e| error!("{}", e))?; - } - - Ok(()) - } - pub(crate) fn add_wasi_p2(&self) -> Result<(), Error> { *self.has_wasi.borrow_mut() = true; let mut inner = self.inner.borrow_mut(); @@ -433,57 +284,51 @@ impl<'a> LinkerInstance<'a> { /// @yard /// Define a host function in this linker instance. /// - /// @def func_new(name, params, results, &block) + /// Host functions must return wrapped values to specify their types. + /// Use {Type#wrap} to wrap return values with their type information. + /// + /// @example Simple scalar return + /// root.func_new("add") do |a, b| + /// Type::U32.wrap(a + b) + /// end + /// + /// @example Returning a list (the list itself, not array of results) + /// root.func_new("get-numbers") do + /// Type::List(Type::S32).wrap([1, 2, 3]) + /// end + /// + /// @example Multiple return values (array of wrapped values) + /// root.func_new("divide-with-remainder") do |a, b| + /// [Type::U32.wrap(a / b), Type::U32.wrap(a % b)] + /// end + /// + /// @example Complex types (records, results, etc.) + /// root.func_new("make-point") do |x, y| + /// point_type = Type::Record("x" => Type::S32, "y" => Type::S32) + /// point_type.wrap({"x" => x, "y" => y}) + /// end + /// + /// @def func_new(name, &block) /// @param name [String] The function name - /// @param params [Array] The function parameter types - /// @param results [Array] The function result types /// @yield [caller, *args] The block implementing the host function /// @yieldparam caller [Caller] The caller context (not yet fully implemented) /// @yieldparam args [Array] The function arguments, converted from component values - /// @yieldreturn [Object, Array] The function result(s), will be validated and converted + /// @yieldreturn [WrappedValue, Array] Wrapped result(s) with type information /// @return [LinkerInstance] +self+ fn func_new(_ruby: &Ruby, rb_self: Obj, args: &[Value]) -> Result, Error> { - let args = scan_args::<(RString, RArray, RArray), (), (), (), (), Proc>(args)?; - let (name, params_array, results_array) = args.required; + let args = scan_args::<(RString,), (), (), (), (), Proc>(args)?; + let (name,) = args.required; let callable = args.block; - // Extract ComponentType from Type values - let mut param_types = Vec::with_capacity(params_array.len()); - for param_value in unsafe { params_array.as_slice() } { - param_types.push(types::extract_component_type(*param_value)?); - } - - let mut result_types = Vec::with_capacity(results_array.len()); - for result_value in unsafe { results_array.as_slice() } { - result_types.push(types::extract_component_type(*result_value)?); - } - let name_str = unsafe { name.as_str() }?; - // Store type metadata for later validation - let full_key = if rb_self.path.is_empty() { - name_str.to_string() - } else { - format!("{}/{}", rb_self.path, name_str) - }; - // Get parent Linker - we'll store the callable there to prevent GC // (rb_self is ephemeral and won't keep references alive after the block ends) let parent_linker: Obj = Obj::try_convert(rb_self.parent_linker)?; - parent_linker.refs.borrow_mut().push(callable.as_value()); - parent_linker.func_types.borrow_mut().insert( - full_key, - FuncTypeInfo { - path: rb_self.path.clone(), - param_types: param_types.clone(), - result_types: result_types.clone(), - }, - ); // Create the closure that will be called from Wasm - let func_closure = - make_component_func_closure(param_types.clone(), result_types.clone(), callable.into()); + let func_closure = make_component_func_closure(callable.into()); let Ok(mut maybe_instance) = rb_self.inner.try_borrow_mut() else { return err!("LinkerInstance is not reentrant"); @@ -507,9 +352,8 @@ impl<'a> LinkerInstance<'a> { } /// Create a closure that wraps a Ruby Proc for use as a component host function +/// The closure expects wrapped return values (WrappedValue) that carry type information fn make_component_func_closure( - param_types: Vec, - result_types: Vec, callable: Opaque, ) -> impl Fn( wasmtime::StoreContextMut<'_, StoreData>, @@ -528,7 +372,7 @@ fn make_component_func_closure( // Convert Wasm params to Ruby values let rparams = ruby.ary_new_capa(params.len()); - for (i, (param, _param_ty)) in params.iter().zip(param_types.iter()).enumerate() { + for (i, param) in params.iter().enumerate() { let rb_value = convert::component_val_to_rb(&ruby, param.clone(), None).map_err(|e| { wasmtime::Error::msg(format!("failed to convert parameter at index {i}: {e}")) @@ -548,7 +392,8 @@ fn make_component_func_closure( })?; // Handle result conversion based on arity - match result_types.len() { + let num_results = results.len(); + match num_results { 0 => { // No return value expected Ok(()) @@ -556,19 +401,23 @@ fn make_component_func_closure( 1 => { // Single return value - accept either the value directly or in an array let result_value = if let Ok(result_array) = RArray::to_ary(proc_result) { + // User returned [wrapped_value] - unwrap the array if result_array.len() != 1 { - return Err(wasmtime::Error::msg(format!( - "expected 1 result, got {}", - result_array.len() - ))); + store_context.data_mut().set_error(Error::new( + ruby.exception_arg_error(), + format!("expected 1 result, got {}", result_array.len()), + )); + return Err(wasmtime::Error::msg("")); } unsafe { result_array.as_slice()[0] } } else { + // User returned wrapped_value directly (most common case) proc_result }; - let converted = convert::validate_and_convert(result_value, None, &result_types[0]) - .map_err(|e| { + // Extract type and value from WrappedValue, then validate and convert + let converted = + convert::wrapped_to_component_val(result_value, None).map_err(|e| { // Store type errors on StoreData as well store_context.data_mut().set_error(e); wasmtime::Error::msg("") @@ -578,28 +427,30 @@ fn make_component_func_closure( } n => { // Multiple return values - expect an array - let result_array = RArray::to_ary(proc_result) - .map_err(|_| wasmtime::Error::msg("expected array of results"))?; + let result_array = RArray::to_ary(proc_result).map_err(|_| { + store_context.data_mut().set_error(Error::new( + ruby.exception_type_error(), + "expected array of results", + )); + wasmtime::Error::msg("") + })?; if result_array.len() != n { - return Err(wasmtime::Error::msg(format!( - "expected {} results, got {}", - n, - result_array.len() - ))); + store_context.data_mut().set_error(Error::new( + ruby.exception_arg_error(), + format!("expected {} results, got {}", n, result_array.len()), + )); + return Err(wasmtime::Error::msg("")); } - for (i, (result_value, result_ty)) in unsafe { result_array.as_slice() } - .iter() - .zip(result_types.iter()) - .enumerate() - { - let converted = convert::validate_and_convert(*result_value, None, result_ty) + for (i, result_value) in unsafe { result_array.as_slice() }.iter().enumerate() { + let converted = convert::wrapped_to_component_val(*result_value, None) .map_err(|e| { - // Store type errors on StoreData as well - store_context.data_mut().set_error(e); - wasmtime::Error::msg("") - })?; + // Append index information to error + let error_with_context = e.append(format!(" (result at index {i})")); + store_context.data_mut().set_error(error_with_context); + wasmtime::Error::msg("") + })?; results[i] = converted; } Ok(()) diff --git a/ext/src/ruby_api/component/types.rs b/ext/src/ruby_api/component/types.rs index c6ffe99c..84442e5b 100644 --- a/ext/src/ruby_api/component/types.rs +++ b/ext/src/ruby_api/component/types.rs @@ -1,7 +1,7 @@ use crate::error; use magnus::{ - class, function, prelude::*, r_hash::ForEach, Error, Module as _, RArray, RHash, RString, Ruby, - Symbol, TryConvert, TypedData, Value, + class, function, gc::Marker, method, prelude::*, r_hash::ForEach, value::Opaque, Error, + Module as _, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value, }; use std::fmt; @@ -206,6 +206,52 @@ impl RbComponentType { pub fn new(inner: ComponentType) -> Self { Self { inner } } + + /// @yard + /// Wrap a Ruby value with type information for use as a host function return value. + /// @def wrap(value) + /// @param value [Object] The Ruby value to wrap + /// @return [WrappedValue] A wrapped value that can be returned from host functions + pub fn wrap(&self, value: Value) -> WrappedValue { + WrappedValue::new(value, self.inner.clone()) + } +} + +/// @yard +/// @rename Wasmtime::Component::WrappedValue +/// A Ruby value wrapped with component model type information. +/// Returned from {Type#wrap} and used as host function return values. +#[derive(Clone, TypedData)] +#[magnus(class = "Wasmtime::Component::WrappedValue", mark, free_immediately)] +pub struct WrappedValue { + value: Opaque, + component_type: ComponentType, +} +unsafe impl Send for WrappedValue {} + +impl magnus::DataTypeFunctions for WrappedValue { + fn mark(&self, marker: &magnus::gc::Marker) { + marker.mark(self.value); + } +} + +impl WrappedValue { + pub fn new(value: Value, component_type: ComponentType) -> Self { + Self { + value: value.into(), + component_type, + } + } + + /// Get the wrapped Ruby value + pub fn value(&self, ruby: &Ruby) -> Value { + ruby.get_inner(self.value) + } + + /// Get the component type information + pub fn component_type(&self) -> &ComponentType { + &self.component_type + } } pub struct TypeFactory; @@ -378,343 +424,11 @@ pub fn init(ruby: &Ruby, namespace: &magnus::RModule) -> Result<(), Error> { type_class.define_singleton_method("result", function!(TypeFactory::result, 2))?; type_class.define_singleton_method("flags", function!(TypeFactory::flags, 1))?; - Ok(()) -} - -// Make ComponentType accessible from other component modules -pub(super) fn extract_component_type(value: Value) -> Result { - let rb_ty: &RbComponentType = TryConvert::try_convert(value)?; - Ok(rb_ty.inner.clone()) -} - -/// Convert wasmtime's component Type to our ComponentType -/// This is used for validating host function signatures against component imports -pub(super) fn wasmtime_type_to_component_type( - ty: &wasmtime::component::Type, -) -> Result { - use wasmtime::component::types::ComponentItem; - - match ty { - wasmtime::component::Type::Bool => Ok(ComponentType::Bool), - wasmtime::component::Type::S8 => Ok(ComponentType::S8), - wasmtime::component::Type::U8 => Ok(ComponentType::U8), - wasmtime::component::Type::S16 => Ok(ComponentType::S16), - wasmtime::component::Type::U16 => Ok(ComponentType::U16), - wasmtime::component::Type::S32 => Ok(ComponentType::S32), - wasmtime::component::Type::U32 => Ok(ComponentType::U32), - wasmtime::component::Type::S64 => Ok(ComponentType::S64), - wasmtime::component::Type::U64 => Ok(ComponentType::U64), - wasmtime::component::Type::Float32 => Ok(ComponentType::Float32), - wasmtime::component::Type::Float64 => Ok(ComponentType::Float64), - wasmtime::component::Type::Char => Ok(ComponentType::Char), - wasmtime::component::Type::String => Ok(ComponentType::String), - wasmtime::component::Type::List(inner) => { - let inner_ty = wasmtime_type_to_component_type(&inner.ty())?; - Ok(ComponentType::List(Box::new(inner_ty))) - } - wasmtime::component::Type::Record(record) => { - let mut fields = Vec::new(); - for field in record.fields() { - let field_ty = wasmtime_type_to_component_type(&field.ty)?; - fields.push(RecordField { - name: field.name.to_string(), - ty: field_ty, - }); - } - Ok(ComponentType::Record(fields)) - } - wasmtime::component::Type::Tuple(tuple) => { - let mut types = Vec::new(); - for ty in tuple.types() { - types.push(wasmtime_type_to_component_type(&ty)?); - } - Ok(ComponentType::Tuple(types)) - } - wasmtime::component::Type::Variant(variant) => { - let mut cases = Vec::new(); - for case in variant.cases() { - let case_ty = case - .ty - .as_ref() - .map(wasmtime_type_to_component_type) - .transpose()?; - cases.push(VariantCase { - name: case.name.to_string(), - ty: case_ty, - }); - } - Ok(ComponentType::Variant(cases)) - } - wasmtime::component::Type::Enum(enum_ty) => { - let cases: Vec = enum_ty.names().map(|s| s.to_string()).collect(); - Ok(ComponentType::Enum(cases)) - } - wasmtime::component::Type::Option(opt) => { - let inner_ty = wasmtime_type_to_component_type(&opt.ty())?; - Ok(ComponentType::Option(Box::new(inner_ty))) - } - wasmtime::component::Type::Result(result) => { - let ok = result - .ok() - .map(|ty| wasmtime_type_to_component_type(&ty).map(Box::new)) - .transpose()?; - let err = result - .err() - .map(|ty| wasmtime_type_to_component_type(&ty).map(Box::new)) - .transpose()?; - Ok(ComponentType::Result { ok, err }) - } - wasmtime::component::Type::Flags(flags) => { - let names: Vec = flags.names().map(|s| s.to_string()).collect(); - Ok(ComponentType::Flags(names)) - } - wasmtime::component::Type::Own(_) | wasmtime::component::Type::Borrow(_) => Err( - "resource types (own/borrow) are not yet supported for host function validation" - .to_string(), - ), - wasmtime::component::Type::Map(_) => { - Err("map types are not yet supported for host function validation".to_string()) - } - wasmtime::component::Type::Future(_) => { - Err("future types are not yet supported for host function validation".to_string()) - } - wasmtime::component::Type::Stream(_) => { - Err("stream types are not yet supported for host function validation".to_string()) - } - wasmtime::component::Type::ErrorContext => Err( - "error-context types are not yet supported for host function validation".to_string(), - ), - } -} - -/// Extract function parameter and result types from a ComponentFunc -pub(super) fn extract_func_types( - func: &wasmtime::component::types::ComponentFunc, -) -> Result<(Vec, Vec), String> { - let mut param_types = Vec::new(); - for (_name, ty) in func.params() { - param_types.push(wasmtime_type_to_component_type(&ty)?); - } - - let mut result_types = Vec::new(); - // Results is an iterator of Type directly (not tuples) - for ty in func.results() { - result_types.push(wasmtime_type_to_component_type(&ty)?); - } - - Ok((param_types, result_types)) -} - -/// Compare two ComponentTypes for compatibility -/// Returns Ok(()) if types match, Err with descriptive message if they don't -pub(super) fn types_match( - declared: &ComponentType, - expected: &ComponentType, -) -> Result<(), String> { - match (declared, expected) { - // Primitive types must match exactly - (ComponentType::Bool, ComponentType::Bool) - | (ComponentType::S8, ComponentType::S8) - | (ComponentType::U8, ComponentType::U8) - | (ComponentType::S16, ComponentType::S16) - | (ComponentType::U16, ComponentType::U16) - | (ComponentType::S32, ComponentType::S32) - | (ComponentType::U32, ComponentType::U32) - | (ComponentType::S64, ComponentType::S64) - | (ComponentType::U64, ComponentType::U64) - | (ComponentType::Float32, ComponentType::Float32) - | (ComponentType::Float64, ComponentType::Float64) - | (ComponentType::Char, ComponentType::Char) - | (ComponentType::String, ComponentType::String) => Ok(()), - - // List types: element types must match - (ComponentType::List(d_inner), ComponentType::List(e_inner)) => { - types_match(d_inner, e_inner).map_err(|e| format!("list element type mismatch: {}", e)) - } - - // Option types: inner types must match - (ComponentType::Option(d_inner), ComponentType::Option(e_inner)) => { - types_match(d_inner, e_inner).map_err(|e| format!("option type mismatch: {}", e)) - } - - // Record types: must have same fields with matching types - (ComponentType::Record(d_fields), ComponentType::Record(e_fields)) => { - if d_fields.len() != e_fields.len() { - return Err(format!( - "record field count mismatch: declared has {} fields, expected has {}", - d_fields.len(), - e_fields.len() - )); - } - - for (d_field, e_field) in d_fields.iter().zip(e_fields.iter()) { - if d_field.name != e_field.name { - return Err(format!( - "record field name mismatch: declared has '{}', expected has '{}'", - d_field.name, e_field.name - )); - } - types_match(&d_field.ty, &e_field.ty) - .map_err(|e| format!("record field '{}' type mismatch: {}", d_field.name, e))?; - } - Ok(()) - } - - // Tuple types: must have same number of elements with matching types - (ComponentType::Tuple(d_types), ComponentType::Tuple(e_types)) => { - if d_types.len() != e_types.len() { - return Err(format!( - "tuple length mismatch: declared has {} elements, expected has {}", - d_types.len(), - e_types.len() - )); - } - - for (i, (d_ty, e_ty)) in d_types.iter().zip(e_types.iter()).enumerate() { - types_match(d_ty, e_ty) - .map_err(|e| format!("tuple element {} mismatch: {}", i, e))?; - } - Ok(()) - } + // Instance method for wrapping values + type_class.define_method("wrap", method!(RbComponentType::wrap, 1))?; - // Variant types: must have same cases with matching types - (ComponentType::Variant(d_cases), ComponentType::Variant(e_cases)) => { - if d_cases.len() != e_cases.len() { - return Err(format!( - "variant case count mismatch: declared has {} cases, expected has {}", - d_cases.len(), - e_cases.len() - )); - } - - for (d_case, e_case) in d_cases.iter().zip(e_cases.iter()) { - if d_case.name != e_case.name { - return Err(format!( - "variant case name mismatch: declared has '{}', expected has '{}'", - d_case.name, e_case.name - )); - } - - match (&d_case.ty, &e_case.ty) { - (Some(d_ty), Some(e_ty)) => { - types_match(d_ty, e_ty).map_err(|e| { - format!("variant case '{}' type mismatch: {}", d_case.name, e) - })?; - } - (None, None) => {} - (Some(_), None) => { - return Err(format!( - "variant case '{}': declared has payload, expected has none", - d_case.name - )); - } - (None, Some(_)) => { - return Err(format!( - "variant case '{}': declared has no payload, expected has payload", - d_case.name - )); - } - } - } - Ok(()) - } - - // Enum types: must have same cases in same order - (ComponentType::Enum(d_cases), ComponentType::Enum(e_cases)) => { - if d_cases.len() != e_cases.len() { - return Err(format!( - "enum case count mismatch: declared has {} cases, expected has {}", - d_cases.len(), - e_cases.len() - )); - } + // WrappedValue class + let _wrapped_value_class = namespace.define_class("WrappedValue", ruby.class_object())?; - for (d_case, e_case) in d_cases.iter().zip(e_cases.iter()) { - if d_case != e_case { - return Err(format!( - "enum case mismatch: declared has '{}', expected has '{}'", - d_case, e_case - )); - } - } - Ok(()) - } - - // Result types: ok and err types must match - ( - ComponentType::Result { - ok: d_ok, - err: d_err, - }, - ComponentType::Result { - ok: e_ok, - err: e_err, - }, - ) => { - match (d_ok, e_ok) { - (Some(d_ty), Some(e_ty)) => { - types_match(d_ty, e_ty) - .map_err(|e| format!("result ok type mismatch: {}", e))?; - } - (None, None) => {} - (Some(_), None) => { - return Err( - "result ok type mismatch: declared has ok type, expected has none" - .to_string(), - ); - } - (None, Some(_)) => { - return Err( - "result ok type mismatch: declared has no ok type, expected has ok type" - .to_string(), - ); - } - } - - match (d_err, e_err) { - (Some(d_ty), Some(e_ty)) => { - types_match(d_ty, e_ty) - .map_err(|e| format!("result err type mismatch: {}", e))?; - } - (None, None) => {} - (Some(_), None) => { - return Err( - "result err type mismatch: declared has err type, expected has none" - .to_string(), - ); - } - (None, Some(_)) => { - return Err( - "result err type mismatch: declared has no err type, expected has err type" - .to_string(), - ); - } - } - Ok(()) - } - - // Flags types: must have same flags in same order - (ComponentType::Flags(d_flags), ComponentType::Flags(e_flags)) => { - if d_flags.len() != e_flags.len() { - return Err(format!( - "flags count mismatch: declared has {} flags, expected has {}", - d_flags.len(), - e_flags.len() - )); - } - - for (d_flag, e_flag) in d_flags.iter().zip(e_flags.iter()) { - if d_flag != e_flag { - return Err(format!( - "flag mismatch: declared has '{}', expected has '{}'", - d_flag, e_flag - )); - } - } - Ok(()) - } - - // Type mismatch - _ => Err(format!("expected {}, got {}", expected, declared)), - } + Ok(()) } diff --git a/spec/fixtures/host-func-imports/src/lib.rs b/spec/fixtures/host-func-imports/src/lib.rs index 31685caf..429c5385 100644 --- a/spec/fixtures/host-func-imports/src/lib.rs +++ b/spec/fixtures/host-func-imports/src/lib.rs @@ -37,6 +37,18 @@ impl Guest for Component { fn test_multiply(a: u32, b: u32) -> u32 { math::multiply(a, b) } + + fn test_get_numbers() -> Vec { + bindings::get_numbers() + } + + fn test_make_tuple(n: u32, s: String, b: bool) -> (u32, String, bool) { + bindings::make_tuple(n, &s, b) + } + + fn test_analyze_numbers(numbers: Vec) -> (i32, Vec) { + bindings::analyze_numbers(&numbers) + } } bindings::export!(Component with_types_in bindings); diff --git a/spec/fixtures/host-func-imports/wit/world.wit b/spec/fixtures/host-func-imports/wit/world.wit index 03c334b0..47cc0cd6 100644 --- a/spec/fixtures/host-func-imports/wit/world.wit +++ b/spec/fixtures/host-func-imports/wit/world.wit @@ -16,6 +16,9 @@ world host-imports { import sum-list: func(numbers: list) -> s32; import maybe-double: func(n: option) -> option; import safe-divide: func(a: u32, b: u32) -> result; + import get-numbers: func() -> list; + import make-tuple: func(n: u32, s: string, b: bool) -> tuple; + import analyze-numbers: func(numbers: list) -> tuple>; // Nested instance import math: interface { @@ -31,4 +34,7 @@ world host-imports { export test-maybe: func(n: option) -> option; export test-divide: func(a: u32, b: u32) -> result; export test-multiply: func(a: u32, b: u32) -> u32; + export test-get-numbers: func() -> list; + export test-make-tuple: func(n: u32, s: string, b: bool) -> tuple; + export test-analyze-numbers: func(numbers: list) -> tuple>; } diff --git a/spec/fixtures/host_func_imports.wasm b/spec/fixtures/host_func_imports.wasm index 34ccadafe507a64310b7c1b5d1f9558fff2b0596..9667f770580d766f2f734f0d61cc8cd51c25cc13 100644 GIT binary patch literal 14466 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 t.s32, "y" => t.s32) - - linker.root do |root| - root.func_new("make-point", [t.s32, t.s32], [point_type]) do |x, y| - {"x" => x, "y" => y} - end - end - - expect(linker).to be_a(Linker) - end - - it "defines a function with list types" do - linker.root do |root| - root.func_new("sum-list", [t.list(t.s32)], [t.s32]) do |numbers| - numbers.sum - end - end - - expect(linker).to be_a(Linker) - end - - it "defines a function with option types" do - linker.root do |root| - root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) do |n| - n.nil? ? nil : n * 2 - end - end - - expect(linker).to be_a(Linker) - end - - it "defines a function with result types" do - linker.root do |root| - root.func_new( - "safe-divide", - [t.u32, t.u32], - [t.result(t.u32, t.string)] - ) do |a, b| - if b == 0 - Result.error("division by zero") - else - Result.ok(a / b) - end + root.func_new("log") do |_msg| + # No return value for functions with no results end end @@ -136,8 +84,8 @@ module Component context "nested instances" do it "defines functions in nested instances" do linker.instance("math") do |math| - math.func_new("add", [t.u32, t.u32], [t.u32]) do |a, b| - a + b + math.func_new("add") do |a, b| + t.u32.wrap(a + b) end end @@ -149,7 +97,7 @@ module Component it "requires a block" do expect { linker.root do |root| - root.func_new("no-block", [], [t.u32]) + root.func_new("no-block") end }.to raise_error(ArgumentError, /no block given/) end @@ -168,24 +116,38 @@ module Component # Helper to stub all required imports except the specified one(s) # @param except [Symbol, Array, nil] Import(s) to skip stubbing (nil = stub all) - def stub_imports_except(linker, except: nil) + def stub_component_imports(linker, except: nil) skip_funcs = Array(except).map(&:to_s).to_set # Stub root functions linker.root do |root| - root.func_new("greet", [t.string], [t.string]) { |name| name } unless skip_funcs.include?("greet") - root.func_new("add", [t.u32, t.u32], [t.u32]) { |a, b| a + b } unless skip_funcs.include?("add") - root.func_new("get-constant", [], [t.u32]) { 0 } unless skip_funcs.include?("get-constant") + root.func_new("greet") { |name| t.string.wrap(name) } unless skip_funcs.include?("greet") + root.func_new("add") { |a, b| t.u32.wrap(a + b) } unless skip_funcs.include?("add") + root.func_new("get-constant") { t.u32.wrap(0) } unless skip_funcs.include?("get-constant") unless skip_funcs.include?("make-point") - root.func_new("make-point", [t.s32, t.s32], [t.record("x" => t.s32, "y" => t.s32)]) do |x, y| - {"x" => x, "y" => y} + point_type = t.record("x" => t.s32, "y" => t.s32) + root.func_new("make-point") do |x, y| + point_type.wrap({"x" => x, "y" => y}) end end - root.func_new("sum-list", [t.list(t.s32)], [t.s32]) { |nums| nums.sum } unless skip_funcs.include?("sum-list") - root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) { |n| n } unless skip_funcs.include?("maybe-double") + root.func_new("sum-list") { |nums| t.s32.wrap(nums.sum) } unless skip_funcs.include?("sum-list") + root.func_new("maybe-double") { |n| t.option(t.u32).wrap(n) } unless skip_funcs.include?("maybe-double") unless skip_funcs.include?("safe-divide") - root.func_new("safe-divide", [t.u32, t.u32], [t.result(t.u32, t.string)]) do |a, b| - Result.ok(a) + root.func_new("safe-divide") do |a, b| + t.result(t.u32, t.string).wrap(Result.ok(a)) + end + end + root.func_new("get-numbers") { t.list(t.s32).wrap([]) } unless skip_funcs.include?("get-numbers") + unless skip_funcs.include?("make-tuple") + tuple_type = t.tuple([t.u32, t.string, t.bool]) + root.func_new("make-tuple") do |n, s, b| + tuple_type.wrap([n, s, b]) + end + end + unless skip_funcs.include?("analyze-numbers") + tuple_type = t.tuple([t.s32, t.list(t.s32)]) + root.func_new("analyze-numbers") do |nums| + tuple_type.wrap([0, nums]) end end end @@ -193,18 +155,18 @@ def stub_imports_except(linker, except: nil) # Stub math instance unless skipped unless skip_funcs.include?("math") linker.instance("math") do |math| - math.func_new("multiply", [t.u32, t.u32], [t.u32]) { |a, b| a * b } + math.func_new("multiply") { |a, b| t.u32.wrap(a * b) } end end end context "with primitive types" do it "provides a string function" do - stub_imports_except(linker, except: :greet) + stub_component_imports(linker, except: :greet) linker.root do |root| - root.func_new("greet", [t.string], [t.string]) do |name| - "Hello, #{name}!" + root.func_new("greet") do |name| + t.string.wrap("Hello, #{name}!") end end @@ -215,11 +177,11 @@ def stub_imports_except(linker, except: nil) end it "provides a function with multiple params" do - stub_imports_except(linker, except: :add) + stub_component_imports(linker, except: :add) linker.root do |root| - root.func_new("add", [t.u32, t.u32], [t.u32]) do |a, b| - a + b + root.func_new("add") do |a, b| + t.u32.wrap(a + b) end end @@ -230,11 +192,11 @@ def stub_imports_except(linker, except: nil) end it "provides a function with no params" do - stub_imports_except(linker, except: :"get-constant") + stub_component_imports(linker, except: :"get-constant") linker.root do |root| - root.func_new("get-constant", [], [t.u32]) do - 1234 + root.func_new("get-constant") do + t.u32.wrap(1234) end end @@ -247,13 +209,13 @@ def stub_imports_except(linker, except: nil) context "with complex types" do it "provides a function returning a record" do - stub_imports_except(linker, except: :"make-point") + stub_component_imports(linker, except: :"make-point") point_type = t.record("x" => t.s32, "y" => t.s32) linker.root do |root| - root.func_new("make-point", [t.s32, t.s32], [point_type]) do |x, y| - {"x" => x, "y" => y} + root.func_new("make-point") do |x, y| + point_type.wrap({"x" => x, "y" => y}) end end @@ -263,12 +225,30 @@ def stub_imports_except(linker, except: nil) expect(result).to eq({"x" => 10, "y" => 20}) end + it "validates field types in records" do + stub_component_imports(linker, except: :"make-point") + + point_type = t.record("x" => t.s32, "y" => t.s32) + + linker.root do |root| + root.func_new("make-point") do |_x, y| + # Try to use wrong type for x field (string instead of s32) + point_type.wrap({"x" => "not a number", "y" => y}) + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-point") + + expect { func.call(10, 20) }.to raise_error(Wasmtime::Error, /expected s32, got/) + end + it "provides a function accepting a list" do - stub_imports_except(linker, except: :"sum-list") + stub_component_imports(linker, except: :"sum-list") linker.root do |root| - root.func_new("sum-list", [t.list(t.s32)], [t.s32]) do |numbers| - numbers.sum + root.func_new("sum-list") do |numbers| + t.s32.wrap(numbers.sum) end end @@ -279,11 +259,11 @@ def stub_imports_except(linker, except: nil) end it "provides a function with option type" do - stub_imports_except(linker, except: :"maybe-double") + stub_component_imports(linker, except: :"maybe-double") linker.root do |root| - root.func_new("maybe-double", [t.option(t.u32)], [t.option(t.u32)]) do |n| - n.nil? ? nil : n * 2 + root.func_new("maybe-double") do |n| + t.option(t.u32).wrap(n.nil? ? nil : n * 2) end end @@ -294,19 +274,16 @@ def stub_imports_except(linker, except: nil) end it "provides a function with result type" do - stub_imports_except(linker, except: :"safe-divide") + stub_component_imports(linker, except: :"safe-divide") linker.root do |root| - root.func_new( - "safe-divide", - [t.u32, t.u32], - [t.result(t.u32, t.string)] - ) do |a, b| - if b == 0 + root.func_new("safe-divide") do |a, b| + result_val = if b == 0 Result.error("division by zero") else Result.ok(a / b) end + t.result(t.u32, t.string).wrap(result_val) end end @@ -316,15 +293,64 @@ def stub_imports_except(linker, except: nil) expect(func.call(10, 2)).to eq(Result.ok(5)) expect(func.call(10, 0)).to eq(Result.error("division by zero")) end + + it "provides a function returning a list" do + stub_component_imports(linker, except: :"get-numbers") + + linker.root do |root| + root.func_new("get-numbers") do + t.list(t.s32).wrap([1, 2, 3, 4, 5]) + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-get-numbers").call + + expect(result).to eq([1, 2, 3, 4, 5]) + end + + it "provides a function returning a tuple" do + stub_component_imports(linker, except: :"make-tuple") + + tuple_type = t.tuple([t.u32, t.string, t.bool]) + + linker.root do |root| + root.func_new("make-tuple") do |n, s, b| + tuple_type.wrap([n, s, b]) + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-make-tuple").call(42, "hello", true) + + expect(result).to eq([42, "hello", true]) + end + + it "provides a function returning a tuple containing a list" do + stub_component_imports(linker, except: :"analyze-numbers") + + tuple_type = t.tuple([t.s32, t.list(t.s32)]) + + linker.root do |root| + root.func_new("analyze-numbers") do |numbers| + tuple_type.wrap([numbers.sum, numbers.sort]) + end + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-analyze-numbers").call([5, 1, 3, 2, 4]) + + expect(result).to eq([15, [1, 2, 3, 4, 5]]) + end end context "with nested instances" do it "provides functions in nested instances" do - stub_imports_except(linker, except: :math) + stub_component_imports(linker, except: :math) linker.instance("math") do |math| - math.func_new("multiply", [t.u32, t.u32], [t.u32]) do |a, b| - a * b + math.func_new("multiply") do |a, b| + t.u32.wrap(a * b) end end @@ -339,12 +365,12 @@ def stub_imports_except(linker, except: nil) it "maintains state across calls" do counter = 0 - stub_imports_except(linker, except: :"get-constant") + stub_component_imports(linker, except: :"get-constant") linker.root do |root| - root.func_new("get-constant", [], [t.u32]) do + root.func_new("get-constant") do counter += 1 - counter + t.u32.wrap(counter) end end @@ -359,12 +385,12 @@ def stub_imports_except(linker, except: nil) it "allows accessing Ruby objects" do log = [] - stub_imports_except(linker, except: :greet) + stub_component_imports(linker, except: :greet) linker.root do |root| - root.func_new("greet", [t.string], [t.string]) do |name| + root.func_new("greet") do |name| log << name - "Hello, #{name}!" + t.string.wrap("Hello, #{name}!") end end @@ -380,10 +406,10 @@ def stub_imports_except(linker, except: nil) context "error handling" do it "propagates Ruby exceptions" do - stub_imports_except(linker, except: :"get-constant") + stub_component_imports(linker, except: :"get-constant") linker.root do |root| - root.func_new("get-constant", [], [t.u32]) do + root.func_new("get-constant") do raise "Something went wrong" end end @@ -395,77 +421,53 @@ def stub_imports_except(linker, except: nil) end it "validates return values match declared types" do - stub_imports_except(linker, except: :add) + stub_component_imports(linker, except: :add) linker.root do |root| - root.func_new("add", [t.u32, t.u32], [t.u32]) do |_a, _b| - "not a number" + root.func_new("add") do |_a, _b| + t.u32.wrap("not a number") end end instance = linker.instantiate(store, @host_imports_component) func = instance.get_func("test-add") - expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /expected u32, got "not a number"/) + expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /expected u32, got/) end - it "validates host function signatures" do - stub_imports_except(linker, except: :add) + it "raises clear error when return value is not wrapped" do + stub_component_imports(linker, except: :add) linker.root do |root| - root.func_new("add", [t.s32, t.u32], [t.u32]) do |a, b| - a + b + root.func_new("add") do |a, b| + a + b # Forgot to wrap with Type::U32.wrap() end end - expect { - linker.instantiate(store, @host_imports_component) - }.to raise_error(Wasmtime::Error, /host function "add" parameter 0 has incompatible type/) - end - - it "validates nested instance function signatures" do - stub_imports_except(linker, except: :math) - - linker.instance("math") do |math| - math.func_new("multiply", [t.s32, t.s32], [t.u32]) do |a, b| - a * b - end - end + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-add") - expect { - linker.instantiate(store, @host_imports_component) - }.to raise_error(Wasmtime::Error, /host function "math\/multiply" parameter 0 has incompatible type/) + expect { func.call(1, 2) }.to raise_error( + TypeError, + /host function must return wrapped value/ + ) end - it "validates result type mismatches" do - stub_imports_except(linker, except: :add) + it "raises Wasmtime error when wrapper type mismatches component expectation" do + stub_component_imports(linker, except: :add) linker.root do |root| - root.func_new("add", [t.u32, t.u32], [t.s32]) do |a, b| - a + b + root.func_new("add") do |a, b| + # Component expects u32, but we return s32 + t.s32.wrap(a + b) end end - expect { - linker.instantiate(store, @host_imports_component) - }.to raise_error(Wasmtime::Error, /host function "add" result 0 has incompatible type/) - end - - it "validates complex type mismatches" do - stub_imports_except(linker, except: :"make-point") - - # Define point with wrong field types (x should be s32, not u32) - wrong_point_type = t.record("x" => t.u32, "y" => t.s32) - - linker.root do |root| - root.func_new("make-point", [t.s32, t.s32], [wrong_point_type]) do |x, y| - {"x" => x, "y" => y} - end - end + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-add") - expect { - linker.instantiate(store, @host_imports_component) - }.to raise_error(Wasmtime::Error, /host function "make-point" result 0 has incompatible type.*record field 'x' type mismatch/) + # The component crashes at runtime when the wrong type is returned + expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /error while executing at wasm/i) end end end From 15881a8922d928e68ac44a58239d1c7fbf5c461c Mon Sep 17 00:00:00 2001 From: william-stacken Date: Fri, 22 May 2026 11:12:41 +0200 Subject: [PATCH 3/4] Integration tests for all types --- spec/fixtures/host-func-imports/src/lib.rs | 50 +++- spec/fixtures/host-func-imports/wit/world.wit | 50 ++++ spec/fixtures/host_func_imports.wasm | Bin 14466 -> 16818 bytes spec/unit/component/linker_spec.rb | 259 ++++++++++++++---- 4 files changed, 306 insertions(+), 53 deletions(-) diff --git a/spec/fixtures/host-func-imports/src/lib.rs b/spec/fixtures/host-func-imports/src/lib.rs index 429c5385..393a395e 100644 --- a/spec/fixtures/host-func-imports/src/lib.rs +++ b/spec/fixtures/host-func-imports/src/lib.rs @@ -1,7 +1,7 @@ #[allow(warnings)] mod bindings; -use bindings::{math, Guest, Point}; +use bindings::{math, Color, Guest, Permissions, Point, Shape}; struct Component; @@ -49,6 +49,54 @@ impl Guest for Component { fn test_analyze_numbers(numbers: Vec) -> (i32, Vec) { bindings::analyze_numbers(&numbers) } + + fn test_s8(n: i8) -> i8 { + bindings::echo_s8(n) + } + + fn test_u8(n: u8) -> u8 { + bindings::echo_u8(n) + } + + fn test_s16(n: i16) -> i16 { + bindings::echo_s16(n) + } + + fn test_u16(n: u16) -> u16 { + bindings::echo_u16(n) + } + + fn test_s64(n: i64) -> i64 { + bindings::echo_s64(n) + } + + fn test_u64(n: u64) -> u64 { + bindings::echo_u64(n) + } + + fn test_f32(n: f32) -> f32 { + bindings::echo_f32(n) + } + + fn test_f64(n: f64) -> f64 { + bindings::echo_f64(n) + } + + fn test_char(c: char) -> char { + bindings::echo_char(c) + } + + fn test_enum(c: Color) -> Color { + bindings::echo_enum(c) + } + + fn test_variant(s: Shape) -> Shape { + bindings::echo_variant(s) + } + + fn test_flags(p: Permissions) -> Permissions { + bindings::echo_flags(p) + } } bindings::export!(Component with_types_in bindings); diff --git a/spec/fixtures/host-func-imports/wit/world.wit b/spec/fixtures/host-func-imports/wit/world.wit index 47cc0cd6..1461589a 100644 --- a/spec/fixtures/host-func-imports/wit/world.wit +++ b/spec/fixtures/host-func-imports/wit/world.wit @@ -20,6 +20,44 @@ world host-imports { import make-tuple: func(n: u32, s: string, b: bool) -> tuple; import analyze-numbers: func(numbers: list) -> tuple>; + // Additional integer types + import echo-s8: func(n: s8) -> s8; + import echo-u8: func(n: u8) -> u8; + import echo-s16: func(n: s16) -> s16; + import echo-u16: func(n: u16) -> u16; + import echo-s64: func(n: s64) -> s64; + import echo-u64: func(n: u64) -> u64; + + // Float types + import echo-f32: func(n: f32) -> f32; + import echo-f64: func(n: f64) -> f64; + + // Char type + import echo-char: func(c: char) -> char; + + // Enum, variant, flags + enum color { + red, + green, + blue, + } + + variant shape { + circle(f32), + rectangle(tuple), + point, + } + + flags permissions { + read, + write, + execute, + } + + import echo-enum: func(c: color) -> color; + import echo-variant: func(s: shape) -> shape; + import echo-flags: func(p: permissions) -> permissions; + // Nested instance import math: interface { multiply: func(a: u32, b: u32) -> u32; @@ -37,4 +75,16 @@ world host-imports { export test-get-numbers: func() -> list; export test-make-tuple: func(n: u32, s: string, b: bool) -> tuple; export test-analyze-numbers: func(numbers: list) -> tuple>; + export test-s8: func(n: s8) -> s8; + export test-u8: func(n: u8) -> u8; + export test-s16: func(n: s16) -> s16; + export test-u16: func(n: u16) -> u16; + export test-s64: func(n: s64) -> s64; + export test-u64: func(n: u64) -> u64; + export test-f32: func(n: f32) -> f32; + export test-f64: func(n: f64) -> f64; + export test-char: func(c: char) -> char; + export test-enum: func(c: color) -> color; + export test-variant: func(s: shape) -> shape; + export test-flags: func(p: permissions) -> permissions; } diff --git a/spec/fixtures/host_func_imports.wasm b/spec/fixtures/host_func_imports.wasm index 9667f770580d766f2f734f0d61cc8cd51c25cc13..8c70e7a8c3f3373b707219f7ddd7e617b735abd4 100644 GIT binary patch delta 4636 zcma)AU2Ggz6+Y+A%+Bo0{*3>fKW8@HB;Ldudu?aEO^ECzjgmmA;DJ_11hTd#$tL!$ zv$N~ONt`h$rBKq4=2H4YK|ECA2_X?AN}IMxXbUP26)#AHm(mA#;H9b%>I({I?u>VK z(@M3YxqIiF@1A?^x#!;bPOhB&%@cV2sN0DPl2o(jl#?~vuE;a<4O=%5l-cThwI+e2 z?qF56v~*UA%$94j^ENI)v}VuNOO=!KqJXkkEm!IQVvuvHv}j8ZQG2l>$u+w)r<|*m z>$Ym2w`Uu5Tkk+H7VX+X*>TF%iX*{BO*L>D_3SK-qPgoB^Tshee*u>-0MnBQ`aP9!Joe z#zJg>$&G-AaBQ&?XpEzj85xoTsE*407dPvf$S4$VR^OtSqDB;EW4Bp^x>(BF)jOc2a0BBwD@f)4LIp zrAldj`GTFTxW4z;uWA$|tXzi6dJI9eXHQkLP60Or@)cMK<&6Sv4CI&Kl32En&*gDb zptuMZLq&rWn*+spI3Fq;H$Q<}0?j!%7it=$7zh+guoNmzjE~{gK+%9ki-Ja2zGAix zbv=O)6>iO*D%CI*Xk?H>jqR-;xA`hZ1y$^m7_?NXmAxz&^p%TIh>GwE<%#*yNe9#T z%1J0W3kU%CSn@b?UGN%ok4vuCN3RR?;0mrB$4hvLL~vXorBGkS%jx5?yRt$;0P4}7 z7`(DVKjIbG&!s;8N8FVP*8-A~u2rk`wsZyg2HW^`A7oRStbsJ&;qft0{851*KLHV6 zpodFZ8)UTP8mN4TS*Y=&Y|0f-CzoFX`oBHyB7{xa?sgN6 z!WtkZ0AfG((LJC`6XS*fEC#CghbYM?DG_R8NH0;zd*Uk4u1KIl!XO;PV*C~xlCH}s z{x6onExbeO!vUU=Hse-4y|;&f#g9pw+>}0h3s{-oZ{<$^BFKc)(cP65V!;RNj#O!$3oi}nUx z!sdM}ZOdd#j0z426u2d)%p3L~& zbroW0ShB^6vLzK+-TW#4oBTNbp68UVp4U7*7{!lPZ?8h}7vG`Qe2srYIfbwDyUMTd z7GI2f8Q-{hE0REbga27|4t(a2z|60OnGd|y{7f9bs6C2r^7phak^}MR!Ok`Z(UQo! znP?tg;x9&rsJ$BW2vUjy{Sn z^8f1TuVfy?d43k5i2MAZh6UyoPiE}=Jf!&qKVW3U92@bw{EBf5U*;behuo3R8)-pN zI%_^Drk2hc>+$J(!l(5hpUMkpZXHhC$Ft`5g%{N0>DYE!%$H(Y@CW=h@UX${Nh8E$lB^S-p<`4PJ_#T3N7#}5ASK?a)t0gjc zmA{;Ll=@S84?e>&IYj+k$rN7Wk0jIlUcz*r7jCre!yWTSp774P;&WXr`cql(X{)!* zXFcI<;|QYL%$Z0tX?olno<7Ccw1tP_V1b2nMvAiOXPU)~@%4oEKJ73U)8D7=y4LVx z4{o^Y-MQy{>v-1q1ZDch(Z>o`21!T>ZJyaCrA4>7M1O@s+Y;u3Kz}K@$Wn^mjZjeh ze-{ag; zp+g4y;J02kwx){}*ezdYNkdG_d&sPS(s}fuNM?#Gcr$21^R&OPqR{hKlh^4^I%*!I z_8D^y-@f^=ImhrV{&?3k-77D2{f=&_uXZofmGxftPDZGYd#9)!>pMZ%7yHst`gzlO zTE(~d(V>q>voCXqM5~#18Q$iHhYR=)Umbo5|8P^worgn5>V)}J& z`6)(=qRZ;d;KhrVg%>`n-a3MNXXOFR)(gvakHpj%7E^dCO$1NV#EmCW~l+OmbwMv0cE4Fdm?0S$tG_z zn-vUn_Gb3Ww6Nw{5RPz}KKQ^Iu_5!mgAc2TTNiScTm>PaK zO4OW5Aon3eB=47^s0IR96KJBt|1r_E51|_+U%&d_{k{F}c5*Ntn)i#uE$!xsU1?N)!Va<2WH<#Wa4ST}IdY|i)M*+? zc9P|e(6V5An1aY|=Ai@1$xr0dj|@@7K*@9&UJ)!vT2Mz0LWKM#bUQ~BYu!a_Z7yn! zJ>qu$*ZhHj-Q=h;+->yI*%0UZaPOgFuB86*K(mSAULY(U{ zd`fBoq?8GPB&0lQ%LpoM1dh@w#F3EltPMx?i@;Ipg*dNg!GQ#rAX+HH`4%3y=h4_gRL!d2}Xm_81zA59V^gnZRc(W zc0P21fMEDzz6bG1kv}2=iZkIw6XSz1(V)DlQDb~o5`8cvCZ2oSbsZz2ZO;9^-}%0C ze&6{{&$*u-#Un4Dd!nFW48=?$owUGGRRrn4Y%-ZcK_&&mL>|ZUkXIE1Au%z5qAXzy z2=F6#4kUA(`?47;m&oK$DsFgX9`p0SR3CzxPD~{`XR;~Mx@3dR%*Rm9%%#VZvz83= zs*a#og}HR+bjr%1BCBjFA4ANz!R?q&C-URT&WY^Y_;eCgGBAmGw92G3)j-g!#C}?n zI+U77qNhksb`&uffgt2`XA5>|085cxBri4va`qrFxskbpvao67}E2GRZ?3iOqcJ|Ylb z1w?SWNC&daC#81JaUtYNC)3&4JOlpMa1#SRzcm!z0)e`HiSg9lnXHxDn@d``&Pvz8 zst1*J+CUXQJ6xwjNO3k$#z2^x(NF=Zbq85W7pT#kdGY3;)>!4BPAx)?*?ONdS-vZ1 zD5{+tfySco^xSkVH8Y)uNKxse9yAdbh1gSoz~nE;%|@5U;Xa#8OiyR`K{r1sxA;0X zdAik`yW1FO^vm*Ho)-R#90{FSSXclLYNlwixM&J-mNU+Bzw&&?1zY3O;vX-3xd8Fs zY-zl2W1z+FIoNxzIT(LSIfxheedPvT;#b{I;^oDE++M`XT+pnMhi)mEd8Fy$I(}CT z^Y67Dyuz7h2VUT#o{_)`;%EvE>sr3(X~Z-9uBV;keNPL?h;EXMI&w@8!< z_a!{Xclbv6kkQ)okfl4IU#Kx5UNdRD&KHaha(>O&Np$`;n(-J9`E9aW{AfF%XDi;|mwHa%M~fRaPa)pq=X=lKHQv^@f#h?2z0DVk6g^C%q_|R=P=Y(R&NWHAZ6Fd7+zKEAf$E7U;S4H@yur=Qe$%V8rqVOn*_=hSKdbO( z`!oE9{vPw+Vt;{P;Bd-r7_=sn%;W~GgQ+zBgbH7?^+zv$g%3Cq*j6`xdY~h+Y}R&0 ze;i6W&E_D8l1sK-@H+#&J_!*RxYo;5k^#H<`oW0D8TPXyRQyz47_2F*tg~!Ig%z~Q z0w)$5AxOnG=5z>fc`BO{$Q3OP6s25(-7cNTfU;DO6q}n0%=9wEG72icxh+~4rrzpi z0*D%dOR%5BUoHW#1Sm@b2?f$qO(!XmWW5aC%OL{nqH5I*I`)7UH-is*!N5N7BNcS) zhafP7favOA6dQr+BtNu$-AI+__?1F}MoqP%@}dSV3OFq-S9HZi^DxmQybPkwf8AbR zT@8{JL|9qt(Z~HmHQ^c}>smHnd@u0x=ur6aTIaONvPFs{2Ej#KiX^FgJAqdyR!~QV z)MZ1C0z>?rq0LX#Q~OiLZ^H{vRD%VyyrG diff --git a/spec/unit/component/linker_spec.rb b/spec/unit/component/linker_spec.rb index eeaba4ed..0d32f656 100644 --- a/spec/unit/component/linker_spec.rb +++ b/spec/unit/component/linker_spec.rb @@ -39,68 +39,32 @@ module Component describe "LinkerInstance#func_new" do let(:t) { Type } - context "simple host functions" do - it "defines a function with primitives" do - linker.root do |root| - root.func_new("greet") do |name| - t.string.wrap("Hello, #{name}!") - end + it "defines a function" do + linker.root do |root| + root.func_new("greet") do |name| + t.string.wrap("Hello, #{name}!") end - - expect(linker).to be_a(Linker) end - it "defines a function with multiple params" do - linker.root do |root| - root.func_new("add") do |a, b| - t.u32.wrap(a + b) - end - end - - expect(linker).to be_a(Linker) - end + expect(linker).to be_a(Linker) + end - it "defines a function with no params" do - linker.root do |root| - root.func_new("get-constant") do - t.u32.wrap(42) - end + it "defines functions in nested instances" do + linker.instance("math") do |math| + math.func_new("add") do |a, b| + t.u32.wrap(a + b) end - - expect(linker).to be_a(Linker) end - it "defines a function with no results" do - linker.root do |root| - root.func_new("log") do |_msg| - # No return value for functions with no results - end - end - - expect(linker).to be_a(Linker) - end + expect(linker).to be_a(Linker) end - context "nested instances" do - it "defines functions in nested instances" do - linker.instance("math") do |math| - math.func_new("add") do |a, b| - t.u32.wrap(a + b) - end + it "requires a block" do + expect { + linker.root do |root| + root.func_new("no-block") end - - expect(linker).to be_a(Linker) - end - end - - context "error cases" do - it "requires a block" do - expect { - linker.root do |root| - root.func_new("no-block") - end - }.to raise_error(ArgumentError, /no block given/) - end + }.to raise_error(ArgumentError, /no block given/) end end @@ -150,6 +114,29 @@ def stub_component_imports(linker, except: nil) tuple_type.wrap([0, nums]) end end + # Additional integer types + root.func_new("echo-s8") { |n| t.s8.wrap(n) } unless skip_funcs.include?("echo-s8") + root.func_new("echo-u8") { |n| t.u8.wrap(n) } unless skip_funcs.include?("echo-u8") + root.func_new("echo-s16") { |n| t.s16.wrap(n) } unless skip_funcs.include?("echo-s16") + root.func_new("echo-u16") { |n| t.u16.wrap(n) } unless skip_funcs.include?("echo-u16") + root.func_new("echo-s64") { |n| t.s64.wrap(n) } unless skip_funcs.include?("echo-s64") + root.func_new("echo-u64") { |n| t.u64.wrap(n) } unless skip_funcs.include?("echo-u64") + # Float types + root.func_new("echo-f32") { |n| t.float32.wrap(n) } unless skip_funcs.include?("echo-f32") + root.func_new("echo-f64") { |n| t.float64.wrap(n) } unless skip_funcs.include?("echo-f64") + # Char type + root.func_new("echo-char") { |c| t.char.wrap(c) } unless skip_funcs.include?("echo-char") + # Enum, variant, flags + root.func_new("echo-enum") { |c| t.enum(["red", "green", "blue"]).wrap(c) } unless skip_funcs.include?("echo-enum") + unless skip_funcs.include?("echo-variant") + variant_type = t.variant( + "circle" => t.float32, + "rectangle" => t.tuple([t.float32, t.float32]), + "point" => nil + ) + root.func_new("echo-variant") { |s| variant_type.wrap(s) } + end + root.func_new("echo-flags") { |p| t.flags(["read", "write", "execute"]).wrap(p) } unless skip_funcs.include?("echo-flags") end # Stub math instance unless skipped @@ -342,6 +329,174 @@ def stub_component_imports(linker, except: nil) expect(result).to eq([15, [1, 2, 3, 4, 5]]) end + + it "provides a function with s8" do + stub_component_imports(linker, except: :"echo-s8") + + linker.root do |root| + root.func_new("echo-s8") { |n| t.s8.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-s8").call(-42)).to eq(-42) + expect(instance.get_func("test-s8").call(127)).to eq(127) + end + + it "provides a function with u8" do + stub_component_imports(linker, except: :"echo-u8") + + linker.root do |root| + root.func_new("echo-u8") { |n| t.u8.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-u8").call(255)).to eq(255) + end + + it "provides a function with s16" do + stub_component_imports(linker, except: :"echo-s16") + + linker.root do |root| + root.func_new("echo-s16") { |n| t.s16.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-s16").call(-1000)).to eq(-1000) + expect(instance.get_func("test-s16").call(32767)).to eq(32767) + end + + it "provides a function with u16" do + stub_component_imports(linker, except: :"echo-u16") + + linker.root do |root| + root.func_new("echo-u16") { |n| t.u16.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-u16").call(65535)).to eq(65535) + end + + it "provides a function with s64" do + stub_component_imports(linker, except: :"echo-s64") + + linker.root do |root| + root.func_new("echo-s64") { |n| t.s64.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-s64").call(-9_223_372_036_854_775_808)).to eq(-9_223_372_036_854_775_808) + expect(instance.get_func("test-s64").call(9_223_372_036_854_775_807)).to eq(9_223_372_036_854_775_807) + end + + it "provides a function with u64" do + stub_component_imports(linker, except: :"echo-u64") + + linker.root do |root| + root.func_new("echo-u64") { |n| t.u64.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-u64").call(18_446_744_073_709_551_615)).to eq(18_446_744_073_709_551_615) + end + + it "provides a function with f32" do + stub_component_imports(linker, except: :"echo-f32") + + linker.root do |root| + root.func_new("echo-f32") { |n| t.float32.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + result = instance.get_func("test-f32").call(3.14) + expect(result).to be_within(0.01).of(3.14) + end + + it "provides a function with f64" do + stub_component_imports(linker, except: :"echo-f64") + + linker.root do |root| + root.func_new("echo-f64") { |n| t.float64.wrap(n) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-f64").call(3.141592653589793)).to eq(3.141592653589793) + end + + it "provides a function with char" do + stub_component_imports(linker, except: :"echo-char") + + linker.root do |root| + root.func_new("echo-char") { |c| t.char.wrap(c) } + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-char").call("A")).to eq("A") + expect(instance.get_func("test-char").call("🎉")).to eq("🎉") + end + + it "provides a function with enum" do + stub_component_imports(linker, except: :"echo-enum") + + linker.root do |root| + root.func_new("echo-enum") do |color| + t.enum(["red", "green", "blue"]).wrap(color) + end + end + + instance = linker.instantiate(store, @host_imports_component) + expect(instance.get_func("test-enum").call("red")).to eq("red") + expect(instance.get_func("test-enum").call("green")).to eq("green") + expect(instance.get_func("test-enum").call("blue")).to eq("blue") + end + + it "provides a function with variant" do + stub_component_imports(linker, except: :"echo-variant") + + variant_type = t.variant( + "circle" => t.float32, + "rectangle" => t.tuple([t.float32, t.float32]), + "point" => nil + ) + + linker.root do |root| + root.func_new("echo-variant") do |shape| + variant_type.wrap(shape) + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-variant") + + result = func.call(Variant.new("circle", 5.0)) + expect(result.name).to eq("circle") + expect(result.value).to be_within(0.01).of(5.0) + + result = func.call(Variant.new("rectangle", [10.0, 20.0])) + expect(result.name).to eq("rectangle") + expect(result.value).to eq([10.0, 20.0]) + + result = func.call(Variant.new("point", nil)) + expect(result.name).to eq("point") + expect(result.value).to be_nil + end + + it "provides a function with flags" do + stub_component_imports(linker, except: :"echo-flags") + + linker.root do |root| + root.func_new("echo-flags") do |perms| + t.flags(["read", "write", "execute"]).wrap(perms) + end + end + + instance = linker.instantiate(store, @host_imports_component) + func = instance.get_func("test-flags") + + expect(func.call([])).to eq([]) + expect(func.call(["read"])).to eq(["read"]) + expect(func.call(["read", "write"])).to eq(["read", "write"]) + expect(func.call(["read", "write", "execute"])).to eq(["read", "write", "execute"]) + end end context "with nested instances" do From 64e411eedf2b7207b9a291002988f16b6db46f69 Mon Sep 17 00:00:00 2001 From: william-stacken Date: Fri, 22 May 2026 13:05:45 +0200 Subject: [PATCH 4/4] Remove type warpping of return values --- ext/src/ruby_api/component.rs | 2 - ext/src/ruby_api/component/convert.rs | 271 +--------------- ext/src/ruby_api/component/func.rs | 2 +- ext/src/ruby_api/component/linker.rs | 96 ++---- ext/src/ruby_api/component/types.rs | 434 -------------------------- spec/unit/component/linker_spec.rb | 165 ++++------ 6 files changed, 100 insertions(+), 870 deletions(-) delete mode 100644 ext/src/ruby_api/component/types.rs diff --git a/ext/src/ruby_api/component.rs b/ext/src/ruby_api/component.rs index a4dc72d5..1ceb68ff 100644 --- a/ext/src/ruby_api/component.rs +++ b/ext/src/ruby_api/component.rs @@ -2,7 +2,6 @@ mod convert; mod func; mod instance; mod linker; -mod types; mod wasi_command; use super::root; @@ -170,7 +169,6 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { linker::init(ruby, &namespace)?; instance::init(ruby, &namespace)?; func::init(ruby, &namespace)?; - types::init(ruby, &namespace)?; convert::init(ruby)?; wasi_command::init(ruby, &namespace)?; diff --git a/ext/src/ruby_api/component/convert.rs b/ext/src/ruby_api/component/convert.rs index cb1c1dc1..0f4f9f7f 100644 --- a/ext/src/ruby_api/component/convert.rs +++ b/ext/src/ruby_api/component/convert.rs @@ -9,8 +9,6 @@ use magnus::{ }; use wasmtime::component::{Type, Val}; -use super::types::{ComponentType, WrappedValue}; - define_rb_intern!( // For Component::Result OK => "ok", @@ -105,7 +103,7 @@ pub(crate) fn component_val_to_rb( pub(crate) fn rb_to_component_val( value: Value, - _store: &StoreContextValue, + _store: Option<&StoreContextValue>, ty: &Type, ) -> Result { let ruby = Ruby::get_with(value); @@ -307,273 +305,6 @@ fn variant_class(ruby: &Ruby) -> RClass { ruby.get_inner(&VARIANT_CLASS) } -/// Extract type and value from a wrapped value and convert to Val -/// This is the primary conversion path for host function return values -pub(super) fn wrapped_to_component_val( - value: Value, - _store: Option<&StoreContextValue>, -) -> Result { - let ruby = Ruby::get_with(value); - - // Try to convert to WrappedValue - let wrapped: &WrappedValue = magnus::TryConvert::try_convert(value).map_err(|_| { - Error::new( - ruby.exception_type_error(), - format!( - "host function must return wrapped value (e.g., Type::U32.wrap(value)), got {}", - unsafe { value.classname() } - ), - ) - })?; - - // Extract the inner value and type, then validate and convert - validate_and_convert(wrapped.value(&ruby), _store, wrapped.component_type()) -} - -/// Validate a Ruby value against a ComponentType and convert to Val -/// This is used for host functions where we define types standalone -pub(super) fn validate_and_convert( - value: Value, - _store: Option<&StoreContextValue>, - ty: &ComponentType, -) -> Result { - let ruby = Ruby::get_with(value); - match ty { - ComponentType::Bool => { - if value.as_raw() == ruby.qtrue().as_raw() { - Ok(Val::Bool(true)) - } else if value.as_raw() == ruby.qfalse().as_raw() { - Ok(Val::Bool(false)) - } else { - Err(Error::new( - ruby.exception_type_error(), - format!("expected bool, got {}", unsafe { value.classname() }), - )) - } - } - ComponentType::S8 => i8::try_convert(value) - .map(Val::S8) - .map_err(|_| error!("expected s8, got {}", value.inspect())), - ComponentType::U8 => u8::try_convert(value) - .map(Val::U8) - .map_err(|_| error!("expected u8, got {}", value.inspect())), - ComponentType::S16 => i16::try_convert(value) - .map(Val::S16) - .map_err(|_| error!("expected s16, got {}", value.inspect())), - ComponentType::U16 => u16::try_convert(value) - .map(Val::U16) - .map_err(|_| error!("expected u16, got {}", value.inspect())), - ComponentType::S32 => i32::try_convert(value) - .map(Val::S32) - .map_err(|_| error!("expected s32, got {}", value.inspect())), - ComponentType::U32 => u32::try_convert(value) - .map(Val::U32) - .map_err(|_| error!("expected u32, got {}", value.inspect())), - ComponentType::S64 => i64::try_convert(value) - .map(Val::S64) - .map_err(|_| error!("expected s64, got {}", value.inspect())), - ComponentType::U64 => u64::try_convert(value) - .map(Val::U64) - .map_err(|_| error!("expected u64, got {}", value.inspect())), - ComponentType::Float32 => f32::try_convert(value) - .map(Val::Float32) - .map_err(|_| error!("expected float32, got {}", value.inspect())), - ComponentType::Float64 => f64::try_convert(value) - .map(Val::Float64) - .map_err(|_| error!("expected float64, got {}", value.inspect())), - ComponentType::Char => value - .to_r_string() - .and_then(|s| s.to_char()) - .map(Val::Char) - .map_err(|_| error!("expected char, got {}", value.inspect())), - ComponentType::String => RString::try_convert(value) - .and_then(|s| s.to_string()) - .map(Val::String) - .map_err(|_| error!("expected string, got {}", value.inspect())), - ComponentType::List(element_ty) => { - let rarray = RArray::try_convert(value) - .map_err(|_| error!("expected list (array), got {}", value.inspect()))?; - - let mut vals: Vec = Vec::with_capacity(rarray.len()); - for (i, item_value) in unsafe { rarray.as_slice() }.iter().enumerate() { - let component_val = validate_and_convert(*item_value, _store, element_ty) - .map_err(|e| e.append(format!(" (list item at index {i})")))?; - vals.push(component_val); - } - Ok(Val::List(vals)) - } - ComponentType::Record(fields) => { - let hash = RHash::try_convert(value) - .map_err(|_| error!("expected record (hash), got {}", value.inspect()))?; - - let mut kv = Vec::with_capacity(fields.len()); - for field in fields { - let field_value = hash - .get(field.name.as_str()) - .ok_or_else(|| error!("record field missing: {}", field.name)) - .and_then(|v| { - validate_and_convert(v, _store, &field.ty) - .map_err(|e| e.append(format!(" (record field \"{}\")", field.name))) - })?; - - kv.push((field.name.clone(), field_value)) - } - Ok(Val::Record(kv)) - } - ComponentType::Tuple(types) => { - let rarray = RArray::try_convert(value) - .map_err(|_| error!("expected tuple (array), got {}", value.inspect()))?; - - if types.len() != rarray.len() { - return Err(error!( - "expected tuple with {} elements, got {}", - types.len(), - rarray.len() - )); - } - - let mut vals: Vec = Vec::with_capacity(rarray.len()); - for (i, (ty, item_value)) in types - .iter() - .zip(unsafe { rarray.as_slice() }.iter()) - .enumerate() - { - let component_val = validate_and_convert(*item_value, _store, ty) - .map_err(|e| e.append(format!(" (tuple element at index {i})")))?; - vals.push(component_val); - } - - Ok(Val::Tuple(vals)) - } - ComponentType::Variant(cases) => { - let name: RString = value - .funcall(NAME.into_id_with(&ruby), ()) - .map_err(|_| error!("expected variant, got {}", value.inspect()))?; - let name = name.to_string()?; - - let case = cases - .iter() - .find(|c| c.name == name.as_str()) - .ok_or_else(|| { - error!( - "invalid variant case \"{}\", valid cases: [{}]", - name, - cases - .iter() - .map(|c| format!("\"{}\"", c.name)) - .collect::>() - .join(", ") - ) - })?; - - let payload_rb: Value = value.funcall(VALUE.into_id_with(&ruby), ())?; - let payload_val = match (&case.ty, payload_rb.is_nil()) { - (Some(ty), _) => validate_and_convert(payload_rb, _store, ty) - .map(|val| Some(Box::new(val))) - .map_err(|e| e.append(format!(" (variant value for \"{}\")", &name))), - - (None, true) => Ok(None), - - (None, false) => err!( - "expected no value for variant case \"{}\", got {}", - &name, - payload_rb.inspect() - ), - }?; - - Ok(Val::Variant(name, payload_val)) - } - ComponentType::Enum(cases) => { - let rstring = RString::try_convert(value) - .map_err(|_| error!("expected enum (string), got {}", value.inspect()))?; - let case_name = rstring.to_string()?; - - if !cases.contains(&case_name) { - return Err(error!( - "invalid enum case \"{}\", valid cases: [{}]", - case_name, - cases - .iter() - .map(|c| format!("\"{}\"", c)) - .collect::>() - .join(", ") - )); - } - - Ok(Val::Enum(case_name)) - } - ComponentType::Option(inner_ty) => { - if value.is_nil() { - Ok(Val::Option(None)) - } else { - validate_and_convert(value, _store, inner_ty) - .map(|v| Val::Option(Some(Box::new(v)))) - } - } - ComponentType::Result { ok, err } => { - let is_ok = value - .funcall::<_, (), bool>(IS_OK.into_id_with(&ruby), ()) - .map_err(|_| error!("expected result, got {}", value.inspect()))?; - - if is_ok { - let ok_value = value.funcall::<_, (), Value>(OK.into_id_with(&ruby), ())?; - match ok { - Some(ty) => validate_and_convert(ok_value, _store, ty) - .map(|val| Val::Result(Result::Ok(Some(Box::new(val))))), - None => { - if ok_value.is_nil() { - Ok(Val::Result(Ok(None))) - } else { - err!( - "expected nil for result<_, E> ok branch, got {}", - ok_value.inspect() - ) - } - } - } - } else { - let err_value = value.funcall::<_, (), Value>(ERROR.into_id_with(&ruby), ())?; - match err { - Some(ty) => validate_and_convert(err_value, _store, ty) - .map(|val| Val::Result(Result::Err(Some(Box::new(val))))), - None => { - if err_value.is_nil() { - Ok(Val::Result(Err(None))) - } else { - err!( - "expected nil for result error branch, got {}", - err_value.inspect() - ) - } - } - } - } - } - ComponentType::Flags(flag_names) => { - let flags_vec = Vec::::try_convert(value).map_err(|_| { - error!("expected flags (array of strings), got {}", value.inspect()) - })?; - - // Validate that all flags are valid - for flag in &flags_vec { - if !flag_names.contains(flag) { - return Err(error!( - "invalid flag \"{}\", valid flags: [{}]", - flag, - flag_names - .iter() - .map(|f| format!("\"{}\"", f)) - .collect::>() - .join(", ") - )); - } - } - - Ok(Val::Flags(flags_vec)) - } - } -} - pub fn init(ruby: &Ruby) -> Result<(), Error> { // Warm up let _ = result_class(ruby); diff --git a/ext/src/ruby_api/component/func.rs b/ext/src/ruby_api/component/func.rs index 437f391b..9e115b68 100644 --- a/ext/src/ruby_api/component/func.rs +++ b/ext/src/ruby_api/component/func.rs @@ -152,7 +152,7 @@ fn convert_params<'a>( .try_into() .map_err(|_| Error::new(ruby.exception_arg_error(), "too many params"))?; - let component_val = rb_to_component_val(*value, store, &ty.1) + let component_val = rb_to_component_val(*value, Some(store), &ty.1) .map_err(|error| error.append(format!(" (param at index {i})")))?; params.push(component_val); diff --git a/ext/src/ruby_api/component/linker.rs b/ext/src/ruby_api/component/linker.rs index e5672e9a..f89616eb 100644 --- a/ext/src/ruby_api/component/linker.rs +++ b/ext/src/ruby_api/component/linker.rs @@ -1,5 +1,4 @@ use super::convert; -use super::types; use super::{Component, Instance}; use crate::{ err, @@ -284,36 +283,34 @@ impl<'a> LinkerInstance<'a> { /// @yard /// Define a host function in this linker instance. /// - /// Host functions must return wrapped values to specify their types. - /// Use {Type#wrap} to wrap return values with their type information. + /// Host functions return plain Ruby values which are automatically validated + /// and converted based on the function's type signature from the component. /// /// @example Simple scalar return /// root.func_new("add") do |a, b| - /// Type::U32.wrap(a + b) + /// a + b # Returns u32 /// end /// - /// @example Returning a list (the list itself, not array of results) + /// @example Returning a list /// root.func_new("get-numbers") do - /// Type::List(Type::S32).wrap([1, 2, 3]) + /// [1, 2, 3] # Returns list /// end /// - /// @example Multiple return values (array of wrapped values) - /// root.func_new("divide-with-remainder") do |a, b| - /// [Type::U32.wrap(a / b), Type::U32.wrap(a % b)] + /// @example Returning a tuple + /// root.func_new("make-tuple") do |n, s, b| + /// [n, s, b] # Returns tuple /// end /// /// @example Complex types (records, results, etc.) /// root.func_new("make-point") do |x, y| - /// point_type = Type::Record("x" => Type::S32, "y" => Type::S32) - /// point_type.wrap({"x" => x, "y" => y}) + /// {"x" => x, "y" => y} # Returns record with x and y fields /// end /// /// @def func_new(name, &block) /// @param name [String] The function name - /// @yield [caller, *args] The block implementing the host function - /// @yieldparam caller [Caller] The caller context (not yet fully implemented) + /// @yield [*args] The block implementing the host function /// @yieldparam args [Array] The function arguments, converted from component values - /// @yieldreturn [WrappedValue, Array] Wrapped result(s) with type information + /// @yieldreturn [Object] Result value matching the function's return type. Use arrays for lists and tuples, hashes for records. /// @return [LinkerInstance] +self+ fn func_new(_ruby: &Ruby, rb_self: Obj, args: &[Value]) -> Result, Error> { let args = scan_args::<(RString,), (), (), (), (), Proc>(args)?; @@ -352,7 +349,7 @@ impl<'a> LinkerInstance<'a> { } /// Create a closure that wraps a Ruby Proc for use as a component host function -/// The closure expects wrapped return values (WrappedValue) that carry type information +/// The closure uses the function's type signature for automatic validation and conversion fn make_component_func_closure( callable: Opaque, ) -> impl Fn( @@ -365,7 +362,7 @@ fn make_component_func_closure( + Sync + 'static { move |mut store_context: wasmtime::StoreContextMut<'_, StoreData>, - _func: wasmtime::component::types::ComponentFunc, + func: wasmtime::component::types::ComponentFunc, params: &[Val], results: &mut [Val]| { let ruby = Ruby::get().unwrap(); @@ -391,69 +388,36 @@ fn make_component_func_closure( wasmtime::Error::msg("") })?; + // Get expected result types from function signature + let results_types: Vec<_> = func.results().collect(); + let num_results = results_types.len(); + // Handle result conversion based on arity - let num_results = results.len(); + // Note: WIT only supports 0 or 1 return values (use tuples for multiple values) match num_results { 0 => { // No return value expected Ok(()) } 1 => { - // Single return value - accept either the value directly or in an array - let result_value = if let Ok(result_array) = RArray::to_ary(proc_result) { - // User returned [wrapped_value] - unwrap the array - if result_array.len() != 1 { - store_context.data_mut().set_error(Error::new( - ruby.exception_arg_error(), - format!("expected 1 result, got {}", result_array.len()), - )); - return Err(wasmtime::Error::msg("")); - } - unsafe { result_array.as_slice()[0] } - } else { - // User returned wrapped_value directly (most common case) - proc_result - }; - - // Extract type and value from WrappedValue, then validate and convert - let converted = - convert::wrapped_to_component_val(result_value, None).map_err(|e| { - // Store type errors on StoreData as well + // Single return value - convert directly + // Don't unwrap arrays - the value might be a list or tuple type + let expected_ty = &results_types[0]; + let converted = convert::rb_to_component_val(proc_result, None, expected_ty) + .map_err(|e| { store_context.data_mut().set_error(e); wasmtime::Error::msg("") })?; results[0] = converted; Ok(()) } - n => { - // Multiple return values - expect an array - let result_array = RArray::to_ary(proc_result).map_err(|_| { - store_context.data_mut().set_error(Error::new( - ruby.exception_type_error(), - "expected array of results", - )); - wasmtime::Error::msg("") - })?; - - if result_array.len() != n { - store_context.data_mut().set_error(Error::new( - ruby.exception_arg_error(), - format!("expected {} results, got {}", n, result_array.len()), - )); - return Err(wasmtime::Error::msg("")); - } - - for (i, result_value) in unsafe { result_array.as_slice() }.iter().enumerate() { - let converted = convert::wrapped_to_component_val(*result_value, None) - .map_err(|e| { - // Append index information to error - let error_with_context = e.append(format!(" (result at index {i})")); - store_context.data_mut().set_error(error_with_context); - wasmtime::Error::msg("") - })?; - results[i] = converted; - } - Ok(()) + _ => { + // WIT doesn't support multiple return values - this should never happen + store_context.data_mut().set_error(Error::new( + ruby.exception_runtime_error(), + format!("unexpected number of results: {}", num_results), + )); + Err(wasmtime::Error::msg("")) } } } diff --git a/ext/src/ruby_api/component/types.rs b/ext/src/ruby_api/component/types.rs deleted file mode 100644 index 84442e5b..00000000 --- a/ext/src/ruby_api/component/types.rs +++ /dev/null @@ -1,434 +0,0 @@ -use crate::error; -use magnus::{ - class, function, gc::Marker, method, prelude::*, r_hash::ForEach, value::Opaque, Error, - Module as _, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value, -}; -use std::fmt; - -/// Standalone component type system that can be constructed independently -/// of a component instance. Used for defining host function signatures. -#[derive(Clone, Debug)] -pub enum ComponentType { - Bool, - S8, - U8, - S16, - U16, - S32, - U32, - S64, - U64, - Float32, - Float64, - Char, - String, - List(Box), - Record(Vec), - Tuple(Vec), - Variant(Vec), - Enum(Vec), - Option(Box), - Result { - ok: Option>, - err: Option>, - }, - Flags(Vec), -} - -#[derive(Clone, Debug)] -pub struct RecordField { - pub name: String, - pub ty: ComponentType, -} - -#[derive(Clone, Debug)] -pub struct VariantCase { - pub name: String, - pub ty: Option, -} - -impl fmt::Display for ComponentType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ComponentType::Bool => write!(f, "bool"), - ComponentType::S8 => write!(f, "s8"), - ComponentType::U8 => write!(f, "u8"), - ComponentType::S16 => write!(f, "s16"), - ComponentType::U16 => write!(f, "u16"), - ComponentType::S32 => write!(f, "s32"), - ComponentType::U32 => write!(f, "u32"), - ComponentType::S64 => write!(f, "s64"), - ComponentType::U64 => write!(f, "u64"), - ComponentType::Float32 => write!(f, "float32"), - ComponentType::Float64 => write!(f, "float64"), - ComponentType::Char => write!(f, "char"), - ComponentType::String => write!(f, "string"), - ComponentType::List(inner) => write!(f, "list<{}>", inner), - ComponentType::Record(fields) => { - write!(f, "record {{")?; - for (i, field) in fields.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}: {}", field.name, field.ty)?; - } - write!(f, "}}") - } - ComponentType::Tuple(types) => { - write!(f, "tuple<")?; - for (i, ty) in types.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", ty)?; - } - write!(f, ">") - } - ComponentType::Variant(cases) => { - write!(f, "variant {{")?; - for (i, case) in cases.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", case.name)?; - if let Some(ty) = &case.ty { - write!(f, "({})", ty)?; - } - } - write!(f, "}}") - } - ComponentType::Enum(cases) => { - write!(f, "enum {{")?; - for (i, case) in cases.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", case)?; - } - write!(f, "}}") - } - ComponentType::Option(inner) => write!(f, "option<{}>", inner), - ComponentType::Result { ok, err } => { - write!(f, "result<")?; - if let Some(ok) = ok { - write!(f, "{}", ok)?; - } else { - write!(f, "_")?; - } - write!(f, ", ")?; - if let Some(err) = err { - write!(f, "{}", err)?; - } else { - write!(f, "_")?; - } - write!(f, ">") - } - ComponentType::Flags(flags) => { - write!(f, "flags {{")?; - for (i, flag) in flags.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", flag)?; - } - write!(f, "}}") - } - } - } -} - -/// @yard -/// @rename Wasmtime::Component::Type -/// Ruby wrapper for ComponentType - stored as opaque Rust data -/// Factory methods for creating component types -/// @see https://docs.wasmtime.dev/api/wasmtime/component/enum.Val.html -/// -/// @!method self.bool -/// @return [Type] A boolean type -/// @!method self.s8 -/// @return [Type] A signed 8-bit integer type -/// @!method self.u8 -/// @return [Type] An unsigned 8-bit integer type -/// @!method self.s16 -/// @return [Type] A signed 16-bit integer type -/// @!method self.u16 -/// @return [Type] An unsigned 16-bit integer type -/// @!method self.s32 -/// @return [Type] A signed 32-bit integer type -/// @!method self.u32 -/// @return [Type] An unsigned 32-bit integer type -/// @!method self.s64 -/// @return [Type] A signed 64-bit integer type -/// @!method self.u64 -/// @return [Type] An unsigned 64-bit integer type -/// @!method self.float32 -/// @return [Type] A 32-bit floating point type -/// @!method self.float64 -/// @return [Type] A 64-bit floating point type -/// @!method self.char -/// @return [Type] A Unicode character type -/// @!method self.string -/// @return [Type] A UTF-8 string type -/// @!method self.list(element_type) -/// @param element_type [Type] The type of list elements -/// @return [Type] A list type -/// @!method self.record(fields) -/// @param fields [Hash] A hash of field names to types -/// @return [Type] A record (struct) type -/// @!method self.tuple(types) -/// @param types [Array] The types in the tuple -/// @return [Type] A tuple type -/// @!method self.variant(cases) -/// @param cases [Hash] A hash of case names to optional types -/// @return [Type] A variant type -/// @!method self.enum(cases) -/// @param cases [Array] The enum case names -/// @return [Type] An enum type -/// @!method self.option(inner_type) -/// @param inner_type [Type] The type of the optional value -/// @return [Type] An option type -/// @!method self.result(ok_type, err_type) -/// @param ok_type [Type, nil] The type of the ok variant (nil for result<_, E>) -/// @param err_type [Type, nil] The type of the error variant (nil for result) -/// @return [Type] A result type -/// @!method self.flags(flag_names) -/// @param flag_names [Array] The flag names -/// @return [Type] A flags type -#[derive(Clone, TypedData)] -#[magnus(class = "Wasmtime::Component::Type", free_immediately)] -pub struct RbComponentType { - inner: ComponentType, -} - -impl magnus::DataTypeFunctions for RbComponentType {} - -impl RbComponentType { - pub fn new(inner: ComponentType) -> Self { - Self { inner } - } - - /// @yard - /// Wrap a Ruby value with type information for use as a host function return value. - /// @def wrap(value) - /// @param value [Object] The Ruby value to wrap - /// @return [WrappedValue] A wrapped value that can be returned from host functions - pub fn wrap(&self, value: Value) -> WrappedValue { - WrappedValue::new(value, self.inner.clone()) - } -} - -/// @yard -/// @rename Wasmtime::Component::WrappedValue -/// A Ruby value wrapped with component model type information. -/// Returned from {Type#wrap} and used as host function return values. -#[derive(Clone, TypedData)] -#[magnus(class = "Wasmtime::Component::WrappedValue", mark, free_immediately)] -pub struct WrappedValue { - value: Opaque, - component_type: ComponentType, -} -unsafe impl Send for WrappedValue {} - -impl magnus::DataTypeFunctions for WrappedValue { - fn mark(&self, marker: &magnus::gc::Marker) { - marker.mark(self.value); - } -} - -impl WrappedValue { - pub fn new(value: Value, component_type: ComponentType) -> Self { - Self { - value: value.into(), - component_type, - } - } - - /// Get the wrapped Ruby value - pub fn value(&self, ruby: &Ruby) -> Value { - ruby.get_inner(self.value) - } - - /// Get the component type information - pub fn component_type(&self) -> &ComponentType { - &self.component_type - } -} - -pub struct TypeFactory; - -impl TypeFactory { - pub fn bool(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::Bool) - } - - pub fn s8(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::S8) - } - - pub fn u8(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::U8) - } - - pub fn s16(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::S16) - } - - pub fn u16(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::U16) - } - - pub fn s32(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::S32) - } - - pub fn u32(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::U32) - } - - pub fn s64(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::S64) - } - - pub fn u64(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::U64) - } - - pub fn float32(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::Float32) - } - - pub fn float64(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::Float64) - } - - pub fn char(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::Char) - } - - pub fn string(_ruby: &Ruby) -> RbComponentType { - RbComponentType::new(ComponentType::String) - } - - pub fn list(_ruby: &Ruby, element_type: &RbComponentType) -> RbComponentType { - RbComponentType::new(ComponentType::List(Box::new(element_type.inner.clone()))) - } - - pub fn record(_ruby: &Ruby, fields: RHash) -> Result { - let mut record_fields = Vec::new(); - - // Use foreach to iterate over hash - fields.foreach(|key: Value, ty_value: Value| { - let name = RString::try_convert(key)?.to_string()?; - let ty_ref: &RbComponentType = TryConvert::try_convert(ty_value)?; - record_fields.push(RecordField { - name, - ty: ty_ref.inner.clone(), - }); - Ok(ForEach::Continue) - })?; - - Ok(RbComponentType::new(ComponentType::Record(record_fields))) - } - - pub fn tuple(_ruby: &Ruby, types: RArray) -> Result { - let mut tuple_types = Vec::with_capacity(types.len()); - - for ty_value in unsafe { types.as_slice() } { - let ty_ref: &RbComponentType = TryConvert::try_convert(*ty_value)?; - tuple_types.push(ty_ref.inner.clone()); - } - - Ok(RbComponentType::new(ComponentType::Tuple(tuple_types))) - } - - pub fn variant(_ruby: &Ruby, cases: RHash) -> Result { - let mut variant_cases = Vec::new(); - - // Use foreach to iterate over hash - cases.foreach(|key: Value, ty_value: Value| { - let name = RString::try_convert(key)?.to_string()?; - let ty = if ty_value.is_nil() { - None - } else { - let ty_ref: &RbComponentType = TryConvert::try_convert(ty_value)?; - Some(ty_ref.inner.clone()) - }; - variant_cases.push(VariantCase { name, ty }); - Ok(ForEach::Continue) - })?; - - Ok(RbComponentType::new(ComponentType::Variant(variant_cases))) - } - - pub fn enum_type(_ruby: &Ruby, cases: RArray) -> Result { - let mut enum_cases = Vec::with_capacity(cases.len()); - - for case_value in unsafe { cases.as_slice() } { - let case_name = RString::try_convert(*case_value)?.to_string()?; - enum_cases.push(case_name); - } - - Ok(RbComponentType::new(ComponentType::Enum(enum_cases))) - } - - pub fn option(_ruby: &Ruby, inner_type: &RbComponentType) -> RbComponentType { - RbComponentType::new(ComponentType::Option(Box::new(inner_type.inner.clone()))) - } - - pub fn result( - _ruby: &Ruby, - ok_type: Option<&RbComponentType>, - err_type: Option<&RbComponentType>, - ) -> RbComponentType { - RbComponentType::new(ComponentType::Result { - ok: ok_type.map(|t| Box::new(t.inner.clone())), - err: err_type.map(|t| Box::new(t.inner.clone())), - }) - } - - pub fn flags(_ruby: &Ruby, flag_names: RArray) -> Result { - let mut flags = Vec::with_capacity(flag_names.len()); - - for flag_value in unsafe { flag_names.as_slice() } { - let flag_name = RString::try_convert(*flag_value)?.to_string()?; - flags.push(flag_name); - } - - Ok(RbComponentType::new(ComponentType::Flags(flags))) - } -} - -pub fn init(ruby: &Ruby, namespace: &magnus::RModule) -> Result<(), Error> { - let type_class = namespace.define_class("Type", ruby.class_object())?; - - // Factory methods - type_class.define_singleton_method("bool", function!(TypeFactory::bool, 0))?; - type_class.define_singleton_method("s8", function!(TypeFactory::s8, 0))?; - type_class.define_singleton_method("u8", function!(TypeFactory::u8, 0))?; - type_class.define_singleton_method("s16", function!(TypeFactory::s16, 0))?; - type_class.define_singleton_method("u16", function!(TypeFactory::u16, 0))?; - type_class.define_singleton_method("s32", function!(TypeFactory::s32, 0))?; - type_class.define_singleton_method("u32", function!(TypeFactory::u32, 0))?; - type_class.define_singleton_method("s64", function!(TypeFactory::s64, 0))?; - type_class.define_singleton_method("u64", function!(TypeFactory::u64, 0))?; - type_class.define_singleton_method("float32", function!(TypeFactory::float32, 0))?; - type_class.define_singleton_method("float64", function!(TypeFactory::float64, 0))?; - type_class.define_singleton_method("char", function!(TypeFactory::char, 0))?; - type_class.define_singleton_method("string", function!(TypeFactory::string, 0))?; - type_class.define_singleton_method("list", function!(TypeFactory::list, 1))?; - type_class.define_singleton_method("record", function!(TypeFactory::record, 1))?; - type_class.define_singleton_method("tuple", function!(TypeFactory::tuple, 1))?; - type_class.define_singleton_method("variant", function!(TypeFactory::variant, 1))?; - type_class.define_singleton_method("enum", function!(TypeFactory::enum_type, 1))?; - type_class.define_singleton_method("option", function!(TypeFactory::option, 1))?; - type_class.define_singleton_method("result", function!(TypeFactory::result, 2))?; - type_class.define_singleton_method("flags", function!(TypeFactory::flags, 1))?; - - // Instance method for wrapping values - type_class.define_method("wrap", method!(RbComponentType::wrap, 1))?; - - // WrappedValue class - let _wrapped_value_class = namespace.define_class("WrappedValue", ruby.class_object())?; - - Ok(()) -} diff --git a/spec/unit/component/linker_spec.rb b/spec/unit/component/linker_spec.rb index 0d32f656..fc2c1346 100644 --- a/spec/unit/component/linker_spec.rb +++ b/spec/unit/component/linker_spec.rb @@ -37,12 +37,10 @@ module Component end describe "LinkerInstance#func_new" do - let(:t) { Type } - it "defines a function" do linker.root do |root| root.func_new("greet") do |name| - t.string.wrap("Hello, #{name}!") + "Hello, #{name}!" end end @@ -52,7 +50,7 @@ module Component it "defines functions in nested instances" do linker.instance("math") do |math| math.func_new("add") do |a, b| - t.u32.wrap(a + b) + a + b end end @@ -85,64 +83,54 @@ def stub_component_imports(linker, except: nil) # Stub root functions linker.root do |root| - root.func_new("greet") { |name| t.string.wrap(name) } unless skip_funcs.include?("greet") - root.func_new("add") { |a, b| t.u32.wrap(a + b) } unless skip_funcs.include?("add") - root.func_new("get-constant") { t.u32.wrap(0) } unless skip_funcs.include?("get-constant") + root.func_new("greet") { |name| name } unless skip_funcs.include?("greet") + root.func_new("add") { |a, b| a + b } unless skip_funcs.include?("add") + root.func_new("get-constant") { 0 } unless skip_funcs.include?("get-constant") unless skip_funcs.include?("make-point") - point_type = t.record("x" => t.s32, "y" => t.s32) root.func_new("make-point") do |x, y| - point_type.wrap({"x" => x, "y" => y}) + {"x" => x, "y" => y} end end - root.func_new("sum-list") { |nums| t.s32.wrap(nums.sum) } unless skip_funcs.include?("sum-list") - root.func_new("maybe-double") { |n| t.option(t.u32).wrap(n) } unless skip_funcs.include?("maybe-double") + root.func_new("sum-list") { |nums| nums.sum } unless skip_funcs.include?("sum-list") + root.func_new("maybe-double") { |n| n } unless skip_funcs.include?("maybe-double") unless skip_funcs.include?("safe-divide") root.func_new("safe-divide") do |a, b| - t.result(t.u32, t.string).wrap(Result.ok(a)) + Result.ok(a) end end - root.func_new("get-numbers") { t.list(t.s32).wrap([]) } unless skip_funcs.include?("get-numbers") + root.func_new("get-numbers") { [] } unless skip_funcs.include?("get-numbers") unless skip_funcs.include?("make-tuple") - tuple_type = t.tuple([t.u32, t.string, t.bool]) root.func_new("make-tuple") do |n, s, b| - tuple_type.wrap([n, s, b]) + [n, s, b] end end unless skip_funcs.include?("analyze-numbers") - tuple_type = t.tuple([t.s32, t.list(t.s32)]) root.func_new("analyze-numbers") do |nums| - tuple_type.wrap([0, nums]) + [0, nums] end end # Additional integer types - root.func_new("echo-s8") { |n| t.s8.wrap(n) } unless skip_funcs.include?("echo-s8") - root.func_new("echo-u8") { |n| t.u8.wrap(n) } unless skip_funcs.include?("echo-u8") - root.func_new("echo-s16") { |n| t.s16.wrap(n) } unless skip_funcs.include?("echo-s16") - root.func_new("echo-u16") { |n| t.u16.wrap(n) } unless skip_funcs.include?("echo-u16") - root.func_new("echo-s64") { |n| t.s64.wrap(n) } unless skip_funcs.include?("echo-s64") - root.func_new("echo-u64") { |n| t.u64.wrap(n) } unless skip_funcs.include?("echo-u64") + root.func_new("echo-s8") { |n| n } unless skip_funcs.include?("echo-s8") + root.func_new("echo-u8") { |n| n } unless skip_funcs.include?("echo-u8") + root.func_new("echo-s16") { |n| n } unless skip_funcs.include?("echo-s16") + root.func_new("echo-u16") { |n| n } unless skip_funcs.include?("echo-u16") + root.func_new("echo-s64") { |n| n } unless skip_funcs.include?("echo-s64") + root.func_new("echo-u64") { |n| n } unless skip_funcs.include?("echo-u64") # Float types - root.func_new("echo-f32") { |n| t.float32.wrap(n) } unless skip_funcs.include?("echo-f32") - root.func_new("echo-f64") { |n| t.float64.wrap(n) } unless skip_funcs.include?("echo-f64") + root.func_new("echo-f32") { |n| n } unless skip_funcs.include?("echo-f32") + root.func_new("echo-f64") { |n| n } unless skip_funcs.include?("echo-f64") # Char type - root.func_new("echo-char") { |c| t.char.wrap(c) } unless skip_funcs.include?("echo-char") + root.func_new("echo-char") { |c| c } unless skip_funcs.include?("echo-char") # Enum, variant, flags - root.func_new("echo-enum") { |c| t.enum(["red", "green", "blue"]).wrap(c) } unless skip_funcs.include?("echo-enum") - unless skip_funcs.include?("echo-variant") - variant_type = t.variant( - "circle" => t.float32, - "rectangle" => t.tuple([t.float32, t.float32]), - "point" => nil - ) - root.func_new("echo-variant") { |s| variant_type.wrap(s) } - end - root.func_new("echo-flags") { |p| t.flags(["read", "write", "execute"]).wrap(p) } unless skip_funcs.include?("echo-flags") + root.func_new("echo-enum") { |c| c } unless skip_funcs.include?("echo-enum") + root.func_new("echo-variant") { |s| s } unless skip_funcs.include?("echo-variant") + root.func_new("echo-flags") { |p| p } unless skip_funcs.include?("echo-flags") end # Stub math instance unless skipped unless skip_funcs.include?("math") linker.instance("math") do |math| - math.func_new("multiply") { |a, b| t.u32.wrap(a * b) } + math.func_new("multiply") { |a, b| a * b } end end end @@ -153,7 +141,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("greet") do |name| - t.string.wrap("Hello, #{name}!") + "Hello, #{name}!" end end @@ -168,7 +156,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("add") do |a, b| - t.u32.wrap(a + b) + a + b end end @@ -183,7 +171,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("get-constant") do - t.u32.wrap(1234) + 1234 end end @@ -198,11 +186,9 @@ def stub_component_imports(linker, except: nil) it "provides a function returning a record" do stub_component_imports(linker, except: :"make-point") - point_type = t.record("x" => t.s32, "y" => t.s32) - linker.root do |root| root.func_new("make-point") do |x, y| - point_type.wrap({"x" => x, "y" => y}) + {"x" => x, "y" => y} end end @@ -215,19 +201,17 @@ def stub_component_imports(linker, except: nil) it "validates field types in records" do stub_component_imports(linker, except: :"make-point") - point_type = t.record("x" => t.s32, "y" => t.s32) - linker.root do |root| root.func_new("make-point") do |_x, y| # Try to use wrong type for x field (string instead of s32) - point_type.wrap({"x" => "not a number", "y" => y}) + {"x" => "not a number", "y" => y} end end instance = linker.instantiate(store, @host_imports_component) func = instance.get_func("test-point") - expect { func.call(10, 20) }.to raise_error(Wasmtime::Error, /expected s32, got/) + expect { func.call(10, 20) }.to raise_error(TypeError, /no implicit conversion of String into Integer/) end it "provides a function accepting a list" do @@ -235,7 +219,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("sum-list") do |numbers| - t.s32.wrap(numbers.sum) + numbers.sum end end @@ -250,7 +234,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("maybe-double") do |n| - t.option(t.u32).wrap(n.nil? ? nil : n * 2) + n.nil? ? nil : n * 2 end end @@ -270,7 +254,7 @@ def stub_component_imports(linker, except: nil) else Result.ok(a / b) end - t.result(t.u32, t.string).wrap(result_val) + result_val end end @@ -286,7 +270,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("get-numbers") do - t.list(t.s32).wrap([1, 2, 3, 4, 5]) + [1, 2, 3, 4, 5] end end @@ -299,11 +283,9 @@ def stub_component_imports(linker, except: nil) it "provides a function returning a tuple" do stub_component_imports(linker, except: :"make-tuple") - tuple_type = t.tuple([t.u32, t.string, t.bool]) - linker.root do |root| root.func_new("make-tuple") do |n, s, b| - tuple_type.wrap([n, s, b]) + [n, s, b] end end @@ -316,11 +298,9 @@ def stub_component_imports(linker, except: nil) it "provides a function returning a tuple containing a list" do stub_component_imports(linker, except: :"analyze-numbers") - tuple_type = t.tuple([t.s32, t.list(t.s32)]) - linker.root do |root| root.func_new("analyze-numbers") do |numbers| - tuple_type.wrap([numbers.sum, numbers.sort]) + [numbers.sum, numbers.sort] end end @@ -334,7 +314,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-s8") linker.root do |root| - root.func_new("echo-s8") { |n| t.s8.wrap(n) } + root.func_new("echo-s8") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -346,7 +326,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-u8") linker.root do |root| - root.func_new("echo-u8") { |n| t.u8.wrap(n) } + root.func_new("echo-u8") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -357,7 +337,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-s16") linker.root do |root| - root.func_new("echo-s16") { |n| t.s16.wrap(n) } + root.func_new("echo-s16") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -369,7 +349,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-u16") linker.root do |root| - root.func_new("echo-u16") { |n| t.u16.wrap(n) } + root.func_new("echo-u16") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -380,7 +360,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-s64") linker.root do |root| - root.func_new("echo-s64") { |n| t.s64.wrap(n) } + root.func_new("echo-s64") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -392,7 +372,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-u64") linker.root do |root| - root.func_new("echo-u64") { |n| t.u64.wrap(n) } + root.func_new("echo-u64") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -403,7 +383,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-f32") linker.root do |root| - root.func_new("echo-f32") { |n| t.float32.wrap(n) } + root.func_new("echo-f32") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -415,7 +395,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-f64") linker.root do |root| - root.func_new("echo-f64") { |n| t.float64.wrap(n) } + root.func_new("echo-f64") { |n| n } end instance = linker.instantiate(store, @host_imports_component) @@ -426,7 +406,7 @@ def stub_component_imports(linker, except: nil) stub_component_imports(linker, except: :"echo-char") linker.root do |root| - root.func_new("echo-char") { |c| t.char.wrap(c) } + root.func_new("echo-char") { |c| c } end instance = linker.instantiate(store, @host_imports_component) @@ -439,7 +419,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("echo-enum") do |color| - t.enum(["red", "green", "blue"]).wrap(color) + color end end @@ -452,15 +432,9 @@ def stub_component_imports(linker, except: nil) it "provides a function with variant" do stub_component_imports(linker, except: :"echo-variant") - variant_type = t.variant( - "circle" => t.float32, - "rectangle" => t.tuple([t.float32, t.float32]), - "point" => nil - ) - linker.root do |root| root.func_new("echo-variant") do |shape| - variant_type.wrap(shape) + shape end end @@ -485,7 +459,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("echo-flags") do |perms| - t.flags(["read", "write", "execute"]).wrap(perms) + perms end end @@ -505,7 +479,7 @@ def stub_component_imports(linker, except: nil) linker.instance("math") do |math| math.func_new("multiply") do |a, b| - t.u32.wrap(a * b) + a * b end end @@ -525,7 +499,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("get-constant") do counter += 1 - t.u32.wrap(counter) + counter end end @@ -545,7 +519,7 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("greet") do |name| log << name - t.string.wrap("Hello, #{name}!") + "Hello, #{name}!" end end @@ -580,49 +554,46 @@ def stub_component_imports(linker, except: nil) linker.root do |root| root.func_new("add") do |_a, _b| - t.u32.wrap("not a number") + "not a number" # Returns string when u32 expected end end instance = linker.instantiate(store, @host_imports_component) func = instance.get_func("test-add") - expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /expected u32, got/) + expect { func.call(1, 2) }.to raise_error(TypeError, /conversion of String into Integer/) end - it "raises clear error when return value is not wrapped" do - stub_component_imports(linker, except: :add) + it "validates field types in returned records" do + stub_component_imports(linker, except: :"make-point") linker.root do |root| - root.func_new("add") do |a, b| - a + b # Forgot to wrap with Type::U32.wrap() + root.func_new("make-point") do |_x, y| + # Return record with wrong field type + {"x" => "not a number", "y" => y} end end instance = linker.instantiate(store, @host_imports_component) - func = instance.get_func("test-add") + func = instance.get_func("test-point") - expect { func.call(1, 2) }.to raise_error( - TypeError, - /host function must return wrapped value/ - ) + expect { func.call(10, 20) }.to raise_error(TypeError, /conversion of String into Integer/) end - it "raises Wasmtime error when wrapper type mismatches component expectation" do - stub_component_imports(linker, except: :add) + it "validates list element types" do + stub_component_imports(linker, except: :"get-numbers") linker.root do |root| - root.func_new("add") do |a, b| - # Component expects u32, but we return s32 - t.s32.wrap(a + b) + root.func_new("get-numbers") do + # Return list with wrong element type (strings instead of s32) + ["not", "numbers"] end end instance = linker.instantiate(store, @host_imports_component) - func = instance.get_func("test-add") + func = instance.get_func("test-get-numbers") - # The component crashes at runtime when the wrong type is returned - expect { func.call(1, 2) }.to raise_error(Wasmtime::Error, /error while executing at wasm/i) + expect { func.call }.to raise_error(TypeError, /conversion of String into Integer/) end end end