WASM; Write Ruby Script and Run on Browsers

In this blog post, I'll give you a very quick introduction to write your own Ruby scripts and run on browsers using WASM.

Demo

At first, please have a look at this Demo page.

Once you click the button, you'll get a number randomly from 1 to 6. This is a very quick simulation of rolling a dice 🎲.

Ruby script

The Ruby script is faily simple, to make this demo as simple as possible. However, I'd like to test some features like class and calling functions, so this example is not a simple "Hello, world" either.

class Dice
    def initialize(items)
        @items = items
    end

    def result
        @items.sample
    end
end

def sample()
    items = %w(1 2 3 4 5 6)
    dice = Dice.new(items)
    dice.result
end

# the evaluated last line will be returned
sample

How to execute the Ruby script

Here is a fun part.

WASM ports of Ruby (in this example, CRuby) is achieved by github.com/ruby/ruby.wasm. This enables running Ruby applications on any environments that is WASI compatible WebAssembly runtimes. That sounds amazing.

In order to run it on browsers, ruby-head-wasm-wasi NPM package is distributed.

At first, you'll need a default Ruby VM. You can get it also from one of distributed package on the CDN as follows.

<head>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser.umd.js"></script>
</head>

<script>
    const { DefaultRubyVM } = window["ruby-wasm-wasi"];
</script>

Next, you need to fetch and initialize a VM instance using ruby.wasm. In order to fetch ruby.wasm, await fetch() is used so the main function should be defined as an async function.

const main = async () => {
    // Download WebAssembly port of CRupy with WASI.
    // https://www.npmjs.com/package/ruby-head-wasm-wasi
    // original: https://github.com/ruby/ruby.wasm
    const response = await fetch("https://cdn.jsdelivr.net/npm/[email protected]/dist/ruby.wasm")

    // initialize a VM instance
    const buffer = await response.arrayBuffer()
    const module = await WebAssembly.compile(buffer)
    const { vm } = await DefaultRubyVM(module)
}

Let's debug that it's working as expected. Print the current VM version. This should output the version string and see if that's what you're expecting at.

// debug
vm.printVersion()
ruby 3.2.0dev (2022-04-09) [wasm32-wasi]

Now the most exciting part. Write your own Ruby script in string, and evaluate that with vm.eval() as follows.

const sourceCode = `
    class Dice
        def initialize(items)
            @items = items
        end

        def result
            @items.sample
        end
    end

    def sample()
        items = %w(1 2 3 4 5 6)
        dice = Dice.new(items)
        dice.result
    end

    # the evaluated last line will be returned
    sample
`

// eval the source code and get the result
const result = vm.eval(sourceCode)

Now the script is evaluated and the return is passed to result const variable. You can do whatever you want with this, for example, writing to the console with console.log(result.toString()). In this demo, the result string is inserted into the div element.

// append the result and give back the feedback to users
const resultDiv = document.getElementById('result')
resultDiv.innerText = result.toString()

Conclusition

This is really amazing that you can run most of Ruby scripts on WASI-compatible runtimes, meaning most of modern browsers. Can't wait to see what we can achieve by this technology.

2022-10-22