Stream: wasmtime

Topic: Appreciate a code review if you have time


view this post on Zulip alisomay (Jun 30 2023 at 11:39):

I played around with generating a wasm module with the component model for a plugin system which I'd like to design.
I'm just playing around if the tech stack works for my case.
Since the ecosystem is bleeding edge and moving fast, I don't know the idiomatic patterns for the stack yet.
This is why I'd like to get a review from the engineers here. If it works out I hope it clarifies stuff for other people also.

I started with following documentation in these repositories.
https://github.com/WebAssembly/component-model
https://github.com/bytecodealliance/wit-bindgen/
https://github.com/bytecodealliance/preview2-prototyping
https://github.com/bytecodealliance/wasmtime
https://github.com/bytecodealliance/wasm-tools
https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md
https://docs.wasmtime.dev

And the crate docs
https://docs.rs/wasmtime-wasi/10.0.1/wasmtime_wasi/
https://docs.rs/wasmtime/10.0.1/wasmtime/

Also reading source code from these two repositories helped
https://github.com/bytecodealliance/preview2-prototyping
https://github.com/bytecodealliance/wasmtime

About the adapters when compiling component model wasm files I've used this source to retrieve them.
https://github.com/bytecodealliance/wasmtime/releases/tag/dev

I started with trying to generate bindings for a simple wit file and get a component-model compiled wasm file.
To do this I've used a different crate since I suppose that the plugin writers would do the same.

Dependencies of the transformer crate is as follows:

[package]
name = "transformer"
version = "0.1.0"
edition = "2021"

[dependencies]
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "8ceb28d" }

[lib]
crate-type = ['cdylib']

The wit file (a simple one which do not deal with error handling or complex types yet)

// Transformer
//
// package-decl ::= 'package' id ':' id ('@' valid-semver)?
// <name-of-the-module>:<name-of-the-world>
package field33:transformer

// Transformer world
world transformer {
  // Gets data transforms it and returns.
  //
  // Meant to be called from the host on demand.
  export transform: func(input: string) -> string
}

lib.rs (Here I wanted to test WASI and getting an input in then returning something)

use std::fs::File;
use std::io::prelude::*;

wit_bindgen::generate!("transformer");

pub struct MyTransformer;

impl Transformer for MyTransformer {
    fn transform(input: String) -> String {
        // format!("Transformed, {}!", input)

        // Create a file
        let mut file = File::create("test.txt").unwrap();

        // Write to the file
        file.write_all(b"Hello, WASI!").unwrap();

        // Read the file
        let mut file = File::open("test.txt").unwrap();
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();

        // Print the contents
        format!("{input} {}", contents)
    }
}

export_transformer!(MyTransformer);

This concludes the source of my plugin crate.

The compilation is done with the command:

cargo build --target wasm32-wasi && wasm-tools component new ../target/wasm32-wasi/debug/transformer.wasm -o transformer.wasm --adapt wasi_snapshot_preview1=./wasi_snapshot_preview1.reactor.wasm

wasi_snapshot_preview1.reactor.wasm adapter is retrieved from https://github.com/bytecodealliance/wasmtime/releases/tag/dev

I do not yet know the differences between the command and reactor adapters but I've chosen the working one.

Now my module is successfully compiled I need to run it in a toy runtime.

Here I mostly followed the https://github.com/bytecodealliance/preview2-prototyping source code.

I initially had a problem with dependencies and needed to pin some stuff but I think this issue is pretty temporary if we think how fast the project moves.

Here are the dependencies for my toy runtime crate.

[dependencies]
wasmtime = { version = "=10.0.1", features = ["component-model"] }

# TODO: Follow updates
wasmtime-wasi = "=10.0.1"
cap-fs-ext = "=1.0.15"
cap-primitives = "=1.0.15"
cap-std = "=1.0.15"

anyhow = "1"
tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] }

And the meat of it (main.rs):

use anyhow::Result;
use wasmtime::component::*;
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::preview2::{wasi, Table, WasiCtx, WasiCtxBuilder, WasiView};

struct PluginCtx {
    table: Table,
    wasi: WasiCtx,
}
impl WasiView for PluginCtx {
    fn table(&self) -> &Table {
        &self.table
    }
    fn table_mut(&mut self) -> &mut Table {
        &mut self.table
    }
    fn ctx(&self) -> &WasiCtx {
        &self.wasi
    }
    fn ctx_mut(&mut self) -> &mut WasiCtx {
        &mut self.wasi
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let dir = cap_std::fs::Dir::open_ambient_dir(".", cap_std::ambient_authority()).unwrap();

    let wasi_ctx_builder = WasiCtxBuilder::new()
        .inherit_stdio()
        .push_preopened_dir(
            dir,
            wasmtime_wasi::preview2::DirPerms::all(),
            wasmtime_wasi::preview2::FilePerms::all(),
            ".",
        )
        .set_args(&Vec::<String>::new());

    let mut table: Table = Table::new();
    let wasi = wasi_ctx_builder.build(&mut table)?;

    let mut config = Config::default();
    config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
    config.async_support(true);
    config.wasm_component_model(true);

    let engine = Engine::new(&config).unwrap();
    let component = Component::from_file(&engine, "transformer/transformer.wasm")?;

    let mut linker = Linker::new(&engine);
    wasi::command::add_to_linker(&mut linker)?;

    let mut store = Store::new(&engine, PluginCtx { table, wasi });

    // let (wasi, instance) =
    //     wasi::command::Command::instantiate_async(&mut store, &component, &linker).await?;
    // let result = wasi.call_run(&mut store).await.unwrap().unwrap();

    let instance = linker.instantiate_async(&mut store, &component).await?;
    let func = instance.get_func(&mut store, "transform").unwrap();

    let mut results = vec![Val::String("".to_owned().into_boxed_str())];
    let params = vec![Val::String("runtime".to_owned().into_boxed_str())];
    func.call_async(&mut store, &params, &mut results)
        .await
        .unwrap();

    dbg!(results);

    Ok(())
}

I'm receiving the result of

[rt/src/main.rs:68] results = [
    String(
        "runtime Hello, WASI!",
    ),
]

which shows that the function arguments and at least WASI functionality related to the file system works fine.

Where to go from here?

I would like to know if this pattern is one which is a good fundamental to build complexity over it?
Are there idiomatic or more smart patterns to achieve the same thing which I haven't realised yet?
What would you do better or would like to correct in this approach?

I haven't played with the PluginCtx yet. I'm going to check that next.

If you feel like commenting on it I'd be greatful. If not that is also fine I know that everybody has a limited time.

Thanks in advance :pray:

Repository for design and specification of the Component Model - GitHub - WebAssembly/component-model: Repository for design and specification of the Component Model
A language binding generator for WebAssembly interface types - GitHub - bytecodealliance/wit-bindgen: A language binding generator for WebAssembly interface types
Polyfill adapter for preview1-using wasm modules to call preview2 functions. - GitHub - bytecodealliance/preview2-prototyping: Polyfill adapter for preview1-using wasm modules to call preview2 func...
A fast and secure runtime for WebAssembly. Contribute to bytecodealliance/wasmtime development by creating an account on GitHub.
Low level tooling for WebAssembly in Rust. Contribute to bytecodealliance/wasm-tools development by creating an account on GitHub.
Repository for design and specification of the Component Model - component-model/design/mvp/WIT.md at main · WebAssembly/component-model
A fast and secure runtime for WebAssembly. Contribute to bytecodealliance/wasmtime development by creating an account on GitHub.

view this post on Zulip alisomay (Jun 30 2023 at 12:43):

Btw I've also tried using wasmtime::component::bindgen! which works fine in the rt. :party_ball:

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:27):

I don't have time to provide a detailed code review today (taking off for vacation at noon) but everything you want to be using from preview2-prototyping repo has found a new home in the wasmtime repo - readme in p2p shows where they ended up

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:28):

the difference between a command and a reactor is whether you are expecting to export a func run() -> result (wit syntax) via your Rust fn main() - if you do, the command adapter takes care of hooking those up. otherwise, use the reactor adapter

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:30):

commands are the appropriate choice if you have some existing command-line app you are porting to wasi: you instantiate the module, execute it once from the only export function run, and then throw it away. reactors can be instantiated, then you can call any of their export functions any number of times.

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:31):

so, more like a "library" than a CLI command

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:35):

your transformer is already a reactor, it just only happens to have one export.

view this post on Zulip Pat Hickey (Jun 30 2023 at 15:38):

the remaining suggestion i have is to use wasmtime::component::bindgen! on your transformer package and then, instead of doing instance.get_func and func.call_async, you'll have something like Transformer::transform(&mut self, &mut Store, in: Vec<String>) -> Result<Vec<String>> generated for you to use

view this post on Zulip alisomay (Jul 01 2023 at 13:48):

Pat Hickey said:

I don't have time to provide a detailed code review today (taking off for vacation at noon) but everything you want to be using from preview2-prototyping repo has found a new home in the wasmtime repo - readme in p2p shows where they ended up

Great that you're taking a vacation! I hope it will be refreshing and well.
This is good info, I might have missed that thank you.

view this post on Zulip alisomay (Jul 01 2023 at 13:57):

Pat Hickey said:

the remaining suggestion i have is to use wasmtime::component::bindgen! on your transformer package and then, instead of doing instance.get_func and func.call_async, you'll have something like Transformer::transform(&mut self, &mut Store, in: Vec<String>) -> Result<Vec<String>> generated for you to use

Yes this is what I have ended up discovering!
It is much more convenient to do it like that.

Here is my checklist and where I'm at in it to see if this approach fits my use case.

Works:
:check: Create a simple plugin in Rust
:check: Access WASI api filesystem access from the plugin
:check: Export the main transformer function from the plugin to the runtime
:check: Call the plugin main transformer function from the runtime to receive results.
:check: Enable async support in the runtime so the functions from the plugin could be called as async functions.
:check: Generate a plugin from a wit file and implementations.
:check: Run a generated component model based plugin in the runtime.

Didn't try yet:

Didn't fully understand how to do yet:

Thank you for your time and interest :pray:

view this post on Zulip alisomay (Jul 02 2023 at 14:00):

What I have recently tried out:

Importing functions from the host is not difficult.
They can be sync and async.
A tuple of args and results could be defined.
They need to implement these traits and a lot of tuple types do so we don't need to bother.

Params: ComponentNamedList + Lift + 'static,
Return: ComponentNamedList + Lower + 'static,

We can get a handle to the global store of host to share context between imported functions.

A sync import may look like this.

pub fn sync_import(context: StoreContextMut<'_, PluginCtx>,  params: (String,)) -> Result<(String, )> {
    let ctx = context.data();
    // A made up value to tests accessing the context.
    let runtime_version = ctx.runtime_version();

    // A function argument which is passed in from the guest.
    let arg = params.0;
    println!("[Naive plugin log - rt v{runtime_version}]: {arg}");
    Ok(())
}

The async counter part may look like this

pub fn async_import<'a>(
    _context: StoreContextMut<'a, PluginCtx>,
    params: (String,),
) -> Box<dyn Future<Output = Result<(String,), anyhow::Error>> + Send + 'a> {
    let arg_1 = params.0;
    let future = async move {
        Ok((format!(
            "This is the result of a plugin calling function (async) on rt. arg_1: {arg_1}",
        ),))
    };
    Box::new(future)
}

Setting environment variables for the guest code through WASI is easy.
Once the WasiCtx is built there is a vector of env vars are exposed.
One can push to it.

To expose the functions as imports through the mutable component linker one can call these methods.

    linker.root().func_wrap
        "my-sync-imported-func",
        sync_import,
    )?;
    linker.root().func_wrap_async(
        "my-async-imported-func",
        async_import,
    )?;

From the wit file they can now be used as imports and the generated code in the guest calls the functions which I've defined. Voila.

I also tried a few libraries and they worked out of the box, probably anything which compiles to wasm32-wasi works ok.

rio_turtle = "0.8"
rio_api = "0.8"
uuid = { version = "1", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }

view this post on Zulip alisomay (Jul 02 2023 at 14:18):

Next step is to try all these with a js based plugin with https://github.com/bytecodealliance/componentize-js

Contribute to bytecodealliance/componentize-js development by creating an account on GitHub.

Last updated: Nov 26 2024 at 01:30 UTC