Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

I’ve seen things you people wouldn’t believe…

Slide 3

Slide 3 text

Node.js running in browsers, powered by WebAssembly…

Slide 4

Slide 4 text

I watched bug reproductions, quick experiments, live documentation examples…

Slide 5

Slide 5 text

All those moments, available for JavaScript, but missing to Ruby…

Slide 6

Slide 6 text

like tears in rain…

Slide 7

Slide 7 text

But it’s not time to die. It’s time to change.

Slide 8

Slide 8 text

Assembling the Future: crafting the missing pieces of the Ruby on Wasm puzzle Svyatoslav Kryukov, Evil Martians EuRuKo, 2024

Slide 9

Slide 9 text

Early in the 21st Century…

Slide 10

Slide 10 text

the Ruby Association advanced programming evolution into the Wasm phase—a runtime virtually identical to native code —known as ruby.wasm

Slide 11

Slide 11 text

RunRuby.dev

Slide 12

Slide 12 text

Agenda Wasm & WASI What ruby.wasm is and why it has it’s limitations How to run ruby.wasm in the Browser How to overcome some of those limitations

Slide 13

Slide 13 text

WebAssembly (Wasm) A crimson nothingness began to compile. Runtime. What does it feel like to be part of the runtime? Runtime. > ...

Slide 14

Slide 14 text

WebAssembly (Wasm) Binary instruction format for a stack-based virtual machine Portable, Fast, Sandboxed Still in development

Slide 15

Slide 15 text

Wasm as LLVM Target

Slide 16

Slide 16 text

Wasm Runtimes Browsers Wasmtime Wasmer and more…

Slide 17

Slide 17 text

Wasm module https://wasdk.github.io/wasmcodeexplorer/

Slide 18

Slide 18 text

Wasm in the Browser let memory = null; const print = (ptr) => { const view = new Uint8Array(memory.buffer); console.log( new TextDecoder().decode(view.subarray(ptr, view.indexOf(0, ptr))) ); } WebAssembly.instantiateStreaming( fetch("hello.wasm"), {sys: {print}} ).then((mod) => { memory = mod.instance.exports.memory; mod.instance.exports.main(); });

Slide 19

Slide 19 text

WASI Feel that in your runtime. WASI. What does it feel like to be part of the sandbox? WASI. > ...

Slide 20

Slide 20 text

WASI Platform independent, secure, portable interface WASI defines a limited list of standard syscalls Still in development Other ABIs: Emscripten, GoJS

Slide 21

Slide 21 text

WASI Preview 1 Example: fd_write(fd: fd, iovs: ciovec_array) -> Result Write to a file descriptor. Note: This is similar to writev in POSIX. Like POSIX, any calls of write (and other functions to read or write) for a regular file by other threads in the WASI process should not be interleaved while write is executed.

Slide 22

Slide 22 text

WASI Architecture module-side: wasi-sdk injects WASI functions into the wasm module host-side: native implementation or shims

Slide 23

Slide 23 text

WASI Shims https://github.com/bjorn3/browser_wasi_shim fd_write(data: Uint8Array): { ret: number; nwritten: number } { if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten: 0 }; if (this.file_pos + BigInt(data.byteLength) > this.file.size) { const old = this.file.data; this.file.data = new Uint8Array( Number(this.file_pos + BigInt(data.byteLength)), ); this.file.data.set(old); } this.file.data.set(data, Number(this.file_pos)); this.file_pos += BigInt(data.byteLength); return { ret: 0, nwritten: data.byteLength }; }

Slide 24

Slide 24 text

WASI Shims https://github.com/bjorn3/browser_wasi_shim let args = ["bin", "arg1", "arg2"]; let env = ["FOO=bar"]; let fds = [stdin, stdout, stderr, new PreopenDirectory(".", [ ["main.rb", mainRb], ])]; let wasi = new WASI(args, env, fds); let wasm = await WebAssembly.compile(fetch("bin.wasm")); let inst = await WebAssembly.instantiateStreaming(wasm, { "wasi_snapshot_preview1": wasi.wasiImport, }); wasi.start(inst);

Slide 25

Slide 25 text

WASI Preview 2 wasi-io wasi-clocks wasi-random wasi-filesystem wasi-sockets wasi-cli wasi-http Shim: https://github.com/WebAssembly/WASI/tree/main/wasip2 WASI Preview 2 contains the following APIs:

Slide 26

Slide 26 text

I can fix that

Slide 27

Slide 27 text

What is ruby.wasm? CRuby compiled to WebAssembly Number of patches in the CRuby sourcecode GC, Fibers, Exceptions catch them all: #if defined(__wasm__) WASI: no threads, networking, dynamic linking, etc RubyKaigi 2022 talk by Yuta Saito

Slide 28

Slide 28 text

ruby.wasm & dependencies Buildtime: Packaging system (see EuRuKo 2024 talk by Yuta Saito) Runtime: dependencies installation via network

Slide 29

Slide 29 text

But how to do it without networking?

Slide 30

Slide 30 text

ruby.wasm & JS interop Export Ruby functions to JavaScript ( @ruby/wasm-wasi npm package) Import JavaScript functions into Ruby (gem js ) Bidirectional data conversion between Ruby and JavaScript (ABI via wit-bindgen )

Slide 31

Slide 31 text

ruby.wasm & JS interop window.MyValue = "🐐" const value = window.rubyVM.eval(` require "js" name = JS.global["window"]["MyValue"] "Hello #{name}" `); console.log(value.toString()) // => "Hello 🐐"

Slide 32

Slide 32 text

Playground implementation Within modules interlinked. Do you dream of JavaScript and Ruby coexisting? Interlinked. > ...

Slide 33

Slide 33 text

Playground implementation Run ruby.wasm in browser Share FS between JS & Ruby Install gems via Bundler Bonus: Spin up a server

Slide 34

Slide 34 text

Running ruby.wasm in the Browser require "js" puts RUBY_VERSION # (Printed to the Web browser console) JS.global[:document].write "Hello, world!"

Slide 35

Slide 35 text

Enhance 2:16 (browser.script.iife.js) import { DefaultRubyVM } from "@ruby/wasm-wasi/browser" const response = fetch(`<...>/ruby+stdlib.wasm`); const module = await WebAssembly.compileStreaming(response); const { vm } = await DefaultRubyVM(module, {}); vm.eval(` require "js" puts RUBY_VERSION JS.global[:document].write "Hello, world!" `)

Slide 36

Slide 36 text

Enhance 5:26 (DefaultRubyVM) import { WASI, File, OpenFile } from "@bjorn3/browser_wasi_shim"; import { RubyVM } from "@ruby/wasm-wasi"; // ... const wasi = new WASI(args, env, fds); const imports = { wasi_snapshot_preview1: wasi.wasiImport, }; const vm = new RubyVM(); vm.addToImports(imports); const instance = await WebAssembly.instantiate(module, imports); // ...

Slide 37

Slide 37 text

Playground implementation Run ruby.wasm in browser Share FS between JS & Ruby Install gems via Bundler Bonus: Spin up a server

Slide 38

Slide 38 text

FS sharing // ... const encoder = new TextEncoder("utf-8") const mainRb = new File(encoder.encode('puts "Hello"')) const fds = [ new OpenFile(new File([])), // stdin new OpenFile(new File([])), // stdout new OpenFile(new File([])), // stderr new PreopenDirectory("/", new Map( ["main.rb", mainRb], )), ];

Slide 39

Slide 39 text

FS sharing // ... vm.eval('puts File.read("/main.rb")') // -> prints the file content

Slide 40

Slide 40 text

FS sharing // ... const editor = CodeMirror.fromTextArea(<...>) editor.on("change", function (event) { encoder = new TextEncoder("utf-8") currentFile.data = encoder.encode(event.doc.getValue()); });

Slide 41

Slide 41 text

FS sharing // ... runButton.addEventListener("click", () => { vm.evalAsync('eval(File.read("/main.rb"))').then( (result) => (resultBlock.innerText += `=> ${result}\n`) ); });

Slide 42

Slide 42 text

Files main.rb Run Code => Hello EuRuKo 2024 str = "EuRuKo 2024" "Hello #{str}" 1 2 3

Slide 43

Slide 43 text

FS sharing FS is just a in-memory JavaScript map It’s possible to manipulate FS from both JS and Ruby

Slide 44

Slide 44 text

Playground implementation Run ruby.wasm in browser Share FS between JS & Ruby Install gems via Bundler Bonus: Spin up a server

Slide 45

Slide 45 text

Bundler support bundle install require "bundler" require "bundler/cli" require "bundler/cli/install" Bundler::CLI::Install.new({path: "./gems"}).run

Slide 46

Slide 46 text

Files main.rb Gemfile Run Code => cannot load such file -- socket :136:in `require' :136:in `require' /usr/local/lib/ruby/3.3.0/net/protocol.rb:22:i n `' :136:in `require' :136:in `require' /usr/local/lib/ruby/3.3.0/net/http.rb:23:in `< top (required)>' :136:in `require' :136:in `require' /usr/local/lib/ruby/3.3.0/bundler/vendored_net _http.rb:9:in `rescue in rescue in ' /usr/local/lib/ruby/3.3.0/bundler/vendored_net _http.rb:6:in `rescue in ' /usr/local/lib/ruby/3.3.0/bundler/vendored_net _http.rb:3:in `' /usr/local/lib/ruby/3.3.0/bundler/vendor/net-h ttp-persistent/lib/net/http/persistent.rb:1:in `require_relative' /usr/local/lib/ruby/3.3.0/bundler/vendor/net-h ttp-persistent/lib/net/http/persistent.rb:1:in `' /usr/local/lib/ruby/3.3.0/bundler/vendored_per sistent.rb:11:in `require_relative' /usr/local/lib/ruby/3.3.0/bundler/vendored_per sistent.rb:11:in `' /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:3 require "bundler" require "bundler/cli" require "bundler/cli/install" Bundler::CLI::Install.new({path: "./gems"}).run 1 2 3 4 5 6

Slide 47

Slide 47 text

ruby.wasm tool #1 Reimplement missing core classes enough `require': cannot load such file -- socket (LoadError)

Slide 48

Slide 48 text

class Socket def initialize(...) raise NotImplementedError, "Socket is not supported" end def self.do_not_reverse_lookup=(value) value end end class SocketError < StandardError end

Slide 49

Slide 49 text

socketRb = new File(new TextEncoder("utf-8").encode(`class Socket<...>`)) fs = [ //... new PreopenDirectory("/stubs", [ ["socket.rb", socketRb], ["io", new Directory([ ["wait.rb", new File([])] ])], // ... ]) ]

Slide 50

Slide 50 text

$LOAD_PATH.unshift "/stubs" require "bundler" require "bundler/cli" require "bundler/cli/install" Bundler::CLI::Install.new({path: "./gems"}).run

Slide 51

Slide 51 text

Files main.rb Gemfile Run Code => Invalid argument @ rb_file_flock - /gems/ru by/3.3.0/bundler.lock /usr/local/lib/ruby/3.3.0/bundler/process_lock .rb:10:in `flock' /usr/local/lib/ruby/3.3.0/bundler/process_lock .rb:10:in `block in lock' /usr/local/lib/ruby/3.3.0/bundler/process_lock .rb:9:in `open' /usr/local/lib/ruby/3.3.0/bundler/process_lock .rb:9:in `lock' /usr/local/lib/ruby/3.3.0/bundler/installer.rb :71:in `run' /usr/local/lib/ruby/3.3.0/bundler/installer.rb :23:in `install' /usr/local/lib/ruby/3.3.0/bundler/cli/install. rb:63:in `run' (eval at eval_async:3):7:in `' eval_async:3:in `eval' eval_async:3:in `' /bundle/gems/js-2.6.2/lib/js.rb:109:in `eval' /bundle/gems/js-2.6.2/lib/js.rb:109:in `block in __eval_async_rb' /bundle/gems/js-2.6.2/lib/js.rb:120:in `block in __async' $LOAD_PATH.unshift "/stubs" require "bundler" require "bundler/cli" require "bundler/cli/install" Bundler::CLI::Install.new({path: "./gems"}).run 1 2 3 4 5 6 7 8

Slide 52

Slide 52 text

ruby.wasm tool #2 Yes, monkey patch it Invalid argument @ rb_file_flock - /gems/ruby/3.3.0/bundler.lock /usr/local/lib/ruby/3.3.0/bundler/process_lock.rb:10:in `flock'

Slide 53

Slide 53 text

# flock is not implemented File.prepend(Module.new do def flock(*);end end) # shim marks all dirs/files as readonly File.singleton_class.prepend(Module.new do def writable?(*) = true end)

Slide 54

Slide 54 text

Files main.rb Gemfile Run Code => uninitialized constant OpenSSL::SSL::VERIFY _PEER /usr/local/lib/ruby/3.3.0/bundler/vendor/net-h ttp-persistent/lib/net/http/persistent.rb:516: in `initialize' /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:2 56:in `new' /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:2 56:in `connection' /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:1 02:in `initialize' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:262:in `new' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:262:in `block in remote_fetchers' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:260:in `to_h' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:260:in `remote_fetchers' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:267:in `fetchers' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:396:in `block in remote_specs' /usr/local/lib/ruby/3.3.0/bundler/index.rb:9:i n `build' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:395:in `remote_specs' /usr/local/lib/ruby/3.3.0/bundler/source/rubyg ems.rb:136:in `specs' /usr/local/lib/ruby/3.3.0/bundler/resolver.rb: 41:in `block in setup_solver' /usr/local/lib/ruby/3.3.0/bundler/resolver.rb: 246:in `all_versions_for' /usr/local/lib/ruby/3.3.0/bundler/resolver.rb: 54:in `block in setup solver' $LOAD_PATH.unshift "/stubs" require "bundler_patches" require "bundler" require "bundler/cli" require "bundler/cli/install" Bundler::CLI::Install.new({path: "./gems"}).run 1 2 3 4 5 6 7 8

Slide 55

Slide 55 text

ruby.wasm tool #3 Use JS to overcome limitations uninitialized constant OpenSSL::SSL::VERIFY_PEER /usr/local/lib/ruby/3.3.0/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb:516:in `initial /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:256:in `new' /usr/local/lib/ruby/3.3.0/bundler/fetcher.rb:256:in `connection'

Slide 56

Slide 56 text

def request(uri, req) js_response = JS.eval(<<~JS).await return fetch('#{uri}', { method: '#{req.method}', headers: #{req.to_hash.transform_values { _1.join(';') }.to_json}, body: #{req.body ? req.body.to_json : 'undefined'} }) .then(response => { return response.text().then(text => ( {status: response.status, headers: response.headers, text} )) } }) JS serialize_js_response(js_response) end

Slide 57

Slide 57 text

require "js_connection" require "rubygems" require "rubygems/commands/install_command" Gem::Request.prepend(Module.new do def perform_request(request) JS::Connection.new.request(Gem::Uri.redact(@uri).to_s, request) end end) Bundler::Fetcher.prepend(Module.new do def connection JS::Connection.new end end)

Slide 58

Slide 58 text

Files main.rb Gemfile Run Code Fetching gem metadata from https://rubygems.or g/ Resolving dependencies... Fetching uri-idna 0.2.2 Installing uri-idna 0.2.2 Bundle complete! 1 Gemfile dependency, 2 gems now installed. Bundled gems are installed into `./gems` => xn--gdkl8fhk5egc.jp $LOAD_PATH.unshift "/stubs" require "bundler_patches" require "bundler_http_patches" Bundler::CLI::Install.new({path: "./gems"}).run require "uri-idna" URI::IDNA.whatwg_to_ascii("ハロー・ワールド.jp") # Should return: # => xn--gdkl8fhk5egc.jp 1 2 3 4 5 6 7 8 9 10 11 12 13

Slide 59

Slide 59 text

ruby.wasm tool #4 Be creative CORS issues: CORS extension or a lambda-proxy Slow startup times: Resolve gems in cloud, use localStorage and counting…

Slide 60

Slide 60 text

Playground implementation Run ruby.wasm in browser Share FS between JS & Ruby Install gems via Bundler Bonus: Spin up a server

Slide 61

Slide 61 text

# app.rb require "sinatra/base" class App < Sinatra::Base get "/goat" do "I don't know how I goat here 🐐 Baaa-ack" end get "/" do "Hello, take a look at
another page" end end # config.ru require_relative 'app' run App

Slide 62

Slide 62 text

Enabling web server

Slide 63

Slide 63 text

Prepare data for Rack in JS const headers = { method: request.method }; request.headers.entries().forEach(([key, value]) => { headers[`HTTP_${key.toUpperCase().replaceAll("-", "_")}`] = value; }) const command = ` request = Rack::MockRequest.env_for( "${request.url}", JSON.parse(%q[${JSON.stringify(headers)}]) ) # ... `;

Slide 64

Slide 64 text

request = Rack::MockRequest.env_for(<...>) app = Rack::Builder.load_file('config.ru') response = Rack::Response[*app.call(request)] status, headers, body_chunks = *response.finish body = body_chunks.reduce(:+) { status:, headers:, body: }

Slide 65

Slide 65 text

Making requests in the browser ServiceWorker addEventListener("fetch", ...) WebRTC sinclairzx81/smoke Custom networking layer

Slide 66

Slide 66 text

ServiceWorker // sw.js self.addEventListener("fetch", (event) => { event.respondWith((async () => { if (!self.RubyVM) { await installRubyWasm(); } command = prepareCommand(event.request); const res = await self.RubyVM.evalAsync(command) const { code, headers, body } = res.toJS(); return new Response(body, { headers, status: code }); })()) });

Slide 67

Slide 67 text

iframe Hello, take a look at another page

Slide 68

Slide 68 text

Can we run Rails? Nokogiri, SQLite3, etc. Database adapter and more… Yes, but that requires even more hacks

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

Thank you!