Hey! I'm following this component tutorial at https://component-model.bytecodealliance.org/language-support/rust.html
I just generated the component via cargo component new add --lib
.
When I build and checked the output with wasm-tools component wit target/wasm32-wasip1/release/add.wasm
instead of just the package and world with the 1 line export I got a ton of wasi related things in there too.
There's no mention of the string wasi
anywhere in the code or toml files, so I'm wondering where the wasi dependency is coming from, I have the latest cargo-component. Cheers :)
> wasm-tools component wit target/wasm32-wasip1/release/add.wasm
package root:component;
world root {
import wasi:cli/environment@0.2.0;
import wasi:cli/exit@0.2.0;
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;
import wasi:clocks/wall-clock@0.2.0;
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
export hello-world: func() -> string;
}
package wasi:io@0.2.0 {
Oh, it seems cargo component build
defaults to using one of the wasi targets, instead of wasm32-unknown-unknown
. Specifying the unknown target fixed the issue.
Hey Trent when building a Rust program those extra WASI deps are included because the program could use stdin
, stdout
, exit
and std::fs
operations (whether they are actually supported by the host that will run the components or not).
As an example, jco
(the WebAssembly JS toolchain) will include wasi:http/outgoing-handler
because components can call fetch()
or NodeJS arbitrarily. In jco
this behavior can be controlled (via a --disable
flag) but the concept is the same.
Running plain cargo build --target=wasm32-wasip1
against the example component does not seem to produce the extra wasi
imports, at first:
package root:root;
world root {
export add: func(x: s32, y: s32) -> s32;
}
package example:component {
world example {
export add: func(x: s32, y: s32) -> s32;
}
}
But if you look at the WAT (wasm-tools print target/wasm32-wasi/debug/add.wasm
), you'll see;
(module $add.wasm
(type (;0;) (func (param i32)))
(type (;1;) (func (param i32 i32) (result i32)))
(type (;2;) (func (param i32 i32 i32)))
(type (;3;) (func (param i32 i32 i32) (result i32)))
(type (;4;) (func (param i32 i32)))
(type (;5;) (func (param i32 i32 i32 i32) (result i32)))
(type (;6;) (func))
(type (;7;) (func (param i32) (result i32)))
(type (;8;) (func (param i32 i32 i32 i32 i32)))
(type (;9;) (func (param i32 i32 i32 i32)))
(type (;10;) (func (result i32)))
(type (;11;) (func (param i32 i32 i32 i32 i32 i32 i32)))
(type (;12;) (func (param i32 i32 i32 i32 i32 i32) (result i32)))
(type (;13;) (func (param i32 i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write" (func $_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17h475f31d58a873943E (;0;) (type 5)))
(import "wasi_snapshot_preview1" "environ_get" (func $__imported_wasi_snapshot_preview1_environ_get (;1;) (type 1)))
(import "wasi_snapshot_preview1" "environ_sizes_get" (func $__imported_wasi_snapshot_preview1_environ_sizes_get (;2;) (type 1)))
(import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (;3;) (type 0)))
(table (;0;) 37 37 funcref)
(memory (;0;) 17)
When cargo component
gets to creating a WebAssembly component out of the module (the output of cargo build --target=wasm32-wasip1
is a module!), the fd_write,
proc_exit and
environ_*` imports (added because you could use them) are resolved into WASI imports.
The explicit import in the generated module explain wasi:cli/exit
and wasi:cli/environment
but what about the rest of them? Well -- wasi:filesystem
's write
method (which is needed for fd_write
happens to import wasi:io/streams
and wasi:clocks
, and you can imagine how the other imports get pulled in.
Building for wasm32-unknown-unknown
removes the expectation of the underlying platform being there, which is why you don't get any of the WASI imports (as you intended!). This is certainly a way to trim down the imports, but the problem is that if you ever use parts of std
, you might get the idea that you were successfully using something like std::env::var
because it would compile, but you'd get no-ops in your code (courtesy of rustc
platform support stubs)
For example the code below builds on wasm32-unknown-unknown
:
#[allow(warnings)]
mod bindings;
use bindings::Guest;
struct Component;
impl Guest for Component {
fn add(_x: i32, _y: i32) -> i32 {
eprintln!("TEST");
// x + y // obviously we'd never get here
}
}
bindings::export!(Component with_types_in bindings);
But you'll never get a chance to actually handle that stderr
output at the host level -- it will be compiled into a no-op, rather than wasi::cli/stderr
calls (that the host or other components can implement).
This is obviously a lot to take in! To try to summarize, I wonder if what we need to do here is:
wasm32-unknown-unknown
target (and include the caveats)rustc
build targets from the Cargo.toml
in the cargo component
config there--wit
option to cargo component new
so that we can target an existing WIT world (whether a local file, in a registry, or possibly somewhere on the internet) when creating a new component (i.e. this command should copy in the WIT file, set the world contained in it as the target
in config, etc)world
in the local WIT file, cargo component
expects the WIT to already be present in a registry, rather than available locally) -- This wouldn't have really solved your problem, but I was surprised by how this worked.What's somewhat interesting here is that by using wasm32-unknown-unknown
you can actually write code like this:
#[allow(warnings)]
mod bindings;
use bindings::Guest;
struct Component;
impl Guest for Component {
fn echo(s: String) -> String {
s + ", ECHO!"
}
}
bindings::export!(Component with_types_in bindings);
And build it into a wasm32-unknown-unknown
module -- at first glance this doesn't make sense (at least to me!) because core WASM modules have no concept of String
s, but it's the bindings::export!
that converts the Rust code above to make it work, using the generated lifting/lowering of Component Model types (inbindings.rs
) from the module (i.e. that's how a fn echo(s: String) -> String
will turn into (func $echo (;34;) (type 2) (param i32 i32) (result i32) ...)
.
Of course, wherever you use the module from you'll need to ensure you have the same lifting/lowering scheme (i.e. the Component Model) working for those input i32
s to go from a String
(on the caller side) into i32
s as input, and for the i32
in the result to turn into a String
as well.
Oh BTW to get more specific about those imports it's the default Rust panic and unwind machinery built into std
that is explicitly using those imports :) -- it's not that you could use them.
To remove it at the std library level you'd have to use nightly Rust to remove the panic/unwind machinery:
First ensure that wasm32-wasip1
is installed on the nightly toolchain:
rustup target add wasm32-wasip1 --toolchain nightly
Then use nightly to build std
with panic being just a straight abort:
RUSTUP_TOOLCHAIN=nightly cargo component build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target=wasm32-wasip1 --release
(this works because cargo component
just passes everything to cargo build
, though it doesn't take the +nightly
modifier, we can use nightly via ENV)
If you use just regular cargo build
:
cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target=wasm32-wasip1 --release
After doing that, if you wasm-tools print
the resulting component, you'll see:
(component
(core module (;0;)
(type (;0;) (func))
(type (;1;) (func (param i32 i32) (result i32)))
(type (;2;) (func (param i32 i32 i32)))
(type (;3;) (func (param i32 i32 i32 i32) (result i32)))
(type (;4;) (func (param i32)))
(type (;5;) (func (param i32) (result i32)))
(type (;6;) (func (param i32 i32 i32) (result i32)))
(type (;7;) (func (param i32 i32)))
(table (;0;) 2 2 funcref)
(memory (;0;) 17)
(global (;0;) (mut i32) i32.const 1048576)
(export "memory" (memory 0))
(export "echo" (func 1))
(export "cabi_post_echo" (func 4))
(export "cabi_realloc_wit_bindgen_0_36_0" (func 6))
(export "cabi_realloc" (func 19))
(elem (;0;) (i32.const 1) func 19)
;; ...
Obviously... this is quite involved :) (and requires nightly rust) -- targeting wasm32-unknown-unknown
is certainly easier, but hopefully at least this definitively answers the question!
Last updated: Dec 23 2024 at 13:07 UTC