Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exclude = [
"spec/fixtures/component-types",
"spec/fixtures/wasi-debug",
"spec/fixtures/wasi-deterministic",
"spec/fixtures/host-func-imports",
]

[profile.release]
Expand Down
4 changes: 2 additions & 2 deletions ext/src/ruby_api/component/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ define_rb_intern!(
pub(crate) fn component_val_to_rb(
ruby: &Ruby,
val: Val,
_store: &StoreContextValue,
_store: Option<&StoreContextValue>,
) -> Result<Value, Error> {
match val {
Val::Bool(bool) => Ok(bool.into_value_with(ruby)),
Expand Down Expand Up @@ -103,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<Val, Error> {
let ruby = Ruby::get_with(value);
Expand Down
6 changes: 3 additions & 3 deletions ext/src/ruby_api/component/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
Expand Down
198 changes: 184 additions & 14 deletions ext/src/ruby_api/component/linker.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use super::convert;
use super::{Component, Instance};
use crate::{
err,
ruby_api::{
errors,
errors::{self, ExceptionMessage},
store::{StoreContextValue, StoreData},
Engine, Module, Store,
},
Expand All @@ -14,10 +15,18 @@ use std::{

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};

/// @yard
Expand Down Expand Up @@ -72,7 +81,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<Value, _> = ruby.yield_value(instance);

instance.take_inner();
Expand All @@ -93,11 +106,14 @@ impl Linker {
/// @return [Linker] +self+
pub fn instance(ruby: &Ruby, rb_self: Obj<Self>, name: RString) -> Result<Obj<Self>, 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<Value, _> = ruby.yield_value(instance);

Expand Down Expand Up @@ -164,13 +180,16 @@ impl Linker {
pub struct LinkerInstance<'a> {
inner: RefCell<MaybeInstanceImpl<'a>>,
refs: RefCell<Vec<Value>>,
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<'_> {}

impl DataTypeFunctions for LinkerInstance<'_> {
fn mark(&self, marker: &Marker) {
marker.mark_slice(self.refs.borrow().as_slice());
marker.mark(self.parent_linker);
}
}

Expand All @@ -193,10 +212,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,
}
}

Expand Down Expand Up @@ -230,12 +255,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<Value, _> = ruby.yield_value(nested_instance);
nested_instance.take_inner();

Expand All @@ -245,6 +280,65 @@ impl<'a> LinkerInstance<'a> {
}
}

/// @yard
/// Define a host function in this linker instance.
///
/// 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|
/// a + b # Returns u32
/// end
///
/// @example Returning a list
/// root.func_new("get-numbers") do
/// [1, 2, 3] # Returns list<s32>
/// end
///
/// @example Returning a tuple
/// root.func_new("make-tuple") do |n, s, b|
/// [n, s, b] # Returns tuple<u32, string, bool>
/// end
///
/// @example Complex types (records, results, etc.)
/// root.func_new("make-point") do |x, 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 [*args] The block implementing the host function
/// @yieldparam args [Array<Object>] The function arguments, converted from component values
/// @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<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
let args = scan_args::<(RString,), (), (), (), (), Proc>(args)?;
let (name,) = args.required;
let callable = args.block;

let name_str = unsafe { name.as_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<Linker> = Obj::try_convert(rb_self.parent_linker)?;
parent_linker.refs.borrow_mut().push(callable.as_value());

// Create the closure that will be called from Wasm
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");
};

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.")
Expand All @@ -254,6 +348,81 @@ impl<'a> LinkerInstance<'a> {
}
}

/// Create a closure that wraps a Ruby Proc for use as a component host function
/// The closure uses the function's type signature for automatic validation and conversion
fn make_component_func_closure(
callable: Opaque<Proc>,
) -> 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) 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}"))
})?;
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("")
})?;

// 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
// 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 - 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(())
}
_ => {
// 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(""))
}
}
}
}

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))?;
Expand All @@ -264,6 +433,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(())
}
2 changes: 2 additions & 0 deletions spec/fixtures/host-func-imports/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
2 changes: 2 additions & 0 deletions spec/fixtures/host-func-imports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/bindings.rs
Cargo.lock
22 changes: 22 additions & 0 deletions spec/fixtures/host-func-imports/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
15 changes: 15 additions & 0 deletions spec/fixtures/host-func-imports/README.md
Original file line number Diff line number Diff line change
@@ -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 ../
)
```
Loading
Loading