site: elaborates concurrency and TinyGo notes; adds Rust notes (#764)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-08-25 12:02:02 +08:00
committed by GitHub
parent 17dcff7dbc
commit c00cb1bd53
5 changed files with 339 additions and 34 deletions

View File

@@ -18,6 +18,6 @@ Under the covers, [lib.rs](testdata/src/lib.rs) does a few things of interest:
Note: We chose to not use CString because it keeps the example similar to how
you would track memory for arbitrary blobs. We also watched function signatures
carefully as Rust compiles different WebAssembly signatures depending on the
input type. All of this is Rust-specific, and wazero isn't a Rust project, but
we hope this gets you started. For next steps, consider reading the
[Rust and WebAssembly book](https://rustwasm.github.io/docs/book/).
input type.
See https://wazero.io/languages/rust/ for more tips.

View File

@@ -24,7 +24,7 @@ to allow users to extend the proxy via [Proxy-Wasm ABI][3]. Maybe you are
writing server-side rendering applications via Wasm, or [OpenPolicyAgent][4]
is using Wasm for plugin system.
However, experienced Golang developers often avoid using CGO because it
However, experienced Go developers often avoid using CGO because it
introduces complexity. For example, CGO projects are larger and complicated to
consume due to their libc + shared library dependency. Debugging is more
difficult for Go developers when most of a library is written in Rustlang.

View File

@@ -5,8 +5,8 @@ layout = "single"
WebAssembly has a virtual machine architecture where the host is the embedding
process and the guest is a program compiled into the WebAssembly Binary Format,
also known as wasm. The first step is to take a source file and compile it into
the wasm bytecode.
also known as Wasm. The first step is to take a source file and compile it into
the Wasm bytecode.
Ex. If your source is in Go, you might compile it with TinyGo.
```goat
@@ -18,8 +18,9 @@ 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.
* [TinyGo](tinygo) Ex. `tinygo build -o X.wasm -target=wasi X.go`
* [Rust](rust) Ex. `rustc -o X.wasm --target wasm32-wasi X.rs`
wazero is a runtime that embeds in Golang applications, not a web browser. As
wazero is a runtime that embeds in Go 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
@@ -27,5 +28,59 @@ 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.
## Concurrency
WebAssembly does not yet support true parallelism; it lacks support for
multiple threads, atomics, and memory barriers. (It may someday; See
the [threads proposal][5].)
For example, a compiler targeting [WASI][3], generates a `_start` function
corresponding to `main` in the original source code. When the WebAssembly
runtime calls `_start`, it remains on the same thread of execution until that
function completes.
Concretely, if using wazero, a Wasm function call remains on the calling
goroutine until it completes.
In summary, while true that host functions can do anything, including launch
processes, Wasm binaries compliant with [WebAssembly Core 2.0][4] cannot do
anything in parallel, unless they use non-standard instructions or conventions
not yet defined by the specification.
### Compiling Parallel Code to Serial Wasm
Until this [changes][5], language compilers cannot generate Wasm that can
control scheduling within a function or safely modify memory in parallel.
In other words, one function cannot do anything in parallel.
This impacts how programming language primitives translate to Wasm:
* Garbage collection invokes on the runtime host's calling thread instead of
in the background.
* Language-defined threads or co-routines fail compilation or are limited to
sequential processing.
* Locks and barriers fail compilation or are implemented unsafely.
* Async functions including I/O execute sequentially.
Language compilers often used shared infrastructure, such as [LLVM][6] and
[Binaryen][7]. One tool that helps in translation is Binaryen's [Asyncify][8],
which lets a language support synchronous operations in an async manner.
### Concurrency via Orchestration
To work around lack of concurrency at the WebAssembly Core abstraction, tools
often orchestrate pools of workers, and ensure a module in that pool is only
used sequentially.
For example, [waPC][9] provides a WASM module pool, so host callbacks can be
invoked in parallel, despite not being able to share memory.
[1]: https://github.com/tetratelabs/wazero/tree/main/site/content/languages
[2]: https://github.com/tetratelabs/wazero/stargazers
[3]: https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
[4]: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/
[5]: https://github.com/WebAssembly/threads
[6]: https://llvm.org
[7]: https://github.com/WebAssembly/binaryen
[8]: https://github.com/WebAssembly/binaryen/blob/main/src/passes/Asyncify.cpp
[9]: https://github.com/wapc/wapc-go

View File

@@ -0,0 +1,225 @@
+++
title = "Rust"
+++
## Introduction
Beginning with 1.30 [Rust][1] can generate `%.wasm` files instead of
architecture-specific binaries through three targets:
* `wasm32-unknown-emscripten`: mostly for browser (JavaScript) use.
* `wasm32-unknown-unknown`: for standalone use in or outside the browser.
* `wasm32-wasi`: for use outside the browser.
This document is maintained by wazero, which is a WebAssembly runtime that
embeds in Go applications. Hence, our notes focus on targets used outside the
browser, tested by wazero: `wasm32-unknown-unknown` and `wasm32-wasi`.
This document also focuses on `rustc` as opposed to `cargo`, for precision and
brevity.
## Overview
When Rust compiles a `%.rs` file with a `wasm32-*` target, the output `%.wasm`
depends on a subset of features in the [WebAssembly 1.0 Core specification][2].
The `wasm32-wasi` target depends on [WASI][3] host functions as well.
Unlike some compilers, Rust also supports importing custom host functions and
exporting functions back to the host.
Here's a basic example of source in Rust:
```rust
#[no_mangle]
pub extern "C" fn add(x: i32, y: i32) -> i32 {
x + y
}
```
The following is the minimal command to build a Wasm file.
```bash
rustc -o lib.wasm --target wasm32-unknown-unknown --crate-type cdylib lib.rs
```
The resulting Wasm exports the `add` function so that the embedding host can
call it, regardless of if the host is written in Rust or not.
### Digging Deeper
There are a few things to unpack above, so let's start at the top.
The rust source includes `#[no_mangle]` and `extern "C"`. Add these to
functions you want to export to the WebAssembly host. You can read more about
this in [The Embedded Rust Book][4].
Next, you'll notice the flag `--crate-type cdylib` passed to `rustc`. This
compiles the source as a library, ex. without a `main` function.
## Disclaimer
This document includes notes contributed by the wazero community. While wazero
includes Rust examples, the community is less familiar with Rust. For more
help, consider the [Rust and WebAssembly book][5].
Meanwhile, please help us [maintain][6] this document and [star our GitHub
repository][7], if it is helpful. Together, we can make WebAssembly easier on
the next person.
## Constraints
Like other compilers that can target wasm, there are constraints using Rust.
These constraints affect the library design and dependency choices in your
source.
The most common constraint is which crates you can depend on. Please refer to
the [Which Crates Will Work Off-the-Shelf with WebAssembly?][8] page in the
[Rust and WebAssembly book][5] for more on this.
## Memory
When Rust compiles rust into Wasm, it configures the WebAssembly linear memory
to an initial size of 17 pages (1.1MB), and marks a position in that memory as
the heap base. All memory beyond that is used for the Rust heap.
Allocations within Rust (compiled to `%.wasm`) are managed as one would expect.
The `global_allocator` can allocate pages (`alloc_pages`) until `memory.grow`
on the host returns -1.
### Host Allocations
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.
The below snippet is a realistic example of a function exported to the host,
who needs to allocate memory first.
```rust
#[no_mangle]
pub unsafe extern "C" fn configure(ptr: u32, len: u32) {
let json = &ptr_to_string(ptr, len);
}
```
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 Rust) 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][9] that shows this.
To allow the host to allocate memory, you need to define your own `malloc` and
`free` functions:
```webassembly
(func (export "malloc") (param $size i32) (result (;$ptr;) i32))
(func (export "free") (param $ptr i32) (param $size i32))
```
The below implements this in Rustlang:
```rust
use std::mem::MaybeUninit;
use std::slice;
unsafe fn ptr_to_string(ptr: u32, len: u32) -> String {
let slice = slice::from_raw_parts_mut(ptr as *mut u8, len as usize);
let utf8 = std::str::from_utf8_unchecked_mut(slice);
return String::from(utf8);
}
#[no_mangle]
pub extern "C" fn alloc(size: u32) -> *mut u8 {
// Allocate the amount of bytes needed.
let vec: Vec<MaybeUninit<u8>> = Vec::with_capacity(size as usize);
// into_raw leaks the memory to the caller.
Box::into_raw(vec.into_boxed_slice()) as *mut u8
}
#[no_mangle]
pub unsafe extern "C" fn free(ptr: u32, size: u32) {
// Retake the pointer which allows its memory to be freed.
let _ = Vec::from_raw_parts(ptr as *mut u8, 0, size as usize);
}
```
As you can see, the above code is quite technical and should be kept separate
from your business logic as much as possible.
## System Calls
WebAssembly is a stack-based virtual machine specification, so operates at a
lower level than an operating system. For functionality the operating system
would otherwise provide, you must use the `wasm32-wasi` target. This imports
host functions defined in [WASI][3], described in [Specifications]({{< ref "/specs" >}}).
For example, `rustc -o hello.wasm --target wasm32-wasi hello.rs` compiles the
below `main` function into a WASI function exported as `_start`.
```rust
fn main() {
println!("Hello World!");
}
```
Note: wazero includes an [example WASI project][10] including [source code][11]
that implements `cat` without any WebAssembly-specific code.
## Concurrency
Please read our overview of WebAssembly and
[concurrency]({{< ref "_index.md#concurrency" >}}). In short, the current
WebAssembly specification does not support parallel processing.
## 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 with `%.wasm` binary size constraints can change their source or set
`rustc` flags to reduce it.
Source changes:
* [wee_alloc][12]: Smaller, WebAssembly-tuned memory allocator.
[`rustc` flags][13]:
* `-C debuginfo=0`: Strips DWARF, but retains the WebAssembly name section.
* `-C opt-level=3`: Includes all size optimizations.
Those using cargo should also use the `--release` flag, which corresponds to
`rustc -C debuginfo=0 -C opt-level=3`.
Those using the `wasm32-wasi` target should consider the [cargo-wasi][14] crate
as it dramatically reduces Wasm size.
### Performance
Those with runtime performance constraints can change their source or set
`rustc` flags to improve it.
[`rustc` flags][13]:
* `-C opt-level=2`: Enable additional optimizations, frequently at the expense
of binary size.
## Frequently Asked Questions
### Why is my `%.wasm` binary so big?
Rust 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.
[1]: https://www.rust-lang.org/tools/install
[2]: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
[3]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
[4]: https://docs.rust-embedded.org/book/interoperability/rust-with-c.html#no_mangle
[5]: https://rustwasm.github.io/docs/book
[6]: https://github.com/tetratelabs/wazero/tree/main/site/content/languages/rust.md
[7]: https://github.com/tetratelabs/wazero/stargazers
[8]: https://rustwasm.github.io/docs/book/reference/which-crates-work-with-wasm.html
[9]: https://github.com/tetratelabs/wazero/tree/main/examples/allocation/rust
[10]: https://github.com/tetratelabs/wazero/tree/main/examples/wasi
[11]: https://github.com/tetratelabs/wazero/tree/main/examples/wasi/testdata/cargo-wasi
[12]: https://github.com/rustwasm/wee_alloc
[13]: https://doc.rust-lang.org/cargo/reference/profiles.html#profile-settings
[14]: https://github.com/bytecodealliance/cargo-wasi

View File

@@ -11,18 +11,18 @@ title = "TinyGo"
* `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
embeds in Go applications. Hence, all notes below will be about TinyGo's
`wasi` target.
## Overview
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.
as well [WASI][3] host functions.
Unlike some compilers, TinyGo also supports importing custom host functions and
exporting functions back to the host.
## Example
Here's a basic example of source in TinyGo:
```go
@@ -34,7 +34,7 @@ func add(x, y uint32) uint32 {
}
```
The following flags will result in the most compact (smallest) wasm file.
The following is the minimal command to build a `%.wasm` binary.
```bash
tinygo build -o main.wasm -target=wasi main.go
```
@@ -60,6 +60,7 @@ 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
@@ -83,6 +84,7 @@ 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.
@@ -103,6 +105,7 @@ standard way to stub a syscall until it is implemented. If you are interested
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
@@ -116,15 +119,16 @@ is also a much more efficient means to communicate bugs vs ad-hoc reports.
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.
the heap base. All memory beyond that is used for the Go heap.
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.
The allocator can [grow][20] until `memory.grow` on the host returns -1.
### Host Allocations
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.
The below snippet is a realistic example of a function exported to the host,
who needs to allocate memory first.
```go
@@ -220,11 +224,14 @@ approach later.
WebAssembly is a stack-based virtual machine specification, so operates at a
lower level than an operating system. For functionality the operating system
would otherwise provide, TinyGo imports host functions, specifically ones
defined in [WASI][3], described in [Specifications]({{< ref "/specs" >}}).
would otherwise provide, TinyGo imports host functions defined in [WASI][3],
described in [Specifications]({{< ref "/specs" >}}).
Notably, if you compile and run below program with the target `wasi`, you'll
see that the effective `GOARCH=wasm` and `GOOS=linux`.
For example, `tinygo build -o main.wasm -target=wasi main.go` compiles the
below `main` function into a WASI function exported as `_start`.
When the WebAssembly runtime calls `_start`, you'll see the effective
`GOARCH=wasm` and `GOOS=linux`.
```go
package main
@@ -239,6 +246,9 @@ func main() {
}
```
Note: wazero includes an [example WASI project][21] including [source code][22]
that implements `cat` without any WebAssembly-specific code.
### WASI Internals
While developing WASI in TinyGo is outside the scope of this document, the
@@ -260,13 +270,19 @@ 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.
Please read our overview of WebAssembly and
[concurrency]({{< ref "_index.md#concurrency" >}}). In short, the current
WebAssembly specification does not support parallel processing.
TinyGo, however, supports goroutines by default and acts like `GOMAXPROCS=1`.
Tinygo uses only one core/thread regardless of target. This happens to be a
good match for Wasm's current lack of support for (multiple) threads. Tinygo's
goroutine scheduler on Wasm currently uses Binaryen's [Asyncify][23], a Wasm
postprocessor also used by other languages targeting Wasm to provide similar
concurrency.
For example, the following code will run with the expected output, even if
the goroutines are defined in opposite dependency order.
In summary, TinyGo supports goroutines by default and acts like `GOMAXPROCS=1`.
Since [goroutines are not threads][24], the following code will run with the
expected output, despite goroutines defined in opposite dependency order.
```go
package main
@@ -288,9 +304,9 @@ func main() {
}
```
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.
There are some glitches to this. For example, if that same function was
exported (`//export notMain`), and called while main wasn't running, the line
that creates a goroutine currently [panics at runtime][25].
Given problems like this, some choose a compile-time failure instead, via
`-scheduler=none`. Since code often needs to be custom in order to work with
@@ -303,17 +319,21 @@ performance vs defaults. Note that sometimes one sacrifices the other.
### Binary size
Those with size constraints can reduce the `%.wasm` binary size by changing
`tinygo` flags. For example, a simple `cat` program can reduce from default of
260KB to 60KB using both flags below.
Those with `%.wasm` binary size constraints can set `tinygo` flags to reduce
it. For example, a simple `cat` program can reduce from default of 260KB to
60KB using both flags below.
* `-scheduler=none`: Reduces size, but fails at compile time on goroutines.
* `--no-debug`: Strips DWARF, but retains the WebAssembly name section.
### Performance
Those with runtime performance constraints can set `tinygo` flags to improve
it.
* `-gc=leaking`: Avoids GC which improves performance for short-lived programs.
* `-opt=2`: Can also improve performance.
* `-opt=2`: Enable additional optimizations, frequently at the expense of binary
size.
## Frequently Asked Questions
@@ -332,7 +352,7 @@ when their neither has a main function nor uses memory. At least implementing
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?
### Why is my `%.wasm` binary 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.
@@ -344,7 +364,7 @@ functions, such as `fmt.Println`, which can require 100KB of wasm.
[1]: https://tinygo.org/
[2]: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/
[3]: https://github.com/WebAssembly/WASI
[3]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
[4]: https://tinygo.org/docs/guides/webassembly/
[5]: https://github.com/tinygo-org/tinygo#getting-help
[6]: https://github.com/tetratelabs/wazero/tree/main/site/content/languages/tinygo.md
@@ -362,3 +382,8 @@ functions, such as `fmt.Println`, which can require 100KB of wasm.
[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
[21]: https://github.com/tetratelabs/wazero/tree/main/examples/wasi
[22]: https://github.com/tetratelabs/wazero/tree/main/examples/wasi/testdata/tinygo
[23]: https://github.com/WebAssembly/binaryen/blob/main/src/passes/Asyncify.cpp
[24]: http://tleyden.github.io/blog/2014/10/30/goroutines-vs-threads/
[25]: https://github.com/tinygo-org/tinygo/issues/3095