site: Addresses feedback from TinyGo folks (#762)

Thanks to @aykevl @dkegel-fastly @mathetake @anuraaga and jimmysl lee
for all the feedback. Hopefully, this gets most of it.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-08-24 14:01:41 +08:00
committed by GitHub
parent 2742ad9936
commit 15686988dc
2 changed files with 195 additions and 30 deletions

View File

@@ -15,13 +15,17 @@ Ex. If your source is in Go, you might compile it with TinyGo.
'-----+-----' '----------------------' '-----------'
```
Below are notes wazero contributed so far, in alphabetical order by language:
Below are notes wazero contributed so far, in alphabetical order by language.
* [TinyGo](tinygo) Ex. `tinygo build -o X.wasm -scheduler=none --no-debug -target=wasi X.go`
* [TinyGo](tinygo) Ex. `tinygo build -o X.wasm -target=wasi X.go`
Note: These are not official documentation, and may be out of date. Please help
us [maintain][1] these and [star our GitHub repository][2] if they are helpful.
Together, we can help make WebAssembly easier on the next person.
wazero is a runtime that embeds in Golang applications, not a web browser. As
such, these notes bias towards backend use of WebAssembly, not browser use.
Disclaimer: These are not official documentation, nor represent the teams who
maintain language compilers. If you see any errors, please help [maintain][1]
these and [star our GitHub repository][2] if they are helpful. Together, we can
make WebAssembly easier on the next person.
[1]: https://github.com/tetratelabs/wazero/tree/main/site/content/languages
[2]: https://github.com/tetratelabs/wazero/stargazers

View File

@@ -5,11 +5,21 @@ title = "TinyGo"
## Introduction
[TinyGo][1] is an alternative compiler for Go source code. It can generate
`%.wasm` files instead of architecture-specific binaries through its `wasi`
target. The resulting wasm depends on a subset of features in the [WebAssembly
2.0 Core specification][2], as well [WASI][3] host imports. TinyGo also
supports importing custom host functions and exporting functions back to the
host.
`%.wasm` files instead of architecture-specific binaries through two targets:
* `wasm`: for browser (JavaScript) use.
* `wasi`: for use outside the browser.
This document is maintained by wazero, which is a WebAssembly runtime that
embeds in Golang applications. Hence, all notes below will be about TinyGo's
`wasi` target.
When TinyGo compiles a `%.go` file with its `wasi` target, the output `%.wasm`
depends on a subset of features in the [WebAssembly 2.0 Core specification][2],
as well [WASI][3] host imports.
Unlike some compilers, TinyGo also supports importing custom host functions and
exporting functions back to the host.
## Example
@@ -26,7 +36,7 @@ func add(x, y uint32) uint32 {
The following flags will result in the most compact (smallest) wasm file.
```bash
tinygo build -o main.wasm -scheduler=none --no-debug -target=wasi main.go
tinygo build -o main.wasm -target=wasi main.go
```
The resulting wasm exports the `add` function so that the embedding host can
@@ -49,6 +59,7 @@ Like other compilers that can target wasm, there are constraints using TinyGo.
These constraints affect the library design and dependency choices in your Go
source.
### Partial Reflection Support
The first constraint people notice is that `encoding/json` usage compiles, but
panics at runtime.
```go
@@ -71,7 +82,7 @@ This is due to limited support for reflection, and effects other [serialization
tools][18] also. See [Frequently Asked Questions](#frequently-asked-questions)
for some workarounds.
### Unimplemented System Calls
You may also notice some other features not yet work. For example, the below
will compile, but print "readdir unimplemented : errno 54" at runtime.
@@ -89,8 +100,9 @@ func main() {
The underlying error is often, but not always `syscall.ENOSYS` which is the
standard way to stub a syscall until it is implemented. If you are interested
in more, see the [System Calls heading below](#system-calls).
in more, see [System Calls](#system-calls).
### Mitigating Constraints
Realities like this are not unique to TinyGo as they will happen compiling any
language not written specifically with WebAssembly in mind. Knowing the same
code compiled to wasm may return errors or worse panic, the main mitigation
@@ -102,26 +114,107 @@ is also a much more efficient means to communicate bugs vs ad-hoc reports.
## Memory
When TinyGo compiles go into wasm, it treats a portion of the WebAssembly
linear memory as heap. The embedding host can allocate and free memory using
TinyGo's allocator via WebAssembly exported functions: `malloc` and `free`.
When TinyGo compiles go into wasm, it configures the WebAssembly linear memory
to an initial size of 2 pages (16KB), and marks a position in that memory as
the heap base. All memory beyond that is used for the Go heap, which can
[grow][20] until `memory.grow` on the host returns -1.
Here is what the signatures look like when inspecting wasm generated by TinyGo:
Allocations within Go (compiled to `%.wasm`) are managed as one would expect.
Sometimes a host function needs to allocate memory directly. For example, to
write JSON of a given length before invoking an exported function to parse it.
### Host Allocations
The below snippet is a realistic example of a function exported to the host,
who needs to allocate memory first.
```go
//export configure
func configure(ptr uintptr, size uint32) {
json := ptrToString(ptr, size)
}
```
Note: WebAssembly uses 32-bit memory addressing, so a `uintptr` is 32-bits.
The general flow is that the host allocates memory by calling an allocation
function with the size needed. Then, it writes data, in this case JSON, to the
memory offset (`ptr`). At that point, it can call a host function, ex
`configure`, passing the `ptr` and `size` allocated. The guest wasm (compiled
from Go) will be able to read the data. To ensure no memory leaks, the host
calls a free function, with the same `ptr`, afterwards and unconditionally.
Note: wazero includes an [example project][8] that shows this.
There are two ways to implement this pattern, and they affect how to implement
the `ptrToString` function above:
* Built-in `malloc` and `free` functions
* Custom `malloc` and `free` functions
While both patterns are used in practice, TinyGo maintainers only support the
custom approach. See the following issues for clarifications:
* [WebAssembly exports for allocation][9]
* [Memory ownership of TinyGo allocated pointers][10]
#### Built-in `malloc` and `free` functions
The least code way to allow the host to allocate memory is to call the built-in
`malloc` and `free` functions exported by TinyGo:
```webassembly
(func (export "malloc") (param $size i32) (result (;$ptr;) i32))
(func (export "free") (param $ptr i32))
```
Note that TinyGo compiles a `unsafe.Pointer` as a linear memory offset.
The general flow is that the host allocates memory by calling `malloc`, then
using the result as the memory offset to write data. Once the host is finished,
it calls `free` with that same memory offset. wazero includes an [example
project][8] that shows allocation in wasm generated by TinyGo.
Go code (compiled to %.wasm) can read this memory directly by first coercing it
to a `reflect.SliceHeader`.
```go
func ptrToString(ptr uintptr, size uint32) string {
return *(*string)(unsafe.Pointer(&reflect.SliceHeader{
Data: ptr,
Len: uintptr(size),
Cap: uintptr(size),
}))
}
```
These are not documented, though widely used. See the following issues for
clarifications:
* [WebAssembly exports for allocation][9]
* [Memory ownership of TinyGo allocated pointers][10]
The reason TinyGo maintainers do not recommend this approach is there's a risk
of garbage collection interference, albeit unlikely in practice.
#### Custom `malloc` and `free` functions
The safest way to allow the host to allocate memory is to define your own
`malloc` and `free` functions with names that don't collide with TinyGo's:
```webassembly
(func (export "my_malloc") (param $size i32) (result (;$ptr;) i32))
(func (export "my_free") (param $ptr i32))
```
The below implements the custom approach, in Go using a map of byte slices.
```go
func ptrToString(ptr uintptr, size uint32) string {
// size is ignored as the underlying map is pre-allocated.
return string(alivePointers[ptr])
}
var alivePointers = map[uintptr][]byte{}
//export my_malloc
func my_malloc(size uint32) uintptr {
buf := make([]byte, size)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
alivePointers[unsafePtr] = buf
return unsafePtr
}
//export my_free
func my_free(ptr uintptr) {
delete(alivePointers, ptr)
}
```
Note: Even if you define your own functions, you should still keep the same
signatures as the built-in. For example, a `size` parameter on `ptrToString`,
even if you don't use it. This gives you more flexibility to change the
approach later.
## System Calls
@@ -165,6 +258,63 @@ in [runtime_wasm_wasi.go][13]. `syscall.Chdir` is implemented with the same
[syscall_libc.go][14] used for other architectures, while `syscall.ReadDirent`
is stubbed (returns `syscall.ENOSYS`), in [syscall_libc_wasi.go][15].
## Concurrency
Current versions of the WebAssembly specification do not support parallelism,
such as threads or atomics needed to safely work in parallel.
TinyGo, however, supports goroutines by default and acts like `GOMAXPROCS=1`.
For example, the following code will run with the expected output, even if
the goroutines are defined in opposite dependency order.
```go
package main
import "fmt"
func main() {
msg := make(chan int)
finished := make(chan int)
go func() {
<-msg
fmt.Println("consumer")
finished <- 1
}()
go func() {
fmt.Println("producer")
msg <- 1
}()
<-finished
}
```
However, creating goroutines after main (`_start` in WASI) has undefined
behavior. For example, if that same function was exported (`//export:notMain`),
and called after main, the line that creates a goroutine panics at runtime.
Given problems like this, some choose a compile-time failure instead, via the
`-scheduler=none`. Since code often needs to be custom in order to work with
wasm anyway, there may be limited effect to removing goroutine support.
## Optimizations
Below are some commonly used configurations that allow optimizing for size or
performance vs defaults. Note that sometimes one sacrifices the other.
### Binary size
Those who aren't concerned about binary (`%.wasm` file) size should not
interfere with TinyGo's defaults. However, the following flags are commonly
used to reduce the binary size, sometimes to below 100KB.
* `-scheduler=none`: Reduces size, but fails at compile time on goroutines.
* `--no-debug`: Strips DWARF, but retains the WebAssembly name section.
### Performance
* `-gc=leaking`: Avoids GC which improves performance for short-lived programs.
* `-opt=2`: Can also improve performance.
## Frequently Asked Questions
### How do I use json?
@@ -172,12 +322,21 @@ TinyGo doesn't yet implement [reflection APIs][16] needed by `encoding/json`.
Meanwhile, most users resort to non-reflective parsers, such as [gjson][17].
### Why does my wasm import WASI functions even when I don't use it?
TinyGo does not have a standalone wasm target, rather only `wasi`. Some users
are surprised to see [WASI][3] imports even when there is no main function and
the compiled function uses no memory. Most notably, `fd_write` is used to
implement panics.
TinyGo has a `wasm` target (for browsers) and a `wasi` target for runtimes that
support [WASI][3]. This document is written only about the `wasi` target.
Some users are surprised to see imports from WASI (`wasi_snapshot_preview1`),
when their neither has a main function nor uses memory. At least implementing
`panic` requires writing to the console, and `fd_write` is used for this.
A bare or standalone WebAssembly target doesn't yet exist, but if interested,
you can follow [this issue][19].
### Why is my wasm so big?
TinyGo defaults can be overridden for those who can sacrifice features or
performance for a [smaller binary](#binary-size). After that, tuning your
source code may reduce binary size further.
TinyGo minimally needs to implement garbage collection and `panic`, and the
wasm to implement that is often not considered big (~4KB). What's often
surprising to users are APIs that seem simple, but require a lot of supporting
@@ -201,3 +360,5 @@ functions, such as `fmt.Println`, which can require 100KB of wasm.
[16]: https://github.com/tinygo-org/tinygo/issues/2660
[17]: https://github.com/tidwall/gjson
[18]: https://github.com/tinygo-org/tinygo/issues/447
[19]: https://github.com/tinygo-org/tinygo/issues/3068
[20]: https://github.com/tinygo-org/tinygo/blob/v0.25.0/src/runtime/arch_tinygowasm.go#L47-L62