Skip to content

Commit

Permalink
Merge pull request #77 from kate-goldenring/go-language-guide
Browse files Browse the repository at this point in the history
Add Go language guide
  • Loading branch information
kate-goldenring authored Apr 9, 2024
2 parents 6c7387b + 2be771c commit e209a6f
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 2 deletions.
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 @@ -13,7 +13,7 @@ world example {
```

The application uses [`wasmtime`](https://github.com/bytecodealliance/wasmtime)
crates to generate Rust bindings, bring in WASI worlds, and executes the `run`
crates to generate Rust bindings, bring in WASI worlds, and executes the `add`
function of the component.

It takes in two operands and a path to a component. It passes the operands to
Expand Down
1 change: 1 addition & 0 deletions component-model/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Rust](./language-support/rust.md)
- [Javascript](./language-support/javascript.md)
- [Python](./language-support/python.md)
- [Go](./language-support/go.md)
- [Creating and Consuming Components](./creating-and-consuming.md)
- [Authoring Components](./creating-and-consuming/authoring.md)
- [Composing Components](./creating-and-consuming/composing.md)
Expand Down
2 changes: 1 addition & 1 deletion component-model/src/creating-and-consuming/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

You can write WebAssembly core modules in a wide variety of languages, and the set of languages that can directly create components is growing. See the [Language Support](../language-support.md) section for information on building components directly from source code.

If your preferred language supports WebAssembly but not components, you can still create components using the [`wasm-tools component`](https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-component) tool. (A future version of this page will cover this in more detail.)
If your preferred language supports WebAssembly but not components, you can still create components using the [`wasm-tools component`](https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-component) tool. (A future version of this page will cover this in more detail.)
288 changes: 288 additions & 0 deletions component-model/src/language-support/go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# Go Tooling

The [TinyGo toolchain](https://tinygo.org/docs/guides/webassembly/wasi/) has native support for WASI
and can build Wasm core modules. With the help of some component model tooling, we can then take
that core module and embed it in a component. To demonstrate how to use the tooling, this guide
walks through building a component that implements the `example` world defined in the [`add.wit`
package](../../examples/example-host/add.wit). The component will implement a simple add function.

## Overview of Building a Component with TinyGo

There are several steps to building a component in TinyGo:

1. Determine which world the component will implement
2. Build a Wasm core module using the native TinyGo toolchain
3. Convert the Wasm core module to a component using
[`wasm-tools`](https://github.com/bytecodealliance/wasm-tools)

The following sections will walk through these steps, producing a core Wasm module that targets WASI
preview 1 and converting this core module to a component that supports WASI preview 2.

### 1: The `example` World

The next two sections walk through creating a component that implements the the following [`example`
world](https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/example-host/add.wit):

```wit
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
```

This is a simple world that exports one `add` function. If you want to go beyond a quick start to a
more realistic example, jump to the [section on implementing worlds with
interfaces](#implementing-worlds-with-interfaces-with-tinygo-and-wit-bindgen).

### 2: Creating a TinyGo Core Wasm Module

The TinyGo toolchain natively supports compiling Go programs to core Wasm modules. Let's create one that implements the `add` function in the [`example`
world](https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/example-host/add.wit).

First, implement a simple add function in `add.go`:

```go
package main

//go:wasm-module yourmodulename
//export add
func add(x, y int32) int32 {
return x + y
}

// main is required for the `wasi` target, even if it isn't used.
func main() {}
```

Note, we must still provide a `main` function. This is a limitation of TinyGo's support of WASI as it currently only supports `main` packages - commands that run start-to-finish and
then exit. Our example program, however, is more like a library which exports an add function that
can be called multiple times; and nothing will ever call its `main` function.

Now, we can use TinyGo to build our core Wasm module:

```sh
tinygo build -o add.wasm -target=wasi add.go
```

You should now have an `add.wasm` module. But at the moment, this is a core module. In the next section, we will convert it into a component.

### 3: Converting a Wasm Core Module to a Component

In the previous step, we produced a core module that implements our `example` world. We now want to
convert to a component to gain the benefits of the component model, such as the ability to compose
with it with other components as done in the [`calculator` component in the
tutorial](../tutorial.md#the-calculator-interface).
TinyGo is actively developing a `wasip2` target (in this [PR](https://github.com/tinygo-org/tinygo/pull/4027)), but for now we must take additional steps to convert the module to a component.

We will use
[`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), a low level tool for manipulating
Wasm modules. Download the
latest release from the [project's
repository](https://github.com/bytecodealliance/wasm-tools/releases/tag/wasm-tools-1.0.44).

We also need to download the WASI preview 1 adapter. TinyGo (similar to C) targets preview 1 of WASI
which does not support the component model (`.wit` files). Fortunately, [Wasmtime provides
adapters](https://github.com/bytecodealliance/wit-bindgen#creating-components-wasi) for adapting
preview 1 modules to preview 2 components. There are adapters for both [reactor and command
components](../creating-and-consuming/authoring.md#command-and-reactor-components). Our `add.wit`
world defines a reactor component, so download the `wasi_snapshot_preview1.reactor.wasm` adapter
from [the latest Wasmtime release](https://github.com/bytecodealliance/wasmtime/releases).

Now that we have all the prerequisites downloaded, we can use the `wasm-tools component` subcommand
to componentize our Wasm module, first embedding component metadata inside the core module and then
encoding the module as a component using the WASI preview 1 adapter.

```sh
export COMPONENT_ADAPTER_REACTOR=/path/to/wasi_snapshot_preview1.reactor.wasm
wasm-tools component embed --world example ./add.wit add.wasm -o add.embed.wasm
wasm-tools component new -o add.component.wasm --adapt wasi_snapshot_preview1="$COMPONENT_ADAPTER_REACTOR" add.embed.wasm
```

We now have an add component that satisfies our `example` world, exporting the `add` function, which
we can confirm using another `wasm-tools` command:

```sh
$ wasm-tools component wit add.component.wit
package root:component

world root {
import wasi:io/streams
import wasi:filesystem/types
import wasi:filesystem/preopens
import wasi:cli/stdin
import wasi:cli/stdout
import wasi:cli/stderr
import wasi:cli/terminal-input
import wasi:cli/terminal-output
import wasi:cli/terminal-stdin
import wasi:cli/terminal-stdout
import wasi:cli/terminal-stderr

export add: func(x: s32, y: s32) -> s32
}
```

### Testing an `add` Component

To run our add component, we need to use a host program with a WASI runtime that understands the
`example` world. We've provided an [`example-host`](../../examples/example-host/README.md) to do
just that. It calls the `add` function of a passed in component providing two operands. To use it,
clone this repository and run the Rust program:

```sh
git clone [email protected]:bytecodealliance/component-docs.git
cd component-docs/component-model/examples/example-host
cargo run --release -- 1 2 /path/to/add.component.wasm
```

## Implementing Worlds with Interfaces with TinyGo and Wit-Bindgen

The [`example`
world](https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/example-host/add.wit) we were using in the previous sections simply exports a function. However, to use your component from another component, it must export an interface. This means we will need to use a tool to generate bindings to use as glue code, and adds a couple more steps (2-3) to building Wasm components with TinyGo:

1. Determine which world the component will implement
2. Generate bindings for that world using
[`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen#creating-a-component)
3. Implement the interface defined in the bindings
4. Build a Wasm core module using the native TinyGo toolchain
5. Convert the Wasm core module to a component using
[`wasm-tools`](https://github.com/bytecodealliance/wasm-tools)

For this example, we will use the following world, which moves the add function behind an `add` interface:

```wit
package docs:[email protected];
interface add {
add: func(a: u32, b: u32) -> u32;
}
world adder {
export add;
}
```

Our new steps use a low-level tool, [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen?tab=readme-ov-file#guest-tinygo) to generate bindings, or wrapper code, for implementing the desired world.

First, install [a release of `wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen/releases), updating the environment variables for your desired version, architecture and OS:

```sh
export VERSION=0.24.0 ARCH=aarch64 OS=macos
wget https://github.com/bytecodealliance/wit-bindgen/releases/download/v$VERSION/wit-bindgen-$VERSION-$ARCH-$OS.tar.gz
tar -xzf wit-bindgen-$VERSION-$ARCH-$OS.tar.gz
mv wit-bindgen-$VERSION-$ARCH-$OS/wit-bindgen ./
rm -rf wit-bindgen-$VERSION-$ARCH-$OS.tar.gz wit-bindgen-$VERSION-$ARCH-$OS
```

Now, create your Go project:

```sh
mkdir add && cd add
go mod init example.com
```

Next, run `wit-bindgen`, specifying TinyGo as the target language, the path to the
[`add.wit`](../../examples/example-host/add.wit) package, the name of the world in that package to
generate bindings for (`example`), and a directory to output the generated code (`gen`):

```sh
wit-bindgen tiny-go ./add.wit --world example --out-dir=gen
```

The `gen` directory now contains several files:

```sh
$ tree gen
gen
├── adder.c
├── adder.go
└── adder.h
```

The `adder.go` file defines an `ExportsDocsAdder0_1_0_Add` interface that matches the structure of our `add`
interface. The name of the interface is taken from the WIT package name (`docs:[email protected]`) combined with the interface name (`add`). In our Go module, first implement the `ExportsDocsAdder0_1_0_Add` interface by defining the `Add` function.

```go
package main

import (
. "example.com/gen"
)

type AdderImpl struct {
}

// Implement the `ExportsDocsAdder0_1_0_Add` interface to ensure the component satisfies the
// `adder` world
func (i AdderImpl) Add(x, y uint32) uint32 {
return x + y
}

// main is required for the `wasi` target, even if it isn't used.
func main() {}
```

After implementing the adder world, we need to load it by passing it to the `SetExportsDocsAdder0_1_0_Add`
function from our bindings (`adder.go`). Since our component is a library, `main` will not be called. However, only Go
programs with `main` can target WASI currently. As a loophole, we will initialize our `AdderImpl`
type inside an `init` function. Go's `init` functions are used to do initialization tasks that
should be done before any other tasks. In this case, we are using it to export the `Add` function and
make it callable using the generated C bindings (`adder.c`). After populating the `init` function,
our complete implementation looks similar to the following:

```go
package main

import (
. "example.com/gen"
)

type AdderImpl struct {
}

// Implement the ExportsDocsAdder0_1_0_Add interface to ensure the component satisfies the
// `adder` world
func (i AdderImpl) Add(x, y uint32) uint32 {
return x + y
}

// To enable our component to be a library, implement the component in the
// `init` function which is always called first when a Go package is run.
func init() {
example := AdderImpl{}
SetExportsDocsAdder0_1_0_Add(example)
}

// main is required for the `WASI` target, even if it isn't used.
func main() {}
```

Once again, we can build our core module using TinyGo, componentize it, and adapt it for WASI 0.2:
```sh
export COMPONENT_ADAPTER_REACTOR=/path/to/wasi_snapshot_preview1.reactor.wasm
tinygo build -o add.wasm -target=wasi add.go
wasm-tools component embed --world example ./add.wit add.wasm -o add.embed.wasm
wasm-tools component new -o add.component.wasm --adapt wasi_snapshot_preview1="$COMPONENT_ADAPTER_REACTOR" add.embed.wasm
```

We now have an add component that satisfies our `adder` world, exporting the `add` function, which
we can confirm using the `wasm-tools component wit` command:

```sh
wasm-tools component wit add.component.wasm
package root:component;

world root {
import wasi:io/[email protected];
import wasi:io/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:clocks/[email protected];
import wasi:filesystem/[email protected];
import wasi:filesystem/[email protected];

export docs:adder/[email protected];
}
```

0 comments on commit e209a6f

Please sign in to comment.