Lightweight introduction to mruby

by Paweł Świątkowski
25 Sep 2024

I knew that mruby existed for a long time. I knew it’s a lightweight version of Ruby, designed mostly to be embedded in other programs. I knew that DragonRuby has some kind of relation to mruby - be it written in it or using some kind of fork of it. But that’s pretty much it.

Prompted by a few discussions here and there lately, I decided to take a closer look at it.

Installation

If you are using a Ruby version manager - and you should - installing mruby should be very simple. In my case, in asdf, it is as much as running:

asdf install ruby mruby-3.3.0

Wait a couple of seconds and we’re ready for the test drive.

Getting around

And interactive REPL for mruby is called mirb and that’s how you start it. When fired up, you can check that even if it looks a bit different than irb, it’s still Ruby all around

$ mirb
mirb - Embeddable Interactive Ruby Shell

> Time.now.to_i
 => 1727202850
> "test".object_id
 => 95853644613216

We can also create a simple Ruby script and run it.

# frozen_string_literal: true

3.times do
  puts "test".object_id
end
$ mruby test.rb
99069440489104
99069440488816
99069440488528

From this we can see that a magic comment freezing string literals does not work in mruby. Not that it’s very important, but useful to know.

Speaking of the things that don’t work, the docs say that it’s on par with features from Ruby 3.3, except pattern matching. And indeed, even the simplest example from the docs does not work.

case [1, 2, 3]
in [Integer, Integer]
  p "matched"
else
  p "not matched"
end
$ mruby matching.rb
matching.rb:2:2: syntax error, unexpected "'in'", expecting "'when'"
matching.rb:4:4: syntax error, unexpected "'else'", expecting end of file

That’s a real bummer. I don’t know if it’s some technical difficulties that lead to not implementing pattern matching in mruby, but it’s simply not there. Other modern features, like endless methods, work.

> def oneline = "I'm a oneliner"
 => :oneline
> oneline
 => "I'm a oneliner"

Bytecode

As surprising as it may sound, this might be pretty much the limit of what we can do with a Ruby source file and mruby executable. We cannot even split the code into multiple files, because mruby does not have require. We cannot really use gems too, at least I haven’t found a way to do that.

It seems that mruby is, indeed, built with the purpose of embedding in mind.

One additional thing we can do is to take our Ruby program and compile it to bytecode using mrbc executable.  

$ echo 'puts "Hello world"' >> hello.rb
$ mrbc hello.rb
$ hexdump hello.mrb
0000000 4952 4554 3330 3030 0000 5c00 414d 5a54
0000010 3030 3030 5249 5045 0000 4000 3330 3030
0000020 0000 3400 0100 0400 0000 0000 0000 0a00
0000030 0251 2d00 0001 3801 6901 0100 0000 480b
0000040 6c65 6f6c 7720 726f 646c 0000 0001 7004
0000050 7475 0073 4e45 0044 0000 0800          
000005c

Now we can run it like this:

$ mruby -b hello.mrb
Hello world

What for? Let’s see.

Embedding

  ”Executing Ruby code with mruby” - a super-useful resource on official mruby pages - lists an interesting way to run mruby programs under section “Bytecode (.c)”:

This variant is interesting for those who want to integrate Ruby code directly into their C code. It will create a C array containing the bytecode which you then have to execute by yourself.

Let’s try it. First we are going to complicate our hello world a little:

class Greeter
  def initialize(name)
    @name = name
  end

  def invoke!
    "Hello, #{@name}"
  end
end

puts Greeter.new("world").invoke!

Now let’s compile it to bytecode embedded in C file by using mrbc -Bgreeter hello.rb. This is how the result (hello.c) looks like for me:

#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t greeter[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x01,0x2c,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xf8,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0x52,0x00,0x01,0x00,0x04,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x1b,
0x11,0x01,0x11,0x02,0x5c,0x01,0x00,0x5e,0x01,0x00,0x1d,0x01,0x00,0x51,0x02,0x00,
0x2f,0x01,0x01,0x01,0x2f,0x01,0x02,0x00,0x38,0x01,0x69,0x00,0x01,0x00,0x00,0x05,
0x77,0x6f,0x72,0x6c,0x64,0x00,0x00,0x03,0x00,0x07,0x47,0x72,0x65,0x65,0x74,0x65,
0x72,0x00,0x00,0x03,0x6e,0x65,0x77,0x00,0x00,0x07,0x69,0x6e,0x76,0x6f,0x6b,0x65,
0x21,0x00,0x00,0x00,0x00,0x3d,0x00,0x01,0x00,0x03,0x00,0x02,0x00,0x00,0x00,0x00,
0x00,0x12,0x63,0x01,0x58,0x02,0x00,0x5f,0x01,0x00,0x63,0x01,0x58,0x02,0x01,0x5f,
0x01,0x01,0x38,0x01,0x00,0x00,0x00,0x02,0x00,0x0a,0x69,0x6e,0x69,0x74,0x69,0x61,
0x6c,0x69,0x7a,0x65,0x00,0x00,0x07,0x69,0x6e,0x76,0x6f,0x6b,0x65,0x21,0x00,0x00,
0x00,0x00,0x28,0x00,0x03,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x34,
0x04,0x00,0x00,0x01,0x03,0x01,0x1a,0x03,0x00,0x38,0x03,0x00,0x00,0x00,0x01,0x00,
0x05,0x40,0x6e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x35,0x00,0x02,0x00,0x04,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x34,0x00,0x00,0x00,0x51,0x02,0x00,0x19,0x03,
0x00,0x52,0x02,0x38,0x02,0x00,0x01,0x00,0x00,0x07,0x48,0x65,0x6c,0x6c,0x6f,0x2c,
0x20,0x00,0x00,0x01,0x00,0x05,0x40,0x6e,0x61,0x6d,0x65,0x00,0x4c,0x56,0x41,0x52,
0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x01,0x00,0x04,0x6e,0x61,0x6d,0x65,0x00,0x00,
0xff,0xff,0xff,0xff,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

Now let’s prepare a boilerplate C file just like the article suggests:

#include <mruby.h>
#include <mruby/irep.h>
#include "hello.c"

int
main(void)
{
  mrb_state *mrb = mrb_open();
  if (!mrb) { /* handle error */ }
  mrb_load_irep(mrb, greeter);
  mrb_close(mrb);
  return 0;
}

And now for the compilation:

gcc -std=c99 -I ~/.asdf/installs/ruby/mruby-3.3.0/include greeter.c -o greeter ~/.asdf/installs/ruby/mruby-3.3.0/lib/libmruby.a -lm

(of course your exact paths might be different)

As a result we have a full standalone binary, which we can execute:

$ ./greeter
Hello, world

  The binary has almost 6MB on my machine, which suggests it really it standalone.

Recap

We’ve touched a couple of things here:

  • Installation of mruby

  • Using REPL and executing one-file scripts

  • Learning some limitations compared to CRuby

  • Compiling script to binary (both standalone and embedded in a C file)

  • With a little boilerplate, creating a fully standalone binary

That’s pretty nice start. Next steps would be to use some gems and build something a little more feature-full than just a hello world program.

end of the article

Tags: ruby mruby

This article was written by me – Paweł Świątkowski – on 25 Sep 2024. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.