-
Notifications
You must be signed in to change notification settings - Fork 62
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
docs: Clarify setup for "Rust Components" tutorial #218
base: main
Are you sure you want to change the base?
Changes from all commits
2e207e4
d22b430
97d6d55
262d47f
661ac99
d23903d
a2228c0
02e4760
4c1c58c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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] | ||
mkatychev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> 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}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fantastic, this will save a ton of work! |
||
``` | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vados-cosmonic I've removed the numbering for the headers to simplify anchor link generation, happy to add them back in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd love feedback from @kate-goldenring here -- I think it's mostly a style thing. I think numbers help people refer to steps (ex. "please see step 3") but I could definitely go the other way. That said, if we want to remove the numbers here, we should remove them everywhere (which isn't a hard change either, happy to help here or in a follow up if you'd rather not do it). Thanks for working on this! 🙇 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, I was mostly following style patterns off of the rust book, including using present continuous tense for titles, ex: including v.s. include. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this makes sense... Maybe what we want is actually demarcation at a higher level (ex. in the Rust book the sections are clearly demarcated in the ToC on the left), then we can go with continuous tense and still be able to refer to things? Happy either way here, I think the Rust book is an excellent example to follow so maybe we should change to look more like them, even if I'm partial to "steps". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind elaborating in your 'including demarcation' comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into this a bit and it looks like adding subchapters based on anchor links is still an open issue: the component docs currently use numbered subchapters, however they're currently broken up by language: We could break up the subchapters into their own markdown page so for example
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another problem with having numbered headers is that any external references to the section (such as https://component-model.bytecodealliance.org/language-support/rust.html#6-build-the-component) will become broken if the section gets reordered. I wonder if there's a cleaner way of doing header numbering in the same way as numbered lists where the numbering gets automatically reordered:
Codeblock above produces this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmnnn OK, rather than make any large changes in this PR, let's maintain consistency at least then discuss it -- It looks like there are a bunch of options here, and we're probably not the only two people who want to chat it out! |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah so my stance on that is that writing into a console is not normally syntax highlighted either, so it matches. If we have a
and if it's code, then it should be highlighted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've done it in this PR: #232 |
||
```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`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't quite right -- we're actually inspecting the built component itself, which happens to contain the WIT that was used to create it. I think the old wording was right here. |
||
|
||
```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 { | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure being explicit about the namespace and package herehelps -- not everyone knows the more qualified naming schemes for WIT interfaces. Someone might think "what is the I think we want people to think in terms of worlds, which import/export interfaces -- the namespace and package are much less important IMO. |
||
|
||
The application uses [`wasmtime`](https://github.com/bytecodealliance/wasmtime) crates to generate | ||
Rust bindings, bring in WASI worlds, and execute the component. | ||
|
@@ -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}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not against reverting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the includes -- certainly saves us time 👍 |
||
``` | ||
|
||
## 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. | ||
mkatychev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> [!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: | ||
|
||
|
@@ -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 | ||
``` | ||
|
||
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 | ||
|
||
|
@@ -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. | ||
|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this comment makes sense, because someone coming to this would probably think "as opposed to what?" and we don't have an example of exporting the function directly anymore.
Maybe this comment could be expanded to a more general statement about bindings pathing (explaining the module hierarchy a little bit --
exports
because it's an export,docs
being the namepace,adder
being the package, etc)