C Extensions, FFI and WebAssembly

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. Ruby is a high-level language, and sooner or later a program has to cross its boundary: to reuse a C library, to make a hot loop fast, to read a hardware or operating-system interface that has no Ruby binding, or to run Ruby somewhere that has no Ruby installed. This page covers the four routes Ruby offers, in decreasing order of power and increasing order of convenience: a C extension written against the Ruby C API; Fiddle, a libffi wrapper that was a default gem and became a bundled gem in Ruby 4.0; the community ffi gem; and ruby.wasm, the WebAssembly port of CRuby for browsers and WASI runtimes. It also covers the Memory View C API, the experimental way for extensions to share raw arrays without copying.

The scenario is the running shelf gem. The ISBN-13 check digit that its Isbn value object computes in Ruby (see Struct, Data and Value Objects) is reimplemented here as a few lines of C and reached through every route. That is a deliberately tiny job: for ISBN validation a C extension is not worth its cost, and the page says so in the section on when to use one.

Every Ruby example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 (arm64, macOS, Apple clang) and linted with RuboCop using the configuration of the shelf gem. The C code was compiled with the compiler and flags that mkmf chose, and every output shown is the real output of that run. Two things were not run, and are labelled where they appear: the ffi gem and every WebAssembly runtime are not installed in the environment used to prepare this page, so their examples are copied in shape from the official documentation and only syntax-checked.

Packaging and publishing a gem that contains an extension is in Creating and Publishing Gems, and Rake; what the garbage collector does with the objects an extension keeps alive is in Garbage Collection and Memory; the GVL that every extension must respect is in Threads and the GVL; and the Ractor rules that extension authors opt in to are in Ractors. The same problem in other languages is in Rust FFI and C interop, Go cgo, Node.js native addons, WebAssembly and FFI and, for the C side of the fence, C pointers and C build and tooling.

Choosing a route

The routes differ in what you must write, what can go wrong and where the result runs.

Route You write Strengths Costs and limits

C extension

C code against the Ruby C API, an extconf.rb, a gemspec entry

Full access to the VM: define classes, wrap structs, raise exceptions, mark and compact objects, release the GVL, share memory views. Fastest call path.

A compiler at install time (or a precompiled gem per platform). Mistakes crash the interpreter. Tied to CRuby’s C API.

Fiddle

Ruby code only

Ships with Ruby (a bundled gem since 4.0), no compiler needed, calls any C function in a shared library, supports callbacks and struct layouts.

No type checking: a wrong signature or pointer crashes the process. Needs libffi. Needs a Gemfile entry under Bundler since 4.0.

ffi gem

Ruby code only (community gem)

A friendlier DSL than Fiddle: attach_function, structs, enums, callbacks, automatic release of native memory. Also runs on JRuby and TruffleRuby.

A third-party dependency that compiles a small extension on CRuby. Same fundamental danger as Fiddle.

ruby.wasm

Ruby code, plus JavaScript or a host program

CRuby compiled to WebAssembly: runs in a browser, Node.js, wasmtime and other WASI runtimes with nothing installed.

No threads and no networking on the WASI target; native gems must be built for WebAssembly; the JavaScript bridge is its own API.

flowchart TD start([A Ruby program needs native code]) --> q1{Is a C library
already available
as a shared library?} q1 -- no --> q2{Does the work need
the Ruby object model
or GC integration?} q1 -- yes --> q3{Is a compiler
acceptable at install
time?} q2 -- yes --> cext[C extension] q2 -- no --> q4{Is it a hot loop
that YJIT does not
already speed up?} q4 -- yes --> cext q4 -- no --> ruby[Stay in Ruby] q3 -- yes --> q5{Does it need structs
by value or deep
Ruby integration?} q3 -- no --> fiddle[Fiddle or ffi gem] q5 -- yes --> cext q5 -- no --> fiddle start --> q6{Must Ruby run in a
browser or sandbox?} q6 -- yes --> wasm[ruby.wasm]

A useful rule of thumb: measure first. YJIT (see YJIT, ZJIT and the VM) and better algorithms remove most reasons to drop to C. Reach for native code when a library already exists, when a system interface has no Ruby binding, or when profiling (see Performance, Benchmarking and Profiling) shows a loop that Ruby cannot make fast enough.

C extensions

A C extension is a shared library that the Ruby VM loads with require. When the library is loaded, Ruby calls the function named Init_ followed by the file name, for example Init_shelf_native for shelf_native, and that function defines the classes, modules and methods the extension provides. The official guide is the Extension documentation; it is the source for the API facts below unless a line says otherwise.

The build pipeline: extconf.rb and mkmf

An extension directory holds an extconf.rb and the C sources. Running ruby extconf.rb asks the standard mkmf library to probe the compiler and the platform and to write a Makefile; make then compiles and links the shared library.

flowchart LR A[extconf.rb] -->|ruby extconf.rb
mkmf probes the compiler| B[Makefile] C[shelf_native.c] --> D B -->|make| D[shelf_native.o] D -->|link| E[shelf_native.bundle
or .so, .dll] E -->|require| F[Init_shelf_native runs
in the VM]
# frozen_string_literal: true

require "mkmf"

append_cflags("-Wshadow")
have_func("rb_memory_view_get", "ruby/memory_view.h")
create_makefile("shelf/shelf_native")

Building it:

$ ruby extconf.rb
checking for whether -Wshadow is accepted as CFLAGS... yes
checking for rb_memory_view_get() in ruby/memory_view.h... yes
creating Makefile
$ make
compiling shelf_native.c
linking shared-object shelf/shelf_native.bundle

What each line does:

  • require "mkmf" loads the MakeMakefile module, whose methods are the checks.

  • append_cflags adds a compiler flag only if the compiler accepts it; here -Wshadow was accepted and appears in the generated Makefile. The flags Ruby itself was built with (warning options, optimisation level, architecture) are already part of the compile command, as mkmf.log shows. append_cppflags and append_ldflags do the same for the preprocessor and the linker.

  • have_func(func, headers) checks that a function exists and, if it does, defines HAVE_ plus the upper-cased name (here HAVE_RB_MEMORY_VIEW_GET) for the C code to test with #ifdef. Related checks: have_header, have_library, find_library, have_macro, have_type, have_struct_member, have_const, check_sizeof, find_executable, create_header.

  • create_makefile("shelf/shelf_native") names the target. The part before the slash is the directory under the load path where the library will be installed (shelf/), the part after is the file name, and it must match the suffix of Init_. The mismatch between the target name and the Init_ function is the most common reason for "undefined symbol" load errors.

Two files help when a check fails. mkmf.log records every probe, with the exact command and the compiler’s output, and is the first thing to read when extconf.rb exits with a *** extconf.rb failed *** banner. The Makefile records the flags in use, which come from RbConfig::CONFIG, so the extension is compiled against the same ABI as the Ruby that ran extconf.rb: an extension built for one Ruby minor version must be rebuilt for the next.

Values, types and conversions

Every Ruby object in C is a VALUE, a machine word. Small integers, nil, true, false and symbols are encoded in the word itself; every other object is a pointer to a heap structure. The extension guide lists the basic types as T_NIL, T_FALSE, T_TRUE, T_FIXNUM, T_STRING, T_ARRAY, T_HASH, T_DATA and so on, the singletons as Qnil, Qfalse and Qtrue, and a fixnum as a 63-bit integer when long is 64 bits. Never compare a VALUE with 0 or treat it as a pointer without checking its type.

C API What it does

FIXNUM_P(obj), Check_Type(value, T_STRING)

Test or assert the type; Check_Type raises TypeError on a mismatch.

NUM2INT, NUM2LONG, INT2FIX, INT2NUM, LONG2NUM, ULONG2NUM

Convert between C integers and Ruby integers. INT2FIX is only for values that surely fit a fixnum; INT2NUM and LONG2NUM fall back to a big integer.

StringValue(str), RSTRING_PTR(str), RSTRING_LEN(str)

Coerce with to_str (raising TypeError: no implicit conversion of Integer into String otherwise), then read the bytes and the length. The buffer is not NUL-terminated and belongs to the object.

rb_str_new(ptr, len), rb_str_new_cstr(ptr), rb_str_buf_new(capa), rb_str_cat(str, ptr, len)

Create and grow strings from C data.

rb_sprintf("%"PRIsVALUE, obj)

printf for Ruby, where PRIsVALUE prints the to_s of an object.

A string handed to C is bytes plus an encoding; the C API does not decode it. How encodings work, and why a check digit routine must not assume that a Ruby string is ASCII, is in Encodings and Unicode.

A complete extension: check digits, strings and a wrapped struct

The extension below defines a module ShelfNative with module functions that compute and validate parts of an ISBN-13 and a class ShelfNative::Tally that wraps a C struct. It is shown in three parts; together they are the single file that was compiled.

The first part holds the plain C helper and two methods that read and build Ruby strings.

#include <ruby.h>
#include <ruby/memory_view.h>

/* ISBN-13: weights 1,3,1,3,... over the first 12 digits. */
static int
check_digit_of(const char *digits)
{
    int sum = 0;

    for (int i = 0; i < 12; i++) {
        sum += (digits[i] - '0') * (i % 2 == 0 ? 1 : 3);
    }
    return (10 - sum % 10) % 10;
}

/* ShelfNative.check_digit("978030640615") => 7 */
static VALUE
shelf_check_digit(VALUE self, VALUE str)
{
    StringValue(str);
    if (RSTRING_LEN(str) != 12) {
        rb_raise(rb_eArgError, "expected 12 digits, got %ld bytes", RSTRING_LEN(str));
    }

    const char *p = RSTRING_PTR(str);
    for (int i = 0; i < 12; i++) {
        if (p[i] < '0' || p[i] > '9') {
            rb_raise(rb_eArgError, "not a digit at index %d", i);
        }
    }

    int digit = check_digit_of(p);
    RB_GC_GUARD(str); /* keep str (and so p) alive up to here */
    return INT2FIX(digit);
}

/* ShelfNative.digits_only("978-0-306-40615-7") => "9780306406157" */
static VALUE
shelf_digits_only(VALUE self, VALUE str)
{
    StringValue(str);

    VALUE out = rb_str_buf_new(RSTRING_LEN(str));
    const char *p = RSTRING_PTR(str);
    long len = RSTRING_LEN(str);

    for (long i = 0; i < len; i++) {
        if (p[i] >= '0' && p[i] <= '9') {
            rb_str_cat(out, p + i, 1); /* may allocate, and so trigger a GC */
        }
    }
    RB_GC_GUARD(str);
    return out;
}

The second part wraps a C struct in a Ruby object. The section "Wrapping C structs with TypedData" below explains every line of it.

/* A C struct owned by a Ruby object: ShelfNative::Tally. */
typedef struct {
    long total;
    long count;
    VALUE label;
} tally_t;

static void
tally_mark(void *ptr)
{
    tally_t *t = ptr;
    rb_gc_mark_movable(t->label);
}

static void
tally_compact(void *ptr)
{
    tally_t *t = ptr;
    t->label = rb_gc_location(t->label);
}

static size_t
tally_memsize(const void *ptr)
{
    return sizeof(tally_t);
}

static const rb_data_type_t tally_type = {
    .wrap_struct_name = "shelf_native/tally",
    .function = {
        .dmark = tally_mark,
        .dfree = RUBY_DEFAULT_FREE,
        .dsize = tally_memsize,
        .dcompact = tally_compact,
    },
    .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
};

static VALUE
tally_alloc(VALUE klass)
{
    tally_t *t;
    VALUE obj = TypedData_Make_Struct(klass, tally_t, &tally_type, t);

    t->label = Qnil;
    return obj;
}

static VALUE
tally_initialize(VALUE self, VALUE label)
{
    tally_t *t;

    TypedData_Get_Struct(self, tally_t, &tally_type, t);
    RB_OBJ_WRITE(self, &t->label, label);
    return self;
}

static VALUE
tally_add(VALUE self, VALUE n)
{
    tally_t *t;

    TypedData_Get_Struct(self, tally_t, &tally_type, t);
    t->total += NUM2LONG(n);
    t->count += 1;
    return self;
}

static VALUE
tally_total(VALUE self)
{
    tally_t *t;

    TypedData_Get_Struct(self, tally_t, &tally_type, t);
    return LONG2NUM(t->total);
}

static VALUE
tally_label(VALUE self)
{
    tally_t *t;

    TypedData_Get_Struct(self, tally_t, &tally_type, t);
    return t->label;
}

static VALUE
tally_inspect(VALUE self)
{
    tally_t *t;

    TypedData_Get_Struct(self, tally_t, &tally_type, t);
    return rb_sprintf("#<ShelfNative::Tally %"PRIsVALUE": %ld loans, total %ld>",
                      t->label, t->count, t->total);
}

The third part consumes a Memory View (see "The Memory View C API" below) and holds Init_shelf_native, which registers everything.

/* ShelfNative.byte_sum(obj): consumer side of the Memory View API. */
static VALUE
shelf_byte_sum(VALUE self, VALUE obj)
{
    rb_memory_view_t view;

    if (!rb_memory_view_get(obj, &view, RUBY_MEMORY_VIEW_SIMPLE)) {
        rb_raise(rb_eTypeError, "%"PRIsVALUE" does not export a memory view", rb_obj_class(obj));
    }

    unsigned long sum = 0;
    const unsigned char *bytes = view.data;
    for (ssize_t i = 0; i < view.byte_size; i++) {
        sum += bytes[i];
    }
    rb_memory_view_release(&view);
    return ULONG2NUM(sum);
}

void
Init_shelf_native(void)
{
    rb_ext_ractor_safe(true);

    VALUE mod = rb_define_module("ShelfNative");
    rb_define_module_function(mod, "check_digit", shelf_check_digit, 1);
    rb_define_module_function(mod, "digits_only", shelf_digits_only, 1);
    rb_define_module_function(mod, "byte_sum", shelf_byte_sum, 1);

    VALUE tally = rb_define_class_under(mod, "Tally", rb_cObject);
    rb_define_alloc_func(tally, tally_alloc);
    rb_define_method(tally, "initialize", tally_initialize, 1);
    rb_define_method(tally, "add", tally_add, 1);
    rb_define_method(tally, "total", tally_total, 0);
    rb_define_method(tally, "label", tally_label, 0);
    rb_define_method(tally, "inspect", tally_inspect, 0);
}

Using it from Ruby, with the real results:

# frozen_string_literal: true

require_relative "../ext/shelf_native/shelf_native"

ShelfNative.digits_only("978-0-306-40615-7") # => "9780306406157"
ShelfNative.check_digit("978030640615")      # => 7

begin
  ShelfNative.check_digit("97803064")
rescue ArgumentError => e
  e.message # => "expected 12 digits, got 8 bytes"
end

begin
  ShelfNative.check_digit(42)
rescue TypeError => e
  e.message # => "no implicit conversion of Integer into String"
end

tally = ShelfNative::Tally.new("Ada")
tally.add(3).add(4)
tally.inspect # => #<ShelfNative::Tally Ada: 2 loans, total 7>
tally.total # => 7
GC.compact
tally.label # => "Ada"

The C source compiled without warnings under the flags mkmf chose plus -Wshadow, and the script ran without warnings under ruby -w.

Defining modules, classes and methods

The registration calls in Init_shelf_native are the heart of the API:

  • rb_define_module(name), rb_define_class(name, superclass), rb_define_class_under(outer, name, superclass) and rb_define_module_under create or reopen modules and classes.

  • rb_define_method(klass, "name", func, argc) defines an instance method. rb_define_module_function defines a method that is both a private instance method and a public singleton method, which is what Math.sqrt is. rb_define_singleton_method, rb_define_private_method and rb_define_alloc_func cover class methods, visibility and allocation.

  • The argc argument selects the C signature. A non-negative number n means the function receives the receiver followed by exactly n VALUE arguments, and Ruby raises ArgumentError for any other count. -1 means the function receives (int argc, VALUE *argv, VALUE self) and handles any count, typically with rb_scan_args; -2 passes the arguments as one Ruby array.

  • rb_scan_args(argc, argv, "11", &required, &optional) decodes a format string: digits give the required and optional counts, and further letters add a rest argument, post-required arguments, keyword arguments and a block. rb_check_arity only checks the count. The implicit and explicit conversions that these argument checks rely on are described in Conversions and Coercion.

Ruby method names with punctuation ([], empty?, name=) are just strings in the registration call. The C function name is a free choice, and static keeps it out of the shared library’s exported symbols.

Exceptions and non-local exits

rb_raise(klass, "format", ...) builds an exception and unwinds the stack; it does not return. That is a longjmp-style non-local exit, and it is the source of the classic leak: any resource acquired before the call, such as memory from malloc, a file descriptor or a library handle, is leaked when something deeper raises. The guide’s tools:

  • rb_protect(func, arg, &state) calls func and returns with a non-zero state instead of unwinding when it raises. Release your resource, then re-raise with rb_jump_tag(state).

  • rb_ensure(body, arg1, cleanup, arg2) is the C counterpart of Ruby ensure: cleanup always runs. Use it when an acquired resource must be released whether or not the body raises. The Memory View consumer above releases the view on the normal path; a body that could raise between rb_memory_view_get and rb_memory_view_release should be wrapped in rb_ensure.

  • Prefer allocation macros that fail safely. The guide says to use TypedData_Make_Struct rather than "allocate, then wrap", because the second form can leak the structure if NoMemoryError is raised between the two steps.

  • Validate arguments before acquiring anything. shelf_check_digit raises before it allocates, so there is nothing to clean up.

How Ruby-level exceptions map to classes and messages is in Exceptions and Error Handling.

Garbage-collector safety

CRuby’s collector is conservative about the machine stack: it treats anything on the stack or in registers that looks like a pointer to a Ruby object as a reference. Two consequences drive every rule in this section.

An object only referenced by a C local that the compiler optimised away can be collected. The guide’s example takes RSTRING_PTR(s) into a local pointer and then calls an allocating function; after the last use of the VALUE s itself, an optimising compiler may reuse its register, the collector sees no reference, and the pointer dangles. RB_GC_GUARD(s) placed after the last use of the pointer keeps the VALUE visible. It protects VALUE`s only, not converted C data. If you instead call any non-inlined function on `s after the last use of the pointer, such as rb_str_modify(s), no guard is needed. Both shelf_check_digit and shelf_digits_only above follow the pattern; shelf_digits_only is the case that matters, because rb_str_cat allocates and a collection can start in it. The guide prefers RB_GC_GUARD to the volatile keyword, because it affects only its call site and does not hurt optimisation.

A VALUE stored where the collector cannot see it is not kept alive. The rules:

  • A VALUE held in a C global or static variable must be registered: rb_global_variable(&var) for the variable or rb_gc_register_mark_object(obj) for the object itself (both from the guide’s section on global variables shared between C and Ruby).

  • A VALUE inside a wrapped struct must be marked by that struct’s dmark function (next section).

  • A VALUE in malloc-ed memory that no dmark knows about is invisible. Keep references in Ruby containers (an Array, a Hash or a Struct) owned by a wrapped object instead; the guide recommends this to avoid writing write barriers by hand.

  • The Appendix D advice stands: do not touch internal pointers directly (RARRAY_PTR and friends) and use the functions (rb_ary_aref, rb_ary_store) instead; inserting write barriers by hand "is a very difficult hack" that the guide discourages unless profiling proves it necessary. RB_OBJ_WRITE(parent, &slot, value) in tally_initialize is the supported way to store a reference into a write-barrier-protected object.

  • rb_gc_adjust_memory_usage(diff) tells the collector about external memory your object owns, so that it collects earlier when a small Ruby object pins a large native buffer.

The collector itself, its tuning knobs and compaction are in Garbage Collection and Memory.

Wrapping C structs with TypedData

To expose a C struct as a Ruby object, describe how Ruby should manage it with an rb_data_type_t, allocate the object with TypedData_Make_Struct and fetch the struct back with TypedData_Get_Struct. The fields of rb_data_type_t are, from the guide:

Field Meaning

wrap_struct_name

A process-unique identifier used for statistics (not required to be a valid identifier). "shelf_native/tally" above.

function.dmark

Called during GC. Must mark every VALUE the struct references with rb_gc_mark or rb_gc_mark_movable. No Ruby allocation is allowed inside it.

function.dfree

Releases the struct. RUBY_DEFAULT_FREE just frees the pointer; supply your own when the struct owns more resources. No Ruby allocation inside it either.

function.dsize

Reports the memory consumed, for ObjectSpace.memsize_of. May be 0, but the guide recommends against.

function.dcompact

Called when compaction moved objects. References marked with rb_gc_mark_movable are updated here with rb_gc_location.

parent, data

parent makes TypedData_Get_Struct accept objects of a derived type; data is yours.

flags

RUBY_TYPED_FREE_IMMEDIATELY frees during GC instead of deferring, allowed only when dfree never takes Ruby’s lock. RUBY_TYPED_WB_PROTECTED declares that you issue write barriers on every write. RUBY_TYPED_FROZEN_SHAREABLE lets a frozen instance become Ractor-shareable. RUBY_TYPED_DECL_MARKING replaces dmark with a declarative list of edges.

The class must derive from Object, and its allocator must be set with rb_define_alloc_func, which is why Init_shelf_native defines tally_alloc before the methods. Allocation initialises label to Qnil immediately, because dmark can run before initialize. GC.compact in the usage example moves objects and calls tally_compact; the label survives the move with its value intact.

For a struct with a single VALUE member that needs no logic, the guide offers the declarative form: set RUBY_TYPED_DECL_MARKING, declare the edges with RUBY_REFERENCES, RUBY_REF_EDGE and RUBY_REF_END, and pass RUBY_REFS_LIST_PTR as the dmark. The GC then marks and updates those references without callbacks.

The GVL, blocking calls and threads

While C code runs, it holds the global VM lock (see Threads and the GVL), so a long computation or a blocking system call in an extension freezes every other Ruby thread. The header ruby/thread.h declares rb_thread_call_without_gvl, which "allows the passed function to run in parallel with other Ruby threads"; code running inside it must not touch Ruby objects or raise. To get back in, Ruby 4.0 changed rb_thread_call_with_gvl so that it "now works with or without the GVL", which lets gems stop checking ruby_thread_has_gvl_p, though the NEWS still asks authors to be diligent about the GVL. The same release deprecated rb_thread_fd_close, which is now a no-op: to hand a file descriptor to Ruby from C, create an IO with RUBY_IO_MODE_EXTERNAL and close it with rb_io_close, which also interrupts pending operations on it.

Ractor safety

By default Ruby assumes that a C extension is not safe to call from a non-main Ractor, and a call from one raises Ractor::UnsafeError. An extension declares otherwise by calling rb_ext_ractor_safe(true) at the top of its Init_ function, which marks all methods defined afterwards as safe. The promise is the author’s: no unshareable object may leak between Ractors through a C global (class and module objects are shareable, so cFoo = rb_define_class(...) is fine), the code must be thread-safe (the guide points to ruby/thread_native.h and ruby/atomic.h), and any library it calls must be too. Instances of a data type declared with RUBY_TYPED_FROZEN_SHAREABLE can become shareable once they are frozen.

The usage below calls the same method from a worker Ractor. The first build declares the extension Ractor-safe; the second is an identical build with the rb_ext_ractor_safe(true) line removed.

# frozen_string_literal: true

Warning[:experimental] = false
require_relative "../ext/shelf_native/shelf_native"

worker = Ractor.new { ShelfNative.check_digit("978030640615") }
worker.value # => 7

With the declaration the program prints 7. Without it, the same program fails when the result is read:

Ractor::RemoteError: Ractor::UnsafeError: ractor unsafe method called from not main ractor

(Ruby also writes the worker’s backtrace to standard error before this line.) The Ractor API itself is in Ractors.

C API changes in Ruby 4.0

The Ruby 4.0.0 NEWS lists these items for extension authors:

  • rb_path_check was removed. It supported the $SAFE path checking that Ruby removed in 2.7.

  • rb_thread_fd_close is deprecated and is a no-op; see above.

  • rb_thread_call_with_gvl works with or without the GVL; see above.

  • A C API for Set was added: rb_set_new, rb_set_new_capa, rb_set_add, rb_set_lookup, rb_set_delete, rb_set_clear, rb_set_size and rb_set_foreach. Set itself became core in 4.0 (see Sets).

  • The NEWS "implementation improvements" say that "generic ivar" objects such as String, Array and TypedData now use a new internal fields object for faster instance variable access, which is invisible to extensions that use the public API and a reason not to reach into object internals.

The Memory View C API

A Memory View lets extensions share a block of contiguous, homogeneous, fixed-size elements, such as the buffer behind a numeric array or an image, without copying it. The guide describes two sides: a producer registers a Memory View entry for its class, and a consumer asks an object for its view. The documentation states plainly that "this feature is still experimental" and that the specification may change, so pin your Ruby version when you depend on it.

The structure rb_memory_view_t carries the owning object (obj; Ruby keeps it alive while the view is held, so the consumer does not need to guard it), the data pointer (data), the size in bytes (byte_size), whether it is read-only (readonly), an element format string (NULL means unsigned byte), the item_size, and for multidimensional data ndim, shape, strides and sub_offsets. The consumer calls:

  • rb_memory_view_available_p(obj) to ask whether the object can export a view at all (true does not guarantee rb_memory_view_get succeeds);

  • rb_memory_view_get(obj, &view, flags) to fill the view, returning false and leaving it untouched if the request cannot be met;

  • rb_memory_view_release(&view), which must be called, or the view leaks;

  • helpers: rb_memory_view_item_size_from_format, rb_memory_view_get_item_pointer, rb_memory_view_get_item, rb_memory_view_is_contiguous (and the row- and column-major variants), rb_memory_view_fill_contiguous_strides, rb_memory_view_init_as_byte_array and rb_memory_view_prepare_item_desc.

shelf_byte_sum above is a consumer. The exporting side can be exercised from Ruby with Fiddle: on Ruby 4.0.7 a Fiddle::Pointer exports a Memory View and String does not. Both facts below were observed by running the extension.

# frozen_string_literal: true

require "fiddle"
require_relative "../ext/shelf_native/shelf_native"

pointer = Fiddle::Pointer.malloc(4, Fiddle::RUBY_FREE)
pointer[0, 4] = [1, 2, 3, 250].pack("C*")

ShelfNative.byte_sum(pointer) # => 256

begin
  ShelfNative.byte_sum("abc")
rescue TypeError => e
  e.message # => "String does not export a memory view"
end

Fiddle::MemoryView.export(pointer) { |view| ... } yields a Ruby-side Fiddle::MemoryView for the same data, with byte_size, format, item_size, ndim, shape, strides, readonly? and [] (checked by listing the class’s methods on 4.0.7).

Packaging an extension in a gem

An extension reaches users through a gem whose gemspec lists the extconf.rb in extensions. gem install (or bundle install) runs extconf.rb and make on the user’s machine, copies the result into the gem’s lib directory and puts that directory on the load path, which is why the usage code can say require "shelf/shelf_native". This gemspec for a gem with an extension was accepted by gem build (the build is local and offline; installing was not done).

# frozen_string_literal: true

Gem::Specification.new do |spec|
  spec.name = "shelf"
  spec.version = "0.1.0"
  spec.summary = "A library catalogue"
  spec.authors = ["Shelf Authors"]
  spec.license = "MIT"
  spec.required_ruby_version = ">= 4.0"
  spec.files = Dir["lib/**/*.rb", "ext/**/*.{rb,c}"]
  spec.require_paths = ["lib"]
  spec.metadata["rubygems_mfa_required"] = "true"
  spec.extensions = ["ext/shelf_native/extconf.rb"]
end

gem build listed metadata.gz, data.tar.gz and checksums.yaml.gz in the resulting package, and Gem::Specification.load returned ["ext/shelf_native/extconf.rb"] for extensions. RubyGems chooses the builder from the file name: the class list in the Ruby 4.0 documentation includes builders for extconf.rb (Gem::Ext::ExtConfBuilder), Rake, CMake, configure and Cargo (Gem::Ext::CargoBuilder, for extensions written in Rust).

Tools that are not run on this page and are described from their official documentation:

  • rake-compiler (community, 1.3.2 on RubyGems when checked) adds rake compile through Rake::ExtensionTask.new("shelf_native"), copies the built library into lib, can package native gems per platform with an ExtensionTask fed with the gemspec, and supports cross compilation. Its configuration table lists lib_dir (default lib) and config_script (default extconf.rb). See Creating and Publishing Gems, and Rake for Rake itself.

  • Precompiled platform gems avoid the compiler on the user’s machine; they are an ordinary gem per platform, built with the tool above, and pinned by Bundler per platform as described in RubyGems and Bundler.

A native gem should degrade gracefully when the extension is missing, for example on a platform where it failed to build or under JRuby. The shelf version below prefers the C implementation and falls back to Ruby; it was run with the library present and with it removed.

# frozen_string_literal: true

module Shelf
  # ISBN-13 check digit, in C when the extension is built and in Ruby otherwise.
  module CheckDigit
    def self.pure(digits)
      sum = digits.each_char.with_index.sum { |char, i| char.to_i * (i.even? ? 1 : 3) }
      (10 - (sum % 10)) % 10
    end

    begin
      require "shelf/shelf_native"
      def self.call(digits) = ShelfNative.check_digit(digits)
      NATIVE = true
    rescue LoadError
      def self.call(digits) = pure(digits)
      NATIVE = false
    end
  end
end

With the compiled library on the load path Shelf::CheckDigit::NATIVE is true; without it, false; in both cases call("978030640615") and pure("978030640615") return 7. The rescue is of LoadError only, so a genuine bug in the extension’s Ruby code still surfaces. See Code Loading: require and autoload for how the load path resolves shelf/shelf_native.

Security note for the same reason: an extension runs with the full privileges of the process and a memory error in it is a memory-safety bug in the whole program. Treat third-party native gems as you would any native dependency, as described in Security.

Fiddle

Fiddle is "a libffi wrapper for Ruby", in the words of its README: it calls functions in dynamically loaded C libraries from pure Ruby, with no compiler. In Ruby 4.0 fiddle moved from the default gems to the bundled gems (the NEWS lists fiddle 1.1.8 among the gems promoted), which is the same change that made benchmark, logger, ostruct, pstore, irb and rdoc bundled. A bundled gem is installed with Ruby and require "fiddle" works in a plain script, but under Bundler it must be in the Gemfile (gem "fiddle") or the require fails with LoadError. Ruby 4.0.7 ships Fiddle 1.1.8.

The examples use a tiny plain C library, libisbn, that has no Ruby headers and so can be loaded by Fiddle and by FFI. It is the same check_digit_of function as above, exported with a C name:

/* A plain C library, with no Ruby headers: usable from Fiddle and FFI. */
int
isbn13_check_digit(const char *digits)
{
    int sum = 0;

    for (int i = 0; i < 12; i++) {
        sum += (digits[i] - '0') * (i % 2 == 0 ? 1 : 3);
    }
    return (10 - sum % 10) % 10;
}

Building a shared library is platform-specific. On macOS (what was used here) cc -Wall -Wextra -O2 -dynamiclib -o libisbn.dylib isbn.c compiled it without warnings; on Linux the equivalent is cc -shared -fPIC -o libisbn.so isbn.c. The file extension of shared libraries is available portably as RbConfig::CONFIG["SOEXT"].

Functions and handles

# frozen_string_literal: true

require "fiddle"
require "rbconfig"

library = Fiddle.dlopen(File.join(__dir__, "native", "libisbn.#{RbConfig::CONFIG['SOEXT']}"))
check_digit = Fiddle::Function.new(
  library["isbn13_check_digit"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
)

check_digit.call("978030640615") # => 7
check_digit.call("978014312854") # => 0

begin
  library["no_such_symbol"]
rescue Fiddle::DLError => e
  e.message # => "unknown symbol \"no_such_symbol\""
end

The pieces, as the README describes and the run confirms:

  • Fiddle.dlopen(path) returns a handle to a shared library (Fiddle::Handle); Fiddle::Handle::DEFAULT is the process’s own symbols, which include the C library. Opening a missing library raises Fiddle::DLError with the loader’s message, and so does a missing symbol, as above.

  • handle["name"] returns the symbol’s address; Fiddle::Function.new(address, argument_types, return_type) wraps it. The types are the constants Fiddle::TYPE_INT, TYPE_LONG, TYPE_DOUBLE, TYPE_VOIDP, TYPE_VOID and their siblings. A Ruby String passed as TYPE_VOIDP is passed as a pointer to its bytes.

  • function.call(...) converts the arguments, calls the function and converts the result (a crash backtrace taken during testing showed the call running through rb_nogvl, so by default the GVL is released while the C function runs). Calling with the wrong number of arguments raises ArgumentError (wrong number of arguments (given 0, expected 1) was observed).

The signature is a promise that nothing checks. Fiddle trusts the argument types you declare and the values you pass. In a test run on 4.0.7, passing a Ruby Integer (12) where a pointer was declared did not raise: Fiddle used the number as an address, and the interpreter crashed with a segmentation fault at address 0xc. Likewise libisbn reads twelve bytes from whatever pointer it receives, so a shorter string reads past its end. Validate in Ruby before the call, wrap the foreign function in a Ruby method with a precise contract, and test that method on every platform you support.

Declaring prototypes: Fiddle::Importer

Fiddle::Importer (loaded with require "fiddle/import") lets you declare functions with C prototype strings instead of type arrays and defines a Ruby method for each. The same example also shows a callback: Fiddle::Closure::BlockCaller turns a Ruby block into a C function pointer, here the comparison function that qsort needs.

# frozen_string_literal: true

require "fiddle"
require "fiddle/import"

module Libc
  extend Fiddle::Importer

  dlload Fiddle::Handle::DEFAULT
  extern "size_t strlen(const char *)"
  extern "int abs(int)"
  extern "void qsort(void *, size_t, size_t, void *)"
end

Libc.strlen("shelf") # => 5
Libc.abs(-12)        # => 12

days = [14, 3, 21, 7].pack("l*")
buffer = Fiddle::Pointer.malloc(days.bytesize, Fiddle::RUBY_FREE)
buffer[0, days.bytesize] = days

compare = Fiddle::Closure::BlockCaller.new(
  Fiddle::TYPE_INT, [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
) do |left, right|
  left[0, 4].unpack1("l") <=> right[0, 4].unpack1("l")
end

Libc.qsort(buffer, 4, 4, compare)
buffer[0, days.bytesize].unpack("l*") # => [3, 7, 14, 21]

The run printed 5, 12 and [3, 7, 14, 21]. Notes on what it uses:

  • Inside the closure the two VOIDP arguments arrive as Fiddle::Pointer objects (checked), so pointer[offset, length] reads raw bytes, and String#unpack1("l") decodes a native 32-bit signed integer. Byte order and the width of int are the platform’s.

  • Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) allocates native memory and registers a free function, so the memory is released when the Ruby object is collected. A pointer obtained from a C function is not owned by Ruby unless you give it a free function; release it with the library’s own function and never twice.

  • The Fiddle::Closure object must stay referenced while C may call it; if it is collected, a later call jumps into freed memory.

  • The README documents Fiddle::Importer struct declarations (struct ["int college_id", "char college_name[50]"], with nested hashes for nested structs) for libraries that pass records; the same warnings apply to layout, alignment and padding.

Fiddle is the right tool for a handful of functions in a stable C library. Once a binding grows to dozens of functions, structs and callbacks, the ffi gem’s DSL is usually less error-prone, and once the work needs Ruby objects or the GVL, an extension is.

The ffi gem

ffi is a community gem (not part of Ruby) whose README describes it as a gem "for programmatically loading dynamically-linked native libraries, binding functions within them, and calling those functions from Ruby code". Its feature list: an intuitive DSL, all C native types, C structs (also nested), enums and global variables, callbacks from C to Ruby, automatic garbage collection of native memory, and use in Ractors (with a how-to on its wiki). Its distinguishing property is that an FFI binding "works without changes" on CRuby, JRuby and TruffleRuby, and on JRuby and TruffleRuby the gem is preinstalled. On CRuby it needs a C compiler to install, uses the system libffi when new enough and otherwise a bundled copy (configurable with --enable-system-libffi or --disable-system-libffi). The latest version on RubyGems when this page was written was 1.17.4.

This example was not run: the ffi gem is not installed in the environment used for this page, so only its syntax was checked, and it is copied in shape from the README. It binds the same libisbn function as the Fiddle example above.

# frozen_string_literal: true

# Not run: the ffi gem is not installed in the environment used for this page.
require "ffi"

module Shelf
  module Native
    extend FFI::Library

    ffi_lib File.join(__dir__, "native", "libisbn.#{FFI::Platform::LIBSUFFIX}")
    attach_function :isbn13_check_digit, [:string], :int
  end
end

Shelf::Native.isbn13_check_digit("978030640615") # => 7

How it compares with Fiddle, from the two projects' own descriptions:

Fiddle ffi gem

Distribution

Bundled gem with Ruby 4.0 (needs a Gemfile entry under Bundler)

Third-party gem; needs a compiler to install on CRuby

Implementations

CRuby

CRuby, JRuby, TruffleRuby

Declaration style

Fiddle::Function.new(...), or prototype strings with Fiddle::Importer

extend FFI::Library, attach_function :name, [types], return_type

Structs, callbacks, memory

Struct declarations via the importer, Fiddle::Closure, Fiddle::Pointer with explicit free functions

FFI::Struct, FFI::Function/callbacks, FFI::MemoryPointer released by the garbage collector

Safety

Unchecked; wrong signatures crash

Unchecked; wrong signatures crash

Both pay a cost per call, which makes them a poor fit for an inner loop over many tiny calls; write the loop in C and call it once.

ruby.wasm

ruby.wasm is, in its README’s words, "a collection of WebAssembly ports of CRuby". It lets a Ruby program run in a browser, in a WASI-compatible runtime such as wasmtime, and on edge platforms, with no Ruby installed on the host. It is maintained under the ruby GitHub organisation. This section is described from its README, FAQ and cheat sheet; nothing in it was run, because no WebAssembly runtime or Node.js package for it was installed for this page. Check the project documentation before you pin versions.

Builds, targets and profiles

A build is a combination of a Ruby version, a profile and a target. The README lists:

  • Targets (triples): wasm32-unknown-wasip1, for WASI Preview 1 compatible environments (Node.js, browsers with a polyfill, wasmtime); and wasm32-unknown-emscripten, for JavaScript environments including Node.js and browsers.

  • Profiles: minimal, with no standard extension libraries such as json, yaml or stringio; and full, with all of them.

  • Prebuilt binaries are published in the GitHub Releases of the project, for example ruby-4.0-wasm32-unknown-wasip1-full.tar.gz.

  • npm packages exist for JavaScript hosts: @ruby/4.0-wasm-wasi (CRuby 4.0 on WASI with JavaScript interop), @ruby/3.4-wasm-wasi, @ruby/3.3-wasm-wasi, @ruby/3.2-wasm-wasi and @ruby/head-wasm-wasi; the Emscripten build (@ruby/head-wasm-emscripten) is described as "not well tested". The helper package @ruby/wasm-wasi provides DefaultRubyVM.

Limitations the README states: the WASI target does not support Thread APIs, because WASI has no thread API yet, and there is no networking. So the thread and socket pages of this reference (Threads and the GVL, Networking and HTTP) do not apply to this target; fibers and Ractor-free code do. Plan on Ruby code that is pure computation plus a bridge to the host.

Ruby in a browser

The shortest path is a script tag that loads the browser bundle from a CDN and runs type="text/ruby" scripts. This snippet is the README’s quick example; it was not executed here.

<html>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    require "js"

    puts RUBY_VERSION # (Printed to the Web browser console)
    JS.global[:document].write "Hello, world!"
  </script>
</html>

Facts the documentation gives about that example:

  • require "js" loads the bridge. JS.global is the JavaScript global object, and []/[]= read and write its properties, so JS.global[:document] is the page’s document.

  • By default puts goes to the JavaScript console (console.log); the FAQ shows replacing $stdout with an object that has a write method to send output into the page instead.

  • The data-env attribute on the script tag passes environment variables to the Ruby VM when it starts, as a JSON object whose values are all strings; the cheat sheet’s example sets RUBY_BOX and RUBY_FIBER_MACHINE_STACK_SIZE.

  • To control the VM from JavaScript instead, load @ruby/wasm-wasi, compile the ruby+stdlib.wasm binary, call DefaultRubyVM(module) and use vm.eval("..."). In Node.js the same API reads ruby.wasm from node_modules and runs vm.eval.

Ruby as a WASI application

To run a Ruby script under a WASI runtime, the README packs the interpreter, the standard library and your code into one .wasm file with the rbwasm command from the ruby_wasm gem and runs it with wasmtime. Quoted in shape, not run:

$ gem install ruby_wasm
$ curl -LO https://github.com/ruby/ruby.wasm/releases/latest/download/ruby-4.0-wasm32-unknown-wasip1-full.tar.gz
$ tar xfz ruby-4.0-wasm32-unknown-wasip1-full.tar.gz
$ mv ruby-4.0-wasm32-unknown-wasip1-full/usr/local/bin/ruby ruby.wasm
$ mkdir src && echo "puts 'Hello'" > src/my_app.rb
$ rbwasm pack ruby.wasm --dir ./src::/src --dir ./ruby-4.0-wasm32-unknown-wasip1-full/usr::/usr -o my-ruby-app.wasm
$ wasmtime my-ruby-app.wasm /src/my_app.rb
Hello

The opposite direction, running a WebAssembly module from Ruby, is a different job: the ruby.wasm FAQ points to the wasmtime gem for it. That is a way to host untrusted or portable code inside a Ruby program, and a separate choice from compiling Ruby itself to WebAssembly.

A C extension is compiled for one platform. A gem with a native extension therefore cannot be loaded into a WebAssembly Ruby unless that extension was compiled for WebAssembly and included in the build; the README’s profiles describe which of the standard extension libraries a build contains, and for your own extension you should consult the project’s contribution and build documentation rather than assume. The sibling pages on WebAssembly in other languages show the host-side concerns: Rust and WebAssembly and Go, WebAssembly and WASI.

Where the books are outdated

Notes on the two reference books used for the structure of this section.

  • Neither book covers C extensions, Fiddle, the ffi gem or ruby.wasm; the whole of this page comes from the official documentation and the projects' own repositories.

  • The books describe benchmark and logger as parts of the standard library. In Ruby 4.0 they are bundled gems, and so is fiddle (promoted from a default gem in 4.0, per the NEWS), so under Bundler each needs a Gemfile entry.

  • A C extension written to older guides can use APIs that Ruby 4.0 removed or changed: rb_path_check is gone and rb_thread_fd_close is a no-op. Re-read the extension guide for the Ruby you target rather than copying an old extension.

  • Any older advice about "moving hot loops to C" predates YJIT; measure on Ruby 4.0 first.

Common errors

Symptom Usual cause

LoadError: ... undefined symbol: Init_foo (or a platform equivalent)

The create_makefile target name does not match the Init_ function name, or the function is static, or the extension was built for a different Ruby.

extconf.rb failed with "Could not create Makefile due to some reason"

A have_ check that the script treats as required failed; read mkmf.log for the failing probe and compiler output.

Random crashes or corrupt strings in an extension that "works"

A VALUE or RSTRING_PTR pointer used after the last reference to its object (missing RB_GC_GUARD), a VALUE stored in a C global that is not registered, or a dmark that forgets a member.

Ractor::UnsafeError: ractor unsafe method called from not main ractor

The extension did not call rb_ext_ractor_safe(true); see the Ractor section.

TypeError: ... does not export a memory view

The object’s class has not registered a Memory View entry; check rb_memory_view_available_p first.

Fiddle::DLError: dlopen(...): tried: ... (no such file)

Wrong path or file extension for the platform; build the path from RbConfig::CONFIG["SOEXT"].

Fiddle::DLError: unknown symbol "..."

The name is misspelled, C++ name-mangled, or not exported by the library.

A segmentation fault from Fiddle::Function#call or an FFI call

A wrong declared type or a non-pointer passed as a pointer; the foreign library cannot be asked to check.

LoadError: cannot load such file — fiddle under Bundler

fiddle is a bundled gem in Ruby 4.0 and the Gemfile lacks it.