Redis Scripting with MRuby


MRuby is a lightweight Ruby. It was created by Matz with the purpose of having an embeddable version of the language. Even if it just reached the version 1.0, the hype around MRuby wasn’t high. However, there are already projects that are targeting Nginx, Go, iOS, V8, and even Arduino.

The direct competitor in this huge market is Lua: a lightweight scripting language. Since the version 2.6.0 Redis introduced scripting capabilities with Lua.

# redis-cli
> eval "return 5" 0
(integer) 5

Today is the 5th Redis birthday, and I’d like celebrate this event by embedding my favorite language.

Hello, MRuby

MRuby is shipped with an interpreter (mruby) to execute the code via a VM. This usage is equivalent to the well known Ruby interpreter ruby. MRuby can also generate a bytecode from a script, via the mrbc bin.

What’s important for our purpose are the C bindings. Let’s write an Hello World program.

We need a *NIX OS, gcc and bison. I’ve extracted the MRuby code into ~/Code/mruby and built it with make.

#include <mruby.h>
#include <mruby/compile.h>

int main(void) {
  mrb_state *mrb = mrb_open();
  char code[] = "p 'hello world!'";

  mrb_load_string(mrb, code);
  return 0;
}

The compiler needs to know where are the headers and the libs:

gcc -I/Users/luca/Code/mruby/include hello_world.c \
  /Users/luca/Code/mruby/build/host/lib/libmruby.a \
  -lm -o hello_world

This is a really basic example, we don’t have any control on the context where this code is executed. We can parse it and wrap into a Proc.

#include <mruby.h>
#include <mruby/proc.h>

int main(int argc, const char * argv[]) {
  mrb_state *mrb = mrb_open();
  mrbc_context *cxt;
  mrb_value val;
  struct mrb_parser_state *ps;
  struct RProc *proc;

  char code[] = "1 + 1";

  cxt = mrbc_context_new(mrb);
  ps = mrb_parse_string(mrb, code, cxt);
  proc = mrb_generate_code(mrb, ps);
  mrb_pool_close(ps->pool);

  val = mrb_run(mrb, proc, mrb_top_self(mrb));
  mrb_p(mrb, val);

  mrbc_context_free(mrb, cxt);
  return 0;
}

Hello, Redis

As first thing we need to make Redis dependend on MRuby libraries. We extract the language source code under deps/mruby and then we hook inside the deps/Makefile mechanisms:

mruby: .make-prerequisites
       @printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
       cd mruby && $(MAKE)

see the commit

During the startup, Redis initializes its features. We add our own mrScriptingInit(), where we initialize the interpreter and assign to server.mrb.

# src/mruby-scripting.c
void mrScriptingInit(void) {
  mrb_state *mrb = mrb_open();
  server.mrb = mrb;
}

see the commit

Then we can add another command REVAL with the same syntax of EVAL, but in our case MRuby will be in charge of execute it.

# src/redis.c
{"reval",mrEvalCommand,-3,"s",0,zunionInterGetKeys,0,0,0,0,0},

That mrEvalCommand function will be responsible to handle that command. It’s similar to the Hello World above, the only difference is that the code is passed as argument to the redis client (c->argv[1]->ptr).

# src/mruby-scripting.c
void mrEvalCommand(redisClient *c) {
  mrb_state *mrb = server.mrb;

  struct mrb_parser_state *ps;
  struct RProc *proc;
  mrbc_context *cxt;
  mrb_value val;

  cxt = mrbc_context_new(mrb);
  ps = mrb_parse_string(mrb, c->argv[1]->ptr, cxt);
  proc = mrb_generate_code(mrb, ps);
  mrb_pool_close(ps->pool);

  val = mrb_run(mrb, proc, mrb_top_self(mrb));
  mrAddReply(c, mrb, val);

  mrbc_context_free(mrb, cxt);
}

see the commit

Now we can compile the server and start it.

make && src/redis-server

From another shell, start the CLI.

src/redis-cli
> reval "2 + 3" 0
"5"

This was the first part of this implementation. In a future article, I’ll cover how to access Redis data within the MRuby context.

For the time being, feel free to play with my fork.

Luca Guidi

Family man, software engineer, Open Source indie developer, speaker.

Rome, Italy https://lucaguidi.com