iTranslated by AI
Using await in ruby.wasm
I've been playing with ruby.wasm all the time lately.
On 2023/5/19, ruby.wasm 2.0 was released.
In ruby.wasm 1.0, await sometimes didn't work correctly, but it has started working properly in 2.0. To celebrate, I've summarized what I've done since my previous article.
await
There are two problems with using await in ruby.wasm.
- Ruby scripts need to be executed with
evalAsyncinstead ofeval. - The stack size is small, leading to frequent
SystemStackErrorerrors.
Ruby scripts need to be executed with evalAsync instead of eval
If you casually write a Ruby script in HTML using <script type="text/ruby">, using await results in an error. (In ruby.wasm 1.0, it didn't error but returned nil).
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
require 'js'
def start
p JS.global.fetch('hoge.html').await
#=> JS::Object#await can be called only from evalAsync (RuntimeError)
end
start
</script>
Changing the part that executes the Ruby script from eval to evalAsync would fix it, but it's tedious.
Actually, since evalAsync runs eval inside a Fiber, you just need to run the code you want to await inside a Fiber.
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
require 'js'
def start
p JS.global.fetch('hoge.html').await
#=> [objcet Response]
end
Fiber.new{start}.transfer
</script>
Easy!
To set event handlers for HTML elements, you use addEventListener, but you cannot use await directly inside the event handler.
<input id="b" type="button" value="button"></input>
<script type="text/ruby">
require 'js'
Document = JS.global[:document]
b = Document.getElementById('b')
b.addEventListener('click') do |e|
p JS.global.fetch('hoge.html').await
#=> in `await': JS::Object#await can be called only from evalAsync (RuntimeError)
end
</script>
In this case as well, you can use await by using a Fiber.
<input id="b" type="button" value="button"></input>
<script type="text/ruby">
require 'js'
Document = JS.global[:document]
b = Document.getElementById('b')
b.addEventListener('click') do |e|
Fiber.new do
p JS.global.fetch('hoge.html').await
#=> [objcet Response]
end.transfer
end
</script>
The stack size is small, leading to frequent SystemStackError errors
The Fiber stack size is small, so even a script like this results in a SystemStackError.
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
require 'js'
def start
pp [1] * 30
#=> Uncaught (in promise) Error: SystemStackError: stack level too deep
end
Fiber.new{start}.transfer
</script>
Since there's no way around this, you have to rebuild ruby.wasm. It's a hassle.
Modify the new WASI({}); part here to:
const wasi = new WASI({
env: { "RUBY_FIBER_MACHINE_STACK_SIZE": "1048576" }
});
and then rebuild.
However, since that's tedious, a simpler way is to forcibly replace the content within the pre-built file like this:
curl https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js |
sed -e 's/const wasi = new s.*/const wasi = new s({env:{"RUBY_FIBER_MACHINE_STACK_SIZE":"1048576"}});/' > browser.script.iife.js
Then you can use it like this:
<script src="browser.script.iife.js"></script>
<script type="text/ruby">
require 'js'
def start
pp [1] * 30
end
Fiber.new{start}.transfer
</script>
While writing this, I noticed that in the ruby.wasm main branch, the default has been changed to 16777216. It might work without any extra effort in the next version.
If you're okay with a daily build, using https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0-2023-05-21-a/dist/browser.script.iife.js worked fine.
Making it Ruby-like
I've made various other adjustments to allow writing in a more Ruby-like way.
Calling function names in snake_case
The JS library of ruby.wasm is a thin wrapper, so JavaScript function names like getElementById() are used as-is.
Since it doesn't feel very Ruby-like, I made it possible to call them in snake_case, such as get_element_by_id().
module JSrb
def method_missing(sym, *args, &block)
# Convert snake_case to camelCase
sym = sym.to_s.gsub(/_([a-z])/){$1.upcase}.intern
super(sym, *args, &block)
end
end
class JS::Object
prepend JSrb
end
Something like that.
Referencing properties in .prop_name format
To reference a property of JS::Object, you normally use something like [:propName], but I also want to be able to reference it as JS::Object.prop_name.
It seems like converting prop_name to propName and calling [:propName] should work.
module JSrb
def method_missing(sym, *args, &block)
sym = sym.to_s.gsub(/_([a-z])/){$1.upcase}.intern
v = self.method(:[]).super_method.call(sym.intern)
v = self.call(sym, *args, &block) if v.typeof == 'function'
v
end
end
Like this. If the property is a function, it calls it.
To set a property, you can call []= in a similar way if sym ends with =.
Converting primitive types to Ruby objects
Since JavaScript values appear as JS::Object from Ruby, they are somewhat difficult to use directly as-is.
You end up having to use JS::Object#to_i or JS::Object#to_s depending on the type of value.
To handle this, I'm checking JS::Object#typeof and converting it, albeit in a simple way.
case v.typeof
when 'number'
v.to_s =~ /\./ ? v.to_f : v.to_i
when 'bigint'
v.to_i
when 'string'
v.to_s
when 'boolean'
v.to_s == 'true'
else
if v.to_s =~ /\A\[object .*(List|Collection)\]\z/
v.length.times.map{|i| v[i]}
elsif v == JS::Null || v == JS::Undefined
nil
else
v
end
end
Objects with names like ...List or ...Collection are converted to an Array, and null or undefined are converted to nil.
This part is quite rough, so it might not work perfectly in some cases.
Summary
With this, a script that was previously written like this:
children = JS.global[:document].getElementById('hoge')[:children]
children[:length].to_i.times do |i|
p children[i][:tagName]
end
can now be written as follows:
JS.global.document.get_element_by_id('hoge').children.each do |c|
p c.tag_name
end
It looks much more like Ruby now.
The full code for making it Ruby-like is available at https://mysql-params.tmtms.net/lib/jsrb.rb. I will likely continue to make changes, but feel free to check it out for reference.
Discussion