Stream: wasmtime

Topic: ✔ how to write a data transformer using wasm?


view this post on Zulip Bo Liu (Aug 02 2024 at 20:53):

Hi there,
I've been trying to write a data transformer function which takes string as input and outputs a f32 in a wasm.
Also, I want to use wasmtime crate to have this wasm function run inside a wasmtime module/sandbox.

I've been using wit-bindgen to generate the necessary bindings and export a function parse_cost() and parse_unit(),

mod bindings;

use bindings::exports::component::parser::parse::Guest;

struct Component;

impl Guest for Component {
    fn parse_cost(cost: String) -> f32 {
        cost.parse().unwrap_or(0.0)
    }

    fn parse_unit(unit: String) -> f32 {
        unit.parse().unwrap_or(0.0)
    }
}

bindings::export!(Component with_types_in bindings);

however, when using get_typed_func(), the compile complained about required for std::string::String to implement WasmParams``

fn parse_cost(&self, func_id: &str, cost: &str) -> Result<f32, String> {
        let guard = self.modules.lock().unwrap();
        let module = guard.get(func_id).ok_or("Module not found")?;

        let mut store = Store::new(&self.engine, ());
        let instance = Instance::new(&mut store, module, &[]).unwrap();

        let parse_cost = instance.get_typed_func::<(String), f32>(&mut store, "parse_cost").unwrap();

        let result = parse_cost.call(&mut store, cost.to_string()).map_err(|e| e.to_string())?;

        Ok(result)
    }

Before this, I figured out a way to manually allocate memory and convert string to {ptr, len}, it worked but it was not perfect, I was hoping wit-bindgen could help me avoid this manual efforts, now I wonder if it is possible?

thanks in advance.

view this post on Zulip Joel Dice (Aug 02 2024 at 21:27):

Try changing (String) to (String,) in the host code.

view this post on Zulip Joel Dice (Aug 02 2024 at 21:28):

Also, consider using the wasmtime::component::bindgen macro, which will make the host side easier.

view this post on Zulip Bo Liu (Aug 02 2024 at 21:45):

@Joel Dice thanks for the suggestion, seems not work, either.

    |                                   --------------   ^^^^^^^^^ the trait `WasmParams` is not implemented for `(std::string::String,)`
    |                                   |
    |                                   required by a bound introduced by this call

view this post on Zulip Joel Dice (Aug 02 2024 at 21:57):

Oops, I see -- you're using the Wasmtime module API rather than the component API. You'll need to convert your module into a component using e.g. wasm-tools component new or else use cargo-component to build it. Then, in the host, you'll need to use the wasmtime::component api to instantiate the component.

view this post on Zulip Joel Dice (Aug 02 2024 at 21:58):

This might be helpful: https://component-model.bytecodealliance.org/

view this post on Zulip Bo Liu (Aug 02 2024 at 22:13):

@Joel Dice
Hmm, still confused as I'm new to wasm.

so what I am doing is to write a grpc server and in this grpc server process, host a few wasm functions by wasm sandboxes, each function in one wasmtime sandbox, would wasmtime::component be able to support the sandbox?

view this post on Zulip Joel Dice (Aug 02 2024 at 22:23):

Yes, components are run with the same isolation guarantees as modules are.

view this post on Zulip Bo Liu (Aug 02 2024 at 22:33):

Just to confirm, is wasmtime::component::Linker what you referred to?

    fn parse_cost(&self, func_id: &str, cost: &str) -> Result<f32, String> {
        let guard = self.modules.lock().unwrap();
        let component = guard.get(func_id).ok_or("Module not found")?;

        let mut linker = Linker::new(&self.engine);
        let instance = linker.instantiate(&mut store, &component).map_err(|e| e.to_string())?;

        let func = instance.get_func(&mut store, "parse_cost").ok_or("Function not found")?;
        let mut result = [Val::F32(0.0)];
        func.call(&mut store, &[Val::String(cost.to_string()), &mut result)?;

        Ok(result[0])
    }

view this post on Zulip Joel Dice (Aug 02 2024 at 22:39):

Yes, but keep in mind you'll need to give it a Wasm component -- not a module -- or it will fail at runtime.

view this post on Zulip Joel Dice (Aug 02 2024 at 22:41):

Also, you should be able to use e.g. https://docs.rs/wasmtime/23.0.1/wasmtime/component/struct.Instance.html#method.get_typed_func with (String,) as the parameter type.

view this post on Zulip Joel Dice (Aug 02 2024 at 22:41):

Or better yet, https://docs.rs/wasmtime/23.0.1/wasmtime/component/macro.bindgen.html

view this post on Zulip Bo Liu (Aug 02 2024 at 22:42):

for wasm component, I've created a .wit file , and use cargo component build --target wasm32-wasip1 --release, I assume it will give me a component back, right?

package component:parser;

interface parse {
    parse-cost: func(cost: string) -> f32;
    parse-unit: func(unit: string) -> f32;
}

world parser {
    export parse;
}

view this post on Zulip Joel Dice (Aug 02 2024 at 22:42):

I think so, yes. I haven't used cargo component for a while, but that's what it's made for.

view this post on Zulip Joel Dice (Aug 02 2024 at 22:43):

I'm guessing you don't need to specify --target when using cargo component

view this post on Zulip Bo Liu (Aug 02 2024 at 22:43):

Joel Dice said:

Or better yet, https://docs.rs/wasmtime/23.0.1/wasmtime/component/macro.bindgen.html

by looking at its example, I'm confused where are the definition of exported functions

view this post on Zulip Bo Liu (Aug 02 2024 at 22:46):

Joel Dice said:

I'm guessing you don't need to specify --target when using cargo component

Yes, that's true

view this post on Zulip Joel Dice (Aug 02 2024 at 22:46):

You can use e.g. cargo-expand to see the code generated by that macro. This example might be helpful, also: https://docs.rs/wasmtime/23.0.1/wasmtime/component/bindgen_examples/_5_all_world_export_kinds/index.html

view this post on Zulip Bo Liu (Aug 02 2024 at 22:53):

Joel Dice said:

Also, you should be able

Does (String,) refer to (Val::String,)?

view this post on Zulip Joel Dice (Aug 02 2024 at 22:53):

no, (std::string::String,)

view this post on Zulip Joel Dice (Aug 02 2024 at 22:54):

Val is only used for the dynamic (i.e. slower, not-statically-type-checked) API

view this post on Zulip Joel Dice (Aug 02 2024 at 22:55):

you should use get_typed_func if you know the types at compile time

view this post on Zulip Joel Dice (Aug 02 2024 at 22:55):

and that's what wasmtime::component::bindgen uses in its generated code

view this post on Zulip Bo Liu (Aug 02 2024 at 23:04):

I guess for result, (f32,) is also needed?

error[E0277]: the trait bound `f32: ComponentNamedList` is not satisfied
   --> src/server.rs:36:57
    |
36  |         let func = instance.get_typed_func::<(String,), f32>(&mut store, "parse_cost").map_err(|e| e.to_string())?;
    |                             --------------              ^^^ the trait `ComponentNamedList` is not implemented for `f32`

view this post on Zulip Joel Dice (Aug 02 2024 at 23:05):

Maybe so -- it's been a while since I used that API directly instead of using the bindgen macro

view this post on Zulip Bo Liu (Aug 02 2024 at 23:12):

ok, let me also try bindgen macro directly

view this post on Zulip Bo Liu (Aug 02 2024 at 23:30):

Joel Dice said:

You can use e.g. cargo-expand to see the code generated by that macro. This example might be helpful, also: https://docs.rs/wasmtime/23.0.1/wasmtime/component/bindgen_examples/_5_all_world_export_kinds/index.html

quick question, if one wants to implement bytes-to-string differently, like

format!("hello {}", bytes)

where should we put the customization?

view this post on Zulip Joel Dice (Aug 02 2024 at 23:34):

You're refering to the code in the example? Are you wanting to modify the example for some reason? Or are you trying to translate what the example does to your use case and WIT world? I'm not quite understanding the question.

view this post on Zulip Bo Liu (Aug 02 2024 at 23:50):

sure, I want to have a better understanding of bindgen!(), by using cargo expand, I did see the function definition,

                        let callee = unsafe {
                            wasmtime::component::TypedFunc::<
                                (u64,),
                                (wasmtime::component::__internal::String,),
                            >::new_unchecked(self.bytes_to_string)

but it's not clear to me where is bytes_to_string defined,

IOW, if we want to use a different implementation for bytes_to_string() in the 5_all_world_export example, can you please suggest how to do it?

view this post on Zulip Joel Dice (Aug 02 2024 at 23:52):

The bytes_to_string function is implemented in the component in that example. The host is calling a function that the component exports.

view this post on Zulip Bo Liu (Aug 03 2024 at 00:04):

Thank you!

I see, it's using function from
let component = Component::from_file(&engine, "./your-component.wasm")?;
where bytes-to-string is defined.
somehow I've been missing this line over and over again.

view this post on Zulip Bo Liu (Aug 05 2024 at 19:51):

Hello,
Following this thread, I was wondering if wit can support references, say if I want to pass a json vec, besides in world.wit I can use list<u8>, which is interpreted into Vec<u8> after bindgen, is it possible to have &[u8] which holds a pointer from host?

package component:parser;

world parse {
    export parse: func(payload: list<u8>) -> list<u8>;
}

view this post on Zulip Pat Hickey (Aug 05 2024 at 19:57):

Nope, the component needs to have the list copied into its own linear memory- wasm can’t express a pointer into something that’s not a memory, and components need to instantiate their own memory, not import it from the host, to make sure they can be composed with other components

view this post on Zulip Bo Liu (Aug 05 2024 at 20:00):

Pat Hickey said:

Nope, the component needs to have the list copied into its own linear memory- wasm can’t express a pointer into something that’s not a memory, and components need to instantiate their own memory, not import it from the host, to make sure they can be composed with other components

Thanks!
So in this case can you please suggest what is the idiomatic way to represent binary bytes of json in wit?

view this post on Zulip Ramon Klass (Aug 05 2024 at 20:10):

json has no notion of bytes or binary, if you want to transport serialized json, you'd probably use string, if you have something that contains an u8[] buffer then json is simply not able to handle this. If it has to be json, people use base64, but I would suggest something like messagepack that can serialize arrays of bytes directly

view this post on Zulip Bo Liu (Aug 05 2024 at 20:26):

sounds good, I'm going to explore more with base64

view this post on Zulip Pat Hickey (Aug 05 2024 at 21:11):

json is a string so you can just use the string type, you dont have to use base64

view this post on Zulip Ramon Klass (Aug 05 2024 at 22:56):

what I meant is: if your data contains a Uint8Array, you can't toJson it, but if that's not the case use string :)

view this post on Zulip Bo Liu (Aug 05 2024 at 23:03):

Ramon Klass said:

what I meant is: if your data contains a Uint8Array, you can't toJson it, but if that's not the case use string :)

Right, I want to reduce json size during the transportation, and I'm using a https://docs.rs/serde_json/1.0.122/serde_json/fn.to_vec.html# instead of to_string(), so on wasm guest, I'll have to use list<u8> to serve json's Vec<u8>

view this post on Zulip Pat Hickey (Aug 05 2024 at 23:14):

the size during transportation wont be changed by using to_vec instead of to_string, the underlying representation is the same, string is just asserting with the rust type system that the vec contents are utf-8 encoded, but json guarantees that anyway

view this post on Zulip Bo Liu (Aug 05 2024 at 23:16):

I see, thank you for the clarification.

view this post on Zulip Ramon Klass (Aug 05 2024 at 23:42):

again, several binary formats would be smaller if payload size if a concern

view this post on Zulip Bo Liu (Aug 14 2024 at 19:24):

Hi, for the same wit file, I'm playing with python wasm support, however, it kept reporting TypeError: Can't instantiate abstract class Parse without an implementation for abstract method 'parse' while I did follow https://component-model.bytecodealliance.org/language-support/python.html

# cat wit/world.wit
package component:parser;

world parse {
    export parse: func(payload: list<u8>) -> list<u8>;
}
# cat src/lib.py
import parser

class Parse(parser.Parse):
    def parse(self, payload: bytes) -> bytes:
        # todo: implement parsing logic
        return payload
# /mnt/wasm/path/to/venv/bin/componentize-py -d ./wit/world.wit -w parse componentize -p ./src parse -o parser.wasm
Traceback (most recent call last):
  File "/mnt/wasm/path/to/venv/bin/componentize-py", line 8, in <module>
    sys.exit(script())
             ^^^^^^^^
AssertionError:

Caused by:
    TypeError: Can't instantiate abstract class Parse without an implementation for abstract method 'parse'

I checked the bindings output, looks like I've used the exactly same class and function name, not sure where went wrong.

# /mnt/wasm/path/to/venv/bin/componentize-py -d ./wit/world.wit -w parse bindings tmp
root@a23ad5413a51:/mnt/wasm/parser_py# cat tmp/parse/
__init__.py  types.py
root@a23ad5413a51:/mnt/wasm/parser_py# cat tmp/parse/__init__.py
from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self
from enum import Flag, Enum, auto
from dataclasses import dataclass
from abc import abstractmethod
import weakref

from .types import Result, Ok, Err, Some



class Parse(Protocol):

    @abstractmethod
    def parse(self, payload: bytes) -> bytes:
        raise NotImplementedError

Did I miss anything?

view this post on Zulip Joel Dice (Aug 14 2024 at 19:27):

It looks like you told componentize-py the module name is "parse", but the file name is lib.py. Can you try changing that to parse.py?

view this post on Zulip Joel Dice (Aug 14 2024 at 19:27):

actually, nevermind; try changing the module name you give componentize-py to lib instead

view this post on Zulip Joel Dice (Aug 14 2024 at 19:28):

There might be a conflict if the world name and the implementation module name are the same

view this post on Zulip Joel Dice (Aug 14 2024 at 19:28):

e.g. componentize-py -d ./wit/world.wit -w parse componentize -p ./src lib -o parser.wasm

view this post on Zulip Bo Liu (Aug 14 2024 at 19:29):

Thanks! oh, right, now I got a different err,

/mnt/wasm/path/to/venv/bin/componentize-py -d ./wit/world.wit -w parse componentize -p ./src -o parser.wasm lib
Traceback (most recent call last):
  File "/mnt/wasm/path/to/venv/bin/componentize-py", line 8, in <module>
    sys.exit(script())
             ^^^^^^^^
AssertionError: Traceback (most recent call last):
  File "/0/lib.py", line 1, in <module>
    import parser
ModuleNotFoundError: No module named 'parser'


Caused by:
    ModuleNotFoundError: No module named 'parser'

view this post on Zulip Joel Dice (Aug 14 2024 at 19:30):

hmm, I don't see lib in your command

view this post on Zulip Joel Dice (Aug 14 2024 at 19:30):

oops, there it is at the end

view this post on Zulip Bo Liu (Aug 14 2024 at 19:30):

at the very end, after -o parser.wasm ;)

view this post on Zulip Joel Dice (Aug 14 2024 at 19:31):

what is the import parser line for? It doesn't look like you're using it.

view this post on Zulip Joel Dice (Aug 14 2024 at 19:32):

ugh, I need to read better before I type; I see you're using the type parser.Parse

view this post on Zulip Bo Liu (Aug 14 2024 at 19:32):

right, trying to get parser.Parse, which is the def from world.wit

view this post on Zulip Joel Dice (Aug 14 2024 at 19:32):

shouldn't it be parse, not parser?

view this post on Zulip Bo Liu (Aug 14 2024 at 19:33):

hmm, maybe.
I thought it was named after package component:parser; part

view this post on Zulip Joel Dice (Aug 14 2024 at 19:34):

no, just the world name

view this post on Zulip Bo Liu (Aug 14 2024 at 19:34):

I see, now I got

y# /mnt/wasm/path/to/venv/bin/componentize-py -d ./wit/world.wit -w parse componentize -p ./src -o parser.wasm lib
Component built successfully

hooray

view this post on Zulip Bo Liu (Aug 14 2024 at 19:35):

Thank you so much!

view this post on Zulip Notification Bot (Sep 09 2024 at 12:18):

Till Schneidereit has marked this topic as resolved.


Last updated: Dec 23 2024 at 13:07 UTC