Skip to content

docs: Clarify setup for "Rust Components" tutorial #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion component-model/examples/example-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ $ cargo run --release -- 1 2 add.wasm
```

> [!NOTE]
> `add.wasm` is available in thsi folder, but can be replaced with your own built WebAssembly component
> `add.wasm` is available in this folder, but can be replaced with your own built WebAssembly component
> at any time (written in any language that supports WebAssembly Components), given that it satisfies
> the `adder` world described above.

Expand Down
3 changes: 2 additions & 1 deletion component-model/examples/tutorial/adder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#[allow(warnings)]
mod bindings;

// Separating out the interface puts it in a sub-module
use bindings::exports::docs::adder::add::Guest;

struct Component;

impl Guest for Component {
fn add(x: u32, y: u32) -> u32 {
a + b
x + y
}
}

Expand Down
4 changes: 2 additions & 2 deletions component-model/src/language-support/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ With `jco transpile` any WebAssembly binary (compiled from any language) can be

Reactor components are WebAssembly components that are long running and meant to be called repeatedly over time. They're analogous to libraries of functionality rather than an executable (a "command" component).

Components expose their interfaces via [WebAssembly Interface Types][docs-wit], hand-in-hand with the [Component Model][docs-component-model] which enables components to use higher level types interchangably.
Components expose their interfaces via [WebAssembly Interface Types][docs-wit], hand-in-hand with the [Component Model][docs-component-model] which enables components to use higher level types interchangeably.


[docs-wit]: ../design/wit.md
Expand Down Expand Up @@ -312,7 +312,7 @@ You should see output like the following:
OK Successfully written string-reverse.wasm.
```

Now that we have a WebAssembly binary, we can *also* use `jco` to run it in a native JavaScript context by *transpiling* the WebAsssembly binary (which could have come from anywhere!) to a JavaScript module.
Now that we have a WebAssembly binary, we can *also* use `jco` to run it in a native JavaScript context by *transpiling* the WebAssembly binary (which could have come from anywhere!) to a JavaScript module.

```console
npx jco transpile string-reverse.wasm -o dist/transpiled
Expand Down
182 changes: 89 additions & 93 deletions component-model/src/language-support/rust.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,103 @@
# Components in Rust

Rust has first-class support for the component model via [the `cargo component` tool](https://github.com/bytecodealliance/cargo-component).
Rust has first-class support for the component model via the [`cargo-component`
tool][cargo-component]. We will be using
the `cargo component` subcommand to create WebAssembly components using Rust as
the component's implementation language.

`cargo component` is is a `cargo` subcommand for creating WebAssembly components
using Rust as the component's implementation language.
> [!NOTE]
> You can find more details about `cargo-component` on [crates.io](https://crates.io/crates/cargo-component).

## 1. Installing `cargo component`

To install `cargo component`, run:
## Setup

Install [`cargo-component`][cargo-component-install]:
```sh
cargo install cargo-component
cargo install --locked cargo-component
```

> You can find more details about `cargo component` in its [crates.io page](https://crates.io/crates/cargo-component).

## 2. Scaffold a Component with `cargo component`

Create a Rust library that implements the `add` function in the [`adder`world][adder-wit].

First scaffold a project:

Install [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools#installation):
```sh
$ cargo component new add --lib && cd add
cargo install --locked wasm-tools
```
Install [`wasmtime`](https://github.com/bytecodealliance/wasmtime#installation):
```sh
curl https://wasmtime.dev/install.sh -sSf | bash
```
Clone the [component-docs](https://github.com/bytecodealliance/component-docs) repo:
```sh
git clone https://github.com/bytecodealliance/component-docs
```

Note that `cargo component` generates the necessary bindings as a module called `bindings`.
## Scaffolding a Component

## 3. Add the WIT world the Component will implement
We will create a component in Rust that implements the `add` function exported
by the [`docs:adder/adder`][docs-adder] world in the
`docs:adder`
[package](https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#package-names).

Next, update `wit/world.wit` to match `add.wit`:
First `cd` into the `tutorial` directory found in the repo we just cloned:
```sh
cd component-docs/component-model/examples/tutorial
```

Now create a new WebAssembly component package called `add`:
```sh
cargo component new add --lib && cd add
```
package docs:[email protected];

interface add {
add: func(x: u32, y: u32) -> u32;
}
## Adding the WIT world

world adder {
export add;
}
We now need to change our generated `wit/world.wit` to match `docs:adder`:
```wit
{{#include ../../examples/tutorial/wit/adder/world.wit}}
```

The `component` section of `Cargo.toml` should look like the following:
The `package.metadata.component` section of our `Cargo.toml` should be changed
to the following:

```toml
[package.metadata.component]
package = "docs:adder"
```

## 4. Generate bindings for our component
## Generating bindings

After performing these changes, we can re-generate bindings with `cargo component bindings`:
Now that we've updated our `world.wit` and `Cargo.toml`, we can re-generate
bindings with the command below:

```console
```sh
cargo component bindings
```

`cargo component bindings` will generate bindings for the world specified in a package's `Cargo.toml`. In particular,
`cargo component` will create a `Guest` trait that a component should implement.
`cargo-component` will generate bindings for our
world and create a `Guest` trait that a component should
implement

## 5. Implement the generated `Guest` trait
## Implementing the `Guest` trait

Implement the `Guest` trait in `src/lib.rs`, using the scaffolded code. Your code should look something like the following:
Implement the `Guest` trait in `src/lib.rs`, using the scaffolded code. Your
code should look something like the following:

```rs
#[allow(warnings)]
mod bindings;

use bindings::exports::docs::adder::add::Guest;

struct Component;

impl Guest for Component {
fn add(x: u32, y: u32) -> u32 {
return x + y;
}
}

bindings::export!(Component with_types_in bindings);
{{#include ../../examples/tutorial/adder/src/lib.rs}}
```

## 6. Build the component
## Building a Component

Now, use `cargo component` to build the component, being sure to optimize with a release build.
Now, let's build our component, being sure to optimize with a release build:

```console
```sh
cargo component build --release
```

You can use `wasm-tools component wit` to output the WIT package of the component:
You can use `wasm-tools` to inspect the WIT package generated by `cargo-component`:

```sh
wasm-tools component wit target/wasm32-wasip1/release/add.wasm
```
$ wasm-tools component wit target/wasm32-wasip1/release/add.wasm

The command above should produce the output below:

```wit
package root:component;

world root {
Expand All @@ -106,10 +110,10 @@ package docs:[email protected] {
}
```

### Running a Component from Rust Applications
### Running a Component

To verify that our component works, lets run it from a Rust application that knows how to run a
component targeting the [`adder` world][adder-wit].
component targeting the [`docs:adder/adder`](#adding-the-wit-world) world.

The application uses [`wasmtime`](https://github.com/bytecodealliance/wasmtime) crates to generate
Rust bindings, bring in WASI worlds, and execute the component.
Expand All @@ -120,46 +124,31 @@ $ cargo run --release -- 1 2 ../add/target/wasm32-wasip1/release/add.wasm
1 + 2 = 3
```

## Exporting an interface with `cargo component`

The [sample `add.wit` file](https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/example-host/add.wit) exports a function. However, you'll often prefer to export an interface, either to comply with an existing specification or to capture a set of functions and types that tend to go together. For example, to implement the following world:

```wit
package docs:[email protected];

interface add {
add: func(x: u32, y: u32) -> u32;
}
## Exporting an interface

world adder {
export add;
}
```
Notice how our `root` world in the `wasm-tools` output exports `add` as part of an _interface_.
It's often preferable to export an interface rather than a function, either to
comply with an existing specification or to capture several functions and types
at once.

you would write the following Rust code:
For example, to implement the [`docs:adder/adder`](#adding-the-wit-world)
world, you would write the following Rust code:

```rust
mod bindings;

// Separating out the interface puts it in a sub-module
use bindings::exports::docs::adder::add::Guest;

struct Component;

impl Guest for Component {
fn add(x: u32, y: u32) -> u32 {
a + b
}
}
{{#include ../../examples/tutorial/adder/src/lib.rs}}
```

## Importing an interface with `cargo component`
## Importing an interface

The world file (`wit/world.wit`) generated for you by `cargo component new --lib` doesn't specify any imports.
The world file (`wit/world.wit`) we generated doesn't specify any imports. If
your component consumes other components, you can edit the `world.wit` file to
import their interfaces.

> `cargo component build`, by default, uses the Rust `wasm32-wasi` target, and therefore automatically imports any required WASI interfaces - no action is needed from you to import these. This section is about importing custom WIT interfaces from library components.
> [!NOTE]
> This section is about importing custom WIT interfaces from library components.
> By default, `cargo-component` imports any required [WASI interfaces](https://wasi.dev/interfaces)
> for us without needing to explicitly declare them.

If your component consumes other components, you can edit the `world.wit` file to import their interfaces.

For example, suppose you have created and built an adder component as explained in the [exporting an interface section](#exporting-an-interface-with-cargo-component) and want to use that component in a calculator component. Here is a partial example world for a calculator that imports the add interface:

Expand All @@ -181,14 +170,19 @@ world calculator {

### Referencing the package to import

Because the `docs:adder` package is in a different project, we must first tell `cargo component` how to find it. To do this, add the following to the `Cargo.toml` file:
Because the `docs:adder` package is in a different project, we must first tell
`cargo component` how to find it. To do this, add the following to the
`Cargo.toml` file:

```toml
[package.metadata.component.target.dependencies]
"docs:adder" = { path = "../adder/wit" } # directory containing the WIT package
"docs:adder" = { path = "../wit/adder" } # directory containing the WIT package
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes they cloned the repo rather than just fetching the wit. I'd prefer to stay with the approach of fetching the one wit file rather than cloning the repo

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on #218 (comment), if we don't specify cloning the repo then we'd have to specify fetching the wit/adder, wit/calculator, and examples/adder directories explicitly, I don't mind doing this but it seems like that adds more complexity than a git clone.

git clone is a temporary approach regardless until #241 (comment) is done.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think fetching a file is the most analogous to what the wkg experience will be like

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that that's what we should aim for, however, calling cargo run --release -- 1 2 ../add/target/wasm32-wasip1/release/adder.wasm for this step requires us to have all seven files from the example-host directory present.

Should I instead add an instruction to wget the directory and extract it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the example host is an exception -- for now, they will need to clone the repo to run it. Maybe this line should be cargo run --release -- 1 2 adder.wasm to not imply that they build the adder component from the repo.

The main thing i am thinking about is that the fact that the example host is not fetchable should not define how we instruct users to build components. We can push the host up in a container for example or release it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a great and useful example that demonstrantes using external logic to execute the user's code.
I haven't found a succinct way, other than git, to fetch the example-host logic unless something like wkg can do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to have a wit in ghcr that they can depend on? Then there would be no question about file paths because they wouldn't be needed when specifying the dependency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitely the plan: upload the packages to ghcr and update the tutorial and guides to use wkg. We just need the work to be prioritized

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue to get them in GHCR: #243

Copy link
Author

@mkatychev mkatychev Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the meantime for this PR, should I revert the path changes and references to git clone?
EDIT: that's what was done

```

Note that the path is to the adder project's WIT _directory_, not to the `world.wit` file. A WIT package may be spread across multiple files in the same directory; `cargo component` will look at all the files.
> [!NOTE]
> The path for `docs:adder` is relative to the `wit` _directory_, not to the `world.wit` file.
>
> A WIT package may be spread across multiple files in the same directory; `cargo component` will search them all.

### Calling the import from Rust

Expand Down Expand Up @@ -292,8 +286,8 @@ As mentioned above, `cargo component build` doesn't generate a WIT file for a co

```toml
[package.metadata.component.target.dependencies]
"docs:calculator" = { path = "../calculator/wit" }
"docs:adder" = { path = "../adder/wit" }
"docs:calculator" = { path = "../wit/calculator" }
"docs:adder" = { path = "../wit/adder" }
```

> If the external package refers to other packages, you need to provide the paths to them as well.
Expand Down Expand Up @@ -581,4 +575,6 @@ If you are hosting a Wasm runtime, you can export a resource from your host for
}
```

[adder-wit]: https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/tutorial/wit/adder/world.wit
[cargo-component]: https://github.com/bytecodealliance/cargo-component
[cargo-component-install]: https://github.com/bytecodealliance/cargo-component#install
[docs-adder]: https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/tutorial/wit/adder/world.wit