Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component host functions #433

Open
jbourassa opened this issue Feb 17, 2025 · 3 comments
Open

Component host functions #433

jbourassa opened this issue Feb 17, 2025 · 3 comments

Comments

@jbourassa
Copy link
Collaborator

jbourassa commented Feb 17, 2025

Add host functions for components.

Wasmtime has 2 APIs for adding host functions to components:

  1. The typed API (e.g. func_wrap) which requires the signature be known at compile time.
  2. 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.

This was referenced Feb 17, 2025
@sandstrom
Copy link
Contributor

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.

@jbourassa
Copy link
Collaborator Author

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.

@sandstrom
Copy link
Contributor

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants