Stream: wasmtime

Topic: wasi pulled in unintentionally


view this post on Zulip Trent (Dec 20 2024 at 05:08):

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 {

view this post on Zulip Trent (Dec 20 2024 at 05:34):

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.

view this post on Zulip Victor Adossi (Dec 21 2024 at 05:29):

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:

The fetch() method of the Window interface starts the process of fetching a resource from the network, returning a promise that is fulfilled once the response is available.
Command-Line Interface (CLI) World for WASI. Contribute to WebAssembly/wasi-cli development by creating an account on GitHub.
Command-Line Interface (CLI) World for WASI. Contribute to WebAssembly/wasi-cli development by creating an account on GitHub.
Empowering everyone to build reliable and efficient software. - rust-lang/rust

view this post on Zulip Victor Adossi (Dec 21 2024 at 05:59):

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 Strings, 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 i32s to go from a String (on the caller side) into i32s as input, and for the i32 in the result to turn into a String as well.

view this post on Zulip Victor Adossi (Dec 21 2024 at 06:28):

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