Froidoh edited issue #8857:
EDIT:
So it all boils down to:
how can I invoke a function on an interface?!
ORIGINAL:
Okay, so I am a total noob with
wasm(time)
and my mistake is totally a layer 8 problem. But after digging I couldn't find the solution, so here I am, opening an issue (sorry for that!)What I want:
I want to leverage
cargo component
andwasmtime
to create a simple plugin system experiment.So I created a new (binary) webassembly component via:
cargo component new test-plugin
.[package] name = "test-plugin" version = "0.1.0" edition = "2021" [dependencies] wit-bindgen-rt = { version = "0.26.0", features = ["bitflags"] } [profile.release] codegen-units = 1 opt-level = "s" debug = false strip = true lto = true # [package.metadata.component] # package = "component:test-plugin" [package.metadata.component.target] path = "wit" world = "test-plugin" [package.metadata.component.dependencies]
package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; }
And my
src/main.rs
#[allow(warnings)] mod bindings; use bindings::exports::justatest::testplugin::greetings::Guest; struct Component; impl Guest for Component { fn hello(s: String) -> String { format!("Hello {s}") } } bindings::export!(Component with_types_in bindings); fn main() { println!("Hello from test-plugin!"); }
I then generated the binary via
cargo component build --release
and let it run via:wasmtime target/wasm32-wasi/release/test-plugin.wasm
Hello from test-plugin!As expected. Via the
wasmtime
cli I cannot--invoke
any functions on awasm32-wasi
webassembly module (at least that's what the cli tells me)...So then I created a
simple-host
as a "normal", "native" rust project, compiled viallvm
:Cargo.toml:
[package] name = "simple-host" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1.0.86" wasmtime = "22.0.0" wasmtime-wasi = "22.0.0" #wit-component = "0.211.1"
use wasmtime::{ component::{Component, Linker, Val}, Config, Engine, }; use wasmtime_wasi::{WasiCtxBuilder, WasiView}; /* fn convert_to_component(path: impl AsRef<Path>) -> wasmtime::Result<Vec<u8>> { let bytes = &std::fs::read(&path).context("failed to read input file")?; wit_component::ComponentEncoder::default() .module(&bytes)? .encode() } */ struct Host { ctx: wasmtime_wasi::WasiCtx, table: wasmtime_wasi::ResourceTable, } impl WasiView for Host { fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { &mut self.table } fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { &mut self.ctx } } fn main() -> wasmtime::Result<()> { let mut config = Config::new(); config.wasm_component_model(true); //config.async_support(true); let engine = Engine::new(&config)?; // Load the component from disk let component = std::fs::read("../test-plugin/target/wasm32-wasi/release/test-plugin.wasm")?; let component = Component::from_binary(&engine, &component)?; //println!("bytes: {bytes:?}"); // As `cargo component` by default builds as `wasm32-wasi` webassembly module, we need to also include `wasmtime-wasi` and provide certain native functionality (in my own words) let mut wasi = WasiCtxBuilder::new(); wasi.arg("--help"); let host = Host { ctx: wasi.build(), table: wasmtime_wasi::ResourceTable::new(), }; let mut store = wasmtime::Store::new(&engine, host); // Configure the linker let mut linker = Linker::new(&engine); wasmtime_wasi::add_to_linker_sync(&mut linker)?; // The component expects one import `name` that // takes no params and returns a string linker .root() //.func_wrap("name", |_store, _params: ()| Ok((String::from("Alice"),)))? ; println!("have linker"); // Instantiate the component let instance = linker.instantiate(&mut store, &component)?; // TODO: find out whethere there is a way to programmatically "explore" an instance, // to see it's "API" so to speak. // Could be useful for generating some docs or whatnot /* let mut exports = instance.exports(&mut store); let root = exports.instance("greetings").expect("greetings to exist"); let modules = root.modules(); for (name, module) in modules { println!("module: {module:?}") } println!("no modules?!"); */ println!("have instance"); /* // TODO: I definitely misunderstood modules, because "greetings" does not exist let module = instance .get_module(&mut store, "greetings") .expect("module not found"); */ // Call the `greet` function let func = instance .get_func(&mut store, "hello") .expect("hello export not found"); let mut result = [wasmtime::component::Val::String("".into())]; func.call( &mut store, &[Val::String("freund nachtigall".to_string())], &mut result, )?; println!("Greeting: {:?}", result); Ok(()) }
When I run this I get a
panic
forhello export not found
.What am I doing wrong? I also tried with:
greetings.hello
andgreetings:hello
forget_func
but to no avail.On the other hand, if I create a library component via
cargo component new test-plugin-lib --lib
which creates a default wit file and impl for it:package component:test-plugin-lib; /// An example world for the component to target. world example { export hello-world: func() -> string; }
and then load this and call it like so:
let func = instance .get_func(&mut store, "hello-world") .expect("hello export not found"); let mut result = [wasmtime::component::Val::String("".into())]; func.call( &mut store, &[], //&[Val::String("freund nachtigall".to_string())], &mut result, )?; println!("Greeting: {:?}", result);
it works like a charm.
So I guess my problem boils down to one or two things:
1) How do I get a function in an
interface
programmatically likehello
if the wit file looks like:package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; }
~2) Maybe it's different when it's a "
binary
" webassembly component in contrast to a lib?~I can answer that one: It's not!
If I add a
helloto
function like to:package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; export helloto: func(name: string) -> string; }
and implement it like so:
#[allow(warnings)] mod bindings; use bindings::exports::justatest::testplugin::greetings::Guest; use bindings::Guest as WorldGuest; struct Component; impl Guest for Component { fn hello(s: String) -> String { format!("Hello {s}") } } impl WorldGuest for Component { fn helloto(name: String) -> String { format!("Bonjour {name}") } } bindings::export!(Component with_types_in bindings); fn main() { println!("Hello from test-plugin!"); }
then I can invoke
helloto
just fine :)So it all boils down to:
how can I invoke a function on an interface?!
pchickey commented on issue #8857:
Hi! You did a lot of figuring things out in your journey to this point, in a brand new technology with docs we know could use some work, so great job so far. It looks like the bit missing here is that when
hello
is in an interface calledgreetings
, so first you must get thegreetings
instance out of your root instance:instance.exports(&mut store).instance("greetings").expect("root has greetings instance").typed_func("hello")
.An alternative is to use
wasmtime::component::bindgen!
in your host, which will generate, among other things, aTestPlugin
struct, which has typed, string-name-free methods to access all of the world's exports. You can usecargo doc --document-private-items
to see the extent of what that macro generates, or ifWASMTIME_DEBUG_BINDGEN
is set during build, the generated code will be inOUT_DIR
. That generated code should be able to replace everything in your host from println!(have linker) down.
pchickey edited a comment on issue #8857:
Hi! You did a lot of figuring things out in your journey to this point, in a brand new technology with docs we know could use some work, so great job so far. It looks like the bit missing here is that when
hello
is in an interface calledgreetings
, so first you must get thegreetings
instance out of your root instance:instance.exports(&mut store).instance("greetings").expect("root has greetings instance").typed_func("hello")
.An alternative is to use
wasmtime::component::bindgen!
in your host, which will generate, among other things, aTestPlugin
struct, which has typed, string-name-free methods to access all of the world's exports. You can usecargo doc --document-private-items
to see the extent of what that macro generates, or ifWASMTIME_DEBUG_BINDGEN
is set during build, the generated code will be inOUT_DIR
. That generated code should be able to replace everything in your host from println!(have linker) down.The other reason to recommend bindgen is that wasmtime 23 will change the way instance exports work in order to make reuse more efficient, so the answer in the first paragraph will need to be rewritten (https://docs.wasmtime.dev/api/wasmtime/component/struct.Instance.html, you'll need to call, pardon shorthand,
get_typed_func(get_export(get_export("greetings"), "hello"))
)
pchickey edited a comment on issue #8857:
Hi! You did a lot of figuring things out in your journey to this point, in a brand new technology with docs we know could use some work, so great job so far. It looks like the bit missing here is that when
hello
is in an interface calledgreetings
, so first you must get thegreetings
instance out of your root instance:instance.exports(&mut store).instance("greetings").expect("root has greetings instance").typed_func("hello")
.An alternative is to use
wasmtime::component::bindgen!
in your host, which will generate, among other things, aTestPlugin
struct, which has typed, string-name-free methods to access all of the world's exports. You can usecargo doc --document-private-items
to see the extent of what that macro generates, or ifWASMTIME_DEBUG_BINDGEN
is set during build, the generated code will be inOUT_DIR
. That generated code should be able to replace everything in your host from println!(have linker) down.The other reason to recommend bindgen is that wasmtime 23 will change the way instance exports work in order to make reuse more efficient, so the answer in the first paragraph will need to be rewritten (https://docs.wasmtime.dev/api/wasmtime/component/struct.Instance.html, you'll need to call, pardon shorthand,
get_typed_func(get_export(get_export("greetings"), "hello"))
)One final suggestion - its fine to file issues like these, and also we invite this sort of question / debugging journey on https://bytecodealliance.zulipchat.com/.
Froidoh commented on issue #8857:
@pchickey thanks for taking the time to answer!
The problem with the macro approach is that I want to write a dynamic plugin system, so I don't know the wasm components aka plugins that are available at runtime.
I would definitely prefer the "non stringified function calls" but, correct me if wrong, that's currently not possible then.
What I would need to get this dynamically at runtike working is something like a "interpret this component as an instance of this (rust) trait" or a "cast" so to speak
pchickey commented on issue #8857:
The bindgen! happens at rustc compile time, yes, so you have to know the world required to run your component at that time.
If you have some known set of rust traits that are your cast targets, then you might be able to generate code for the set of worlds that correspond to those targets. You could use bindgen! for that, but you don't have to - bindgen is just a convenience on top of the public wasmtime API, so you can get the same type safety by just using Instance exports and typed_func: wasmtime will check at runtime to make sure the component you load corresponds to those types. So, there are many ways to go about solving this depending on your use case.
cryarchy commented on issue #8857:
@pchickey I have a wasm component whose interface is defined as:
package docs:adder@0.1.0; interface add { add: func(a: s32, b: s32) -> s32; } world adder { export add; }
I have tried to invoke the exported add function using the following Rust code:
use anyhow::Result; use wasmtime::component::{Component, Linker, Val}; use wasmtime::{Engine, Store}; use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView}; fn main() -> Result<()> { // Create an engine and store let engine = Engine::default(); let wasi = WasiCtxBuilder::new().inherit_stdio().build(); let mut store = Store::new(&engine, ServerWasiView::new(wasi)); // Load the component let component = Component::from_file(&engine, "target/wasm32-wasip1/release/add.wasm")?; // Create a linker let mut linker = Linker::<ServerWasiView>::new(&engine); // Add WASI to the linker wasmtime_wasi::add_to_linker_sync(&mut linker)?; // Instantiate the component let instance = linker.instantiate(&mut store, &component)?; let possible_names = [ "add#add", "docs:adder/add@0.1.0#add", "adder/add@0.1.0#add", "/add@0.1.0#add", "add@0.1.0#add", "#add", "add", ]; { let mut exports = instance.exports(&mut store); for name in possible_names.iter() { if exports.instance(name).is_some() { println!("Found add instance with name: {}", name); } } } // Try to get the add function with different possible names let add = instance .get_func(&mut store, "add") .or_else(|| instance.get_func(&mut store, "docs:adder/add@0.1.0#add")) .or_else(|| instance.get_func(&mut store, "adder/add@0.1.0#add")) .or_else(|| instance.get_func(&mut store, "/add@0.1.0#add")) .or_else(|| instance.get_func(&mut store, "add@0.1.0#add")) .or_else(|| instance.get_func(&mut store, "#add")) .or_else(|| instance.get_func(&mut store, "add")) .expect("add function not found"); println!("Found add function"); // Prepare the arguments and results let mut results = [Val::S32(0)]; // Call the function add.call(&mut store, &[Val::S32(5), Val::S32(3)], &mut results)?; // Extract the result let Val::S32(result) = results[0] else { panic!("Unexpected result type"); }; println!("5 + 3 = {}", result); Ok(()) } struct ServerWasiView { table: ResourceTable, ctx: WasiCtx, } impl ServerWasiView { fn new(ctx: WasiCtx) -> Self { let table = ResourceTable::new(); Self { table, ctx } } } impl WasiView for ServerWasiView { fn table(&mut self) -> &mut ResourceTable { &mut self.table } fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } }
as per your suggestion above and have not managed to get a reference to the exported function in the host code. What am I missing?
I have created a help request topic on Zulip.
cryarchy commented on issue #8857:
Thank you, @pchickey.
The solution you provided above is correct. It still took me a while to get things working right. There were some naming mismatches that I kept missing.
I have figured out what I was doing wrong and provided an example for future users who run into the same issue.
alexcrichton closed issue #8857:
EDIT:
So it all boils down to:
how can I invoke a function on an interface?!
ORIGINAL:
Okay, so I am a total noob with
wasm(time)
and my mistake is totally a layer 8 problem. But after digging I couldn't find the solution, so here I am, opening an issue (sorry for that!)What I want:
I want to leverage
cargo component
andwasmtime
to create a simple plugin system experiment.So I created a new (binary) webassembly component via:
cargo component new test-plugin
.[package] name = "test-plugin" version = "0.1.0" edition = "2021" [dependencies] wit-bindgen-rt = { version = "0.26.0", features = ["bitflags"] } [profile.release] codegen-units = 1 opt-level = "s" debug = false strip = true lto = true # [package.metadata.component] # package = "component:test-plugin" [package.metadata.component.target] path = "wit" world = "test-plugin" [package.metadata.component.dependencies]
package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; }
And my
src/main.rs
#[allow(warnings)] mod bindings; use bindings::exports::justatest::testplugin::greetings::Guest; struct Component; impl Guest for Component { fn hello(s: String) -> String { format!("Hello {s}") } } bindings::export!(Component with_types_in bindings); fn main() { println!("Hello from test-plugin!"); }
I then generated the binary via
cargo component build --release
and let it run via:wasmtime target/wasm32-wasi/release/test-plugin.wasm
Hello from test-plugin!As expected. Via the
wasmtime
cli I cannot--invoke
any functions on awasm32-wasi
webassembly module (at least that's what the cli tells me)...So then I created a
simple-host
as a "normal", "native" rust project, compiled viallvm
:Cargo.toml:
[package] name = "simple-host" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1.0.86" wasmtime = "22.0.0" wasmtime-wasi = "22.0.0" #wit-component = "0.211.1"
use wasmtime::{ component::{Component, Linker, Val}, Config, Engine, }; use wasmtime_wasi::{WasiCtxBuilder, WasiView}; /* fn convert_to_component(path: impl AsRef<Path>) -> wasmtime::Result<Vec<u8>> { let bytes = &std::fs::read(&path).context("failed to read input file")?; wit_component::ComponentEncoder::default() .module(&bytes)? .encode() } */ struct Host { ctx: wasmtime_wasi::WasiCtx, table: wasmtime_wasi::ResourceTable, } impl WasiView for Host { fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { &mut self.table } fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { &mut self.ctx } } fn main() -> wasmtime::Result<()> { let mut config = Config::new(); config.wasm_component_model(true); //config.async_support(true); let engine = Engine::new(&config)?; // Load the component from disk let component = std::fs::read("../test-plugin/target/wasm32-wasi/release/test-plugin.wasm")?; let component = Component::from_binary(&engine, &component)?; //println!("bytes: {bytes:?}"); // As `cargo component` by default builds as `wasm32-wasi` webassembly module, we need to also include `wasmtime-wasi` and provide certain native functionality (in my own words) let mut wasi = WasiCtxBuilder::new(); wasi.arg("--help"); let host = Host { ctx: wasi.build(), table: wasmtime_wasi::ResourceTable::new(), }; let mut store = wasmtime::Store::new(&engine, host); // Configure the linker let mut linker = Linker::new(&engine); wasmtime_wasi::add_to_linker_sync(&mut linker)?; // The component expects one import `name` that // takes no params and returns a string linker .root() //.func_wrap("name", |_store, _params: ()| Ok((String::from("Alice"),)))? ; println!("have linker"); // Instantiate the component let instance = linker.instantiate(&mut store, &component)?; // TODO: find out whethere there is a way to programmatically "explore" an instance, // to see it's "API" so to speak. // Could be useful for generating some docs or whatnot /* let mut exports = instance.exports(&mut store); let root = exports.instance("greetings").expect("greetings to exist"); let modules = root.modules(); for (name, module) in modules { println!("module: {module:?}") } println!("no modules?!"); */ println!("have instance"); /* // TODO: I definitely misunderstood modules, because "greetings" does not exist let module = instance .get_module(&mut store, "greetings") .expect("module not found"); */ // Call the `greet` function let func = instance .get_func(&mut store, "hello") .expect("hello export not found"); let mut result = [wasmtime::component::Val::String("".into())]; func.call( &mut store, &[Val::String("freund nachtigall".to_string())], &mut result, )?; println!("Greeting: {:?}", result); Ok(()) }
When I run this I get a
panic
forhello export not found
.What am I doing wrong? I also tried with:
greetings.hello
andgreetings:hello
forget_func
but to no avail.On the other hand, if I create a library component via
cargo component new test-plugin-lib --lib
which creates a default wit file and impl for it:package component:test-plugin-lib; /// An example world for the component to target. world example { export hello-world: func() -> string; }
and then load this and call it like so:
let func = instance .get_func(&mut store, "hello-world") .expect("hello export not found"); let mut result = [wasmtime::component::Val::String("".into())]; func.call( &mut store, &[], //&[Val::String("freund nachtigall".to_string())], &mut result, )?; println!("Greeting: {:?}", result);
it works like a charm.
So I guess my problem boils down to one or two things:
1) How do I get a function in an
interface
programmatically likehello
if the wit file looks like:package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; }
~2) Maybe it's different when it's a "
binary
" webassembly component in contrast to a lib?~I can answer that one: It's not!
If I add a
helloto
function like to:package justatest:testplugin@0.1.0; interface greetings { hello: func(s: string) -> string; } world test-plugin { export greetings; export helloto: func(name: string) -> string; }
and implement it like so:
#[allow(warnings)] mod bindings; use bindings::exports::justatest::testplugin::greetings::Guest; use bindings::Guest as WorldGuest; struct Component; impl Guest for Component { fn hello(s: String) -> String { format!("Hello {s}") } } impl WorldGuest for Component { fn helloto(name: String) -> String { format!("Bonjour {name}") } } bindings::export!(Component with_types_in bindings); fn main() { println!("Hello from test-plugin!"); }
then I can invoke
helloto
just fine :)So it all boils down to:
how can I invoke a function on an interface?!
alexcrichton commented on issue #8857:
I think the questions here have been answered so I'm going to close this, but if there are other questions feel free to comment here or open a new issue.
Last updated: Dec 23 2024 at 13:07 UTC