Stream: git-wasmtime

Topic: wasmtime / issue #8857 [Partially answered myself] [Quest...


view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 07:03):

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 and wasmtime 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 a wasm32-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 via llvm:

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"

main.rs

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 for hello export not found.

What am I doing wrong? I also tried with: greetings.hello and greetings:hello for get_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 like hello 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?!

view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 15:33):

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 called greetings, so first you must get the greetings 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, a TestPlugin struct, which has typed, string-name-free methods to access all of the world's exports. You can use cargo doc --document-private-items to see the extent of what that macro generates, or if WASMTIME_DEBUG_BINDGEN is set during build, the generated code will be in OUT_DIR. That generated code should be able to replace everything in your host from println!(have linker) down.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 15:38):

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 called greetings, so first you must get the greetings 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, a TestPlugin struct, which has typed, string-name-free methods to access all of the world's exports. You can use cargo doc --document-private-items to see the extent of what that macro generates, or if WASMTIME_DEBUG_BINDGEN is set during build, the generated code will be in OUT_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")))

view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 15:44):

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 called greetings, so first you must get the greetings 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, a TestPlugin struct, which has typed, string-name-free methods to access all of the world's exports. You can use cargo doc --document-private-items to see the extent of what that macro generates, or if WASMTIME_DEBUG_BINDGEN is set during build, the generated code will be in OUT_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/.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 15:49):

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

view this post on Zulip Wasmtime GitHub notifications bot (Jun 21 2024 at 16:28):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2024 at 17:20):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 22 2024 at 11:43):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 22 2024 at 17:33):

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 and wasmtime 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 a wasm32-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 via llvm:

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"

main.rs

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 for hello export not found.

What am I doing wrong? I also tried with: greetings.hello and greetings:hello for get_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 like hello 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?!

view this post on Zulip Wasmtime GitHub notifications bot (Jul 22 2024 at 17:33):

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: Nov 22 2024 at 17:03 UTC