Skip to content

Commit bf01c03

Browse files
authored
Merge pull request #399 from bytecodealliance/components-func
Replace `Instance#invoke` with `Func#call`
2 parents 5167d9e + 834e57a commit bf01c03

File tree

8 files changed

+249
-77
lines changed

8 files changed

+249
-77
lines changed

bench/component_id.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
component = Wasmtime::Component::Component.from_file(engine, "spec/fixtures/component_types.wasm")
77
store = Wasmtime::Store.new(engine)
88
instance = linker.instantiate(store, component)
9+
id_record = instance.get_func("id-record")
10+
id_u32 = instance.get_func("id-u32")
911

1012
point_record = {"x" => 1, "y" => 2}
1113

1214
x.report("identity point record") do
13-
instance.invoke("id-record", point_record)
15+
id_record.call(point_record)
1416
end
1517

1618
x.report("identity u32") do
17-
instance.invoke("id-u32", 10)
19+
id_u32.call(10)
1820
end
1921
end

ext/src/ruby_api/component.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
161161

162162
linker::init(ruby, &namespace)?;
163163
instance::init(ruby, &namespace)?;
164+
func::init(ruby, &namespace)?;
164165
convert::init(ruby)?;
165166

166167
Ok(())

ext/src/ruby_api/component/func.rs

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,118 @@
1-
use super::convert::{component_val_to_rb, rb_to_component_val};
2-
use crate::ruby_api::{errors::ExceptionMessage, store::StoreContextValue};
3-
use magnus::{exception::arg_error, prelude::*, value, Error, IntoValue, RArray, Value};
1+
use crate::ruby_api::{
2+
component::{
3+
convert::{component_val_to_rb, rb_to_component_val},
4+
Instance,
5+
},
6+
errors::ExceptionMessage,
7+
store::{Store, StoreContextValue},
8+
};
9+
use magnus::{
10+
class, exception::arg_error, gc::Marker, method, prelude::*, typed_data::Obj, value,
11+
DataTypeFunctions, Error, IntoValue, RArray, RModule, Ruby, TypedData, Value,
12+
};
413
use wasmtime::component::{Func as FuncImpl, Type, Val};
514

6-
pub struct Func;
15+
/// @yard
16+
/// @rename Wasmtime::Component::Func
17+
/// Represents a WebAssembly component Function
18+
/// @see https://docs.wasmtime.dev/api/wasmtime/component/struct.Func.html Wasmtime's Rust doc
19+
///
20+
/// == Component model types conversion
21+
///
22+
/// Here's how component model types map to Ruby objects:
23+
///
24+
/// bool::
25+
/// Ruby +true+ or +false+, no automatic conversion happens.
26+
/// s8, u8, s16, u16, etc.::
27+
/// Ruby +Integer+. Overflows raise.
28+
/// f32, f64::
29+
/// Ruby +Float+.
30+
/// string::
31+
/// Ruby +String+. Exception will be raised if the string is not valid UTF-8.
32+
/// list<T>::
33+
/// Ruby +Array+.
34+
/// tuple::
35+
/// Ruby +Array+ of the same size of tuple. Example: +tuple<T, U>+ would be converted to +[T, U]+.
36+
/// record::
37+
/// Ruby +Hash+ where field names are +String+s.
38+
/// result<O, E>::
39+
/// {Result} instance. When converting a result branch of the none
40+
/// type, the {Result}’s value MUST be +nil+.
41+
///
42+
/// Examples of none type in a result: unparametrized +result+, +result<O>+, +result<_, E>+.
43+
/// option<T>::
44+
/// +nil+ is mapped to +None+, anything else is mapped to +Some(T)+.
45+
/// flags::
46+
/// Ruby +Array+ of +String+s.
47+
/// enum::
48+
/// Ruby +String+. Exception will be raised of the +String+ is not a valid enum value.
49+
/// variant::
50+
/// {Variant} instance wrapping the variant's name and optionally its value.
51+
/// Exception will be raised for:
52+
/// - invalid {Variant#name},
53+
/// - unparametrized variant and not nil {Variant#value}.
54+
/// resource (own<T> or borrow<T>)::
55+
/// Not yet supported.
56+
#[derive(TypedData)]
57+
#[magnus(class = "Wasmtime::Component::Func", size, mark, free_immediately)]
58+
pub struct Func {
59+
store: Obj<Store>,
60+
instance: Obj<Instance>,
61+
inner: FuncImpl,
62+
}
63+
unsafe impl Send for Func {}
64+
65+
impl DataTypeFunctions for Func {
66+
fn mark(&self, marker: &Marker) {
67+
marker.mark(self.store);
68+
marker.mark(self.instance);
69+
}
70+
}
771

872
impl Func {
9-
pub fn invoke(
10-
store: &StoreContextValue,
11-
func: &FuncImpl,
12-
args: &[Value],
13-
) -> Result<Value, Error> {
14-
let results_ty = func.results(store.context()?);
73+
/// @yard
74+
/// Calls a Wasm component model function.
75+
/// @def call(*args)
76+
/// @param args [Array<Object>] the function's arguments as per its Wasm definition
77+
/// @return [Object] the function's return value as per its Wasm definition
78+
/// @see Func Func class-level documentation for type conversion logic
79+
pub fn call(&self, args: &[Value]) -> Result<Value, Error> {
80+
Func::invoke(self.store, &self.inner, args)
81+
}
82+
83+
pub fn from_inner(inner: FuncImpl, instance: Obj<Instance>, store: Obj<Store>) -> Self {
84+
Self {
85+
store,
86+
instance,
87+
inner,
88+
}
89+
}
90+
91+
pub fn invoke(store: Obj<Store>, func: &FuncImpl, args: &[Value]) -> Result<Value, Error> {
92+
let store_context_value = StoreContextValue::from(store);
93+
let results_ty = func.results(store.context_mut());
1594
let mut results = vec![wasmtime::component::Val::Bool(false); results_ty.len()];
16-
let params = convert_params(store, &func.params(store.context()?), args)?;
95+
let params = convert_params(
96+
&store_context_value,
97+
&func.params(store.context_mut()),
98+
args,
99+
)?;
17100

18-
func.call(store.context_mut()?, &params, &mut results)
19-
.map_err(|e| store.handle_wasm_error(e))?;
101+
func.call(store.context_mut(), &params, &mut results)
102+
.map_err(|e| store_context_value.handle_wasm_error(e))?;
20103

21104
let result = match results_ty.len() {
22105
0 => Ok(value::qnil().as_value()),
23-
1 => component_val_to_rb(results.into_iter().next().unwrap(), store),
106+
1 => component_val_to_rb(results.into_iter().next().unwrap(), &store_context_value),
24107
_ => results
25108
.into_iter()
26-
.map(|val| component_val_to_rb(val, store))
109+
.map(|val| component_val_to_rb(val, &store_context_value))
27110
.collect::<Result<RArray, Error>>()
28111
.map(IntoValue::into_value),
29112
};
30113

31-
func.post_return(store.context_mut()?)
32-
.map_err(|e| store.handle_wasm_error(e))?;
114+
func.post_return(store.context_mut())
115+
.map_err(|e| store_context_value.handle_wasm_error(e))?;
33116

34117
result
35118
}
@@ -65,3 +148,10 @@ fn convert_params(
65148

66149
Ok(params)
67150
}
151+
152+
pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> {
153+
let func = namespace.define_class("Func", class::object())?;
154+
func.define_method("call", method!(Func::call, -1))?;
155+
156+
Ok(())
157+
}

ext/src/ruby_api/component/instance.rs

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::error;
55
use magnus::{
66
class,
77
error::ErrorType,
8-
exception::arg_error,
8+
exception::{arg_error, type_error},
99
function,
1010
gc::Marker,
1111
method,
@@ -17,7 +17,7 @@ use magnus::{
1717
DataTypeFunctions, Error, RArray, Ruby, TryConvert, TypedData, Value,
1818
};
1919
use magnus::{IntoValue, RModule};
20-
use wasmtime::component::{Instance as InstanceImpl, Type, Val};
20+
use wasmtime::component::{ComponentExportIndex, Instance as InstanceImpl, Type, Val};
2121

2222
/// @yard
2323
/// Represents a WebAssembly component instance.
@@ -43,33 +43,63 @@ impl Instance {
4343
}
4444

4545
/// @yard
46-
/// Retrieves a Wasm function from the component instance and calls it.
46+
/// Retrieves a Wasm function from the component instance.
4747
///
48-
/// @def invoke(name, *args)
49-
/// @param name [String] The name of function to run.
50-
/// @param (see Component::Func#call)
51-
/// @return (see Component::Func#call)
52-
/// @see Component::Func#call
53-
pub fn invoke(&self, args: &[Value]) -> Result<Value, Error> {
54-
let name = RString::try_convert(*args.first().ok_or_else(|| {
48+
/// @def get_func(handle)
49+
/// @param handle [String, Array<String>] The path of the function to retrieve
50+
/// @return [Func, nil] The function if it exists, nil otherwise
51+
///
52+
/// @example Retrieve a top-level +add+ export:
53+
/// instance.get_func("add")
54+
///
55+
/// @example Retrieve an +add+ export nested under an +adder+ instance top-level export:
56+
/// instance.get_func(["adder", "add"])
57+
pub fn get_func(rb_self: Obj<Self>, handle: Value) -> Result<Option<Func>, Error> {
58+
let func = rb_self
59+
.export_index(handle)?
60+
.and_then(|index| rb_self.inner.get_func(rb_self.store.context_mut(), index))
61+
.map(|inner| Func::from_inner(inner, rb_self, rb_self.store));
62+
63+
Ok(func)
64+
}
65+
66+
fn export_index(&self, handle: Value) -> Result<Option<ComponentExportIndex>, Error> {
67+
let invalid_arg = || {
5568
Error::new(
56-
magnus::exception::type_error(),
57-
"wrong number of arguments (given 0, expected 1+)",
69+
type_error(),
70+
format!(
71+
"invalid argument for component index, expected String | Array<String>, got {}",
72+
handle.inspect()
73+
),
5874
)
59-
})?)?;
75+
};
76+
77+
let index = if let Some(name) = RString::from_value(handle) {
78+
self.inner
79+
.get_export(self.store.context_mut(), None, unsafe { name.as_str()? })
80+
} else if let Some(names) = RArray::from_value(handle) {
81+
unsafe { names.as_slice() }
82+
.iter()
83+
.try_fold::<_, _, Result<_, Error>>(None, |index, name| {
84+
let name = RString::from_value(*name).ok_or_else(invalid_arg)?;
6085

61-
let func = self
62-
.inner
63-
.get_func(self.store.context_mut(), unsafe { name.as_str()? })
64-
.ok_or_else(|| error!("function \"{}\" not found", name))?;
86+
Ok(self
87+
.inner
88+
.get_export(self.store.context_mut(), index.as_ref(), unsafe {
89+
name.as_str()?
90+
}))
91+
})?
92+
} else {
93+
return Err(invalid_arg());
94+
};
6595

66-
Func::invoke(&self.store.into(), &func, &args[1..])
96+
Ok(index)
6797
}
6898
}
6999

70100
pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> {
71101
let instance = namespace.define_class("Instance", class::object())?;
72-
instance.define_method("invoke", method!(Instance::invoke, -1))?;
102+
instance.define_method("get_func", method!(Instance::get_func, 1))?;
73103

74104
Ok(())
75105
}

spec/fixtures/component_adder.wat

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
(component
2-
(core module $m
3-
(func (export "add") (param $a i32) (param $b i32) (result i32)
4-
local.get $a
5-
local.get $b
6-
i32.add
2+
;; Define a nested component so we can export an instance of a component
3+
(component $c
4+
(core module $m
5+
(func (export "add") (param $a i32) (param $b i32) (result i32)
6+
local.get $a
7+
local.get $b
8+
i32.add
9+
)
710
)
11+
(core instance $i (instantiate $m))
12+
(func $add (param "a" s32) (param "b" s32) (result s32) (canon lift (core func $i "add")))
13+
(export "add" (func $add))
814
)
9-
(core instance $i (instantiate $m))
10-
(func $add (param "a" s32) (param "b" s32) (result s32) (canon lift (core func $i "add")))
11-
(export "add" (func $add))
15+
(instance $adder (instantiate $c))
16+
17+
;; Export the adder instance
18+
(export "adder" (instance $adder))
19+
20+
;; Re-export add as a top level
21+
(export "add" (func $adder "add"))
1222
)

spec/unit/component/convert_spec.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ module Component
1010
let(:linker) { Linker.new(GLOBAL_ENGINE) }
1111
let(:instance) { linker.instantiate(Store.new(GLOBAL_ENGINE), @types_component) }
1212

13+
def call_func(name, *args)
14+
func = instance.get_func(name)
15+
raise "Unknown func: #{name}" if func.nil?
16+
17+
func.call(*args)
18+
end
19+
1320
describe "successful round-trips" do
1421
[
1522
["bool", true, false],
@@ -37,17 +44,17 @@ module Component
3744
].each do |type, *values|
3845
values.each do |v|
3946
it "#{type} #{v.inspect}" do
40-
expect(instance.invoke("id-#{type}", v)).to eq(v)
47+
expect(call_func("id-#{type}", v)).to eq(v)
4148
end
4249
end
4350
end
4451

4552
it "returns FLOAT::INFINITY on f32 overflow" do
46-
expect(instance.invoke("id-f32", 5 * 10**40)).to eq(Float::INFINITY)
53+
expect(call_func("id-f32", 5 * 10**40)).to eq(Float::INFINITY)
4754
end
4855

4956
it "returns FLOAT::INFINITY on f64 overflow" do
50-
expect(instance.invoke("id-f64", 2 * 10**310)).to eq(Float::INFINITY)
57+
expect(call_func("id-f64", 2 * 10**310)).to eq(Float::INFINITY)
5158
end
5259
end
5360

@@ -95,22 +102,22 @@ module Component
95102
["flags", 1, /no implicit conversion of Integer into Array/]
96103
].each do |type, value, klass, msg|
97104
it "fails on #{type} #{value.inspect}" do
98-
expect { instance.invoke("id-#{type}", value) }.to raise_error(klass, msg)
105+
expect { call_func("id-#{type}", value) }.to raise_error(klass, msg)
99106
end
100107
end
101108

102109
it "has item index in list conversion error" do
103-
expect { instance.invoke("id-list", [1, "foo"]) }
110+
expect { call_func("id-list", [1, "foo"]) }
104111
.to raise_error(TypeError, /list item at index 1/)
105112
end
106113

107114
it "has tuple index in tuple conversion error" do
108-
expect { instance.invoke("id-tuple", ["foo", 1]) }
115+
expect { call_func("id-tuple", ["foo", 1]) }
109116
.to raise_error(TypeError, /tuple value at index 0/)
110117
end
111118

112119
it "has field name in record conversion error" do
113-
expect { instance.invoke("id-record", {"y" => 1, "x" => nil}) }
120+
expect { call_func("id-record", {"y" => 1, "x" => nil}) }
114121
.to raise_error(TypeError, /struct field "x"/)
115122
end
116123
end

0 commit comments

Comments
 (0)