I'm building a web platform similar to Netlify/Heroku, and I'd like users to write code in Rust/AssemblyScript/... and have my platform run their code (not targeted for web). The issue I'm facing is that wasm lacks support for advanced types like structs, strings, bools, etc.
Would the best solution for now be to make my users write their code with wit-bindgen?
(Their code will be generated with https://github.com/thalo-rs/esdl, and implemented in Rust).
I know the code users write will be very basic functions (if else kind of business logic), and all their functions are basic scalar types (string/int/float/bool) or structs with fields only of these types.
I would go with writing your SDK using wit-bindgen, but handle user's data via serialization. Otherwise you will need generate your host app for each of the user's data type (or work with dynamic types of wasm imports/exports, which is not convenient at all).
I see. I am finding it quite hard to figure out how to use wit-bindgen.
Would the users need to run the cli themselves? The thing about my situation is that my library provides codegen, and they define their traits in a separate schema language.. so I will know what functions are available already.
After trying and looking through Github issues, I have had no luck.
I came across this issue: https://github.com/rustwasm/wasm-bindgen/issues/2471
It seems like the type interface isn't ready and won't be for quite some time sadly
It looks like your user will run Rust compiler right?
They don't need CLI then. Instead of using cli you use procedural macro in your SDK or generated code:
wit_bindgen_rust::export("some-wit-file.wit");
wit_bindgen_rust::import("other-wit-file.wit");
And then use types generated. For export
to work you will need a new structure and implement a trait. Compiler will guide you through this (by error messages).
You can use CLI to generate another copy of the code just to see what is being generated (or alternatively use cargo expand
)
Generally, yes it's a bit rough. And documentation is lacking.
(fwiw, we're very much aware. All of this is evolving rapidly and will be in a much better shape with fewer rough edges and more stability soon(-ish))
Is the wit_bindgen_rust
crate published on crates.io? Or could I import it from git directly?
Importing from git works just fine
I was struggling with it because it doesnt seem to be in the cargo workspace, and is nested in crates directory
Pretty much what I'm trying to do is:
I have that in my Cargo.toml :
[dependencies]
wit-bindgen-rust = {git = "https://github.com/bytecodealliance/wit-bindgen.git"}
Can I write rust code and have my wit file generated from the Rust code?
There is https://github.com/bnjjj/witgen but I'm not sure what the state it is.
Ah okay, sorry I'm still trying to understand the details.
Would the library author of the wasm code use wit_bindgen_rust::export
, and my rust code that wants to use their wasm would use wit_bindgen_rust::import
?
For example, I want to write an add function. I have a wit file:
add: function(input: string) -> string
And then Rust lib.rs file:
use wit_bindgen_rust::import;
import!("./witgen.wit");
pub fn add(input: String) -> String {
format!("{} there", input)
}
But I'm not exactly sure what I'm doing.. if I need to use a proc macro on my add function (eg wasm_bindgen, or if wit_bindgen is a replacement for that..)
This example might explain it:
use wit_bindgen_rust::import;
import!("./witgen.wit");
pub use witgen::add;
fn main() {
println!("Hello {:?}", add("hello"));
}
Importing is to get this function from the host app. import
macro will declare a function in a module that is named after the file. So common case is to rexport it from the module that uses import
macro (pub use module_name::func_name;
).
Ah okay so import is for importing a wit file.. but how should I export?
The export macro seems to expect me to define a type with the same name or something
Also, should I be using a specific wasm builder with this?
Like wasm-pack, or wasmtime, etc
Yeah, you just make an empty structure, and put function on it's implementation (note no self, just use type as a module):
export!("witgen.wit");
struct Witgen;
impl witgent::Witgen for Witgen {
fn add(inp: String) -> String {
format!("{} there", input)
}
}
Also, should I be using a specific wasm builder with this?
No. No specific builder, just regular cargo build --target=wasm32-wasi
(or wasm32-unknown-unknown
)
Okay so I have made a project.
/app.wit
:
add: function(input: string) -> string
/src/lib.rs
:
wit_bindgen_rust::export!("./app.wit");
struct App;
impl app::App for App {
fn add(input: String) -> String {
format!("{} there", input)
}
}
/src/main.rs
:
use wit_bindgen_rust::import;
import!("./app.wit");
pub use app::add;
fn main() {
println!("Hello {:?}", add("hello"));
}
Then I build the lib:
$ cargo build --target wasm32-wasi --lib --release
And try to run the app:
$ cargo run
But I get this:
error: linking with
cc
failed: exit status: 1
I must be missing something.. I'm not sure where it imports the wasm file from
I tried to move the app.wasm file from /target to the root of my project
Oh. well well, wit_bindgen_rust
is only for in-wasm part. And you have to write full wasmtime-based runner/host for that. And use wit_bindgen_wasmtime
for the host app.
Along the lines of this: https://github.com/bytecodealliance/wasmtime/blob/main/examples/wasi/main.rs
Unless what you're trying it to link two wasm modules, then wasmtime-cli might have a some way to do that, I'm not sure.
Oh cool thank you, I'll have a look!
I've made a Github repo showing what I'm trying and how its not working. If anyone could take a look, it would be really appreciated... I feel like I'm almost there, but maybe missing one last thing.
https://github.com/tqwewe/wasmer-wasi
The README.md shows the commands and my error
https://github.com/tqwewe/wasmer-wasi/blob/main/runner/src/main.rs
Should use wit_bindgen_wasmtime
(not wit_bindgen_rust
, _rust
is for wasm-compiled modules)
Hm I tried it in the runner package main.rs, but I get an error:
pub use domain::add;
// ^^^^^^^^^^^ no `add` in `domain`
This is just from replacing wit_bindgen_rust
with wit_bindgen_wasmtime
in Cargo.toml and main.rs.
I thought the domain package (lib) also needs to use wit_bindgen_wasmtime
, but when trying to build the wasm I get a bunch of errors:
thread 'main' panicked at 'error when identifying target: "no supported isa found for arch
wasm32
"', /Users/ari/.cargo/registry/src/github.com-1ecc6299db9ec823/cranelift-codegen-0.80.0/build.rs:42:53
I see.. looks like I need to pub use domain::Domain
... and then use Domain::new(...)
I just need to figure out the right arguments to provide for ::new(...)
... and caller: impl AsContextMut<Data = T>,
for when calling .add()
https://github.com/tqwewe/wasmer-wasi/blob/main/runner/src/main.rs
That works... but I don't know if my approach is correct.
I did:
let mut wasi = WasiCtxBuilder::new()...;
let domain_index = wasi.table().push(Box::new(DomainData::default()))?;
Domain::instantiate(&mut store, &module, &mut linker, move |s| {
s.table().get_mut::<DomainData>(domain_index).unwrap() // This doesnt seem very right...
})?;
It looks like you've pushed your domain data to the WASI file descriptor table. It will work, but there is a simpler way.
// create your own Data structure
struct Data {
//wasi structure here
wasi: wasmtime_wasi:sync::WasiCtx,
// and any extra data for wasm here, including domain data:
domain: DomainData,
}
// then instead of passing wasi directly to `Store` use your own Data:
Store::new(&engine, Data { wasi, domain });
// in all linking functions use an attribute
wasmtime_wasi::add_to_linker(&mut linker, |s| &mut s.wasi)
Domain::instantiate(&mut store, &module, &mut linker, |s| &mut s.domain)
In some cases you need to specify type on these lambdas like |s: &mut Data| s.wasi
.
Worked perfectly! Thank you so much, this is exactly what I was looking for since a few days ago
So this approach should allow possibly Golang or Python to write the library side of it?
In theory, yes. But I think there is no codegen for them yet. Also in Go I think standard compiler is tied to browser (GOOS=js), although tinygo should work if you write bindings manually. Similarly with Python there are few ways to compile it for wasm including RustPython and Pyodide, I'm not sure what is the state of any of that.
Is it possible to dynamically call functions with wit? For example, load a wit file and execute a function at runtime of a Rust app?
In theory yes. With current implementation -- no, as far as I understand. Wasmtime allows to call functions dynamically, so you can use the parser of wit and build function arguments manually.
But I'm also not sure what's the point. If you want to user-specified JSON and give it to wasm for processing, just feed that json to wasm directly instead of deserializing it and building wasmtime signature of it.
I see, I guess the simple approach is json (de)serialize like you suggested.
But in my case, I am building a platform where users can upload/submit a wasm file, but along with the wasm file is a schema file similar to .wit files.
So it would be beneficial for my system to be able to call their functions directly as I know the function signatures available
Otherwise the users have to use a standardized function which takes the data as json and they have to deserialize it themselves
The only problem with wasmtime dynamic calling, is I don't think it is easy to do with non-number types
https://docs.rs/wasmtime/latest/wasmtime/struct.Instance.html#method.get_typed_func
Typed func needs params and return types to be WasmTy
, which is only implemented for numeric types it seems.. unless there's a different way of doing it
Take a look what wit-bindgen-cli wasmtime your.wit
generates. You have to use ints as pointers and read the wasm VM's memory.
Yeah I was just about to say that
(deleted)
I guess I'd have to rewrite this code
Screen-Shot-2022-01-29-at-10.47.32-pm.png
Generally it looks like you'll be using wit as json-schema. I'm not sure this is a the best idea. JSON schema (or most other validators) are more powerful and rich in types. Although, I like variant types in wit, so it may be good idea.
I made a schema language https://github.com/thalo-rs/esdl
From ESDL files, it generates Rust code for the user. So given that I know the Rust code from this schema, I could probably some library that can dynamically call based on the schema file.. but it seems like a deep rabbit hole to go down, especially since I'm not very familiar with how wasm works under the hood
I could probably piggy back off the wit_bindgen_wasmtime
import macro in some way if I did go down that route
So you probably can generate structures and serde signatures for esdl, and make it transparent for users whether there is serde or wit under the hood?
I'm not sure I understand completely, but my main goal is go keep things as simple as possible for users writing the code which will be compiled to wasm.
https://github.com/thalo-rs/thalo/tree/main/examples/bank-account
I'd like them to be able to just write this basic code, and upload the WASM + ESDL/Wit file to my platform, and my platform can run their commands defined
Hm. It looks like you're doing exactly that (generating serde models) in generated code :)
Yep, additionally a trait for users to implement
(in the example, it's BankAccountCommand
trait)
So I'd use some process_event: function (data: list<u8>) -> list<u8>
signature and use the same codegen or a macro to generate necessary ser/de calls. So users don't have to come with their own wit
file for each esdl
file. This sounds friendlier to users. No?
Yeah if I understand correctly, I started doing something like that. Making a standard wit file they use:
apply: function(event: string) -> string
handle: function(command: string) -> string
And all the wasm files use this wit file. And the users code would have some kind of macro or helper to define these methods which kind of wires things up from within the wasm module itself
Going with that approach seems to make the most sense
Is _
valid in wit files?
I was looking here: https://github.com/WebAssembly/interface-types/blob/main/proposals/interface-types/Explainer.md#interface-types
And I see for expected:
(expected <intertype>? (error <intertype>)?)
And tried apply: function(event: list<u8>) -> expected<_, s64>
in the online playground and it seemed to work
Note the first generic of expected is _
in expected<_, s64>
Yeah, looks like it stands for rustish ()
. I think it doesn't work in any other case except in expected
though.
I think I got a nice generic approach which should work with many different implementations.
// domain.wit
// Errors which can occur
variant error {
command(list<u8>),
deserialize-state,
deserialize-event,
deserialize-command,
serialize-state,
}
// Creates a new instance
// Returns state
new-instance: function(id: string) -> expected<list<u8>, error>
// Takes state, event
// Returns new state
apply-event: function(state: list<u8>, event: list<u8>) -> expected<list<u8>, error>
// Takes state, command
// Returns list of events
handle-command: function(state: list<u8>, command: list<u8>) -> expected<list<list<u8>>, error>
I wrote a bunch of magic in the thalo::include_aggregate!("BankAccount");
macro, and it makes that bank example code I sent above just work as is (just need to add the domain.wit file to the project
Regarding the struct which implements the trait from wit_bindgen_rust::export!("./domain.wit");
... I cannot store anything there in wasm?
As in, none of the wit methods take self, so I cannot maintain state there.
wit_bindgen_rust::export!("./domain.wit");
struct Domain { ... }; // the state inside cannot be used from my wasm module?
impl domain::Domain for Domain {
fn my_function(foo: String) { ... } // Doesn't take self... cannot use any of the state?
}
I put my function calls in a loop 100,000 times and timed it.
From Rust, calling wasm functions I got: ~920ms average
When I call the functions in pure Rust without compiling to wasm, I get: ~140ms average
Is that just the performance of wasm? Can I expect that to improve with time?
You can use resource
to keep state: https://github.com/bytecodealliance/wit-bindgen/blob/main/WIT.md#item-resource (or just use global/static variables in Rust).
Regarding performance. I can't comment on specific numbers. But generally yes, some overhead is expected. You can probably send events in batches to make the overhead smaller.
Are resources still a thing in the wit spec?
I'm trying to use it on the wit playground <https://bytecodealliance.github.io/wit-bindgen/>... but it doesn't parse the syntax correctly.
I don't know when the playground was last deployed or if it even uses git version of wit but at this point wit is so much in fluctuation that I would always test locally
Ramon Klass said:
I don't know when the playground was last deployed or if it even uses git version of wit but at this point wit is so much in fluctuation that I would always test locally
I also tested it locally and sadly got the same results. I was even using the dependency from git main branch
Resources remain part of the WIT spec but have been (temporarily) removed from implementations to ease transition to the component model. ref: https://github.com/bytecodealliance/wit-bindgen/pull/346
Resources are not currently part of the component model but the plan is that they're the next feature to add
Is there anywhere I can follow updates about resources support?
https://github.com/WebAssembly/component-model/pull/129
Last updated: Dec 23 2024 at 12:05 UTC