Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

CGO-less Foreign Function Interface With WebAss...

mathetake
October 07, 2022

CGO-less Foreign Function Interface With WebAssembly

GopherCon 2022

mathetake

October 07, 2022
Tweet

More Decks by mathetake

Other Decks in Programming

Transcript

  1. 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
  2. func main () { rustFn() zigFn() …. } export fn

    zigFn() void { … } main.go lib.zig pub extern "C" fn rustFn() {...} lib.rs
  3. When/Why do we want FFI? • Reusing softwares in other

    languages ◦ Don’t want to rewrite 100k loc in C
  4. 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
  5. func main () { rustFn() zigFn() …. } export fn

    zigFn() void { … } main.go lib.zig pub extern "C" fn rustFn() {...} lib.rs
  6. 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
  7. What’s the protocol between Go and another lang? How Go

    runtime behaves beyond Go world? CGO
  8. 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
  9. “CGO is not Go” Gopherfest 2015 | Go Proverbs with

    Rob Pike https://youtu.be/PAAkCSZUG1c
  10. CGO troubles • Dynamic vs Static binary: portability issue •

    Cross compilation • CGO is slow • Security
  11. CGO troubles • Dynamic vs Static binary: portability issue •

    Cross compilation • CGO is slow • Security
  12. $ go build main.go $ ldd main not a dynamic

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

    "C" func main() { println("hello") }
  14. $ 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") }
  15. 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
  16. 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
  17. 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
  18. WebAssembly (Wasm) • Binary instruction format for a stack-based virtual

    machine (VM) • Polyglot • Security-oriented design ◦ Memory guard ◦ Deny system calls by default
  19. Wasm is not only for the browsers • Core spec

    is decoupled from the web concept • Embeddable in any application with VM implementation
  20. 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
  21. 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 ?
  22. 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!
  23. 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 ?
  24. 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
  25. 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
  26. Go program // Instantiate a Zig Wasm binary. zig :=

    r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory
  27. 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)
  28. 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)
  29. 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)
  30. 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)
  31. 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)
  32. 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)
  33. 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)
  34. // 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, ...)
  35. • Running a Wasm-compiled SQLite inside Go, without CGO •

    Possible implementation of CGO-less and sandboxed SQL Driver.
  36. • 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!
  37. 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
  38. 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
  39. 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
  40. Interpreter mode • Runs on any platform (GOOS/GOARCH) • Fast

    startup time • Slow execution wazero.NewRuntimeConfigInterpreter()
  41. 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()
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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!