wa-lang.org/wazero@v1.0.2/site/content/languages/tinygo.md (about) 1 +++ 2 title = "TinyGo" 3 +++ 4 5 ## Introduction 6 7 [TinyGo][1] is an alternative compiler for Go source code. It can generate 8 `%.wasm` files instead of architecture-specific binaries through two targets: 9 10 * `wasm`: for browser (JavaScript) use. 11 * `wasi`: for use outside the browser. 12 13 This document is maintained by wazero, which is a WebAssembly runtime that 14 embeds in Go applications. Hence, all notes below will be about TinyGo's 15 `wasi` target. 16 17 ## Overview 18 19 When TinyGo compiles a `%.go` file with its `wasi` target, the output `%.wasm` 20 depends on a subset of features in the [WebAssembly 2.0 Core specification] 21 ({{< ref "/specs#core" >}}) and [WASI]({{< ref "/specs#wasi" >}}) host 22 functions. 23 24 Unlike some compilers, TinyGo also supports importing custom host functions and 25 exporting functions back to the host. 26 27 Here's a basic example of source in TinyGo: 28 29 ```go 30 package main 31 32 //export add 33 func add(x, y uint32) uint32 { 34 return x + y 35 } 36 37 // main is required for the `wasi` target, even if it isn't used. 38 func main() {} 39 ``` 40 41 The following is the minimal command to build a `%.wasm` binary. 42 ```bash 43 tinygo build -o main.wasm -target=wasi main.go 44 ``` 45 46 The resulting wasm exports the `add` function so that the embedding host can 47 call it, regardless of if the host is written in Go or not. 48 49 ## Disclaimer 50 51 This document includes notes contributed by the wazero community. While wazero 52 includes TinyGo examples, and maintainers often contribute to TinyGo, this 53 isn't a TinyGo official document. For more help, consider the [TinyGo Using 54 WebAssembly Guide][4] or joining the [#TinyGo channel on the Gophers Slack][5]. 55 56 Meanwhile, please help us [maintain][6] this document and [star our GitHub 57 repository][7], if it is helpful. Together, we can make WebAssembly easier on 58 the next person. 59 60 ## Constraints 61 62 Please read our overview of WebAssembly and 63 [constraints]({{< ref "_index.md#constraints" >}}). In short, expect 64 limitations in both language features and library choices when developing your 65 software. 66 67 ### Unsupported standard libraries 68 69 TinyGo does not completely implement the Go standard library when targeting 70 `wasi`. What is missing is documented [here][26]. 71 72 The first constraint people notice is that `encoding/json` usage compiles, but 73 panics at runtime. 74 ```go 75 package main 76 77 import "encoding/json" 78 79 type response struct { 80 Ok bool `json:"ok"` 81 } 82 83 func main() { 84 var res response 85 if err := json.Unmarshal([]byte(`{"ok": true}`), &res); err != nil { 86 println(err) 87 } 88 } 89 ``` 90 This is due to limited support for reflection, and effects other [serialization 91 tools][18] also. See [Frequently Asked Questions](#frequently-asked-questions) 92 for some workarounds. 93 94 ### Unsupported System Calls 95 96 You may also notice some other features not yet work. For example, the below 97 will compile, but print "readdir unimplemented : errno 54" at runtime. 98 99 ```go 100 package main 101 102 import "os" 103 104 func main() { 105 if _, err := os.ReadDir("."); err != nil { 106 println(err) 107 } 108 } 109 ``` 110 111 The underlying error is often, but not always `syscall.ENOSYS` which is the 112 standard way to stub a syscall until it is implemented. If you are interested 113 in more, see [System Calls](#system-calls). 114 115 ## Memory 116 117 When TinyGo compiles go into wasm, it configures the WebAssembly linear memory 118 to an initial size of 2 pages (128KB), and marks a position in that memory as 119 the heap base. All memory beyond that is used for the Go heap. 120 121 Allocations within Go (compiled to `%.wasm`) are managed as one would expect. 122 The allocator can [grow][20] until `memory.grow` on the host returns -1. 123 124 ### Host Allocations 125 126 Sometimes a host function needs to allocate memory directly. For example, to 127 write JSON of a given length before invoking an exported function to parse it. 128 129 The below snippet is a realistic example of a function exported to the host, 130 who needs to allocate memory first. 131 ```go 132 //export configure 133 func configure(ptr uintptr, size uint32) { 134 json := ptrToString(ptr, size) 135 } 136 ``` 137 Note: WebAssembly uses 32-bit memory addressing, so a `uintptr` is 32-bits. 138 139 The general flow is that the host allocates memory by calling an allocation 140 function with the size needed. Then, it writes data, in this case JSON, to the 141 memory offset (`ptr`). At that point, it can call a host function, ex 142 `configure`, passing the `ptr` and `size` allocated. The guest wasm (compiled 143 from Go) will be able to read the data. To ensure no memory leaks, the host 144 calls a free function, with the same `ptr`, afterwards and unconditionally. 145 146 Note: wazero includes an [example project][8] that shows this. 147 148 There are two ways to implement this pattern, and they affect how to implement 149 the `ptrToString` function above: 150 * Built-in `malloc` and `free` functions 151 * Custom `malloc` and `free` functions 152 153 While both patterns are used in practice, TinyGo maintainers only support the 154 custom approach. See the following issues for clarifications: 155 * [WebAssembly exports for allocation][9] 156 * [Memory ownership of TinyGo allocated pointers][10] 157 158 #### Built-in `malloc` and `free` functions 159 160 The least code way to allow the host to allocate memory is to call the built-in 161 `malloc` and `free` functions exported by TinyGo: 162 ```webassembly 163 (func (export "malloc") (param $size i32) (result (;$ptr;) i32)) 164 (func (export "free") (param $ptr i32)) 165 ``` 166 167 Go code (compiled to %.wasm) can read this memory directly by first coercing it 168 to a `reflect.SliceHeader`. 169 ```go 170 func ptrToString(ptr uintptr, size uint32) string { 171 return *(*string)(unsafe.Pointer(&reflect.SliceHeader{ 172 Data: ptr, 173 Len: uintptr(size), 174 Cap: uintptr(size), 175 })) 176 } 177 ``` 178 179 The reason TinyGo maintainers do not recommend this approach is there's a risk 180 of garbage collection interference, albeit unlikely in practice. 181 182 #### Custom `malloc` and `free` functions 183 184 The safest way to allow the host to allocate memory is to define your own 185 `malloc` and `free` functions with names that don't collide with TinyGo's: 186 ```webassembly 187 (func (export "my_malloc") (param $size i32) (result (;$ptr;) i32)) 188 (func (export "my_free") (param $ptr i32)) 189 ``` 190 191 The below implements the custom approach, in Go using a map of byte slices. 192 ```go 193 func ptrToString(ptr uintptr, size uint32) string { 194 // size is ignored as the underlying map is pre-allocated. 195 return string(alivePointers[ptr]) 196 } 197 198 var alivePointers = map[uintptr][]byte{} 199 200 //export my_malloc 201 func my_malloc(size uint32) uintptr { 202 buf := make([]byte, size) 203 ptr := &buf[0] 204 unsafePtr := uintptr(unsafe.Pointer(ptr)) 205 alivePointers[unsafePtr] = buf 206 return unsafePtr 207 } 208 209 //export my_free 210 func my_free(ptr uintptr) { 211 delete(alivePointers, ptr) 212 } 213 ``` 214 215 Note: Even if you define your own functions, you should still keep the same 216 signatures as the built-in. For example, a `size` parameter on `ptrToString`, 217 even if you don't use it. This gives you more flexibility to change the 218 approach later. 219 220 ## System Calls 221 222 Please read our overview of WebAssembly and 223 [System Calls]({{< ref "_index.md#system-calls" >}}). In short, WebAssembly is 224 a stack-based virtual machine specification, so operates at a lower level than 225 an operating system. 226 227 For functionality the operating system would otherwise provide, TinyGo imports 228 host functions defined in [WASI]({{< ref "/specs#wasi" >}}). 229 230 For example, `tinygo build -o main.wasm -target=wasi main.go` compiles the 231 below `main` function into a WASI function exported as `_start`. 232 233 When the WebAssembly runtime calls `_start`, you'll see the effective 234 `GOARCH=wasm` and `GOOS=linux`. 235 236 ```go 237 package main 238 239 import ( 240 "fmt" 241 "runtime" 242 ) 243 244 func main() { 245 fmt.Println(runtime.GOARCH, runtime.GOOS) 246 } 247 ``` 248 249 Note: wazero includes an [example WASI project][21] including [source code][22] 250 that implements `cat` without any WebAssembly-specific code. 251 252 ### WASI Internals 253 254 While developing WASI in TinyGo is outside the scope of this document, the 255 below pointers will help you understand the underlying architecture of the 256 `wasi` target. Ideally, these notes can help you frame support or feature 257 requests with the TinyGo team. 258 259 A close look at the [wasi target][11] reveals how things work. Underneath, 260 TinyGo leverages the `wasm32-unknown-wasi` LLVM target for the system call 261 layer (libc), which is eventually implemented by the [wasi-libc][12] library. 262 263 Similar to normal code, TinyGo decides which abstraction to use with GOOS and 264 GOARCH specific suffixes and build flags. 265 266 For example, `os.Args` is implemented directly using WebAssembly host functions 267 in [runtime_wasm_wasi.go][13]. `syscall.Chdir` is implemented with the same 268 [syscall_libc.go][14] used for other architectures, while `syscall.ReadDirent` 269 is stubbed (returns `syscall.ENOSYS`), in [syscall_libc_wasi.go][15]. 270 271 ## Concurrency 272 273 Please read our overview of WebAssembly and 274 [concurrency]({{< ref "_index.md#concurrency" >}}). In short, the current 275 WebAssembly specification does not support parallel processing. 276 277 Tinygo uses only one core/thread regardless of target. This happens to be a 278 good match for Wasm's current lack of support for (multiple) threads. Tinygo's 279 goroutine scheduler on Wasm currently uses Binaryen's [Asyncify][23], a Wasm 280 postprocessor also used by other languages targeting Wasm to provide similar 281 concurrency. 282 283 In summary, TinyGo supports goroutines by default and acts like `GOMAXPROCS=1`. 284 Since [goroutines are not threads][24], the following code will run with the 285 expected output, despite goroutines defined in opposite dependency order. 286 ```go 287 package main 288 289 import "fmt" 290 291 func main() { 292 msg := make(chan int) 293 finished := make(chan int) 294 go func() { 295 <-msg 296 fmt.Println("consumer") 297 finished <- 1 298 }() 299 go func() { 300 fmt.Println("producer") 301 msg <- 1 302 }() 303 <-finished 304 } 305 ``` 306 307 There are some glitches to this. For example, if that same function was 308 exported (`//export notMain`), and called while main wasn't running, the line 309 that creates a goroutine currently [panics at runtime][25]. 310 311 Given problems like this, some choose a compile-time failure instead, via 312 `-scheduler=none`. Since code often needs to be custom in order to work with 313 wasm anyway, there may be limited impact to removing goroutine support. 314 315 ## Optimizations 316 317 Below are some commonly used configurations that allow optimizing for size or 318 performance vs defaults. Note that sometimes one sacrifices the other. 319 320 ### Binary size 321 322 Those with `%.wasm` binary size constraints can set `tinygo` flags to reduce 323 it. For example, a simple `cat` program can reduce from default of 260KB to 324 60KB using both flags below. 325 326 * `-scheduler=none`: Reduces size, but fails at compile time on goroutines. 327 * `--no-debug`: Strips DWARF, but retains the WebAssembly name section. 328 329 ### Performance 330 331 Those with runtime performance constraints can set `tinygo` flags to improve 332 it. 333 334 * `-gc=leaking`: Avoids GC which improves performance for short-lived programs. 335 * `-opt=2`: Enable additional optimizations, frequently at the expense of binary 336 size. 337 338 ## Frequently Asked Questions 339 340 ### Why do I have to define main? 341 342 If you are using TinyGo's `wasi` target, you should define at least a no-op 343 `func main() {}` in your source. 344 345 If you don't, instantiation of the WebAssembly will fail unless you've exported 346 the following from the host: 347 ```webassembly 348 (func (import "env" "main.main") (param i32) (result i32)) 349 ``` 350 351 ### How do I use json? 352 TinyGo doesn't yet implement [reflection APIs][16] needed by `encoding/json`. 353 Meanwhile, most users resort to non-reflective parsers, such as [gjson][17]. 354 355 ### Why does my wasm import WASI functions even when I don't use it? 356 TinyGo has a `wasm` target (for browsers) and a `wasi` target for runtimes that 357 support [WASI]({{< ref "/specs#wasi" >}}). This document is written only about 358 the `wasi` target. 359 360 Some users are surprised to see imports from WASI (`wasi_snapshot_preview1`), 361 when their neither has a main function nor uses memory. At least implementing 362 `panic` requires writing to the console, and `fd_write` is used for this. 363 364 A bare or standalone WebAssembly target doesn't yet exist, but if interested, 365 you can follow [this issue][19]. 366 367 ### Why is my `%.wasm` binary so big? 368 TinyGo defaults can be overridden for those who can sacrifice features or 369 performance for a [smaller binary](#binary-size). After that, tuning your 370 source code may reduce binary size further. 371 372 TinyGo minimally needs to implement garbage collection and `panic`, and the 373 wasm to implement that is often not considered big (~4KB). What's often 374 surprising to users are APIs that seem simple, but require a lot of supporting 375 functions, such as `fmt.Println`, which can require 100KB of wasm. 376 377 [1]: https://tinygo.org/ 378 [4]: https://tinygo.org/docs/guides/webassembly/ 379 [5]: https://github.com/tinygo-org/tinygo#getting-help 380 [6]: https://github.com/tetratelabs/wazero/tree/main/site/content/languages/tinygo.md 381 [7]: https://github.com/tetratelabs/wazero/stargazers 382 [8]: https://github.com/tetratelabs/wazero/tree/main/examples/allocation/tinygo 383 [9]: https://github.com/tinygo-org/tinygo/issues/2788 384 [10]: https://github.com/tinygo-org/tinygo/issues/2787 385 [11]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/targets/wasi.json 386 [12]: https://github.com/WebAssembly/wasi-libc 387 [13]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/src/runtime/runtime_wasm_wasi.go#L34-L62 388 [14]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/src/syscall/syscall_libc.go#L85-L92 389 [15]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/src/syscall/syscall_libc_wasi.go#L263-L265 390 [16]: https://github.com/tinygo-org/tinygo/issues/2660 391 [17]: https://github.com/tidwall/gjson 392 [18]: https://github.com/tinygo-org/tinygo/issues/447 393 [19]: https://github.com/tinygo-org/tinygo/issues/3068 394 [20]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/src/runtime/arch_tinygowasm.go#L47-L62 395 [21]: https://github.com/tetratelabs/wazero/tree/main/imports/wasi_snapshot_preview1/example 396 [22]: https://github.com/tetratelabs/wazero/tree/main/imports/wasi_snapshot_preview1/example/testdata/tinygo 397 [23]: https://github.com/WebAssembly/binaryen/blob/main/src/passes/Asyncify.cpp 398 [24]: http://tleyden.github.io/blog/2014/10/30/goroutines-vs-threads/ 399 [25]: https://github.com/tinygo-org/tinygo/issues/3095 400 [26]: https://tinygo.org/docs/reference/lang-support/stdlib/