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
-
#include <mruby.h>
The main header. It pulls in the core types and functions. You will usually also include<mruby/string.h>,<mruby/array.h>,<mruby/hash.h>,<mruby/error.h>, or whatever types you need. -
mrb_state *mrb
Every mruby C function receives a pointer to the VM state. This is your connection to the Ruby runtime. It holds the stack, the GC, and all class references. -
mrb_value self
The Ruby object thatselfrefers to when the method is called. For a method defined onKernel, this ismain(the top-level object). -
mrb_str_new_lit(mrb, "...")
Creates a Ruby String from a C string literal. There is no copying of the literal, so use it only with real string literals. -
mrb_define_method(mrb, mrb->kernel_module, "hello", ...)
Defines a method namedhelloon Ruby's Kernel module, which means it is available everywhere (like Ruby'sputsorsleep). -
MRB_ARGS_NONE()
Declares that the method takes no arguments. Other argument macros areMRB_ARGS_REQ(n),MRB_ARGS_OPT(n),MRB_ARGS_ANY(), andMRB_ARGS_ARG(n1, n2)for required-plus-optional. -
mrb_mygem_gem_initandmrb_mygem_gem_final
The init function is called when the gem is loaded. The final function is called when the VM is torn down. Both must exist, even if final does nothing.
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
-
mrb_get_args(mrb, "S", &name)
The format string"S"expects one String argument. mruby checks the type and raisesTypeErrorautomatically if the caller passes something else. The result is stored in themrb_valuevariablename. -
mrb_str_concat(mrb, result, name)
Appends the Ruby stringnameto the Ruby stringresult. This works likeresult << namein Ruby. -
mrb_str_cat_lit(mrb, result, "!")
Appends a C string literal to a Ruby string.
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
-
mrb_define_class(mrb, "Counter", mrb->object_class)
Creates a class namedCounterthat inherits fromObject. The returnedstruct RClass*is used to attach methods. -
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@count"), ...)
Sets an instance variable.mrb_intern_litcreates a Ruby symbol from a C string literal. Instance variable names start with@in the symbol, just like in Ruby. -
mrb_iv_get(mrb, self, ...)
Reads an instance variable. Returnsnilif not set. -
mrb_fixnum_value(n)
Wraps a Cmrb_intinto anmrb_value. This is the most common way to return integers from a C function. -
MRB_ARGS_OPT(1)
Theinitializemethod accepts one optional argument, matching Ruby'sdef initialize(initial = 0).
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
-
mrb_raise(mrb, E_ARGUMENT_ERROR, "...")
Raises a Ruby exception. The second argument is the exception class. mruby providesE_RUNTIME_ERROR,E_TYPE_ERROR,E_ARGUMENT_ERROR,E_INDEX_ERROR,E_KEY_ERROR, and others. These are all available through theE_*macros. -
mrb_define_module(mrb, "MyMath")
Defines a Ruby module. It returns astruct RClass*(modules and classes share the same C type in mruby). -
mrb_define_module_function(mrb, math_mod, "square", ...)
Defines a module function. This makessquareavailable as bothMyMath.squareand as a private method on any class that includesMyMath. -
mrb_sys_fail(mrb, "message")
A related function. Usemrb_sys_failwhen a POSIX system call fails. It raisesRuntimeErrorwith the message plusstrerror(errno).
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
-
mrb_data_type
A struct that pairs a type name (for debugging) with a free function. The free function is called when the Ruby object is garbage collected. This is where you release the underlying C resource. -
Data_Wrap_Struct(mrb, class, &file_type, ptr)
Wraps a C pointer into a Ruby object. The third argument is the data type descriptor. The return value is anmrb_valuethat holds your C pointer. -
DATA_PTR(self)
Gets the raw C pointer from a data-wrapped Ruby object. Use this when you just need the pointer and do not need type checking. -
Data_Get_Struct(mrb, self, &file_type, FILE, fp)
Gets the pointer with type checking. RaisesTypeErrorif the data type does not match. The last two arguments are the C type name and the variable name (used by a macro to declare the local variable).
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.