-- Views
April 27, 26
スライド概要
RubyKagi 2026 の発表資料です。
これまでruby.wasmでやって来たブラウザ上でRubyからJavaScriptを呼ぶための実装の紹介と、これからJavaScriptからRubyを呼ぶためにしていく実装の作戦を発表しました。
ruby.wasm can also enable JavaScript to call Ruby libraries. 2026/04/23 RubyKaigi 2026
Self Introduction Luxiar co., Ltd. Shigeru Nakajima, Chief Architect ledsun on GitHub and Twitter ko.rb organizer 5/11 meetup in Tokyo 株式会社ラグザイアはRubyKaigi 2026のシルバースポンサーです。 1/48
Past RubyKaigi Presentations Year Title 2023 Load Gems from the Browser 2024 Using Ruby in the Browser Is Wonderful 2025 dRuby on Browser Again! I was blown away by kateinoigakukun-san’s 2022 Keynote titled “Ruby meets WebAssembly,” which reminded me of how much I’d wanted to write browser 2/48 applications in Ruby.
RubyKaigi 2023: Load Gems from the Browser The concept of require_relative Implementing require_relative on top of the browser’s fetch API made it possible to load Ruby applications composed of multiple files in the browser. 3/48
RubyKaigi 2024: Using Ruby in the Browser Is Wonderful Implemented require_relative Example implementation Ruby applications running in the browser Frontend frameworks built with Ruby I named that framework “Orbital ring”. 4/48
RubyKaigi 2025: dRuby on Browser Again! Implemented Ruby’s TCP-compatible sockets using the browser’s WebSocket API Called Ruby objects on a dRuby server from a browser-based dRuby client Presented different dRuby implementations with youchan-san 5/48
Summary of Previous Presentations Ruby applications running in the browser Focused on “Ruby in the Browser” Improved Ruby-to-JavaScript integration 6/48
Table of Contents Prologue: Improving Ruby-to-JavaScript Calls 1: Why ruby.wasm Isn’t Used in Production 2: What if JavaScript-to-Ruby was possible? 3: Project Ruby’s Blanket 4: Practice Conclusion 7/48
Prologue: Improving Ruby-to-JavaScript Calls 8/48
Added to_js method I added: Methods for Ruby classes in ruby.wasm Conversion from Ruby objects to JS::Object instances [1, 2, 3].to_js { a: 1, b: "two" }.to_js nil.to_js 9/48
What is JS::Object? Ruby objects works in wasm memory, while JavaScript objects works in JavaScript memory. Ruby objects cannot directly reference JavaScript objects, and vice versa. In ruby.wasm, JS::Object is a wrapper class that allows Ruby to reference JavaScript objects. 10/48
Implemented JS::RequireRemote An internal class for require_relative require 'js/require_remote' module Kernel def require_relative(path) JS::RequireRemote.instance.load(path) end end 11/48
Changed the parent of JS::Object from
Object to BasicObject
Method calls on JS::Object use
method_missing
No longer conflicts with Object#send
ws = JS.global[:WebSocket].new("ws://localhost:9292")
ws[:onopen] = -> (event) { ws.send("Hello") }
12/48
Accessing properties as method calls results in a TypeError In ruby.wasm, to access JavaScript properties, you need to use the [] syntax instead of method calls. JS.global.location => TypeError JS.global[:location] => Correct Usage! 13/48
Conversion from new method to constructor JavaScript has a new operator to create instances of classes, while Ruby uses the new method. // JavaScript new URLSearchParams(location.search) # Ruby JS.global[:URLSearchParams].new(JS.global[:location][:search]) 14/48
Added JS::True/JS::False JavaScript’s false is not the same as Ruby’s false, so JavaScript boolean methods would always appear truthy in Ruby. searchParams = JS.global[:URLSearchParams].new(JS.global[:location][:search]) # before if searchParams.has('phrase') # => always true # after if searchParams.has('phrase') == JS::True # => true/false 15/48
Method Calls with a ? This feature was implemented by Kateinoigakukun-san. Expanded method calls If the method name ends with ?, it returns a Ruby boolean value searchParams = JS.global[:URLSearchParams].new(JS.global[:location][:search]) if searchParams.has?('q') # => true/false 16/48
Methods Can Accept Blocks The constructor of JavaScript’s Promise requires a function Initialize a Promise using Ruby block syntax // JavaScript new Promise((resolve) => resolve(42)); # Ruby JS.global[:Promise].new do |resolve, reject| resolve.call(42) end 17/48
Promise Scheduler It can await a Promise This is a unique feature of ruby.wasm. Neither Ruby nor JavaScript has a built-in way to wait for Promise completion like this. result = JS.global[:Promise].new do |resolve, reject| resolve.call(42) end.await # => 42 18/48
1: Why ruby.wasm Isn’t Used in Production 19/48
Calling JavaScript from Ruby Is Well Supported But ruby.wasm is not used in production yet. 20/48
Why Isn’t It Used in Production? The Wasm binary is large JavaScript-to-Ruby support is weak 21/48
The Wasm Binary Is Large https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/ruby+stdlib.wasm: Uncompressed file size: 33,343,825 bytes (31.80 MiB) Brotli compressed download size: 9,266,486 bytes (8.84 MiB) 22/48
Is the Wasm Binary Size a Problem? For BtoC browser applications, download size is important For BtoB, download size may not be a significant issue Stable network environment Effective browser caching Is this a critical reason? PicoRuby.Wasm may be a solution for reducing the binary size. 23/48
JavaScript-to-Ruby Support Is Weak We may need to take the reverse approach. Instead of calling JavaScript from Ruby, we should enable JavaScript to call Ruby. 24/48
Ruby-to-JavaScript vs JavaScript-to-Ruby Feature eval Method Call Property Access Object Conversion Load Ruby Scripts R→J Yes Yes Yes Yes browser J→R Yes call N/A string No 25/48
Currently, JavaScript calls Ruby using eval const script = ` class Dog def name()= 'john' end Dog.new ` const dog = vm.eval(script); dog.call("name").toString(); // => "John" 26/48
Currently, JavaScript calls Ruby using eval Currently, Ruby can be executed from JavaScript using eval. However, there is no proper API for working with the return values on the JavaScript side. Could this be one of the reasons it has not been adopted in production? 27/48
2: What if JavaScript-to-Ruby was possible? 28/48
What if JavaScript-to-Ruby was possible? For example: You could use a format converter written in Ruby in the browser You could run Rails’ money calculation logic in the browser 29/48
Using a format converter written in Ruby in the browser Convert text annotations between JSON and plain-text formats Text format is more LLM-friendly than JSON Wrote the converter in Ruby 30/48
Using a format converter written in Ruby in the browser Called it via the Rails API This introduced a frontend server dependency Rewrote it in JavaScript 31/48
Maintaining two separate libraries is tough https://rubygems.org/gems/ simple_inline_text_annotation https://www.npmjs.com/package/@pubann/ simple-inline-text-annotation 32/48
If you could call Ruby libraries from JavaScript using ruby.wasm In reality, the Wasm binary would increase the size of the distributed assets, so it is unclear whether this would be worth doing You could create a PoC before porting the library without creating a Rails API 33/48
Running Rails’ money calculation logic in the browser You could run Rails’ money calculation logic in the browser for previews Port it to JavaScript? 34/48
Supproting Edge Cases is Difficult JavaScript does not have a built-in decimal type Use a Decimal library Even if you verify compatibility with automated tests, can you cover edge case behavior? For BtoB applications, this might still be practical 35/48
3: Project Ruby’s Blanket 36/48
Implementing an interface to call Ruby from JavaScript The features for calling JavaScript from Ruby are implemented mainly in the JS::Object class in ruby.wasm. Method call Constructor Property access Object conversion 37/48
Implove RbValue To implement an interface for calling Ruby from JavaScript, we need to expand RbValue that wraps Ruby objects. I call this project as Ruby’s Blanket. 38/48
Ruby’s Blanket Ruby’s Blanket is named after a song by GLAY, a famous band from Hakodate, Japan. 39/48
Roadmap 1. Load Ruby scripts in Node.js 2. Method call 3. Convert JavaScript values to RbValue (arguments) 4. Convert RbValue to JavaScript values (return values) 40/48
Imagine vm.evalFile("dog.rb"); const dog = vm.eval("Dog.new"); dog.vow(); dog.vow(); const count = dog.count_of_vows.toInt(); How reference constants? Maybe window.ruby.Dog.new() ? 41/48
4: Practice 42/48
Before that, there were things to do Support for Ruby 4.0 (explicitly install rdoc) Upgrade magnus Follow Prism fixes in ruby head Unskip npm run test 43/48
Demo vm.eval(` require "js/require_local/relative_shim" JS::RequireLocal.instance.base_dir = "require_local" require_relative "./dog" `); const dog = vm.eval("Dog.new"); console.log(dog.call("vow").toString()); // woof console.log(dog.call("vow").toString()); // woof console.log(dog.call("count_of_vows").toString()); // 2 44/48
Unrecognized Change of Coding Agent main().catch((error) => { if (String(error) === "Symbol(kExitCode)") { return; } throw error; }); There is one more option that needs to be specified in Node.js 45/48
Thanks to the Coding Agent Thanks to the coding agent, we were able to dig deeper into debugging than before. As we dug deeper, it became harder to decide what to fix first. But by working through those difficult parts instead of skipping them, future development will become easier. 46/48
Conclusion I’ve only just started working on this, but I’ll be setting things up so you can call Ruby from JavaScript, so please stay tuned! 47/48
bye! Let's hang out at ko.rb 48/48