diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ff0fc926..e369e1ee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -280,7 +280,8 @@ jobs: key: output-example-jit-cargo-${{ hashFiles('example/src/main.rs', 'example/Cargo.lock', 'example/hello.wit') }} - name: Test Example - run: cd example && npm run build && ./test.sh + working-directory: examples/hello-world + run: bash test.sh test-aot: runs-on: ubuntu-latest @@ -349,4 +350,5 @@ jobs: key: output-example-aot-cargo-${{ hashFiles('example/src/main.rs', 'example/Cargo.lock', 'example/hello.wit') }} - name: Test Example - run: cd example && npm run build && ./test.sh + working-directory: examples/hello-world + run: bash test.sh diff --git a/example/README.md b/example/README.md deleted file mode 100644 index e1376bd9..00000000 --- a/example/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Example - -```sh -npm install -npm run build -npm run test -``` \ No newline at end of file diff --git a/example/test.sh b/example/test.sh deleted file mode 100755 index 51513788..00000000 --- a/example/test.sh +++ /dev/null @@ -1,4 +0,0 @@ - -if ! npm run test | grep -q 'Hello ComponentizeJS'; then - exit 1 -fi diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..95b889af --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Examples + +This folder contains example projects that use `componentize-js`. diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md new file mode 100644 index 00000000..c8a814eb --- /dev/null +++ b/examples/hello-world/README.md @@ -0,0 +1,132 @@ +# Example Javascript component + +This folder contains an example Javascript project that uses `componentize-js` +as a library to build a basic [WebAssembly component][cm-book]. + +[cm-book]: https://component-model.bytecodealliance.org/ + +## Overview + +This folder contains *two* codebases: + +- `guest` contains the Javascript WebAssembly Component +- `host` contains a Rust host that has been configured to run the component + +### `guest` - A WebAssembly component written in Javascript + +The [WebAssembly Interface Types ("WIT")][wit] interface ([`hello.wit`](./guest/hello.wit)) for the component is: + +```wit +package local:hello; + +world component { + export hello: func(name: string) -> string; +} +``` + +A Javascript (ES) module that conforms to the interface shown above looks like the following: + +```js +export function hello (name) { + return `Hello ${name}`; +} +``` + +> [!NOTE] +> The ES module is assumed implicitly to *be* the targeted `world`. +> +> This means that the JS export of the `hello` function maps to the +> WIT `hello` `export` of the `component` world. +> +> The world does not have to be called `component`. + +See [`hello.js`](./guest/hello.js) for the full code listing. + +We call the produced WebAssembly component "guest" as it is code that will run on +the WebAssembly virtual machine/runtime. + +[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md + +## `host` - A WebAssembly runtime embedding written in Rust + +Since our component does not export a standardized way to run it (in this case, +the standard we *could* have used would be [WASI CLI][wasi-cli]), we must use a custom host which +embeds a WebAssembly runtime ([`wasmtime`][wasmtime] in this case) to run the WebAssembly Component. + +`wasmtime` is easiest to use from [Rust][rust], so we have the `host` that contains +setup code which enables use of the component we wrote, and calls it. + +See [`host/src/main.rs`](./host/src/main.rs) for the full code listing. + +[wasmtime]: https://github.com/bytecodealliance/wasmtime +[wasi-cli]: https://github.com/WebAssembly/wasi-cli +[rust]: https://rust-lang.org + +## Build the component + +To build the WebAssembly component, enter the `guest` directory and install dependencies: + +```console +npm install +``` + +Then either run the `componentize.js` script directly: + +```console +node componentize.js +``` + +Or use the pre-configured `build` script: + +```console +npm run build +``` + +## Run the component + +### Via automation + +To run the component and test it's output, use the included bash script: + +```console +./test.sh +``` + +### Manually + +To run the component manually, we must run our custom `wasmtime` embedding manually. + +First enter the `host` directory and use `cargo run`: + +```console +cargo run +``` + +## Common Issues + +### No such file or directory + +If you get an error that looks like the following: + +``` +thread 'main' panicked at src/main.rs:39:67: +called `Result::unwrap()` on an `Err` value: failed to read from `../../guest/hello.component.wasm` + +Caused by: + No such file or directory (os error 2) +``` + +This means that the default path (which is relative, and embedded in the binary) to the component +produced by the `guest` is not present. + +To fix this, specify `COMPONENT_WASM_PATH` as an environment variable before `cargo run`: + +```console +COMPONENT_WASM_PATH=/absolute/path/to/hello.component.wasm cargo run +``` + +If you're running the produced `wasmtime-test` binary itself: + +```console +COMPONENT_WASM_PATH=/absolute/path/to/hello.component.wasm path/to/wasmtime-test +``` diff --git a/example/componentize.js b/examples/hello-world/guest/componentize.js similarity index 73% rename from example/componentize.js rename to examples/hello-world/guest/componentize.js index 7eada9c4..8f250158 100644 --- a/example/componentize.js +++ b/examples/hello-world/guest/componentize.js @@ -1,8 +1,10 @@ -import { componentize } from '@bytecodealliance/componentize-js'; import { readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; -const enableAot = process.env.ENABLE_AOT == '1' +import { componentize } from '@bytecodealliance/componentize-js'; + +// AoT compilation makes use of weval (https://github.com/bytecodealliance/weval) +const enableAot = process.env.ENABLE_AOT == '1'; const jsSource = await readFile('hello.js', 'utf8'); diff --git a/examples/hello-world/guest/hello.component.wasm b/examples/hello-world/guest/hello.component.wasm new file mode 100644 index 00000000..9bf27ec6 Binary files /dev/null and b/examples/hello-world/guest/hello.component.wasm differ diff --git a/example/hello.js b/examples/hello-world/guest/hello.js similarity index 100% rename from example/hello.js rename to examples/hello-world/guest/hello.js diff --git a/example/hello.wit b/examples/hello-world/guest/hello.wit similarity index 79% rename from example/hello.wit rename to examples/hello-world/guest/hello.wit index 1060f6e1..43f85fd4 100644 --- a/example/hello.wit +++ b/examples/hello-world/guest/hello.wit @@ -1,5 +1,5 @@ package local:hello; -world hello { +world component { export hello: func(name: string) -> string; } diff --git a/example/package.json b/examples/hello-world/guest/package.json similarity index 58% rename from example/package.json rename to examples/hello-world/guest/package.json index ec298006..0688020c 100644 --- a/example/package.json +++ b/examples/hello-world/guest/package.json @@ -5,7 +5,6 @@ "@bytecodealliance/componentize-js": "*" }, "scripts": { - "build": "node componentize.js && cargo build --release", - "test": "./target/release/wasmtime-test" + "build": "node componentize.js && cargo build --release" } } diff --git a/example/Cargo.lock b/examples/hello-world/host/Cargo.lock similarity index 100% rename from example/Cargo.lock rename to examples/hello-world/host/Cargo.lock diff --git a/example/Cargo.toml b/examples/hello-world/host/Cargo.toml similarity index 100% rename from example/Cargo.toml rename to examples/hello-world/host/Cargo.toml diff --git a/example/src/main.rs b/examples/hello-world/host/src/main.rs similarity index 61% rename from example/src/main.rs rename to examples/hello-world/host/src/main.rs index 52be8a98..0449863b 100644 --- a/example/src/main.rs +++ b/examples/hello-world/host/src/main.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use anyhow::Result; use wasmtime::{ component::{Component, Linker}, @@ -6,11 +8,23 @@ use wasmtime::{ use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView}; use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -wasmtime::component::bindgen!({ - world: "hello", - path: "hello.wit", - async: true -}); +mod bindings { + // This macro produces generated code that is used to link + // the functionality exposed by the component, and eventually + // call it. + wasmtime::component::bindgen!({ + world: "component", + path: "../guest/hello.wit", + async: true + }); +} + +// Default path to the WebAsssembly component as generated in the 'guest' folder, +// facilitating `cargo run` from the 'host' directory. +// +// If this binary is compiled and used from another folder, this path will likely be invalid, +// and in that case, using the `COMPONENT_PATH` environment variable is preferred. +const DEFAULT_COMPONENT_PATH: &str = "../guest/hello.component.wasm"; #[async_std::main] async fn main() -> Result<()> { @@ -28,7 +42,10 @@ async fn main() -> Result<()> { let engine = Engine::new(&config)?; let mut linker = Linker::new(&engine); - let component = Component::from_file(&engine, "hello.component.wasm").unwrap(); + let component_path = std::env::var("COMPONENT_WASM_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(DEFAULT_COMPONENT_PATH)); + let component = Component::from_file(&engine, component_path).unwrap(); struct CommandExtendedCtx { table: ResourceTable, @@ -63,7 +80,7 @@ async fn main() -> Result<()> { }, ); - let hello = Hello::instantiate_async(&mut store, &component, &linker).await?; + let hello = bindings::Component::instantiate_async(&mut store, &component, &linker).await?; let res = hello.call_hello(&mut store, "ComponentizeJS").await?; println!("{}", res); Ok(()) diff --git a/examples/hello-world/test.sh b/examples/hello-world/test.sh new file mode 100755 index 00000000..f87cd828 --- /dev/null +++ b/examples/hello-world/test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# NOTE: COMPONENT_WASM_PATH will be picked up by the test binary as well +export COMPONENT_WASM_PATH=$(realpath guest)/hello.component.wasm +export TEST_BINARY_PATH=$(realpath host)/target/release/wasmtime-test + +# Build the JS component if not present +echo -e "[info] expecting component WASM at [$COMPONENT_WASM_PATH]..."; +if [ ! -f "$COMPONENT_WASM_PATH" ]; then + cd guest && npm install && npm build +fi + +# Build the Rust embedding test binary if not present +echo -e "[info] expecting test binary at [$COMPONENT_WASM_PATH]..."; +if [ ! -f "$TEST_BINARY_PATH" ]; then + cd host && cargo build --release +fi + +# Run the test binary, capturing the output +CMD_OUTPUT=$($TEST_BINARY_PATH) + +# Ensure hte output contained what we expected +if ! echo $CMD_OUTPUT | grep -q 'Hello ComponentizeJS'; then + echo "[error] test binary output (below) does not contain 'Hello ComponentizeJS':"; + echo "$CMD_OUTPUT"; + exit 1; +fi + +echo "[success] test embedding binary produced expected output";