Skip to content

Commit 6f8e288

Browse files
committed
fix: enable TinyGo custom WIT worlds and re-enable Go components in CI
- Fix TinyGo flag format: --wit-package/-world → -wit-package/-world - Pass full WIT directory with deps/ structure for dependency resolution - Add missing WASI imports required by TinyGo runtime (random, environment, monotonic-clock) - Add WASI 0.2.0 dependencies: filesystem, sockets, random - Fix calculator_with_bindings.go type mismatches and missing cm import - Re-enable Go calculator components in CI (Linux & macOS) - Document TinyGo WASI runtime requirement in README - Enhance wit_structure tool for transitive deps copying Fixes #80, #82
1 parent 2b37f71 commit 6f8e288

File tree

13 files changed

+631
-251
lines changed

13 files changed

+631
-251
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ jobs:
106106
//examples/go_component:calculator_component_debug \
107107
//examples/go_component:calculator_docs \
108108
//examples/go_component:calculator_manual \
109+
//examples/go_component:calculator_component \
110+
//examples/go_component:calculator_simple \
111+
//examples/go_component:calculator_with_bindings \
112+
//examples/go_component:calculator_simple_binding \
109113
//examples/go_component:http_service_component \
110114
//examples/go_component:http_service_docs \
111115
//examples/go_component:multi_file_test \
@@ -130,11 +134,6 @@ jobs:
130134
//tools/... \
131135
//providers/... \
132136
//test/go/... \
133-
-//test/go:test_calculator_component_provides_info \
134-
-//test/go:test_calculator_component_valid_wasm \
135-
-//test/go:test_calculator_exports_verification \
136-
-//test/go:all_go_tests \
137-
-//test/go:go_component_tests \
138137
-//test/go:go_integration_tests \
139138
//test/cpp/... \
140139
//test/unit/... \
@@ -211,6 +210,10 @@ jobs:
211210
//examples/go_component:calculator_component_debug \
212211
//examples/go_component:calculator_docs \
213212
//examples/go_component:calculator_manual \
213+
//examples/go_component:calculator_component \
214+
//examples/go_component:calculator_simple \
215+
//examples/go_component:calculator_with_bindings \
216+
//examples/go_component:calculator_simple_binding \
214217
//examples/go_component:http_service_component \
215218
//examples/go_component:http_service_docs \
216219
//examples/go_component:multi_file_test \
@@ -235,11 +238,6 @@ jobs:
235238
//tools/... \
236239
//providers/... \
237240
//test/go/... \
238-
-//test/go:test_calculator_component_provides_info \
239-
-//test/go:test_calculator_component_valid_wasm \
240-
-//test/go:test_calculator_exports_verification \
241-
-//test/go:all_go_tests \
242-
-//test/go:go_component_tests \
243241
-//test/go:go_integration_tests \
244242
//test/cpp/... \
245243
//test/unit/... \

MODULE.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ register_toolchains("@go_toolchains//:all")
6666
# WASI WIT interface definitions
6767
wasi_wit_ext = use_extension("//wasm:extensions.bzl", "wasi_wit")
6868
wasi_wit_ext.init()
69-
use_repo(wasi_wit_ext, "wasi_cli", "wasi_cli_v020", "wasi_clocks", "wasi_clocks_v020", "wasi_filesystem", "wasi_http", "wasi_io", "wasi_io_v020", "wasi_nn", "wasi_nn_v0_2_0_rc_2024_06_25", "wasi_nn_v0_2_0_rc_2024_08_19", "wasi_random", "wasi_sockets") # Complete WASI ecosystem (0.2.3 + 0.2.0 + all NN versions)
69+
use_repo(wasi_wit_ext, "wasi_cli", "wasi_cli_v020", "wasi_clocks", "wasi_clocks_v020", "wasi_filesystem", "wasi_filesystem_v020", "wasi_http", "wasi_io", "wasi_io_v020", "wasi_nn", "wasi_nn_v0_2_0_rc_2024_06_25", "wasi_nn_v0_2_0_rc_2024_08_19", "wasi_random", "wasi_random_v020", "wasi_sockets", "wasi_sockets_v020") # Complete WASI ecosystem (0.2.3 + 0.2.0 + all NN versions)
7070

7171
# WebAssembly toolchains
7272
wasm_toolchain = use_extension("//wasm:extensions.bzl", "wasm_toolchain")

MODULE.bazel.lock

Lines changed: 269 additions & 219 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/go_component/BUILD.bazel

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,28 @@ wit_library(
1717
name = "calculator_wit",
1818
srcs = ["wit/calculator.wit"],
1919
world = "calculator-world",
20-
deps = ["@wasi_io_v020//:streams"], # Add WASI IO 0.2.0 dependency required by TinyGo
20+
deps = [
21+
"@wasi_io_v020//:streams",
22+
"@wasi_cli_v020//:cli",
23+
"@wasi_clocks_v020//:clocks",
24+
"@wasi_filesystem_v020//:filesystem",
25+
"@wasi_random_v020//:random",
26+
],
2127
)
2228

23-
# Simple calculator WIT - pure interface, TinyGo handles WASI internally
29+
# Simple calculator WIT - reactor mode with TinyGo-required WASI imports
30+
# TinyGo runtime needs these even for empty main() reactor components
2431
wit_library(
2532
name = "simple_calculator_wit",
2633
srcs = ["wit/simple-calculator.wit"],
2734
world = "calculator-world",
35+
deps = [
36+
"@wasi_io_v020//:streams",
37+
"@wasi_cli_v020//:cli",
38+
"@wasi_clocks_v020//:clocks",
39+
"@wasi_filesystem_v020//:filesystem",
40+
"@wasi_random_v020//:random",
41+
],
2842
)
2943

3044
# WIT library definitions for HTTP service interface
@@ -176,3 +190,15 @@ go_wasm_component(
176190
optimization = "debug",
177191
world = "wasi:cli/command",
178192
)
193+
194+
# Pure reactor component - NO main() function, NO WASI imports
195+
# This demonstrates the proper way to build Go library components
196+
go_wasm_component(
197+
name = "calculator_reactor",
198+
srcs = ["calculator_reactor.go"],
199+
go_mod = "go.mod",
200+
go_sum = "go.sum",
201+
optimization = "release",
202+
wit = ":simple_calculator_wit",
203+
world = "calculator-world",
204+
)

examples/go_component/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,90 @@ go_wasm_component(
107107
- **Composability**: Link multiple components together
108108
- **Language Interop**: Call between Go, Rust, C++, JavaScript components
109109

110+
## TinyGo WASI Runtime Requirement
111+
112+
### Why TinyGo Components Always Need WASI
113+
114+
Unlike C/C++ or Rust, **TinyGo reactor components (even with empty `main()`) require WASI imports**. This is due to TinyGo's Go runtime architecture, not a Component Model limitation.
115+
116+
**TinyGo Runtime Architecture:**
117+
- TinyGo's Go runtime is built on WASI primitives for system operations
118+
- Even reactor components with `func main() {}` trigger runtime initialization
119+
- Runtime initialization performs memory management, goroutine setup, and I/O configuration
120+
- This is an architectural decision in TinyGo's design
121+
122+
**Required WASI Imports:**
123+
124+
All TinyGo components (both command and reactor) require these WASI Preview 2 interfaces:
125+
126+
```wit
127+
world tinygo-component {
128+
// I/O streams for runtime initialization
129+
import wasi:io/[email protected];
130+
131+
// CLI interfaces for stdout/stderr/stdin
132+
import wasi:cli/[email protected];
133+
import wasi:cli/[email protected];
134+
import wasi:cli/[email protected];
135+
136+
// Clock for time operations (time.Now(), etc.)
137+
import wasi:clocks/[email protected];
138+
139+
// Filesystem for runtime initialization
140+
import wasi:filesystem/[email protected];
141+
import wasi:filesystem/[email protected];
142+
143+
// Your component's exports
144+
export my-interface;
145+
}
146+
```
147+
148+
**Contrast with Other Languages:**
149+
150+
| Language | Pure Reactor (No WASI) | Why? |
151+
|----------|------------------------|------|
152+
| C/C++ | ✅ Possible | Can compile with `-nostdlib`, no runtime dependencies |
153+
| Rust | ✅ Possible | `#![no_std]` removes runtime, pure library mode |
154+
| **TinyGo** | ❌ Not Possible | Runtime always initializes with WASI dependencies |
155+
156+
**What TinyGo's Compiler Generates:**
157+
158+
Even for a minimal reactor component:
159+
```go
160+
package main
161+
162+
func main() {} // Empty, reactor mode
163+
164+
//export my_function
165+
func my_function() int32 {
166+
return 42
167+
}
168+
```
169+
170+
The TinyGo compiler still generates:
171+
1. Runtime initialization code that calls WASI filesystem APIs
172+
2. I/O stream setup for panic/error handling
173+
3. Clock initialization for time operations
174+
4. Memory allocator setup using WASI primitives
175+
176+
**Practical Implications:**
177+
178+
- **Component Composition**: Your Go component will have WASI import requirements
179+
- **Host Requirements**: The runtime (wasmtime, wasmer) must provide WASI Preview 2
180+
- **Performance**: The imports are lightweight and don't impact performance significantly
181+
- **Deployment**: This is transparent in most scenarios - modern runtimes provide WASI by default
182+
183+
**No Workarounds Available:**
184+
185+
There are no practical workarounds. TinyGo components always require WASI. However:
186+
- Most component hosts (wasmtime, wasmer, etc.) provide WASI Preview 2 by default
187+
- The WASI overhead is minimal and doesn't affect component composability
188+
- This is a known and accepted characteristic of TinyGo's design
189+
190+
**Upstream Tracking:**
191+
192+
See [TinyGo Issue #2703](https://github.com/tinygo-org/tinygo/issues/2703) for discussions about making the runtime more modular.
193+
110194
## Testing Components
111195

112196
```bash
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Package main provides a pure reactor WebAssembly component
2+
// Reactor components use package "main" but with an EMPTY main() function
3+
// This is the TinyGo way to build library components (see TinyGo issue #2703)
4+
package main
5+
6+
// Pure export functions for WIT component interface
7+
// TinyGo will generate proper component exports via -wit-world flag
8+
9+
//export example_calculator_add
10+
func add(a, b float64) float64 {
11+
return a + b
12+
}
13+
14+
//export example_calculator_subtract
15+
func subtract(a, b float64) float64 {
16+
return a - b
17+
}
18+
19+
//export example_calculator_multiply
20+
func multiply(a, b float64) float64 {
21+
return a * b
22+
}
23+
24+
//export example_calculator_divide
25+
func divide(a, b float64) float64 {
26+
// In a real implementation, this would use WIT result type for error handling
27+
// For this simple demo, division by zero returns 0
28+
if b == 0 {
29+
return 0
30+
}
31+
return a / b
32+
}
33+
34+
// EMPTY main() for reactor mode
35+
// This tells TinyGo we want a reactor component, not a command component
36+
// The runtime is initialized in _initialize, not _start
37+
func main() {
38+
// Reactor component - no main execution needed
39+
}
40+
41+
// Prevent compiler from removing exports
42+
var _ = add
43+
var _ = subtract
44+
var _ = multiply
45+
var _ = divide

examples/go_component/calculator_with_bindings.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
// The generated bindings will be at this path
55
"example.com/calculator/example/calculator/calculator"
6+
"go.bytecodealliance.org/cm"
67
)
78

89
// Initialize the calculator component exports with generated bindings
@@ -20,12 +21,20 @@ func init() {
2021
return a * b
2122
}
2223

23-
calculator.Exports.Divide = func(a, b float64) float64 {
24+
calculator.Exports.Divide = func(a, b float64) calculator.CalculationResult {
2425
if b == 0 {
25-
// Return NaN for division by zero
26-
return 0.0 / 0.0
26+
return calculator.CalculationResult{
27+
Success: false,
28+
Error: cm.Some("division by zero"),
29+
Value: cm.None[float64](),
30+
}
31+
}
32+
result := a / b
33+
return calculator.CalculationResult{
34+
Success: true,
35+
Error: cm.None[string](),
36+
Value: cm.Some(result),
2737
}
28-
return a / b
2938
}
3039

3140
calculator.Exports.Calculate = func(operation calculator.Operation) calculator.CalculationResult {

examples/go_component/wit/calculator.wit

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,19 @@ interface calculator {
3636
}
3737

3838
world calculator-world {
39-
/// Import WASI interfaces required by TinyGo stdlib
39+
/// Import WASI interfaces required by TinyGo runtime
40+
/// TinyGo's Go runtime requires these even for reactor components
41+
/// This is a TinyGo architecture requirement, not a Component Model limitation
4042
import wasi:io/streams@0.2.0;
43+
import wasi:cli/stdout@0.2.0;
44+
import wasi:cli/stderr@0.2.0;
45+
import wasi:cli/stdin@0.2.0;
46+
import wasi:cli/environment@0.2.0;
47+
import wasi:clocks/wall-clock@0.2.0;
48+
import wasi:clocks/monotonic-clock@0.2.0;
49+
import wasi:filesystem/types@0.2.0;
50+
import wasi:filesystem/preopens@0.2.0;
51+
import wasi:random/random@0.2.0;
4152

4253
export calculator;
4354
}

examples/go_component/wit/simple-calculator.wit

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ interface calculator {
77
divide: func(a: f64, b: f64) -> f64;
88
}
99

10+
/// Reactor world - WASI imports required by TinyGo runtime
11+
/// TinyGo's Go runtime requires these even for reactor components (empty main())
12+
/// This is a TinyGo architecture requirement, not a Component Model limitation
1013
world calculator-world {
14+
// TinyGo runtime requires these WASI interfaces
15+
import wasi:io/streams@0.2.0;
16+
import wasi:cli/stdout@0.2.0;
17+
import wasi:cli/stderr@0.2.0;
18+
import wasi:cli/stdin@0.2.0;
19+
import wasi:clocks/wall-clock@0.2.0;
20+
import wasi:filesystem/types@0.2.0;
21+
import wasi:filesystem/preopens@0.2.0;
22+
import wasi:random/random@0.2.0;
23+
1124
export calculator;
1225
}

go/defs.bzl

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -394,18 +394,23 @@ def _compile_tinygo_module(ctx, tinygo, go_binary, wasm_opt_binary, wasm_tools,
394394

395395
# CRITICAL: We DO need WIT flags to tell TinyGo to generate custom WIT exports
396396
# Without these flags, TinyGo only generates WASI CLI interfaces, not our calculator functions
397-
# The issue was with WASI component wrapping, not with WIT export generation
397+
# FIX: Use single-dash flags as TinyGo expects: -wit-package and -wit-world (not double-dash)
398+
# FIX: Pass the full WIT directory (with deps/) instead of just the file basename
398399
if ctx.attr.wit and ctx.attr.world:
399-
wit_info = ctx.attr.wit[WitInfo]
400-
wit_files = wit_info.wit_files.to_list()
401-
if wit_files:
402-
# TinyGo expects WIT package path and world name for WIT export generation
403-
# Since TinyGo executes from the workspace directory, use relative path
404-
wit_relative_path = "wit/" + wit_files[0].basename
400+
# Get the WIT library directory that contains the full structure with deps/
401+
wit_library_dir = None
402+
for file in ctx.attr.wit[DefaultInfo].files.to_list():
403+
if file.is_directory:
404+
wit_library_dir = file
405+
break
406+
407+
if wit_library_dir:
408+
# TinyGo expects WIT package directory path for dependency resolution
409+
# The wit_library rule creates a directory with proper deps/ structure
405410
tinygo_args.extend([
406-
"--wit-package",
407-
wit_relative_path,
408-
"--wit-world",
411+
"-wit-package",
412+
wit_library_dir.path,
413+
"-wit-world",
409414
ctx.attr.world,
410415
])
411416

@@ -511,6 +516,11 @@ def _compile_tinygo_module(ctx, tinygo, go_binary, wasm_opt_binary, wasm_tools,
511516
if ctx.attr.wit:
512517
wit_info = ctx.attr.wit[WitInfo]
513518
inputs.extend(wit_info.wit_files.to_list())
519+
# Also include the WIT library directory (with deps/) for dependency resolution
520+
for file in ctx.attr.wit[DefaultInfo].files.to_list():
521+
if file.is_directory:
522+
inputs.append(file)
523+
break
514524

515525
# CRITICAL FIX: TinyGo needs absolute paths for Go binary
516526
# Create a wrapper script that sets up the environment with absolute paths
@@ -551,15 +561,26 @@ def _compile_tinygo_module(ctx, tinygo, go_binary, wasm_opt_binary, wasm_tools,
551561
"",
552562
])
553563

554-
# Add the TinyGo command with arguments, adjusting output path to be absolute
564+
# Add the TinyGo command with arguments, adjusting paths to be absolute
555565
tinygo_cmd = "\"$EXECROOT/{}\"".format(tinygo.path) if not tinygo.path.startswith("/") else "\"{}\"".format(tinygo.path)
556566

557-
# Adjust the output path to be absolute since we're changing directories
567+
# Get the WIT library directory path for adjustment
568+
wit_library_path = None
569+
if ctx.attr.wit and ctx.attr.world:
570+
for file in ctx.attr.wit[DefaultInfo].files.to_list():
571+
if file.is_directory:
572+
wit_library_path = file.path
573+
break
574+
575+
# Adjust paths to be absolute since we're changing directories
558576
adjusted_args = []
559577
for arg in tinygo_args:
560578
if arg == wasm_module.path:
561579
# Make output path absolute
562580
adjusted_args.append("\"$EXECROOT/{}\"".format(wasm_module.path))
581+
elif wit_library_path and arg == wit_library_path:
582+
# Make WIT package path absolute
583+
adjusted_args.append("\"$EXECROOT/{}\"".format(wit_library_path))
563584
else:
564585
adjusted_args.append("\"%s\"" % arg)
565586

0 commit comments

Comments
 (0)