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

    View full-size slide

  2. Foreign Function Interface(FFI)

    View full-size slide

  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

    View full-size slide

  4. func main () {
    }
    main.go

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. When/Why do we want FFI?

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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") }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  32. 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

    View full-size slide

  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

    View full-size slide

  34. We need sandox…

    View full-size slide

  35. 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

    View full-size slide

  36. WebAssembly (Wasm)

    View full-size slide

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

    View full-size slide

  38. Wait, isn’t WebAssembly for the web?

    View full-size slide

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

    View full-size slide

  40. Non-Web examples

    View full-size slide

  41. 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

    View full-size slide

  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
    ?

    View full-size slide

  43. How to run Wasm binary inside Go?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. 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!

    View full-size slide

  47. 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
    ?

    View full-size slide

  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
    wazero

    View full-size slide

  49. => CGO-less Foreign Function Interface
    wazero

    View full-size slide

  50. 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

    View full-size slide

  51. How it works: memory isotation

    View full-size slide

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

    View full-size slide

  53. 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)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

  56. How it works: system call isolation

    View full-size slide

  57. 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)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

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

    View full-size slide

  62. // 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, ...)

    View full-size slide

  63. Example projects!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  67. ● 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!

    View full-size slide

  68. 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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  71. wazero deep dive…

    View full-size slide

  72. Q. How correct is the implementation?

    View full-size slide

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

    View full-size slide

  74. Q. How is wazero tested?

    View full-size slide

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

    View full-size slide

  76. Q. How is the VM implemented?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  79. 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()

    View full-size slide

  80. 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

    View full-size slide

  81. 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

    View full-size slide

  82. 1. Machine code generation
    Assembler Code generation

    View full-size slide

  83. 2. Mmap machine code as executable

    View full-size slide

  84. 3. Jump into machine code

    View full-size slide

  85. 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

    View full-size slide

  86. 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

    View full-size slide

  87. 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

    View full-size slide

  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
    ● Naive single pass compiler: optimizations are TODOs

    View full-size slide

  89. 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!

    View full-size slide

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

    View full-size slide