About

This post is a beginner-friendly introduction to writing C extensions for mruby. If you have used mruby before you know it is a lightweight, embeddable Ruby implementation. What makes it especially useful for embedded systems and performance-sensitive applications is that you can extend it with C code through the mrbgem system. We will walk through the anatomy of an mrbgem, the core C API that mruby provides, and build several small examples from scratch.

By the end you should understand enough to write your own C extensions: defining Ruby methods and classes in C, handling arguments, raising exceptions, and structuring your code as a reusable mrbgem.

Background

What is mruby?

mruby is a lightweight implementation of the Ruby language. It is designed to be embedded into C applications. Unlike CRuby (the standard Ruby interpreter), mruby has a smaller footprint, can be cross-compiled for different platforms, and exposes a clean C API that lets you define Ruby classes, methods, modules, and constants directly from C code.

Why write C for mruby?

The same reasons you might write a C extension for CRuby: performance, access to system libraries, or interfacing with hardware. But with mruby the bar is lower. The API is simpler, the build system is straightforward, and the whole runtime compiles in seconds. You might write C to wrap a native library like libjail on FreeBSD, implement a high-performance data structure, or expose POSIX system calls that mruby's standard library does not cover.

The mrbgem system

mruby extensions are packaged as "mrbgems". An mrbgem is a directory with a mrbgem.rake file that declares metadata and dependencies. It can contain C source files in src/ and Ruby files in mrblib/. When you build mruby, the build system compiles all mrbgems together into a single binary. There is no dynamic loading. Everything is linked statically.

The naming convention

Every mrbgem that contains C code must define two entry points:

void mrb_YOURGEM_gem_init(mrb_state *mrb);
void mrb_YOURGEM_gem_final(mrb_state *mrb);

Replace YOURGEM with your gem name. For example, mruby-jail uses mrb_mruby_jail_gem_init and mrb_mruby_jail_gem_final. The build system finds these symbols automatically.

Anatomy of an mrbgem

The simplest mrbgem has a mrbgem.rake file and at least one C source file. Let's start with something that adds a single method to Ruby's Kernel module.

mygem/
  mrbgem.rake
  src/
    mygem.c

The mrbgem.rake is a Ruby DSL that declares the gem metadata:

MRuby::Gem::Specification.new('mygem') do |spec|
  spec.license = 'MIT'
  spec.author  = 'Your Name'
  spec.summary = 'My first mruby C extension'
end

And src/mygem.c contains the C code:

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

static mrb_value
mrb_hello(mrb_state *mrb, mrb_value self)
{
  return mrb_str_new_lit(mrb, "Hello from C!");
}

void
mrb_mygem_gem_init(mrb_state *mrb)
{
  mrb_define_method(mrb, mrb->kernel_module, "hello",
                    mrb_hello, MRB_ARGS_NONE());
}

void
mrb_mygem_gem_final(mrb_state *mrb)
{
  (void)mrb;
}

Explanation

Working with arguments

A method that takes arguments uses mrb_get_args. This is the mruby equivalent of Ruby's |a, b, c| parameter list. The format string tells mruby what types to expect and where to store them.

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

static mrb_value
mrb_greet(mrb_state *mrb, mrb_value self)
{
  mrb_value name;
  mrb_get_args(mrb, "S", &name);

  mrb_value result = mrb_str_new_lit(mrb, "Hello, ");
  mrb_str_concat(mrb, result, name);
  mrb_str_cat_lit(mrb, result, "!");
  return result;
}

void
mrb_mygem_gem_init(mrb_state *mrb)
{
  mrb_define_method(mrb, mrb->kernel_module, "greet",
                    mrb_greet, MRB_ARGS_REQ(1));
}

Explanation

Common format specifiers

Spec Ruby type C type
o Object mrb_value
S String mrb_value
A Array mrb_value
H Hash mrb_value
s String const char*, mrb_int
z String (null-terminated) const char*
i Integer/Float mrb_int
f Integer/Float mrb_float
b boolean mrb_bool
n String/Symbol mrb_sym
c Class/Module struct RClass*
| Everything after is optional (none)
? Was the preceding arg given? mrb_bool
* Rest arguments mrb_value*, mrb_int
& Block mrb_value

The ! modifier changes behaviour: s! gives (NULL, 0) for nil, z! gives NULL for nil, and so on. The + modifier requests a non-frozen value.

Defining a class

Defining a Ruby class in C works the same way as defining methods on Kernel, but you create the class first with mrb_define_class.

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

static mrb_value
mrb_counter_init(mrb_state *mrb, mrb_value self)
{
  mrb_int initial = 0;
  mrb_get_args(mrb, "|i", &initial);
  mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@count"),
             mrb_fixnum_value(initial));
  return self;
}

static mrb_value
mrb_counter_inc(mrb_state *mrb, mrb_value self)
{
  mrb_value count = mrb_iv_get(mrb, self,
                                mrb_intern_lit(mrb, "@count"));
  mrb_int n = mrb_fixnum(count);
  mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@count"),
             mrb_fixnum_value(n + 1));
  return mrb_fixnum_value(n + 1);
}

static mrb_value
mrb_counter_get(mrb_state *mrb, mrb_value self)
{
  return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@count"));
}

void
mrb_mygem_gem_init(mrb_state *mrb)
{
  struct RClass *counter;

  counter = mrb_define_class(mrb, "Counter", mrb->object_class);
  mrb_define_method(mrb, counter, "initialize",
                    mrb_counter_init, MRB_ARGS_OPT(1));
  mrb_define_method(mrb, counter, "inc",
                    mrb_counter_inc, MRB_ARGS_NONE());
  mrb_define_method(mrb, counter, "get",
                    mrb_counter_get, MRB_ARGS_NONE());
}

In Ruby, this is equivalent to:

class Counter
  def initialize(initial = 0)
    @count = initial
  end

  def inc
    @count += 1
  end

  def get
    @count
  end
end

Explanation

Raising exceptions and defining modules

You can raise Ruby exceptions from C with mrb_raise, and you can organise methods into modules with mrb_define_module.

#include <mruby.h>
#include <mruby/error.h>
#include <mruby/string.h>

static mrb_value
mrb_divide(mrb_state *mrb, mrb_value self)
{
  mrb_int a, b;

  mrb_get_args(mrb, "ii", &a, &b);
  if (b == 0) {
    mrb_raise(mrb, E_ARGUMENT_ERROR,
              "division by zero is not allowed");
  }
  return mrb_fixnum_value(a / b);
}

static mrb_value
mrb_math_square(mrb_state *mrb, mrb_value self)
{
  mrb_int n;

  mrb_get_args(mrb, "i", &n);
  return mrb_fixnum_value(n * n);
}

void
mrb_mygem_gem_init(mrb_state *mrb)
{
  struct RClass *math_mod;

  mrb_define_method(mrb, mrb->kernel_module, "divide",
                    mrb_divide, MRB_ARGS_REQ(2));

  math_mod = mrb_define_module(mrb, "MyMath");
  mrb_define_module_function(mrb, math_mod, "square",
                             mrb_math_square, MRB_ARGS_REQ(1));
}

Explanation

Wrapping a C struct with data types

For wrapping native C structs (like a file descriptor, a database handle, or a system object), mruby provides mrb_data_... functions and MRB_TT_CDATA. This lets you attach a C pointer to a Ruby object and control its lifecycle.

The mruby-jail gem demonstrates this pattern by wrapping FreeBSD's jailparam struct. Here is a simplified example that wraps a POSIX FILE*:

#include <mruby.h>
#include <mruby/string.h>
#include <mruby/data.h>
#include <mruby/error.h>
#include <stdio.h>

static void
file_free(mrb_state *mrb, void *ptr)
{
  FILE *fp = (FILE*)ptr;
  if (fp) fclose(fp);
}

static const mrb_data_type file_type = {
  "File", file_free
};

static mrb_value
mrb_file_open(mrb_state *mrb, mrb_value self)
{
  mrb_value path;
  FILE *fp;

  mrb_get_args(mrb, "S", &path);
  fp = fopen(mrb_string_value_cstr(mrb, &path), "r");
  if (!fp) {
    mrb_sys_fail(mrb, "fopen failed");
  }
  return mrb_obj_value(
    Data_Wrap_Struct(mrb, mrb->object_class, &file_type, fp));
}

static mrb_value
mrb_file_read(mrb_state *mrb, mrb_value self)
{
  FILE *fp = DATA_PTR(self);

  if (!fp) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "file is closed");
  }
  Data_Get_Struct(mrb, self, &file_type, FILE, fp);

  char buf[4096];
  size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
  buf[n] = '\0';
  return mrb_str_new_cstr(mrb, buf);
}

void
mrb_mygem_gem_init(mrb_state *mrb)
{
  mrb_define_method(mrb, mrb->kernel_module, "fopen",
                    mrb_file_open, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, mrb->kernel_module, "fread",
                    mrb_file_read, MRB_ARGS_NONE());
}

Explanation

Build setup

Build configuration

To build your mrbgem, you need a build.rb that tells mruby's build system what to do. Here is a minimal one:

MRuby::Build.new("mygem") do |conf|
  conf.toolchain
  conf.gembox "default"
  conf.gem File.expand_path(__dir__)
end

Build with:

ruby build.rb

The output binary (usually build/mygem/bin/mruby) contains your extension linked in. You can run Ruby code that calls your C functions directly.

Dependencies

If your gem depends on a system library (like libjail or libcurl), add linker flags in mrbgem.rake:

MRuby::Gem::Specification.new('mygem') do |spec|
  spec.license = 'MIT'
  spec.author  = 'Your Name'
  spec.summary = 'Wraps a system library'

  spec.cc.flags << '-Wall'
  spec.cc.flags << '-Wpedantic'
  spec.linker.libraries << 'curl'
end

Dependencies on other mrbgems

If your gem depends on another mrbgem (e.g. mruby-io or mruby-process), add the dependency in mrbgem.rake:

MRuby::Gem::Specification.new('mygem') do |spec|
  spec.license = 'MIT'
  spec.author  = 'Your Name'
  spec.summary = 'A gem with dependencies'

  spec.add_dependency 'mruby-io'
  spec.add_dependency 'mruby-json', github: 'some/repo', branch: 'main'
end

Anatomy reference

Directory structure

A complete mrbgem looks like this:

mygem/
  build.rb          # Build configuration
  mrbgem.rake       # Gem metadata and dependencies
  src/
    mygem.c         # C source files
  mrblib/
    mygem.rb        # Optional Ruby files (loaded after C init)

The init/final contract

Every C-based mrbgem must provide two symbols:

void mrb_YOURGEM_gem_init(mrb_state *mrb) {
    // Called when the gem is loaded.
    // Define classes, modules, methods, and constants here.
}

void mrb_YOURGEM_gem_final(mrb_state *mrb) {
    // Called when the VM closes.
    // Clean up global resources here.
}

The build system finds these symbols automatically by substituting the gem name from mrbgem.rake into the symbol pattern mrb_<name>_gem_init (dots and hyphens are replaced with underscores).

Ruby files in mrblib/

C files are compiled first, then Ruby files in mrblib/ are loaded. This lets you define pure-Ruby methods that call your C functions, or add convenience wrappers on top of the C layer. For example:

# mrblib/mygem.rb
class Counter
  # The C code defines initialize, inc, and get.
  # Add a convenience method in Ruby:
  def reset
    @count = 0
  end
end

Conclusion

Writing C extensions for mruby is more approachable than it might seem. The API surface is small and consistent. You include <mruby.h>, write functions that take (mrb_state*, mrb_value) and return mrb_value, and register them with mrb_define_method or similar. The build system handles the rest.

The examples in this post cover the most common patterns you will need: defining methods and classes, accepting arguments with mrb_get_args, raising exceptions, wrapping C structs with data types, and organising code into modules. Real-world mrbgems like mruby-process, mruby-jail, and mruby-llm all follow these same patterns.

Further topics worth exploring include the mrb_gc_arena_save and mrb_gc_arena_restore macros for managing the GC arena in tight loops, mrb_funcall for calling back into Ruby from C, and the mruby-curl and mruby-http gems for network access from embedded mruby programs.