Component host functions
Add host functions for components.
Wasmtime has 2 APIs for adding host functions to components:
- The typed API (e.g.
func_wrap) which requires the signature be known at compile time. - The dynamic API (e.g.
func_new), where the params are in a&[Val]and return values are to be written to a&mut[Val].
I think (1) essentially requires a rust compiler thus out of question. We're left with (2). But how do we wrap such API in Ruby?
Option A: follow the crate's API
Params are Ruby values converted from Wasm, return values are tagged with their type:
# Imagine the following WIT definitions:
# f(param1: string) -> result<a>
#
# record a {
# field1: u32,
# field2: string,
# }
T = Wasmtime::Component::Type # Hypothetical API, does not exist yet
linker_istance.func_new("f") do |_store, param1|
T::Result.wrap(
Wasmtime::Component::Result.ok(
T::Record.wrap(
field1: T::U32.wrap(42),
field2: T::String.wrap("foo"),
)
)
)
end
With this approach, params are not type-checked. E.g. param1 could be of any type; whatever the caller decides to send. The host function could implement some limited type checking (limited because it can't distinguish between a u8 or u32, for example).
Option B: type the function
Similar to what's done for core Wasm functions, specify the return type when calling #func_new:
T = Wasmtime::Component::Type # Hypothetical API, does not exist yet
A = T::Record.new(
"field1" => T::U32
"field1" => T::String
)
ReturnType = T::Result.new(A)
linker_istance.func_new("f", ReturnType) do |_store, param1|
record = { "field1" => 42, "field2" => "foo" }
Wasmtime::Component::Result.ok(record)
end
This approach can be extended to support params type checking (possibly with a performance hit).
We might be able to generate those types directly by parsing a WIT file.
Both options A and B require mapping a Ruby object to a Wasm component type definition in a standalone way. By standalone I mean not using an index to point into a type in a component, i.e. not using the wasmtime::component::Type enum. The implication of this is the convert code needs to be duplicated or a new abstraction introduced.
There may be a better way. If you think of one, please share! Unless we find a better way, I suggest waiting until users really need this feature before implementing this.
Maybe this isn't helpful at all (I don't know Rust too well), but would it help if we would say that even though Ruby isn't typed, a Ruby host function invoked with an invalid argument (doing type-checking on-the-fly in Ruby land) will just raise in Ruby and never hit the component itself?
It would be a limitation placed on the ruby host function and verified in ruby land, to ensure it complies with the requirements of rust-land.
Ruby host function invoked with an invalid argument (doing type-checking on-the-fly in Ruby land) will just raise in Ruby
IIUC, that's the "do nothing" approach, possible both in (A) and (B) above. The body of the host function can implement its own type checking.
The gotcha is that the Wasm component -> Ruby value conversion is lossy. E.g. if the host sees an Integer, you don't know if wasm sent a u8 or i8 or u32, etc. For Ruby it may not matter, but if that function implementation were to be replaced with a component export, what was considered a valid call could start failing.
to ensure it complies with the requirements of rust-land.
FYI it's not a requirement of Rust. In this case, wasmtime-rb would convert whatever params the component sends to Ruby types. It works, but goes against my expectations. Typically, Wasm components would work with a .wit file and only accept valid types.
The gotcha is that the Wasm component -> Ruby value conversion is lossy. E.g. if the host sees an Integer, you don't know if wasm sent a u8 or i8 or u32, etc. For Ruby it may not matter, but if that function implementation were to be replaced with a component export, what was considered a valid call could start failing.
@jbourassa Ah, got it, thanks!
I personally am trying to use wasmtime-rb to implement user-provided plugins in a Rails app, and it really caught me off guard that there isn't support for components.
If the lack of static typing in Ruby is the primary concern, dry-schema and/or dry-validation might help bridge the gap.
Accounting for different integer sizes in this implementation would probably require a dry-validation contract which checks a value's bit_length against an expected range.