From eb8c177824f7fc40ffc9bee71563fba4c806891a Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 12 Mar 2026 08:04:38 +0900 Subject: [PATCH] mruby-bin-mrb: add compiler-free runtime executor gem The `mrb` command executes only precompiled RiteBinary (.mrb) files without depending on mruby-compiler. This enables smaller binaries for embedded deployments where scripts are precompiled on a development machine. Co-authored-by: Claude --- mrbgems/mruby-bin-mrb/README.md | 86 +++++++ mrbgems/mruby-bin-mrb/bintest/mrb.rb | 73 ++++++ mrbgems/mruby-bin-mrb/mrbgem.rake | 19 ++ mrbgems/mruby-bin-mrb/tools/mrb/mrb.c | 325 ++++++++++++++++++++++++++ 4 files changed, 503 insertions(+) create mode 100644 mrbgems/mruby-bin-mrb/README.md create mode 100644 mrbgems/mruby-bin-mrb/bintest/mrb.rb create mode 100644 mrbgems/mruby-bin-mrb/mrbgem.rake create mode 100644 mrbgems/mruby-bin-mrb/tools/mrb/mrb.c diff --git a/mrbgems/mruby-bin-mrb/README.md b/mrbgems/mruby-bin-mrb/README.md new file mode 100644 index 000000000..7169b9183 --- /dev/null +++ b/mrbgems/mruby-bin-mrb/README.md @@ -0,0 +1,86 @@ +# mruby-bin-mrb + +`mrb` is a lightweight mruby runtime that executes only precompiled +RiteBinary (.mrb) files. Unlike the full `mruby` command, it does not +depend on `mruby-compiler`, resulting in a significantly smaller binary. + +This gem is intended for embedded deployments where Ruby scripts are +precompiled on a development machine and only the runtime is needed on +the target device. + +## Size comparison + +By excluding `mruby-compiler` (and gems that depend on it such as +`mruby-eval`, `mruby-binding`, and `mruby-bin-mirb`), the text segment +can be reduced by approximately 300KB or more, depending on the build +configuration. + +## Usage + +``` +mrb [switches] programfile.mrb [arguments] +``` + +### Options + +- `-d` - set debugging flags (set `$DEBUG` to true) +- `-r library` - load a library (.mrb) before executing your script +- `-v` - print version number, then run in verbose mode +- `--verbose` - run in verbose mode +- `--version` - print the version +- `--copyright` - print the copyright + +## Workflow + +```bash +# On the development machine (with full mruby + mrbc): +mrbc -o program.mrb program.rb + +# On the target device (with mrb only): +mrb program.mrb + +# Load a precompiled library before the main program: +mrb -r lib.mrb program.mrb + +# Pass arguments to the script: +mrb program.mrb arg1 arg2 +``` + +## Build configuration example + +To build a minimal mruby with only the `mrb` runtime: + +```ruby +# build_config/runtime.rb +MRuby::Build.new do |conf| + conf.toolchain + + # Use a gembox that does not pull in the compiler. + # For example, the default gembox does not require it. + conf.gembox 'default' + + # The runtime-only executor (no compiler dependency) + conf.gem :core => 'mruby-bin-mrb' + + # Do NOT include these (they require mruby-compiler): + # conf.gem :core => 'mruby-bin-mruby' + # conf.gem :core => 'mruby-bin-mirb' + # conf.gem :core => 'mruby-eval' + # conf.gem :core => 'mruby-binding' +end +``` + +## Differences from `mruby` command + +| Feature | `mruby` | `mrb` | +| ----------------------- | ------- | -------------------------- | +| Execute .rb files | yes | no | +| Execute .mrb files | yes | yes | +| `-e` inline code | yes | no | +| `-c` syntax check | yes | no | +| `-b` force binary mode | yes | not needed (always binary) | +| Requires mruby-compiler | yes | **no** | + +## License + +MIT License - see the mruby LICENSE file. diff --git a/mrbgems/mruby-bin-mrb/bintest/mrb.rb b/mrbgems/mruby-bin-mrb/bintest/mrb.rb new file mode 100644 index 000000000..bd31c4f59 --- /dev/null +++ b/mrbgems/mruby-bin-mrb/bintest/mrb.rb @@ -0,0 +1,73 @@ +require 'tempfile' +require 'open3' + +def assert_mrb(exp_out, exp_err, exp_success, args) + out, err, stat = Open3.capture3(*(cmd_list("mrb") + args)) + assert "assert_mrb" do + assert_operator(exp_out, :===, out, "standard output") + assert_operator(exp_err, :===, err, "standard error") + assert_equal(exp_success, stat.success?, "exit success?") + end +end + +assert('mrb can execute .mrb files') do + script = Tempfile.new(['test', '.rb']) + bin = Tempfile.new(['test', '.mrb']) + File.write(script.path, 'puts "hello from mrb"') + system("#{cmd('mrbc')} -o #{bin.path} #{script.path}") + o = `#{cmd('mrb')} #{bin.path}`.strip + assert_equal 'hello from mrb', o +end + +assert('mrb $0 value') do + script = Tempfile.new(['test', '.rb']) + bin = Tempfile.new(['test', '.mrb']) + File.write(script.path, 'print $0') + system("#{cmd('mrbc')} -o #{bin.path} #{script.path}") + o = `#{cmd('mrb')} #{bin.path}`.strip + assert_equal bin.path, o +end + +assert('mrb ARGV value') do + script = Tempfile.new(['test', '.rb']) + bin = Tempfile.new(['test', '.mrb']) + File.write(script.path, 'p ARGV') + system("#{cmd('mrbc')} -o #{bin.path} #{script.path}") + o = `#{cmd('mrb')} #{bin.path} foo bar`.strip + assert_equal '["foo", "bar"]', o +end + +assert('mrb with no arguments prints error') do + assert_mrb("", /no program file given/, false, []) +end + +assert('mrb --version') do + assert_mrb(/\Amruby \d+\.\d+/, "", true, %w[--version]) +end + +assert('mrb -r option loads library') do + lib = Tempfile.new(['lib', '.rb']) + main = Tempfile.new(['main', '.rb']) + lib_mrb = Tempfile.new(['lib', '.mrb']) + main_mrb = Tempfile.new(['main', '.mrb']) + + File.write(lib.path, '$lib_loaded = true') + File.write(main.path, 'puts $lib_loaded') + system("#{cmd('mrbc')} -o #{lib_mrb.path} #{lib.path}") + system("#{cmd('mrbc')} -o #{main_mrb.path} #{main.path}") + o = `#{cmd('mrb')} -r #{lib_mrb.path} #{main_mrb.path}`.strip + assert_equal 'true', o +end + +assert('mrb -d sets $DEBUG') do + script = Tempfile.new(['test', '.rb']) + bin = Tempfile.new(['test', '.mrb']) + File.write(script.path, 'print $DEBUG') + system("#{cmd('mrbc')} -o #{bin.path} #{script.path}") + o = `#{cmd('mrb')} -d #{bin.path}`.strip + assert_equal 'true', o +end + +assert('mrb nonexistent file') do + assert_mrb("", /Cannot open/, false, %w[nonexistent.mrb]) +end diff --git a/mrbgems/mruby-bin-mrb/mrbgem.rake b/mrbgems/mruby-bin-mrb/mrbgem.rake new file mode 100644 index 000000000..20d6a8513 --- /dev/null +++ b/mrbgems/mruby-bin-mrb/mrbgem.rake @@ -0,0 +1,19 @@ +MRuby::Gem::Specification.new('mruby-bin-mrb') do |spec| + spec.license = 'MIT' + spec.author = 'mruby developers' + spec.summary = 'mruby runtime command (compiler-free)' + spec.bins = %w(mrb) + + # NOTE: Unlike mruby-bin-mruby, this gem does NOT depend on + # mruby-compiler. This makes it suitable for builds where the + # compiler is excluded to reduce binary size. + # + # To use this gem in your build_config.rb: + # + # MRuby::Build.new do |conf| + # conf.toolchain + # conf.gem :core => 'mruby-bin-mrb' + # # Do NOT include mruby-bin-mruby or mruby-compiler + # # unless other gems require them. + # end +end diff --git a/mrbgems/mruby-bin-mrb/tools/mrb/mrb.c b/mrbgems/mruby-bin-mrb/tools/mrb/mrb.c new file mode 100644 index 000000000..cfe3c6e49 --- /dev/null +++ b/mrbgems/mruby-bin-mrb/tools/mrb/mrb.c @@ -0,0 +1,325 @@ +/* +** mrb - mruby runtime executor (compiler-free) +** +** This is a lightweight alternative to the `mruby` command that only +** executes precompiled RiteBinary (.mrb) files. It does not depend on +** mruby-compiler, making it suitable for embedded deployments where +** binary size matters. +** +** Typical workflow: +** +** # On the development machine (with full mruby + compiler): +** mrbc -o program.mrb program.rb +** +** # On the target device (with mrb only, no compiler): +** mrb program.mrb +** +** By excluding mruby-compiler (and gems that depend on it such as +** mruby-eval, mruby-binding, mruby-bin-mirb), the resulting binary +** can be significantly smaller (~300KB+ savings on the text segment). +*/ + +#include + +#ifdef MRB_NO_STDIO +# error mruby-bin-mrb conflicts 'MRB_NO_STDIO' in your build configuration +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# include +# include +#endif + +struct mrb_args { + FILE *rfp; + char *cmdline; + mrb_bool fname : 1; + mrb_bool verbose : 1; + mrb_bool version : 1; + mrb_bool debug : 1; + int argc; + char **argv; + int libc; + char **libv; +}; + +static void +usage(const char *name) +{ + static const char *const usage_msg[] = { + "switches:", + "-d set debugging flags (set $DEBUG to true)", + "-r library load the library (.mrb) before executing your script", + "-v print version number, then run in verbose mode", + "--verbose run in verbose mode", + "--version print the version", + "--copyright print the copyright", + NULL + }; + const char *const *p = usage_msg; + + printf("Usage: %s [switches] programfile.mrb [arguments]\n", name); + while (*p) + printf(" %s\n", *p++); +} + +struct options { + int argc; + char **argv; + char *program; + char *opt; + char short_opt[2]; +}; + +static void +options_init(struct options *opts, int argc, char **argv) +{ + opts->argc = argc; + opts->argv = argv; + opts->program = *argv; + *opts->short_opt = 0; +} + +static const char * +options_opt(struct options *opts) +{ + /* concatenated short options (e.g. `-dv`) */ + if (*opts->short_opt && *++opts->opt) { + opts->short_opt[0] = *opts->opt; + opts->short_opt[1] = 0; + return opts->short_opt; + } + + while (++opts->argv, --opts->argc) { + opts->opt = *opts->argv; + + /* not start with `-` or just `-` */ + if (opts->opt[0] != '-' || !opts->opt[1]) return NULL; + + if (opts->opt[1] == '-') { + /* `--` */ + if (!opts->opt[2]) { + opts->argv++, opts->argc--; + return NULL; + } + /* long option */ + opts->opt += 2; + *opts->short_opt = 0; + return opts->opt; + } + else { + /* short option */ + opts->opt++; + opts->short_opt[0] = *opts->opt; + opts->short_opt[1] = 0; + return opts->short_opt; + } + } + return NULL; +} + +static const char * +options_arg(struct options *opts) +{ + if (*opts->short_opt && opts->opt[1]) { + /* concatenated short option and argument (e.g. `-rlibrary`) */ + *opts->short_opt = 0; + return opts->opt + 1; + } + --opts->argc, ++opts->argv; + return opts->argc ? *opts->argv : NULL; +} + +static char * +dup_arg_item(mrb_state *mrb, const char *item) +{ + size_t buflen = strlen(item) + 1; + char *buf = (char*)mrb_malloc(mrb, buflen); + memcpy(buf, item, buflen); + return buf; +} + +static int +parse_args(mrb_state *mrb, int argc, char **argv, struct mrb_args *args) +{ + static const struct mrb_args args_zero = { 0 }; + struct options opts[1]; + const char *opt, *item; + + *args = args_zero; + options_init(opts, argc, argv); + while ((opt = options_opt(opts))) { + if (strcmp(opt, "d") == 0) { + args->debug = TRUE; + } + else if (strcmp(opt, "h") == 0) { + usage(opts->program); + exit(EXIT_SUCCESS); + } + else if (strcmp(opt, "r") == 0) { + if ((item = options_arg(opts))) { + if (args->libc == 0) { + args->libv = (char**)mrb_malloc(mrb, sizeof(char*)); + } + else { + args->libv = (char**)mrb_realloc(mrb, args->libv, sizeof(char*) * (args->libc + 1)); + } + args->libv[args->libc++] = dup_arg_item(mrb, item); + } + else { + fprintf(stderr, "%s: No library specified for -r\n", opts->program); + return EXIT_FAILURE; + } + } + else if (strcmp(opt, "v") == 0) { + if (!args->verbose) { + mrb_show_version(mrb); + args->version = TRUE; + } + args->verbose = TRUE; + } + else if (strcmp(opt, "version") == 0) { + mrb_show_version(mrb); + exit(EXIT_SUCCESS); + } + else if (strcmp(opt, "verbose") == 0) { + args->verbose = TRUE; + } + else if (strcmp(opt, "copyright") == 0) { + mrb_show_copyright(mrb); + exit(EXIT_SUCCESS); + } + else { + fprintf(stderr, "%s: invalid option %s%s (-h will show valid options)\n", + opts->program, opt[1] ? "--" : "-", opt); + return EXIT_FAILURE; + } + } + + argc = opts->argc; argv = opts->argv; + if (*argv == NULL) { + if (args->version) exit(EXIT_SUCCESS); + fprintf(stderr, "%s: no program file given (only .mrb files are supported)\n", + opts->program); + return EXIT_FAILURE; + } + args->rfp = strcmp(argv[0], "-") == 0 ? + stdin : fopen(argv[0], "rb"); + if (args->rfp == NULL) { + fprintf(stderr, "%s: Cannot open program file: %s\n", opts->program, argv[0]); + return EXIT_FAILURE; + } + args->fname = TRUE; + args->cmdline = argv[0]; + argc--; argv++; + +#if defined(_WIN32) + if (args->rfp == stdin) { + _setmode(_fileno(stdin), O_BINARY); + } +#endif + args->argv = (char **)mrb_realloc(mrb, args->argv, sizeof(char*) * (argc + 1)); + memcpy(args->argv, argv, (argc+1) * sizeof(char*)); + args->argc = argc; + + return EXIT_SUCCESS; +} + +static void +cleanup(mrb_state *mrb, struct mrb_args *args) +{ + if (args->rfp && args->rfp != stdin) + fclose(args->rfp); + mrb_free(mrb, args->argv); + if (args->libc) { + while (args->libc--) { + mrb_free(mrb, args->libv[args->libc]); + } + mrb_free(mrb, args->libv); + } + mrb_close(mrb); +} + +int +main(int argc, char **argv) +{ + mrb_state *mrb = mrb_open(); + int n = -1; + struct mrb_args args; + mrb_value ARGV; + mrb_value v; + + if (MRB_OPEN_FAILURE(mrb)) { + mrb_print_error(mrb); + mrb_close(mrb); + return EXIT_FAILURE; + } + + n = parse_args(mrb, argc, argv, &args); + if (n == EXIT_FAILURE || args.rfp == NULL) { + cleanup(mrb, &args); + return n; + } + + int ai = mrb_gc_arena_save(mrb); + ARGV = mrb_ary_new_capa(mrb, args.argc); + for (int i = 0; i < args.argc; i++) { + char* utf8 = mrb_utf8_from_locale(args.argv[i], -1); + if (utf8) { + mrb_ary_push(mrb, ARGV, mrb_str_new_cstr(mrb, utf8)); + mrb_utf8_free(utf8); + } + } + mrb_define_global_const(mrb, "ARGV", ARGV); + mrb_gv_set(mrb, mrb_intern_lit(mrb, "$DEBUG"), mrb_bool_value(args.debug)); + + mrb_ccontext *c = mrb_ccontext_new(mrb); + if (args.verbose) + c->dump_result = TRUE; + + /* Set $0 */ + const char *cmdline = args.cmdline ? args.cmdline : "-"; + mrb_gv_set(mrb, mrb_intern_lit(mrb, "$0"), mrb_str_new_cstr(mrb, cmdline)); + + /* Load libraries (.mrb only) */ + for (int i = 0; i < args.libc; i++) { + FILE *lfp = fopen(args.libv[i], "rb"); + if (lfp == NULL) { + fprintf(stderr, "%s: Cannot open library file: %s\n", *argv, args.libv[i]); + mrb_ccontext_free(mrb, c); + cleanup(mrb, &args); + return EXIT_FAILURE; + } + mrb_ccontext_filename(mrb, c, args.libv[i]); + mrb_load_irep_file_cxt(mrb, lfp, c); + fclose(lfp); + mrb_vm_ci_env_clear(mrb, mrb->c->cibase); + mrb_ccontext_cleanup_local_variables(c); + } + + /* Load and execute program (.mrb only) */ + mrb_ccontext_filename(mrb, c, cmdline); + c->no_return_value = TRUE; + v = mrb_load_irep_file_cxt(mrb, args.rfp, c); + + mrb_gc_arena_restore(mrb, ai); + mrb_ccontext_free(mrb, c); + if (mrb->exc) { + MRB_EXC_CHECK_EXIT(mrb, mrb->exc); + if (!mrb_undef_p(v)) { + mrb_print_error(mrb); + } + n = EXIT_FAILURE; + } + + cleanup(mrb, &args); + return n; +}