Talks

Doing terrible things with ruby.wasm

Doing terrible things with ruby.wasm

by Matias Korhonen

The video titled "Doing Terrible Things with ruby.wasm" features Matias Korhonen at the Euruko 2023 conference. The presentation focuses on the use of WebAssembly (wasm) with Ruby, emphasizing its capabilities and challenges.

Key Points:
- Introduction to the Speaker: Matias Korhonen introduces himself as a senior developer at The Venue and co-founder of HelsigiRuby in Finland, which promotes Ruby development.
- Understanding WebAssembly (Wasm): WebAssembly is a low-level assembly-like language that enables near-native performance for languages like C, C++, C#, and Rust on the web. It consists of two file formats: a human-readable text representation and a binary format, .wasm.
- Examples of WebAssembly Applications: Practical applications of WebAssembly include:

- Webercity, a port of Audacity, functioning as an audio editor in the browser.
- ffmpeg for video encoding.
- Image processing tools using libraries like Photon.

- Ruby can also be executed in the browser via the ruby.wasm introduced in Ruby 3.2.0, which supports Wazzy-based WebAssembly.
- Running Ruby with WebAssembly: Demonstrates basic script setups that allow Ruby code to run in browsers and the interaction between Ruby and JavaScript.
- Limitations of Ruby with WebAssembly: Current limitations include the absence of threading and networking capabilities in the WebAssembly build of Ruby.
- Exploring Terrible Ideas: Matias shares his 'terrible ideas' involving:

- Using wasm2c from the WebAssembly Binary Toolkit to convert Ruby WebAssembly code back to C, illustrating inefficiencies in size and performance.
- Attempting to utilize Deno and Bun for executable creation from WebAssembly but facing compatibility issues.
- Finding success with Wasmtime, a runtime for WebAssembly that allows it to run outside the browser, though it results in large binaries and longer execution times.
- Performance Comparison: The performance of Ruby 3.2 versus its WebAssembly counterpart showcases a significant slow-down when executing algorithms, raising questions about the feasibility of practical applications of this approach.
- Conclusion: While Matias humorously concludes that his ideas are terrible and not suitable for production, he encourages thinking about the potential of WebAssembly with Ruby, hinting at further exploration in Rust or other efficient implementations.

00:00:10.400 Okay, so welcome to Doing Terrible Things with ruby.wasm.
00:00:16.800 First of all, who am I? I already introduced myself in the city pitches, but if you missed it, my name is Matias. I also answer to Matt. You can find me on all the usual places like Mastodon and on the Internet, where most people can be found. As for my day job, I'm a senior developer at a company called The Venue, where we handle bookings for event spaces and other event services. For example, if you’re organizing the next Euruko, you can use our service to find the venue. Perhaps you are organizing a Meetup, a wedding, or a birthday, or a company off-site. Basically, anything you want to book a space for, you can do that with us.
00:01:02.760 I'm also one of the co-founders of HelsigiRuby in Finland, which is a non-profit organization that promotes Ruby and other software development initiatives. You might recall us from last year's Euruko in Helsinki; this is the pitch that I gave earlier in the city pitches. This year, we are doing something different. The theme is 'Humanity,' as it’s a human-centered conference for software developers, and we have a keynote from Anna Dash. As mentioned, you can get a 20% discount on tickets, or you can just scan that QR code there, and that works too. But now, let's get back to the topic at hand: doing terrible things with ruby.wasm.
00:01:49.140 Let's start out with some basics. Who here knows what WebAssembly is? Alright, about half, I think. WebAssembly first appeared in 2017 and became an official standard in 2019. Consulting MDN, we find a wonderful wall of text, but it boils down to this: it's a low-level assembly-like language that offers near-native performance, providing languages such as C, C++, C#, and Rust with a compilation target so that they can run on the web alongside JavaScript.
00:02:20.340 When you start looking into it, you'll encounter two different WebAssembly file formats. The first is the text representation, which looks like source code, while the second is wasm, which is the binary representation into which everything else gets compiled. The wasm files are what actually get loaded and run in the browser. So, how do you go from source to wasm? First, let's look at some plain WebAssembly.
00:02:52.560 We will look at an example where we add two integers together. It looks a bit verbose, but it's readable. We define a function that takes two parameters, both integers, and it returns a result, which is also an integer. In Ruby, this would look slightly different.
00:03:13.080 For a more convoluted example, let’s check if a number is prime. This gets a bit more verbose, and we might not want to read all of this right now. In my opinion, what is not just the file extension but also what you say when you have to read WebAssembly code. However, as MDN states, it is not primarily intended to be written by hand. But how do we convert it into a wasm file? There's a project called the WebAssembly Binary Toolkit that contains a tool called WABT. There's also an online demo for it, so you can do it directly in the browser. Realistically, you'll probably use the command line interface. Now that we have our wasm file, how do we use it in the browser?
00:04:10.740 Here’s a basic example, where we can focus on the script tag that contains the main setup. We have a little namespace configured, and then we fetch our wasm file and initialize it. Once that’s done, we can call the add function to add one and two, and then show an alert displaying the result. We can conclude that one plus two equals three, and it was very high performance. That may seem like a stupid example, but the things that wasm enables in the browser are quite exciting.
00:05:13.260 For example, Webercity, which is a WebAssembly port of Audacity, is a full multi-track audio editor that runs directly in the browser without needing to download anything. Or perhaps you’d like to re-encode some videos? You can do that with the ffmpeg port of WebAssembly, also run right in the browser. Another potential use involves image processing, and for that, you could use the Photon library to do your processing before the photo gets uploaded anywhere. Of course, Try Ruby now also uses the WebAssembly build of Ruby.
00:06:01.680 This brings us to ruby.wasm, which was first added in Ruby 3.2.0. It introduces Wazzy-based WebAssembly support. Another term we encounter is Wazzy, which is the WebAssembly System Interface. This provides access to operating system-like features in the WebAssembly environment. Additionally, there is Wazzy VFS, a virtual file system layer for Wazzy, which we’ll encounter later. But how can we use ruby.wasm in the browser? The simplest example consists of loading the library in a script tag, which allows Ruby script tags on the page to work.
00:07:12.240 This setup will print 'Hello, world!' to the browser console, which, of course, is exciting for everyone. Or we can do something a bit more elaborate, such as allowing interaction between the JavaScript side and the Ruby side. Here we write some setup code, fetch the WebAssembly runtime for Ruby, and initialize it, giving us access to the Ruby VM where we can run whatever Ruby code we like. We can also evaluate JavaScript code from within our Ruby code to interact with the JavaScript side.
00:08:18.300 It’s also possible to use Ruby outside of the web. For example, there’s an example I’ve adapted from the README, where we fetch the Ruby WebAssembly runtime, extract it, and create our own little app. Then we use the aforementioned Wazzy VFS to pack everything into a single WebAssembly file that we can run via the command line.
00:09:16.560 There are a couple of limitations in the WebAssembly build of Ruby. First, you don’t get threads, which is both a blessing and a curse, and you also don’t have networking at this time. I believe both of these are being worked on, but currently, they are not available. Now that should be enough basics for now, leading us to my terrible ideas. My first idea was to use something called wasm2c that is also part of the WebAssembly Binary Toolkit to take the Ruby WebAssembly implementation and compile it back into C code.
00:10:54.420 Putting this to use, we can examine our add example from the beginning of the presentation. We can convert it into C using the wasm2c tool and receive a header file and the actual implementation file. The header file is somewhat verbose, containing a lot of boilerplate, but the essentials are rather simple. We look at the implementation side, and it's more verbose again. Remember, this is just for adding two numbers. It might not be the most efficient way, and in fact, it doesn’t even fit properly on this slide. It was supposed to illustrate that it would fit if we rotated it 90 degrees, but that didn’t work out.
00:12:01.020 We also have to write some glue code to use that function, as we need to set up the interpreter and do some cleanup afterwards. The main part involves calling the function to get our result printed. If we compile that setup along with the WebAssembly implementation file, we receive a binary that allows as to add integers.
00:13:04.800 However, as I mentioned, it's not the most efficient; we end up with a 52 kilobyte binary for an addition operation. What does this have to do with Ruby? Well, I thought I could take my Ruby app wasm we created earlier and compile it to C. That part works fine in that it runs and generates C files. However, the issue arises when we try to compile it, leading to a plethora of missing symbols.
00:13:23.880 This situation relates back to the fact that the WebAssembly Binary Toolkit denotes that Wazzy support is still a work in progress. Only a handful of syscalls are supported at this time, and moreover, it does not support everything in the WebAssembly Binary Toolkit. With that, it's back to the drawing board because I didn’t become a senior developer by avoiding terrible ideas.
00:14:14.040 Next up is Deno. If you’re not familiar, it’s a sort of next-generation JavaScript runtime that crucially supports running and compiling scripts into executables. This sounds like the exact type of functionality I need, allowing us to take our WebAssembly code, wrap it in a small JavaScript script, and compile it into an executable. However, in practice, that didn’t work out as we encountered further incompatibilities.
00:15:40.800 So no Deno then. Next, how about Bun? It’s the new hotness in the JavaScript scene. In case you’re unfamiliar, Bun is a JavaScript runtime, package manager, and bundler wrapped into one. While it doesn’t specifically support creating static executable files, I was at this point desperate for any solution to work. We made some small tweaks to the earlier code from Deno, but alas, it also failed due to incompatible returns from methods.
00:16:03.960 Starting from the beginning of this talk, we ran Ruby outside of the web, so it must be possible. I stumbled upon Wasmtime, which is a fast and secure runtime for WebAssembly. It runs WebAssembly code outside of the web and can be utilized both as a command line utility and as a library embedded in larger applications, which aligns with what I want to do. Thankfully, we don’t actually need to write C code here, which is an added bonus. There is even an example that accomplishes what we need.
00:17:06.540 We can adapt this example to load the Ruby WebAssembly runtime. We start by importing some required libraries and set up the Wazzy configurations. Finally, we instantiate the WebAssembly we compiled earlier and call our function. This is all written in about twenty lines of code, and when we run it...
00:18:15.840 ...it works! There are some downsides, however. Firstly, it results in a 50 megabyte binary, which seems a bit excessive for a 'Hello, world!' program — that's roughly 35 floppy disks! It also takes just under three seconds to execute, which isn't ideal. For a little performance comparison, I ran a recursive Fibonacci benchmark where plain Ruby 3.2 took about fifteen seconds to complete. The WebAssembly build surprisingly took about two and a half minutes. In conclusion, while it works, the performance isn't fantastic. Consequently, it’s left as an open question: could something similar be done in Rust? I ran out of time to explore that angle, leaving it as an exercise for the reader. Should you use my terrible ideas in production? No, that was right there in the talk title; they’re indeed very terrible. However, if we overlook performance and size issues, it feels like there could be viable concepts here that can be developed further.