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/