This articles was published on 2012-05-23
My first patch which was non-trivial to mruby was a reimplementation of the IRB. The Interactive Ruby Shell is a quite useful tool all over the Ruby community. It is available for all other implementations and there is even a web-version available. Enough reason to also create an embeddable version.
The first attempt was actually not done by me but by Frank Celler. He was in need for a playground to experiment with mruby in his no-sql database AvocadoDB. The downside of his solution was that he didn’t yet evaluated the return value of each evaluation and the complete thing was implemented in C++ (mruby is written in C99).
As time went by matz implemented this missing return value and I took another look at the API of mruby. The result was a quite naive implementation of an IRB based on mruby:
/* ** mirb - Embeddable Interactive Ruby Shell ** ** This program takes code from the user in ** an interactive way and executes it ** immediately. It's a REPL... */ #include <string.h> #include <mruby.h> #include <mruby/proc.h> #include <mruby/data.h> #include <mruby/compile.h> /* Guess if the user might want to enter more * or if he wants an evaluation of his code now */ int is_code_block_open(struct mrb_parser_state *parser) { int code_block_open = FALSE; /* check for unterminated string */ if (parser->sterm) return TRUE; /* check if parser error are available */ if (0 < parser->nerr) { const char *unexpected_end = "syntax error, unexpected $end"; const char *message = parser->error_buffer[0].message; /* a parser error occur, we have to check if */ /* we need to read one more line or if there is */ /* a different issue which we have to show to */ /* the user */ if (strncmp(message, unexpected_end, strlen(unexpected_end)) == 0) { code_block_open = TRUE; } else if (strcmp(message, "syntax error, unexpected keyword_end") == 0) { code_block_open = TRUE; } else if (strcmp(message, "syntax error, unexpected tREGEXP_BEG") == 0) { code_block_open = TRUE; } return code_block_open; } switch (parser->lstate) { /* all states which need more code */ case EXPR_BEG: /* an expression was just started, */ /* we can't end it like this */ code_block_open = TRUE; break; case EXPR_DOT: /* a message dot was the last token, */ /* there has to come more */ code_block_open = TRUE; break; case EXPR_CLASS: /* a class keyword is not enough! */ /* we need also a name of the class */ code_block_open = TRUE; break; case EXPR_FNAME: /* a method name is necessary */ code_block_open = TRUE; break; case EXPR_VALUE: /* if, elsif, etc. without condition */ code_block_open = TRUE; break; /* now all the states which are closed */ case EXPR_ARG: /* an argument is the last token */ code_block_open = FALSE; break; /* all states which are unsure */ case EXPR_CMDARG: break; case EXPR_END: /* an expression was ended */ break; case EXPR_ENDARG: /* closing parenthese */ break; case EXPR_ENDFN: /* definition end */ break; case EXPR_MID: /* jump keyword like break, return, ... */ break; case EXPR_MAX_STATE: /* don't know what to do with this token */ break; default: /* this state is unexpected! */ break; } return code_block_open; } /* Print a short remark for the user */ void print_hint(void) { printf("mirb - Embeddable Interactive Ruby Shelln"); printf("nThis is a very early version, please test and report errors.n"); printf("Thanks :)nn"); } /* Print the command line prompt of the REPL */ void print_cmdline(int code_block_open) { if (code_block_open) { printf("* "); } else { printf("> "); } } int main(void) { char last_char, ruby_code[1024], last_code_line[1024]; int char_index; struct mrb_parser_state *parser; mrb_state *mrb_interpreter; mrb_value mrb_return_value; int byte_code; int code_block_open = FALSE; print_hint(); /* new interpreter instance */ mrb_interpreter = mrb_open(); /* new parser instance */ parser = mrb_parser_new(mrb_interpreter); memset(ruby_code, 0, sizeof(*ruby_code)); memset(last_code_line, 0, sizeof(*last_code_line)); while (TRUE) { print_cmdline(code_block_open); char_index = 0; while ((last_char = getchar()) != 'n') { if (last_char == EOF) break; last_code_line[char_index++] = last_char; } if (last_char == EOF) { printf("n"); break; } last_code_line[char_index] = '�'; if ((strcmp(last_code_line, "quit") == 0) || (strcmp(last_code_line, "exit") == 0)) { if (code_block_open) { /* cancel the current block and reset */ code_block_open = FALSE; memset(ruby_code, 0, sizeof(*ruby_code)); memset(last_code_line, 0, sizeof(*last_code_line)); continue; } else { /* quit the program */ break; } } else { if (code_block_open) { strcat(ruby_code, "n"); strcat(ruby_code, last_code_line); } else { memset(ruby_code, 0, sizeof(*ruby_code)); strcat(ruby_code, last_code_line); } /* parse code */ parser->s = ruby_code; parser->send = ruby_code + strlen(ruby_code); parser->capture_errors = 1; parser->lineno = 1; mrb_parser_parse(parser); code_block_open = is_code_block_open(parser); if (code_block_open) { /* no evaluation of code */ } else { if (0 < parser->nerr) { /* syntax error */ printf("line %d: %sn", parser->error_buffer[0].lineno, parser->error_buffer[0].message); } else { /* generate bytecode */ byte_code = mrb_generate_code(mrb_interpreter, parser->tree); /* evaluate the bytecode */ mrb_return_value = mrb_run(mrb_interpreter, /* pass a proc for evaulation */ mrb_proc_new(mrb_interpreter, mrb_interpreter->irep[byte_code]), mrb_top_self(mrb_interpreter)); /* did an exception occur? */ if (mrb_interpreter->exc) { mrb_p(mrb_interpreter, mrb_obj_value(mrb_interpreter->exc)); mrb_interpreter->exc = 0; } else { /* no */ printf(" => "); mrb_p(mrb_interpreter, mrb_return_value); } } memset(ruby_code, 0, sizeof(*ruby_code)); memset(ruby_code, 0, sizeof(*last_code_line)); } } } mrb_close(mrb_interpreter); return 0; }
But for some reason matz accepted it and since two weeks (2012-05-12) mruby has now a Read Eval Print Loop which provides an easy way to explore mruby:
boviAir:mruby daniel$ ./bin/mirb mirb - Embeddable Interactive Ruby Shell This is a very early version, please test and report errors. Thanks :) > class Testii * def testoo * puts 1 * end * * def testuu arg * puts arg * end * end => nil > Testii.testoo #<NoMethodError: no method named testoo> > Testii.new.testoo 1 => false > Testii.new.testuu 'hui' hui => false > 1+1 => 2
But having such a tool doesn’t only makes it easy to play with Ruby. It also makes it possible to learn how to interact with mruby.
We start by getting a mruby instance:
mrb_interpreter = mrb_open();
With this instance we are getting a parser instance which we will use to parse our input code:
parser = mrb_parser_new(mrb_interpreter);
We now tell the parser where our ruby code is ->s
, then we have to give him the same again with the length ->send
. For our special use case of a REPL we tell the parser to capture all errors instead of screaming them out to stdio. This is done by ->capture_errors
. With ->lineno
we set the starting point and then we start the parsing with mrb_parser_parse
:
/* parse code */ parser->s = ruby_code; parser->send = ruby_code + strlen(ruby_code); parser->capture_errors = 1; parser->lineno = 1; mrb_parser_parse(parser);
Next up, we want to compile down to byte code for the VM:
/* generate bytecode */ byte_code = mrb_generate_code(mrb_interpreter, parser->tree);
Last but not least we run the byte code inside of the VM and capturing the return value:
/* evaluate the bytecode */ mrb_return_value = mrb_run(mrb_interpreter, /* pass a proc for evaulation */ mrb_proc_new(mrb_interpreter, mrb_interpreter->irep[byte_code]), mrb_top_self(mrb_interpreter))
And that’s it. In this way you can use mruby in any of your application too.