$30 off During Our Annual Pro Sale. View Details »

Ruby meets WebAssembly

Yuta Saito
September 08, 2022

Ruby meets WebAssembly

Yuta Saito

September 08, 2022
Tweet

More Decks by Yuta Saito

Other Decks in Technology

Transcript

  1. Ruby meets WebAssembly
    Yuta Saito / @kateinoigakukun
    1

    View Slide

  2. About this talk
    1. Introduction to Ruby on WebAssembly
    1. Motivation
    2. Live demo: “on-browser” usage
    3. About WebAssembly and WASI
    4. Live demo: “non-web” usage
    2. Technical details: How to port the Ruby Interpreter
    1. Explore missing pieces for porting: Exceptions, Fiber, Conservative GC
    2. A magic technique Asyncify
    3. FAQ & Recap
    2

    View Slide

  3. About me
    • Yuta Saito / @kateinoigakukun
    • Waseda University B4
    • Working at
    • A newbie CRuby / Swift / LLVM committer
    • Porting languages to WebAssembly
    3

    View Slide

  4. Introduction to Ruby on WebAssembly
    Motivation
    On-browser Demo
    Motivation non-Web Demo
    About

    Wasm and WASI

    View Slide

  5. What's Good about Ruby
    • Designed to make programmers happy
    • Fast to write a program
    • Well-developed Gem Ecosystem
    5

    View Slide

  6. What's Dif
    fi
    cult in Ruby?
    1. Some platforms can’t run Ruby easily
    • Some restricted platforms can’t install Ruby interpreter (e.g. Web browser, Mobile device)
    • Hard to run your Ruby program on user machine
    • Run on server? Need to maintain server?
    2. Installation battle 💥
    • Making the
    fi
    rst step easy is important for beginners
    • How many times have you seen BUILD FAILED ?
    6

    View Slide

  7. 🤔 How can we solve them?
    7

    View Slide

  8. Ruby 🤝 WebAssembly
    8

    View Slide

  9. WebAssembly is a game changer
    • A binary instruction format for a stack-based machine
    • Designed to be
    • Portable
    • Language agnostic
    • Size- and Load-time ef
    fi
    cient
    • Secure by Sandbox
    • and more…
    9

    View Slide

  10. What WebAssembly solves
    1. Some platforms can’t run Ruby easily
    → ✅ Now browser is everywhere 🌐
    2. Installation battle 💥
    → ✅ Beginners can try Ruby on browser without installation

    View Slide

  11. Execution Flow of WebAssembly
    C / C++
    Swift
    Rust
    Go

    .wasm Web browsers
    11

    View Slide

  12. Ruby on WebAssembly
    Ruby Interpreter (CRuby)
    ruby.wasm
    app.rb
    Web browsers
    12

    View Slide

  13. Introduction to Ruby on WebAssembly
    Live demo: On browser Ruby
    About

    Wasm and WASI
    On-browser Demo
    Motivation non-Web Demo

    View Slide

  14. github.com/ruby/ruby.wasm

    packages/npm-packages/ruby-wasm-wasi/example/README.md

    View Slide

  15. https://irb-wasm.vercel.app

    View Slide

  16. Introduction to Ruby on WebAssembly
    About WebAssembly and WASI
    About

    Wasm and WASI
    On-browser Demo
    Motivation non-Web Demo

    View Slide

  17. https://emscripten.org/
    Musl libc
    CRuby
    .wasm
    How WebAssembly works
    • Vanilla WebAssembly has:
    • No
    fi
    le system
    • No system clock
    • No networks
    • …
    • Time.now returns … what?
    ?
    17

    View Slide

  18. Musl libc
    CRuby
    JS Glue code
    .wasm
    .js
    Web API
    syscall emulation
    How WebAssembly works
    MemFS …
    JS provides functions to the WebAssembly
    Time.now
    clock_gettime
    Date.now()
    18

    View Slide

  19. • Secure by sandbox, architecture portability, many language support
    are useful for other area
    • Serverless platforms
    • Plugin systems
    • and more…
    • WebAssembly is not always on JavaScript
    WebAssembly is not only for Web
    19

    View Slide

  20. Overview of WASI
    User Application
    .wasm
    WASI interface
    Host Application Web Poly
    fi
    ll
    Bare Metal Serverless platform
    Musl libc

    WASI (WebAssembly System Interface)
    WASI standardize platform independent system call interface
    20

    View Slide

  21. WASI compatible things
    Platforms
    • Node.js / Deno / Wasmtime
    • Fastly Compute@Edge
    • Cloud
    fl
    are Workers
    • VSCode Extensions
    • Fluent Bit
    • Etc…
    Languages
    • C / C++
    • Rust
    • Swift
    • Ruby (NEW)
    • Etc…
    21

    View Slide

  22. • CRuby program itself is now
    portable everywhere!
    • However
    • Need to distribute .rb
    fi
    les
    also
    • Most Wasm integrated tools
    requires “One Binary”
    WASI + VFS = Portable Ruby App
    WASI interface
    WASI Implementation
    app.rb lib.rb
    Host
    User Application
    Musl libc
    .wasm
    22

    View Slide

  23. WASI + VFS = Portable Ruby App
    User Application
    .wasm
    WASI interface
    WASI Implementation
    Musl libc
    Host
    wasi-vfs
    app.rb lib.rb
    • wasi-vfs
    • A VFS layer backed by WASI
    • Read-only in-memory FS
    23

    View Slide

  24. Introduction to Ruby on WebAssembly
    Live demo: Serverless
    About

    Wasm and WASI
    On-browser Demo
    Motivation non-Web Demo
    github.com/kateinoigakukun/ruby-compute-runtime

    View Slide

  25. https://ruby-compute-runtime-
    demo.edgecompute.app/

    View Slide

  26. Ruby 3.2 will support
    WebAssembly and WASI

    View Slide

  27. What’s new around WebAssembly in Ruby 3.2
    • Support WebAssembly / WASI target
    • ruby/ruby.wasm provides npm packages and pre-compiled rubies
    • Let’s
    fi
    nd interesting use cases!
    27

    View Slide

  28. How to Port CRuby to 

    WebAssembly with WASI
    28
    Technical details
    Introduction FAQ & Recap

    View Slide

  29. Port CRuby to WebAssembly with WASI
    Me: OK, we already have C to WebAssembly compiler,

    so it’s easy to port!
    29

    View Slide

  30. Port CRuby to WebAssembly with WASI
    CRuby has many internal dragons…
    • 🐲 Exceptions
    • Heavily depend on setjmp/longjmp, which cannot be implemented on WebAssembly itself
    • 🐲 Fiber
    • WebAssembly itself doesn’t support context-switching
    • 🐲 Conservative GC
    • Need to inspect WebAssembly VM
    30

    View Slide

  31. Technical details
    Exception implementation
    🐲 Conservative GC
    🐲 Fiber
    🐲 Exception ⚔ Asyncify

    View Slide

  32. raise
    rb_raise_jump
    ...
    #define EC_EXEC_TAG() \


    (ruby_setjmp(_tag.buf) ? rb_ec_tag_state(VAR_FROM_MEMORY(_ec)) : (EC_REPUSH_TAG(), 0))


    VALUE


    vm_exec(rb_execution_context_t *ec, bool mjit_enable_p)


    {


    enum ruby_tag_type state;


    VALUE result = Qundef;


    VALUE initial = 0;


    EC_PUSH_TAG(ec);


    _tag.retval = Qnil;


    if ((state = EC_EXEC_TAG()) == TAG_NONE) {


    if (!mjit_enable_p || (result = mjit_exec(ec)) == Qundef) {


    result = vm_exec_core(ec, initial);


    }


    goto vm_loop_start; /* fallback to the VM */


    }


    else {


    result = ec->errinfo;


    rb_ec_raised_reset(ec, RAISED_STACKOVERFLOW | RAISED_NOMEMORY);


    while ((result = vm_exec_handle_exception(ec, state, result, &initial)) == Qundef) {


    /* caught a jump, exec the handler */


    result = vm_exec_core(ec, initial);


    vm_loop_start:


    VM_ASSERT(ec->tag == &_tag);


    /* when caught `throw`, `tag.state` is set. */


    if ((state = _tag.state) == TAG_NONE) break;


    _tag.state = TAG_NONE;


    }


    }


    EC_POP_TAG();


    return result;


    }


    CRuby implements exceptions by setjmp/longjmp
    static void


    rb_raise_jump(VALUE mesg, VALUE cause)


    {


    rb_execution_context_t *ec = GET_EC();


    ...


    rb_vm_pop_frame(ec);


    ...


    rb_longjmp(ec, TAG_RAISE, mesg, cause);


    }
    raise “You got an error”
    setjmp saves the current program state
    longjmp restores the saved program state
    #define EC_EXEC_TAG() \


    (ruby_setjmp(_tag.buf) ? rb_ec_tag_state(VAR_FROM_MEMORY(_ec)) : (EC_REPUSH_TAG(), 0))


    if ((state = EC_EXEC_TAG()) == TAG_NONE) {


    if (!mjit_enable_p || (result = mjit_exec(ec)) == Qundef) {


    result = vm_exec_core(ec, initial);


    }


    goto vm_loop_start; /* fallback to the VM */


    }


    #define EC_EXEC_TAG() \


    View Slide

  33. setjmp/longjmp on x86_64
    musl-libc src/setjmp/x86_64/setjmp.s src/setjmp/x86_64/longjmp.s
    /* Copyright 2011-2012 Nicholas J. Kain, licensed under standard MIT license */


    setjmp:


    mov %rbx,(%rdi) /* rdi is jmp_buf, move registers onto it */


    mov %rbp,8(%rdi)


    mov %r12,16(%rdi)


    mov %r13,24(%rdi)


    mov %r14,32(%rdi)


    mov %r15,40(%rdi)


    lea 8(%rsp),%rdx /* this is our rsp WITHOUT current ret addr */


    mov %rdx,48(%rdi)


    mov (%rsp),%rdx /* save return addr ptr for new rip */


    mov %rdx,56(%rdi)


    xor %eax,%eax /* always return 0 */


    ret


    longjmp:


    xor %eax,%eax


    cmp $1,%esi /* CF = val ? 0 : 1 */


    adc %esi,%eax /* eax = val + !val */


    mov (%rdi),%rbx /* rdi is the jmp_buf, restore regs from it */


    mov 8(%rdi),%rbp


    mov 16(%rdi),%r12


    mov 24(%rdi),%r13


    mov 32(%rdi),%r14


    mov 40(%rdi),%r15


    mov 48(%rdi),%rsp


    jmp *56(%rdi) /* goto saved address without altering rsp */
    longjmp:


    jmp *56(%rdi) /* goto saved address without altering rsp */
    /* Copyright 2011-2012 Nicholas J. Kain, licensed under standard MIT license */


    setjmp:


    mov (%rsp),%rdx /* save return addr ptr for new rip */


    Save and Restore
    • Machine stack position
    • Machine registers
    • Program counter

    (Return address)
    33

    View Slide

  34. WebAssembly Execution Model
    WebAssembly VM
    Code
    Functions[0]
    Functions[1]
    Functions[2]
    Functions[3]
    Push
    (Call)
    Pop
    (Return)
    Protected Stack
    Call Frame
    i32(0x42)
    i64(0xffff0a)
    Call Frame
    Return Address
    Return Address
    🙅 🙅
    Can’t jump!
    Can’t read/write!
    Control-
    fl
    ow is protected by
    WebAssembly VM
    Only goto/call/return are allowed
    34

    View Slide

  35. Missing pieces for porting
    1. Save the current execution state
    2. Unwind to the saved execution state
    35

    View Slide

  36. Technical details
    Fiber Implementation
    🐲 Conservative GC
    🐲 Fiber
    🐲 Exception ⚔ Asyncify

    View Slide

  37. Fiber in CRuby
    Main Fiber
    fi
    b
    Main Fiber
    fi
    b
    fib.resume
    Fiber.yield
    • Semi-coroutine
    • Suspend/Resume programs
    fib = Fiber.new do


    Fiber.yield x = 0


    Fiber.yield y = 1


    loop do


    x, y = y, x + y


    Fiber.yield y


    end


    end


    3.times { puts fib.resume }
    fib.resume
    Fiber.yield
    ...
    37

    View Slide

  38. Fiber context-switch
    • Fiber#resume / Fiber.yield /
    Fiber#transfer switches “context”
    • Context
    • Ruby VM stack
    • Machine stack
    • Machine registers
    • Program counter
    Fiber 1
    Fiber 2
    Context
    Context
    Current
    Context
    Program
    Architecture Speci
    fi
    c
    38

    View Slide

  39. Missing pieces for porting
    1. Save the current execution state
    2. Unwind to the saved execution state

    → Restore an execution state from arbitrary execution state

    (not limited to being within the call stack)
    39

    View Slide

  40. Technical details
    Conservative GC
    Implementation
    🐲 Conservative GC
    🐲 Fiber
    🐲 Exception ⚔ Asyncify

    View Slide

  41. Conservative GC in CRuby
    • Scan data space to
    fi
    nd object-like values
    • CRuby scans:
    • Machine Registers
    • Machine Stack
    Registers
    VALUE VALUE
    non-
    VALUE
    Machine Stack
    VALUE
    non-VALUE
    VALUE
    VALUE
    non-VALUE
    (?)
    41

    View Slide

  42. WebAssembly Execution Model
    WebAssembly VM
    Code
    Functions[0]
    Functions[1]
    Functions[2]
    Functions[3]
    Push
    (Call)
    Pop
    (Return)
    Protected Stack
    (Current) Call Frame
    Write
    Read
    Linear Memory
    C Stack
    Data
    Heap
    Can’t read!
    Space for putting

    address referenced

    local values
    Call Frame
    Return Address
    Locals[1]
    Locals[2]
    Locals[0]
    Return Address
    Value Stack
    Value Stack
    i32(0x42)
    i64(0xffff0a)
    i32(0x42)
    🙅
    Can’t scan!
    🙅
    OK!
    🙆
    Machine Stack = C Stack + Value Stack
    Machine Register = Locals
    42

    View Slide

  43. Missing pieces for porting
    1. Save the current execution state
    2. Restore an execution state from arbitrary execution state

    (not limited to being within the call stack)
    3. Inspect the Locals and Value Stack of all call frames
    43

    View Slide

  44. Technical details
    Asyncify
    🐲 Conservative GC
    🐲 Fiber
    🐲 Exception Asyncify

    View Slide

  45. Asyncify
    fi
    lls the missing pieces 🧙
    • Provides low-level support for pausing and resuming programs
    • Designed to call async JS function from sync C code by Alon Zakai
    • Works by instrumenting .wasm binaries
    • wasm-opt input.wasm --asyncify -o output.wasm
    45

    View Slide

  46. Asyncify Example
    sleep
    Rewind
    sleep
    Call stack
    main main
    foo
    Unwind
    • Unwind: Serialize execution state

    and return the control to root
    • Rewind: Call entrypoint function again

    and rebuild call stack
    void sleep(void) {


    static bool is_sleeping = false;


    if (!is_sleeping) {


    is_sleeping = true;


    asyncify_start_unwind();


    } else {


    is_sleeping = false;


    asyncify_stop_rewind();


    }


    }


    void foo(void) {


    puts("before");


    sleep();


    puts("after");


    }


    int main(void) {


    foo();


    asyncify_stop_unwind();


    puts("sleeping");


    asyncify_start_rewind();


    foo();


    foo
    foo
    46

    View Slide

  47. Asyncify Example
    void sleep(void) {


    int main(void) {


    foo();


    foo foo
    47

    View Slide

  48. void sleep(void) {


    puts("before");




    }


    foo foo
    48

    View Slide

  49. void sleep(void) {


    static bool is_sleeping = false;


    if (!is_sleeping) {


    is_sleeping = true;


    asyncify_start_unwind();


    }
    }


    sleep();


    foo
    Asyncify Example
    49

    View Slide

  50. void sleep(void) {


    sleep();



    }


    foo
    Asyncify Example
    50

    View Slide

  51. void sleep(void) {


    puts("before");


    sleep();


    puts("after"); // skipped


    }


    int main(void) {


    foo();


    asyncify_stop_unwind();


    foo
    Asyncify Example
    51

    View Slide

  52. void sleep(void) {


    puts("sleeping");


    asyncify_start_rewind();


    foo();


    foo
    Asyncify Example
    52

    View Slide

  53. void sleep(void) {


    asyncify_start_rewind();


    foo();


    foo
    Asyncify Example
    53

    View Slide

  54. void sleep(void) {


    static bool is_sleeping = false;


    if (!is_sleeping) {


    } else {


    is_sleeping = false;


    asyncify_stop_rewind();


    }


    }


    void foo(void) {


    // skipped


    sleep();



    }


    foo
    Asyncify Example
    54

    View Slide

  55. void sleep(void) {



    puts("after");


    }


    Asyncify Example
    55

    View Slide

  56. How Asyncify instruments program
    1. Spill out Wasm stack values into Wasm registers
    2. Instrument control
    fl
    ow, around calls and adding skips for rewinding
    3. Instrument Wasm registers saving/restoring.
    56

    View Slide

  57. Asyncify: Instrument program
    static VALUE


    sym_length(VALUE sym)


    {


    return rb_str_length(rb_sym2str(sym));


    }
    C code Wasm stack machine code
    (local.get $sym)


    (call $rb_sym2str)


    (call $rb_str_length)


    (return)
    57

    View Slide

  58. 1. Spill out Wasm stack values into Wasm registers
    Guarantee that each statement doesn’t pop previous results
    static VALUE


    sym_length(VALUE sym)


    {


    register VALUE v1 = rb_sym2str(sym);


    register VALUE v2 = rb_str_length(v1);


    return v2;


    }
    (local.get $sym)


    (call $rb_sym2str)


    (local.set $v1)


    (local.get $v1)


    (call $rb_str_length)


    (local.set $v2)


    (local.get $v2)


    (return)
    C code Wasm stack machine code
    58

    View Slide

  59. 2. Instrument control
    fl
    ow, around calls

    and adding skips for rewinding
    static VALUE


    sym_length(VALUE sym)


    {


    register VALUE v1;


    register VALUE v2;


    if (__asyncify_state == REWINDING) {


    __asyncify_get_call_index();


    }


    if (__asyncify_state == NORMAL || __asyncify_check_call_index(0)) {


    v1 = rb_sym2str(sym);


    if (__asyncify_state == UNWINDING) {


    __asyncify_unwind(0);


    }


    }


    if (__asyncify_state == NORMAL || __asyncify_check_call_index(1)) {


    v2 = rb_str_length(v1);


    if (__asyncify_state == UNWINDING) {


    __asyncify_unwind(1);


    }


    }


    return v2;


    }
    59

    View Slide

  60. static VALUE


    sym_length(VALUE sym)


    {


    register int __asyncify_call_index;


    register VALUE v1;


    register VALUE v2;


    if (__asyncify_state == REWINDING) {


    __asyncify_call_index = *(__asyncify_data--);


    v1 = *(__asyncify_data--);


    v2 = *(__asyncify_data--);


    }


    if (__asyncify_state == NORMAL || __asyncify_check_call_index(0)) {


    v1 = rb_sym2str(sym);


    if (__asyncify_state == UNWINDING) {


    goto __asyncify_unwind(0);


    }


    }


    if (__asyncify_state == NORMAL || __asyncify_check_call_index(1)) {


    v2 = rb_str_length(v1);


    if (__asyncify_state == UNWINDING) {


    goto __asyncify_unwind(1);


    }


    }


    return v2;


    __asyncify_unwind(int call_index):


    *(__asyncify_data++) = v2;


    *(__asyncify_data++) = v1;


    *(__asyncify_data++) = call_index;


    return 0;


    }
    3. Instrument Wasm registers saving/restoring.
    60

    View Slide

  61. Missing pieces for porting
    1. Save the current execution state
    → ✅ Asyncify serializes the execution state into memory space
    2. Restore an execution state from arbitrary execution state

    (not limited to being within the call stack)
    → ✅ Asyncify deserializes the execution state from memory space
    3. Inspect the Locals and Value Stack of all call frames
    → ✅ Asyncify spills Value Stack to Locals, 

    and Locals are serialized to execution state
    61

    View Slide

  62. CRuby now runs on
    WebAssembly 👏
    62

    View Slide

  63. FQA & Recap
    63
    FAQ & Recap
    Technical details
    Introduction

    View Slide

  64. FAQ
    • No Thread related API
    • Wasm and WASI don’t have thread spawning API yet.
    • Throw NotImplementedError
    • C Extension library must be linked statically
    • Dynamic-linking ABI is not yet well-supported
    What are the limitations of Ruby on WebAssembly?
    64

    View Slide

  65. FAQ
    How large is the .wasm binary?
    raw gzip Brotli
    minimal
    No standard extensions
    8.1M 2.9M 1.7M
    full
    All standard extensions
    (like json, yaml,
    or stringio)
    10M 3.4M 2.0M
    full+stdlib
    With stdlib .rb
    fi
    les
    25M 7.3M 5.0M
    65

    View Slide

  66. FAQ
    How fast is CRuby on WebAssembly?
    • Near mruby speed
    • Asyncify adds much overheads
    • Environment
    • Node.js v17.9.0
    • Ubuntu 20.04
    • AMD Ryzen 9 5900HX
    Optcarrot FPS (bigger is better)
    master (native)
    mruby
    master (wasm32-wasi)
    Opal
    0 15 30 45 60
    1.51
    18
    21.6
    54.6
    66

    View Slide

  67. Acknowledgements
    • @mame, @ko1, and other Ruby committers gave me a lot of advice
    • Ruby Association supported the project as a grant project
    • And thanks for all contributors and users
    67

    View Slide

  68. Recap
    • Ruby 3.2 will support WebAssembly and WASI
    • ruby.wasm provides npm packages and pre-compiled rubies
    • Try your favorite gems on https://irb-wasm.vercel.app/
    • Welcome your feedback!
    68

    View Slide

  69. FAQ
    WebAssembly is designed for the mixed goals of them
    Why not use JVM / .NET CLI / NaCL / eBPF / Lua … ?
    Wasm design goal Similar with
    Portability JVM / .NET CLI
    Language independency .NET CLI
    Secure sandbox NaCL, eBPF
    Embeddable runtime Lua
    69

    View Slide