Slide 1

Slide 1 text

CGO-less Foreign Function Interface with WebAssembly Takeshi Yoneda, Open Source Software Engineer at Tetrate

Slide 2

Slide 2 text

Foreign Function Interface(FFI)

Slide 3

Slide 3 text

Foreign Function Interface(FFI) “A foreign function interface (FFI) is a mechanism by which a program written in one programming language can call routines or make use of services written in another.” – wikipedia

Slide 4

Slide 4 text

func main () { } main.go

Slide 5

Slide 5 text

func main () { rustFn() } main.go pub extern "C" fn rustFn() {...} lib.rs

Slide 6

Slide 6 text

func main () { rustFn() zigFn() …. } export fn zigFn() void { … } main.go lib.zig pub extern "C" fn rustFn() {...} lib.rs

Slide 7

Slide 7 text

When/Why do we want FFI?

Slide 8

Slide 8 text

When/Why do we want FFI? ● Reusing softwares in other languages ○ Don’t want to rewrite 100k loc in C

Slide 9

Slide 9 text

When/Why do we want FFI? ● Reusing softwares in other languages ○ Don’t want to rewrite 100k loc in C ● Plugin System via FFI - Polyglot! ○ Allow users to extend your app in any language

Slide 10

Slide 10 text

func main () { rustFn() zigFn() …. } export fn zigFn() void { … } main.go lib.zig pub extern "C" fn rustFn() {...} lib.rs

Slide 11

Slide 11 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool { … } pub extern "C" fn rustFn(v: u32) -> bool {...} lib.rs v v b b main.go lib.zig

Slide 12

Slide 12 text

What’s the protocol between Go and another lang? How Go runtime behaves beyond Go world?

Slide 13

Slide 13 text

What’s the protocol between Go and another lang? How Go runtime behaves beyond Go world? CGO

Slide 14

Slide 14 text

func main () { b := rustFn(v) {} b = zigFn(v) {} …. } export fn zigFn(v: u32) bool { … } main.go lib.zig pub extern "C" fn rustFn(v: u32) -> bool {...} lib.rs v b CGO v b

Slide 15

Slide 15 text

FFI can be done with CGO. The problem solved?

Slide 16

Slide 16 text

FFI can be done with CGO. The problem solved? No.

Slide 17

Slide 17 text

“CGO is not Go” Gopherfest 2015 | Go Proverbs with Rob Pike https://youtu.be/PAAkCSZUG1c

Slide 18

Slide 18 text

CGO troubles ● Dynamic vs Static binary: portability issue ● Cross compilation ● CGO is slow ● Security

Slide 19

Slide 19 text

CGO troubles ● Dynamic vs Static binary: portability issue ● Cross compilation ● CGO is slow ● Security

Slide 20

Slide 20 text

package main func main() { println("hello") }

Slide 21

Slide 21 text

$ go build main.go package main func main() { println("hello") }

Slide 22

Slide 22 text

$ go build main.go $ ldd main package main func main() { println("hello") }

Slide 23

Slide 23 text

$ go build main.go $ ldd main not a dynamic executable package main func main() { println("hello") }

Slide 24

Slide 24 text

package main import "C" func main() { println("hello") }

Slide 25

Slide 25 text

$ go build main.go package main import "C" func main() { println("hello") }

Slide 26

Slide 26 text

$ go build main.go $ ldd main package main import "C" func main() { println("hello") }

Slide 27

Slide 27 text

$ go build main.go $ ldd main linux-vdso.so.1 (0x00007ffd59db2000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fad32c3f000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fad32a4d000) /lib64/ld-linux-x86-64.so.2 (0x00007fad32c83000) package main import "C" func main() { println("hello") }

Slide 28

Slide 28 text

CGO troubles ● Dynamic vs Static binary ● Cross compilation ● CGO is slow ● Security

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

CGO troubles ● Dynamic vs Static binary ● Cross compilation ● CGO is slow ● Security

Slide 31

Slide 31 text

https://github.com/golang/go/issues/19574

Slide 32

Slide 32 text

CGO troubles ● Dynamic vs Static binary ● Cross compilation ● CGO is slow ● Security

Slide 33

Slide 33 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b CGO v b Operating System

Slide 34

Slide 34 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b CGO v b Operating System

Slide 35

Slide 35 text

We need sandox…

Slide 36

Slide 36 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b v b Operating System ? ? Sandbox Sandbox

Slide 37

Slide 37 text

WebAssembly (Wasm)

Slide 38

Slide 38 text

WebAssembly (Wasm) ● Binary instruction format for a stack-based virtual machine (VM) ● Polyglot ● Security-oriented design ○ Memory guard ○ Deny system calls by default

Slide 39

Slide 39 text

Wait, isn’t WebAssembly for the web?

Slide 40

Slide 40 text

Wasm is not only for the browsers ● Core spec is decoupled from the web concept ● Embeddable in any application with VM implementation

Slide 41

Slide 41 text

Non-Web examples

Slide 42

Slide 42 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b v b Operating System ? ? Sandbox Sandbox

Slide 43

Slide 43 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b v b Operating System ?

Slide 44

Slide 44 text

How to run Wasm binary inside Go?

Slide 45

Slide 45 text

Wasm needs a VM runtime! x86_64 aarch64 riscv64 …. runtime

Slide 46

Slide 46 text

wazero.io the zero dependency WebAssembly runtime for Go developers

Slide 47

Slide 47 text

What is wazero? ● Started out as my hobby project: now sponsored by Tetrate ● The Wasm runtime with zero dependency ● Written in pure Go, no CGO!

Slide 48

Slide 48 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b v b Operating System ?

Slide 49

Slide 49 text

func main () { b := rustFn(v) b = zigFn(v) …. } export fn zigFn(v: u32) bool {…} pub extern "C" fn rustFn(v: u32) -> bool {...} v b v b Operating System wazero

Slide 50

Slide 50 text

=> CGO-less Foreign Function Interface wazero

Slide 51

Slide 51 text

FFI with wazero vs CGO ● No CGO ○ Static binary, cross compilation, etc ● Zero dependency ○ E.g. third party toolchains ● Compile once, run everywhere ● Sandbox environment ○ Memory isolation ○ Deny “system calls” by default

Slide 52

Slide 52 text

How it works: memory isotation

Slide 53

Slide 53 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory

Slide 54

Slide 54 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory make([]byte, N) make([]byte, M)

Slide 55

Slide 55 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory make([]byte, N) make([]byte, M)

Slide 56

Slide 56 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory make([]byte, N) make([]byte, M)

Slide 57

Slide 57 text

How it works: system call isolation

Slide 58

Slide 58 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory func readFile(fd int, …) {...} func writeFile(fd int, …) {...} foo.txt bar.txt Operating System // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory read(2) write(2)

Slide 59

Slide 59 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory func readFile(fd int, …) {...} func writeFile(fd int, …) {...} foo.txt bar.txt Operating System // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory read(2) write(2)

Slide 60

Slide 60 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory func readFile(fd int, …) {...} func writeFile(fd int, …) {...} foo.txt bar.txt Operating System // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory read(2) write(2)

Slide 61

Slide 61 text

Go program // Instantiate a Zig Wasm binary. zig := r.InstantiateModuleFromBinary(...) Linear memory func readFile(fd int, …) {...} func writeFile(fd int, …) {...} foo.txt bar.txt Operating System // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory read(2) write(2)

Slide 62

Slide 62 text

System Calls = Go functions Memory = []byte{...}

Slide 63

Slide 63 text

// Create a new WebAssembly Runtime. r := wazero.NewRuntime(ctx) // Instantiate a Rust Wasm binary. rust, _ := r.InstantiateModuleFromBinary(ctx, rustBinary) // Instantiate a Zig Wasm binary. zig, _ := r.InstantiateModuleFromBinary(ctx, zigBinary) // Call functions exported by Wasm modules. ... := rust.ExportedFunction("rustFn").Call(ctx, ...) ... := zig.ExportedFunction("zigFn").Call(ctx, ...)

Slide 64

Slide 64 text

Example projects!

Slide 65

Slide 65 text

● Trivy: vulnerability scanner ● Can extend scanning logics with Wasm, powered by wazero

Slide 66

Slide 66 text

● dapr: portable, serverless application platform ● Can add HTTP middleware in Wasm powered by wazero

Slide 67

Slide 67 text

● Running a Wasm-compiled SQLite inside Go, without CGO ● Possible implementation of CGO-less and sandboxed SQL Driver.

Slide 68

Slide 68 text

● re2: a fast regular expression engine in C++ ● Running Wasm-compiled re2, without CGO ● In some cases, faster that regexp package in the Go std library!

Slide 69

Slide 69 text

Cons of FFI with wazero vs CGO ● Performance degradation ○ Wasm == Virtualization ○ Depends on runtime implementation ● Needs to compile your FFI to Wasm ○ Premature ecosystem ○ Refactor in a Wasm-friendly way

Slide 70

Slide 70 text

Cons of FFI with wazero vs CGO ● Performance degradation ○ Wasm == Virtualization ○ Depends on runtime implementation ● Needs to compile your FFI to Wasm ○ Premature ecosystem ○ Refactor in a Wasm-friendly way

Slide 71

Slide 71 text

Cons of FFI with wazero vs CGO ● Performance degradation ○ Wasm == Virtualization ○ Depends on runtime implementation ● Needs to compile your FFI to Wasm ○ Premature ecosystem ○ Refactor in a Wasm-friendly way

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

wazero deep dive…

Slide 74

Slide 74 text

Q. How correct is the implementation?

Slide 75

Slide 75 text

Q. How correct is the implementation? A. 100% compatible with Wasm spec (1.0&2.0)

Slide 76

Slide 76 text

Q. How is wazero tested?

Slide 77

Slide 77 text

Q. How is wazero tested? A. Specification tests & random binary fuzzing

Slide 78

Slide 78 text

Q. How is the VM implemented?

Slide 79

Slide 79 text

Q. How is the VM implemented? A. Two modes: interpreter and AOT compiler

Slide 80

Slide 80 text

Interpreter mode ● Runs on any platform (GOOS/GOARCH) ● Fast startup time ● Slow execution wazero.NewRuntimeConfigInterpreter()

Slide 81

Slide 81 text

Ahead-Of-Time (AOT) compiler mode ● Runs on {amd64,arm64} x {linux,darwin,windows,freebsd,etc} ● Slow startup time ○ AOT = compile Wasm binary into native machine code before execution ● Fast execution (10x+ faster than interpreter) wazero.NewRuntimeConfigCompiler()

Slide 82

Slide 82 text

How AOT compiler works 1. Creates native machine code semantically equivalent to Wasm binary 2. mmap the machine code []byte as executable 3. Jumps into the “executable” []byte via a Go Assembly function

Slide 83

Slide 83 text

1.Machine code gen 2.Mmap executable vm, err := r.InstantiateModuleFromBinary( ctx, wasmBinary, ) Heap Machine code … = vm.ExportedFunction("myFunc").Call(...) 3.Jump to machine code

Slide 84

Slide 84 text

1. Machine code generation Assembler Code generation

Slide 85

Slide 85 text

2. Mmap machine code as executable

Slide 86

Slide 86 text

3. Jump into machine code

Slide 87

Slide 87 text

Challenges in AOT compiler implementation ● Do not modify Goroutine-stack! (e.g. “call” instruction) ● Do not access Goroutine-stack allocated variable from machine code ● Debugging is extremely difficult ● Single pass compiler: optimizations are TODOs

Slide 88

Slide 88 text

Challenges in AOT compiler implementation ● Do not modify Goroutine-stack! (e.g. “call” instruction) ● Do not access Goroutine-stack allocated variable from machine code ● Debugging is extremely difficult ● Single pass compiler: optimizations are TODOs

Slide 89

Slide 89 text

Heap Wasm function 1 Wasm function 2 Wasm function 3 …. Function calls Wasm Linear memory []byte{...} allocated by Go runtime Module Information, etc A struct allocated by Go runtime Wasm stack []byte{...} allocated by Go runtime Goroutine stack 1 Goroutine stack N …. Generated machine codes

Slide 90

Slide 90 text

Challenges in AOT compiler implementation ● Do not modify Goroutine-stack! (e.g. “call” instruction) ● Do not access Goroutine-stack allocated variable from machine code ● Debugging is extremely difficult ● Naive single pass compiler: optimizations are TODOs

Slide 91

Slide 91 text

Wrap up! ● FFI == Calling non-Go functions from Go ● CGO works, but has some issues ● CGO-less FFI is possible with wazero+WebAssembly ● wazero is written in pure Go, zero dependency!

Slide 92

Slide 92 text

Twitter,GitHub: @mathetake Gopher Slack: #wazero Thank you!