diff --git a/.github/workflows/c-host.yml b/.github/workflows/c-host.yml new file mode 100644 index 00000000..92ffcec7 --- /dev/null +++ b/.github/workflows/c-host.yml @@ -0,0 +1,61 @@ +name: c-host + +on: + merge_group: + push: + branches: + - main + + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + WASMTIME_VERSION: "42.0.1" + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Wasmtime C API + run: | + curl -sL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-x86_64-linux-c-api.tar.xz" \ + | sudo tar xJ --strip-components=1 -C /usr/local + sudo ldconfig + + - name: Build the C host + working-directory: component-model/examples/example-c-host + run: gcc -o adder-host host.c -lwasmtime + + - name: Run the C host + working-directory: component-model/examples/example-c-host + run: | + ./adder-host 1 2 ../example-host/add.wasm | tee output.txt + grep -q '1 + 2 = 3' output.txt + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Build Docker image + working-directory: component-model/examples/example-c-host + run: docker build -t example-c-host:latest . + + - name: Test Docker image + working-directory: component-model/examples/example-c-host + run: | + docker run --rm \ + -v "$(pwd)/../example-host/add.wasm":/component/add.wasm:ro \ + example-c-host:latest | tee output.txt + grep -q '1 + 2 = 3' output.txt diff --git a/component-model/examples/example-c-host/.gitignore b/component-model/examples/example-c-host/.gitignore new file mode 100644 index 00000000..c70dc2df --- /dev/null +++ b/component-model/examples/example-c-host/.gitignore @@ -0,0 +1 @@ +host diff --git a/component-model/examples/example-c-host/Dockerfile b/component-model/examples/example-c-host/Dockerfile new file mode 100644 index 00000000..f603d5db --- /dev/null +++ b/component-model/examples/example-c-host/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 AS build + +ARG WASMTIME_VERSION=42.0.1 +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc libc6-dev curl xz-utils ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install wasmtime C API +RUN set -eux; \ + case "${TARGETARCH}" in \ + amd64) WASMTIME_ARCH=x86_64 ;; \ + arm64) WASMTIME_ARCH=aarch64 ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + curl -sL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-${WASMTIME_ARCH}-linux-c-api.tar.xz" \ + | tar xJ --strip-components=1 -C /usr/local; \ + ldconfig + +WORKDIR /src + +# Build the host +COPY host.c . +RUN gcc -o /usr/local/bin/adder-host host.c -lwasmtime + +# Runtime image +FROM ubuntu:24.04 + +COPY --from=build /usr/local/lib/libwasmtime.so /usr/local/lib/ +RUN ldconfig +COPY --from=build /usr/local/bin/adder-host /usr/local/bin/adder-host + +ENTRYPOINT ["adder-host"] +CMD ["1", "2", "/component/add.wasm"] diff --git a/component-model/examples/example-c-host/adder.wasm b/component-model/examples/example-c-host/adder.wasm new file mode 120000 index 00000000..be994d33 --- /dev/null +++ b/component-model/examples/example-c-host/adder.wasm @@ -0,0 +1 @@ +../example-host/add.wasm \ No newline at end of file diff --git a/component-model/examples/example-c-host/host.c b/component-model/examples/example-c-host/host.c new file mode 100644 index 00000000..fafa0af8 --- /dev/null +++ b/component-model/examples/example-c-host/host.c @@ -0,0 +1,127 @@ +/** + * C host for the adder WebAssembly component. + * + * Uses the Wasmtime C API's component model support to load and run + * a component that exports: docs:adder/add.add(u32, u32) -> u32 + */ + +#include +#include +#include +#include + +static void exit_if_error(const char *step, wasmtime_error_t *error) { + if (error == NULL) + return; + wasm_byte_vec_t error_message; + wasmtime_error_message(error, &error_message); + wasmtime_error_delete(error); + fprintf(stderr, "error: failed to %s\n%.*s\n", step, (int)error_message.size, + error_message.data); + wasm_byte_vec_delete(&error_message); + exit(1); +} + +int main(int argc, char *argv[]) { + if (argc != 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + uint32_t x = (uint32_t)atoi(argv[1]); + uint32_t y = (uint32_t)atoi(argv[2]); + const char *path = argv[3]; + + // 1. Create engine with component model enabled + wasm_config_t *config = wasm_config_new(); + wasmtime_config_wasm_component_model_set(config, true); + wasm_engine_t *engine = wasm_engine_new_with_config(config); + + // 2. Read the component file + FILE *f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "error: could not open %s\n", path); + return 1; + } + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *wasm_bytes = malloc(fsize); + fread(wasm_bytes, 1, fsize, f); + fclose(f); + + // 3. Compile the component + wasmtime_component_t *component = NULL; + exit_if_error("compile component", + wasmtime_component_new(engine, wasm_bytes, fsize, &component)); + free(wasm_bytes); + + // 4. Create linker and add WASI P2 + wasmtime_component_linker_t *linker = + wasmtime_component_linker_new(engine); + exit_if_error("add WASI to linker", + wasmtime_component_linker_add_wasip2(linker)); + + // 5. Create store with WASI config + wasmtime_store_t *store = wasmtime_store_new(engine, NULL, NULL); + wasmtime_context_t *context = wasmtime_store_context(store); + exit_if_error("set WASI config", + wasmtime_context_set_wasi(context, wasi_config_new())); + + // 6. Instantiate + wasmtime_component_instance_t instance; + exit_if_error("instantiate component", + wasmtime_component_linker_instantiate(linker, context, component, &instance)); + + // 7. Look up the exported "add" function. + // The export is nested: first find the "docs:adder/add@0.1.0" instance, + // then the "add" function within it. + wasmtime_component_export_index_t *iface_idx = + wasmtime_component_instance_get_export_index( + &instance, context, NULL, + "docs:adder/add@0.1.0", strlen("docs:adder/add@0.1.0")); + if (iface_idx == NULL) { + fprintf(stderr, "error: could not find export 'docs:adder/add@0.1.0'\n"); + return 1; + } + + wasmtime_component_export_index_t *func_idx = + wasmtime_component_instance_get_export_index( + &instance, context, iface_idx, + "add", strlen("add")); + wasmtime_component_export_index_delete(iface_idx); + if (func_idx == NULL) { + fprintf(stderr, "error: could not find function 'add'\n"); + return 1; + } + + wasmtime_component_func_t func; + bool found = wasmtime_component_instance_get_func( + &instance, context, func_idx, &func); + wasmtime_component_export_index_delete(func_idx); + if (!found) { + fprintf(stderr, "error: could not get function handle for 'add'\n"); + return 1; + } + + // 8. Call the function: add(x, y) -> u32 + wasmtime_component_val_t args[2] = { + {.kind = WASMTIME_COMPONENT_U32, .of.u32 = x}, + {.kind = WASMTIME_COMPONENT_U32, .of.u32 = y}, + }; + wasmtime_component_val_t results[1] = {0}; + + exit_if_error("call 'add'", + wasmtime_component_func_call(&func, context, args, 2, results, 1)); + + printf("%u + %u = %u\n", x, y, results[0].of.u32); + + // 9. Cleanup + wasmtime_component_val_delete(&results[0]); + wasmtime_store_delete(store); + wasmtime_component_linker_delete(linker); + wasmtime_component_delete(component); + wasm_engine_delete(engine); + + return 0; +} diff --git a/component-model/src/language-support/building-a-simple-component/c.md b/component-model/src/language-support/building-a-simple-component/c.md index 3dbc38db..953deaac 100644 --- a/component-model/src/language-support/building-a-simple-component/c.md +++ b/component-model/src/language-support/building-a-simple-component/c.md @@ -29,7 +29,6 @@ with the ability to add more language generators in the future. [wasi]: https://wasi.dev/ [rust]: https://www.rust-lang.org/learn/get-started [sample-wit]: https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/tutorial/wit/adder/world.wit -[cargo-config]: https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/example-host/Cargo.toml ## 1. Download dependencies @@ -303,40 +302,109 @@ world root { ... ``` -### 6. Run the component from the example host +## 6. Run the component with `wasmtime --invoke` -The following section requires you to have [a Rust toolchain][rust] installed. +If you want to quickly run the `add` export without writing a host application that embeds Wasmtime, +you can invoke it directly with the Wasmtime CLI. -> [!WARNING] -> You must be careful to use a version of the adapter (`wasi_snapshot_preview1.wasm`) -> that is compatible with the version of `wasmtime` that will be used, -> to ensure that WASI interface versions (and relevant implementation) match. -> (The `wasmtime` version is specified in [the Cargo configuration file][cargo-config] -> for the example host.) +```console +wasmtime run --invoke 'add(2, 2)' adder.wasm +``` + +Depending on your Wasmtime version, the shorthand form may also work: + +```console +wasmtime --invoke 'add(2,2)' adder.wasm +``` + +## 7. Run the component from the example C host + +This repository includes a C application that can execute components that implement the add interface. This application embeds Wasmtime using the Wasmtime C API: +`component-model/examples/example-c-host/host.c`. + +The application expects three arguments: the two numbers to add and the Wasm component that executed the addition. For example: + +```sh +./adder-host +``` + +You can either use a Dockerfile to execute your add component with the C application or directly run the application. + +### Option A: Compile and run the host directly + +If the Wasmtime C API headers and library are installed on your system, +you can compile and run the host directly: + +On Linux, the following commands install the C API artifacts in `/usr/local` +using the same approach as the `Dockerfile`: + +```console +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + gcc libc6-dev curl xz-utils ca-certificates + +WASMTIME_VERSION=42.0.1 +case "$(uname -m)" in + x86_64) WASMTIME_ARCH=x86_64 ;; + aarch64|arm64) WASMTIME_ARCH=aarch64 ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +curl -sL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-${WASMTIME_ARCH}-linux-c-api.tar.xz" \ + | sudo tar xJ --strip-components=1 -C /usr/local + +sudo ldconfig +``` + +```console +cd component-model/examples/example-c-host +gcc -o adder-host host.c -lwasmtime +./adder-host 1 2 /absolute/path/to/adder.wasm +``` + +If `libwasmtime.so` is not in a default library path on Linux, +set `LD_LIBRARY_PATH` before running: + +```console +LD_LIBRARY_PATH=/path/to/wasmtime/lib ./adder-host 1 2 /absolute/path/to/adder.wasm +``` + +Expected output: + +```sh +1 + 2 = 3 +``` + +### Option B: Run with Docker + +Instead of installing the Wasmtime C API, you can use the provided Dockerfile which builds the C application. -{{#include ../example-host-part1.md}} +From `component-model/examples/example-c-host`: -A successful run should show the following output -(of course, the paths to your example host and adder component will vary, -and you should substitute `adder.wasm` with `adder.component.wasm` -if you followed the manual instructions above): +```console +cd component-model/examples/example-c-host +docker build -t example-c-host:latest . +``` -{{#include ../example-host-part2.md}} +Then run the container, passing in the component as a volume: -## 7. Run the component from C/C++ Applications +```console +docker run --rm \ + -v "$(pwd)/../example-host/add.wasm":/component/add.wasm:ro \ + example-c-host:latest +``` -It is not yet possible to run a WebAssembly Component using the `wasmtime` C API. -See [`wasmtime` issue #6987](https://github.com/bytecodealliance/wasmtime/issues/6987) for more details. -The C API is preferred over directly using the example host Rust crate in C++. +Expected output: -However, C/C++ language guest components can be composed with components written in any other language -and run by their toolchains, -or even composed with a C language command component and run via the `wasmtime` CLI -or any other host. +```sh +1 + 2 = 3 +``` -See the [Rust Tooling guide](./rust.md#running-a-component-from-rust-applications) -for instructions on how to run this component from the Rust `example-host` -(replacing the path to `add.wasm` with your `adder.wasm` or `adder.component.wasm` above). +The default command runs `adder-host 1 2 /component/add.wasm`, +so you can also override the arguments: -[!NOTE]: # -[!WARNING]: # +```console +docker run --rm \ + -v "$(pwd)/../example-host/add.wasm":/component/add.wasm:ro \ + example-c-host:latest 40 2 /component/add.wasm +```