Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CGO-less Foreign Function Interface With WebAssembly

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. CGO-less Foreign Function Interface with WebAssembly Takeshi Yoneda, Open Source

    Software Engineer at Tetrate
  2. Foreign Function Interface(FFI)

  3. 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
  4. func main () { } main.go

  5. func main () { rustFn() } main.go pub extern "C"

    fn rustFn() {...} lib.rs
  6. func main () { rustFn() zigFn() …. } export fn

    zigFn() void { … } main.go lib.zig pub extern "C" fn rustFn() {...} lib.rs
  7. When/Why do we want FFI?

  8. When/Why do we want FFI? • Reusing softwares in other

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

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

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

    runtime behaves beyond Go world? CGO
  14. 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
  15. FFI can be done with CGO. The problem solved?

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

  17. “CGO is not Go” Gopherfest 2015 | Go Proverbs with

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

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

    Cross compilation • CGO is slow • Security
  20. package main func main() { println("hello") }

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

    }
  22. $ go build main.go $ ldd main package main func

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

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

  25. $ go build main.go package main import "C" func main()

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

    "C" func main() { println("hello") }
  27. $ 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") }
  28. CGO troubles • Dynamic vs Static binary • Cross compilation

    • CGO is slow • Security
  29. None
  30. CGO troubles • Dynamic vs Static binary • Cross compilation

    • CGO is slow • Security
  31. https://github.com/golang/go/issues/19574

  32. CGO troubles • Dynamic vs Static binary • Cross compilation

    • CGO is slow • Security
  33. 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
  34. 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
  35. We need sandox…

  36. 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
  37. WebAssembly (Wasm)

  38. WebAssembly (Wasm) • Binary instruction format for a stack-based virtual

    machine (VM) • Polyglot • Security-oriented design ◦ Memory guard ◦ Deny system calls by default
  39. Wait, isn’t WebAssembly for the web?

  40. Wasm is not only for the browsers • Core spec

    is decoupled from the web concept • Embeddable in any application with VM implementation
  41. Non-Web examples

  42. 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
  43. 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 ?
  44. How to run Wasm binary inside Go?

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

  46. wazero.io the zero dependency WebAssembly runtime for Go developers

  47. 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!
  48. 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 ?
  49. 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
  50. => CGO-less Foreign Function Interface wazero

  51. 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
  52. How it works: memory isotation

  53. Go program // Instantiate a Zig Wasm binary. zig :=

    r.InstantiateModuleFromBinary(...) Linear memory // Instantiate a Rust Wasm binary. rust := r.InstantiateModuleFromBinary(...) Linear memory
  54. 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)
  55. 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)
  56. 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)
  57. How it works: system call isolation

  58. 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)
  59. 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)
  60. 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)
  61. 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)
  62. System Calls = Go functions Memory = []byte{...}

  63. // 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, ...)
  64. Example projects!

  65. • Trivy: vulnerability scanner • Can extend scanning logics with

    Wasm, powered by wazero
  66. • dapr: portable, serverless application platform • Can add HTTP

    middleware in Wasm powered by wazero
  67. • Running a Wasm-compiled SQLite inside Go, without CGO •

    Possible implementation of CGO-less and sandboxed SQL Driver.
  68. • 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!
  69. 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
  70. 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
  71. 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
  72. None
  73. wazero deep dive…

  74. Q. How correct is the implementation?

  75. Q. How correct is the implementation? A. 100% compatible with

    Wasm spec (1.0&2.0)
  76. Q. How is wazero tested?

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

    binary fuzzing
  78. Q. How is the VM implemented?

  79. Q. How is the VM implemented? A. Two modes: interpreter

    and AOT compiler
  80. Interpreter mode • Runs on any platform (GOOS/GOARCH) • Fast

    startup time • Slow execution wazero.NewRuntimeConfigInterpreter()
  81. 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()
  82. 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
  83. 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
  84. 1. Machine code generation Assembler Code generation

  85. 2. Mmap machine code as executable

  86. 3. Jump into machine code

  87. 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
  88. 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
  89. 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
  90. 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
  91. 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!
  92. Twitter,GitHub: @mathetake Gopher Slack: #wazero Thank you!