Skip to content

Commit 498227c

Browse files
committed
feat: add sdk example
Signed-off-by: Andrew Steurer <94206073+asteurer@users.noreply.github.com>
1 parent 55ab77a commit 498227c

67 files changed

Lines changed: 13200 additions & 5 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/sdk/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Building SDKs for componentize-go applications
2+
3+
While feasible, it can be tedious to work directly with WIT types in Go. In most cases, it's better to have an SDK layer that handles the conversions from WIT types to idiomatic Go types. This example demonstrates the ways that we handle WIT `imports` and `exports` in order to have shared code feel natural for Go developers to use.
4+
5+
The [application](component/main.go) that's being demonstrated imports the TCP portion of `wasi:sockets` and defines a Run export for `wasi:cli`, both of which have been wrapped in an SDK (see the `pkg` directory).
6+
7+
## componentize-go.toml
8+
9+
```toml
10+
# componentize-go.toml file structure
11+
12+
# The FULL names of the default WIT worlds
13+
worlds = ["example:sdk/test", "foo:bar/baz"]
14+
# The paths in which the WIT files are stored.
15+
wit_paths = ["wit", "../other_wit_path"]
16+
```
17+
18+
Notice in the [component/Makefile](component/Makefile) how the build command is simply `componentize-go build`. Contrast this with the other examples which explicitly specify WIT worlds and paths to the WIT files in their build commands. The [pkg/componentize-go.toml](pkg/componentize-go.toml) file is what enables this behavior.
19+
20+
When building a component, componentize-go will search the go.mod file's dependencies' respective repositories for a componentize-go.toml file in the root. This file indicates where the WIT files are stored and the default worlds that are to be used.
21+
22+
You can override the default worlds via the command line. Note that doing so causes componentize-go to ignore all componentize-go.toml world definitions. You will need to explicitly list every WIT world the component requires.
23+
24+
## Defining Imports
25+
26+
Imports are straight forward since componentize-go generates the bindings. To see how imports are abstracted, see the [sockets package](pkg/sockets/sockets.go).
27+
28+
## Defining Exports
29+
30+
Exports require a bit more structure than imports because we need to have a translation layer for idiomatic, user-defined export functions and the raw WIT function signatures the compiler will recognize.
31+
32+
**Step 1: Define an Exports variable** ([`pkg/bindings/exports/export_wasi_cli_run/wit_bindings.go`](pkg/bindings/exports/export_wasi_cli_run/wit_bindings.go))
33+
34+
We hand-write an `Exports` variable that contains an uninitialized slot for each function export. At startup, something will need to assign a function to each slot; otherwise, the export panics at runtime. We do it this way because the [generated export bindings](pkg/bindings//exports/wit_exports/wit_exports.go) expect a `Run` function in the `bindings/export_wasi_cli_run` package, and we want to avoid making manual edits to any of the generated files.
35+
36+
**Step 2: The SDK wrapper** ([`pkg/cli/cli.go`](pkg/cli/cli.go))
37+
38+
The SDK defines a `RegisterExports` function that both assigns to the generated `Exports` slots and handles conversion between idiomatic Go types (e.g. `error`) and the raw WIT types (e.g. `witTypes.Option[string]`) the bindings expect.
39+
40+
Note the required blank import of `wit_exports`, as the component will not compile without it.
41+
42+
**Step 3: The application** ([`component/main.go`](component/main.go))
43+
44+
The application implements the required function export(s) and calls `RegisterExports` from `init()`. The `main()` function is left empty to make the compiler happy.

examples/sdk/component/Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.PHONY: run build
2+
3+
build:
4+
componentize-go build
5+
6+
run: build
7+
wasmtime run -S p3,inherit-network -W component-model-async main.wasm

examples/sdk/component/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# `sdk` example
2+
3+
## Usage
4+
5+
### Prerequisites
6+
7+
- [**componentize-go**](https://github.com/bytecodealliance/componentize-go) - Latest version
8+
- [**go**](https://go.dev/dl/) - v1.25.9
9+
- [**wasmtime**](https://github.com/bytecodealliance/wasmtime) - v44.0.1
10+
11+
### Run
12+
13+
```sh
14+
make run
15+
16+
# Invoke the application
17+
curl localhost:6767
18+
```

examples/sdk/component/go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module example
2+
3+
go 1.25.5
4+
5+
replace pkg => ../pkg
6+
7+
require pkg v0.0.0-00010101000000-000000000000
8+
9+
require go.bytecodealliance.org/pkg v0.2.1 // indirect

examples/sdk/component/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go.bytecodealliance.org/pkg v0.2.1 h1:TdRagooIcCW3UmlKqVO4cDR3GNDyfDnbiBzGI6TOvyg=
2+
go.bytecodealliance.org/pkg v0.2.1/go.mod h1:OjA+V8g3uUFixeCKFfamm6sYhTJdg8fvwEdJ2GO0GSk=

examples/sdk/component/main.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
wasiExports "pkg/cli"
7+
wasiSockets "pkg/sockets"
8+
)
9+
10+
const numWorkers = 16
11+
12+
type Component struct{}
13+
14+
func (c *Component) Run() error {
15+
socket, err := wasiSockets.NewSocket(wasiSockets.IpAddressFamilyIpv4)
16+
if err != nil {
17+
return err
18+
}
19+
20+
if err := socket.Bind("0.0.0.0:6767"); err != nil {
21+
return err
22+
}
23+
24+
listener, err := socket.Listen()
25+
if err != nil {
26+
return err
27+
}
28+
defer listener.Close()
29+
30+
connCh := make(chan *wasiSockets.TcpSocket, numWorkers)
31+
32+
for range numWorkers {
33+
go func() {
34+
for conn := range connCh {
35+
handleConn(conn)
36+
}
37+
}()
38+
}
39+
40+
for {
41+
conn, err := listener.Accept()
42+
if err != nil {
43+
close(connCh)
44+
if err == io.EOF {
45+
return nil
46+
}
47+
return err
48+
}
49+
connCh <- conn
50+
}
51+
}
52+
53+
func handleConn(conn *wasiSockets.TcpSocket) {
54+
body := "Hello from Go + wasi:sockets!"
55+
response := fmt.Sprintf(
56+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s",
57+
len(body), body,
58+
)
59+
conn.Write([]byte(response))
60+
}
61+
62+
func init() {
63+
wasiExports.RegisterExports(&Component{})
64+
}
65+
66+
func main() {}

examples/sdk/pkg/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
generate-bindings:
2+
@rm -rf bindings/imports
3+
@rm -rf bindings/exports/wit_exports
4+
# The export_wasi_cli_run files are hand-written, and thus are not removed
5+
6+
@componentize-go \
7+
--ignore-toml-files \
8+
bindings \
9+
--output bindings/imports \
10+
--pkg-name pkg/bindings/imports \
11+
--export-pkg-name pkg/bindings/exports \
12+
--format
13+
14+
@mv bindings/imports/wit_exports bindings/exports
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file exists for testing this package without WebAssembly,
2+
// allowing empty function bodies with a //go:wasmimport directive.
3+
// See https://pkg.go.dev/cmd/compile for more information.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Note: This file was NOT generated by componentize-go
2+
3+
package export_wasi_cli_run
4+
5+
import (
6+
witTypes "go.bytecodealliance.org/pkg/wit/types"
7+
)
8+
9+
var Exports struct {
10+
Run func() witTypes.Result[witTypes.Unit, witTypes.Unit]
11+
}
12+
13+
func Run() witTypes.Result[witTypes.Unit, witTypes.Unit] {
14+
return Exports.Run()
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Generated by `wit-bindgen` 0.54.0. DO NOT EDIT!
2+
//
3+
// This code was generated from the following packages:
4+
// wasi:clocks@0.3.0-rc-2026-03-15
5+
// wasi:filesystem@0.3.0-rc-2026-03-15
6+
// wasi:sockets@0.3.0-rc-2026-03-15
7+
// wasi:random@0.3.0-rc-2026-03-15
8+
// wasi:cli@0.3.0-rc-2026-03-15
9+
// example:sdk
10+
11+
package wit_exports
12+
13+
import (
14+
witAsync "go.bytecodealliance.org/pkg/wit/async"
15+
witRuntime "go.bytecodealliance.org/pkg/wit/runtime"
16+
witTypes "go.bytecodealliance.org/pkg/wit/types"
17+
"pkg/bindings/exports/export_wasi_cli_run"
18+
"runtime"
19+
)
20+
21+
var staticPinner = runtime.Pinner{}
22+
var exportReturnArea = uintptr(witRuntime.Allocate(&staticPinner, 0, 1))
23+
var syncExportPinner = runtime.Pinner{}
24+
25+
//go:wasmexport [async-lift]wasi:cli/run@0.3.0-rc-2026-03-15#run
26+
func wasm_export_wasi_cli_run_run() int32 {
27+
return int32(witAsync.Run(func() {
28+
29+
result := export_wasi_cli_run.Run()
30+
var option int32
31+
switch result.Tag() {
32+
case witTypes.ResultOk:
33+
34+
option = int32(0)
35+
case witTypes.ResultErr:
36+
37+
option = int32(1)
38+
default:
39+
panic("unreachable")
40+
}
41+
wasm_export_task_return_wasi_cli_run_run(option)
42+
43+
}))
44+
}
45+
46+
//go:wasmexport [callback][async-lift]wasi:cli/run@0.3.0-rc-2026-03-15#run
47+
func wasm_export_callback_wasi_cli_run_run(event0 uint32, event1 uint32, event2 uint32) uint32 {
48+
return witAsync.Callback(event0, event1, event2)
49+
}
50+
51+
//go:wasmimport [export]wasi:cli/run@0.3.0-rc-2026-03-15 [task-return]run
52+
func wasm_export_task_return_wasi_cli_run_run(arg0 int32)

0 commit comments

Comments
 (0)