Running mruby in a browser

Published on:

tl;dr version: I managed to compile mruby to JavaScript via emscripten, the source code is in a Github repository.

A Little Background

I really love Ruby, yet I'm not that much into Rails. Rails is great indeed, I really learned so much while playing around with Rails. In fact, more than half of my knowledge using Ruby was learned reading the Rails source code. However, I just feel bored writing again and again applications working on a database-_- Whatever new features my app has and whatever facinating features Rails provide, basically I just keep writing code to creating entries in db, reading these entries out, updating and deleting them occasionally(Thank you, CRUD!). After all, the MVC architecture is there. It is simply not fun for me>_<

So I always wonder around looking for interesting stuffs outside of the Rails world. Celluloid, Gosu, Fluentd are all fun to play with. The latest toy around is mruby, a lightweight implementation Ruby. It works for embedded system and can be linked into existing softwares. Well, that sounds like Lua, but as a former-Lisp fan, Ruby is more interesting, isn't it? Sorry I've gone a little off-topic, I could write another post describing my feelings on different languages, but that's not the point today:)

Last Saturday I suddenly got such an idea: mruby is light-weighted (around 20,000 lines of code), has a small footprint(~100k according to Matz's talk) and no threads. So why not try running it in a browser? Having the experiment of writing Web apps using GWT, I never trust that we must use JavaScript to write code running in a browser. Now that I've got some time, let the fun begin:)

Building mruby

Two choices exist to compile C/C++ code into JavaScript: Native Client and Emscripten. I used to fall in love with everything marked with Google, but things have changed. Let's first go with Emscripten and see how everything goes, we have a much larger world of Web instead of Chrome-only kingdoms.

The current mruby compiling process works like this:

  1. Use bison to parse src/parse.y into src/y.tab.c.
  2. Compile every c source in src, the generated object files are then archived into lib/libmruby_core.a.
  3. Compile tools/mrbc/mrbc.c and link with lib/libmruby_core.c to generate the core mruby compiler bin/mrbc.
  4. Use the mruby compiler to standard libraries in mrblib, the generated bytecode is attached as an array to mrblib/init_mrblib.c. The newly created source code is called mrblib/mrblib.c.
  5. Compile mrblib/mrblib.c and add to lib/libmruby_core.a. The result is called lib/libmruby.a.

We need to compile all source code src together with the generated file src/y.tab.c and mrblib/mrblib.c. For simplicity and the unstable state of mruby, I just require building the entire mruby first in my Makefile:

# mruby settings
MRUBY_PATH := ./modules/mruby
MRBLIB_PATH := $(MRUBY_PATH)/mrblib

MRBLIBC := $(MRBLIB_PATH)/mrblib.c
YC := $(MRUBY_SRC_DIR)/y.tab.c

# yacc compile
$(YC) :

    @(cd $(MRUBY_PATH); make)

# mrblib.c compile
$(MRBLIBC) :

    @(cd $(MRUBY_PATH); make)

Now the code can be compiled into a giant JavaScript file.

C compiler vs C++ compiler

In short, it was a tough and mysterious road-_-

The first version compiles okay, but it keeps running into RangeError: Maximum call stack size exceeded error. While browsing for wiki page of emscripten, one sentence in CodeGuidlinesAndLimitations caught my eye.

"Nonportable code that uses low-level features of the native environment, like native stack manipulation (e.g. in conjunction with setjmp/longjmp. We support normal setjmp/longjmp, but not with stack replacing etc.)."

I suspect this may be the reason, what supports my idea is one commit in emscripted-ruby, which is a port for ruby 1.8.7 onto the browser. It basically eliminates all setjmp/longjmp calls with c++ exception. So I decided this may be the direction and started coding right way. Well, now it turns out this is a mistake, I should've done more investigation. Anyway, you will see the mistakes I made.

Then I spent around ten hours figuring out a way to patch mruby to use c++ exception instead of setjmp/longjmp with all tests passed. The compiler is also changed from emcc to em++. Well, now the mruby compiles and works:

$ node build/mruby.js
Ruby is awesome!
Ruby is awesome!
Ruby is awesome!
Ruby is awesome!
Ruby is awesome!

This was Wednesday night and I went to read the FreeBSD book for exam. On Thursday morning, I accidentally compiles the code with em++ but without my patches for setjmp. The code happened to work! Even if I revert the code back to the initial revision with all original build settings, I still cannot reproduce the RangeError error. Well, it remains a mystery and all I can guess is that something is wrong with my installation of Node.js at that day. It might use a different setting that day...

Then another problem came: when compiling with emcc, the code compiles okay, but would run forever without terminating; It would only work when compiling with em++... This is also strange, I will look into this later.

So the lesson I learned is: do more investigation before pumping out coding. There may be another reason.

Future Work

Well, this is really fun. I've sent a post on emscripten-discuss. And I will spend some time on this.

Besides that, a few interesting follow-up works are already in my head:

  • Interfaces to C and JavaScript libraries
  • OpenGL ES 2/WebGL binding
  • Repl in the browser
  • MobiRuby for the browser(well, this may be too ambitious)

Anyway, it is really fun working on this.

Note

If you read all the way to here(I would not read such a long article myself-_-), I want to say sorry for my English. I'm only fluent in Chinese, but I just want more people to be able to read about this:)

Comments

comments powered by Disqus