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 |
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: |
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, |
No threads and no networking on the WASI target; native gems must be built for WebAssembly; the JavaScript bridge is its own API. |
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.
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 theMakeMakefilemodule, whose methods are the checks. -
append_cflagsadds a compiler flag only if the compiler accepts it; here-Wshadowwas accepted and appears in the generatedMakefile. The flags Ruby itself was built with (warning options, optimisation level, architecture) are already part of the compile command, asmkmf.logshows.append_cppflagsandappend_ldflagsdo the same for the preprocessor and the linker. -
have_func(func, headers)checks that a function exists and, if it does, definesHAVE_plus the upper-cased name (hereHAVE_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 ofInit_. The mismatch between the target name and theInit_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 |
|---|---|
|
Test or assert the type; |
|
Convert between C integers and Ruby integers. |
|
Coerce with |
|
Create and grow strings from C data. |
|
|
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)andrb_define_module_undercreate or reopen modules and classes. -
rb_define_method(klass, "name", func, argc)defines an instance method.rb_define_module_functiondefines a method that is both a private instance method and a public singleton method, which is whatMath.sqrtis.rb_define_singleton_method,rb_define_private_methodandrb_define_alloc_funccover class methods, visibility and allocation. -
The
argcargument selects the C signature. A non-negative numbernmeans the function receives the receiver followed by exactlynVALUEarguments, and Ruby raisesArgumentErrorfor any other count.-1means the function receives(int argc, VALUE *argv, VALUE self)and handles any count, typically withrb_scan_args;-2passes 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_arityonly 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)callsfuncand returns with a non-zerostateinstead of unwinding when it raises. Release your resource, then re-raise withrb_jump_tag(state). -
rb_ensure(body, arg1, cleanup, arg2)is the C counterpart of Rubyensure:cleanupalways 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 betweenrb_memory_view_getandrb_memory_view_releaseshould be wrapped inrb_ensure. -
Prefer allocation macros that fail safely. The guide says to use
TypedData_Make_Structrather than "allocate, then wrap", because the second form can leak the structure ifNoMemoryErroris raised between the two steps. -
Validate arguments before acquiring anything.
shelf_check_digitraises 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
VALUEheld in a C global or static variable must be registered:rb_global_variable(&var)for the variable orrb_gc_register_mark_object(obj)for the object itself (both from the guide’s section on global variables shared between C and Ruby). -
A
VALUEinside a wrapped struct must be marked by that struct’sdmarkfunction (next section). -
A
VALUEinmalloc-ed memory that nodmarkknows about is invisible. Keep references in Ruby containers (anArray, aHashor aStruct) 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_PTRand 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)intally_initializeis 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 |
|---|---|
|
A process-unique identifier used for statistics (not required to be a valid identifier). |
|
Called during GC. Must mark every |
|
Releases the struct. |
|
Reports the memory consumed, for |
|
Called when compaction moved objects. References marked with |
|
|
|
|
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_checkwas removed. It supported the$SAFEpath checking that Ruby removed in 2.7. -
rb_thread_fd_closeis deprecated and is a no-op; see above. -
rb_thread_call_with_gvlworks with or without the GVL; see above. -
A C API for
Setwas added:rb_set_new,rb_set_new_capa,rb_set_add,rb_set_lookup,rb_set_delete,rb_set_clear,rb_set_sizeandrb_set_foreach.Setitself became core in 4.0 (see Sets). -
The NEWS "implementation improvements" say that "generic ivar" objects such as
String,ArrayandTypedDatanow 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 guaranteerb_memory_view_getsucceeds); -
rb_memory_view_get(obj, &view, flags)to fill the view, returningfalseand 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_arrayandrb_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 compilethroughRake::ExtensionTask.new("shelf_native"), copies the built library intolib, can package native gems per platform with anExtensionTaskfed with the gemspec, and supports cross compilation. Its configuration table listslib_dir(defaultlib) andconfig_script(defaultextconf.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;
}
Source: Fiddle README: usage
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
Source: Fiddle README: usage
The pieces, as the README describes and the run confirms:
-
Fiddle.dlopen(path)returns a handle to a shared library (Fiddle::Handle);Fiddle::Handle::DEFAULTis the process’s own symbols, which include the C library. Opening a missing library raisesFiddle::DLErrorwith 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 constantsFiddle::TYPE_INT,TYPE_LONG,TYPE_DOUBLE,TYPE_VOIDP,TYPE_VOIDand their siblings. A RubyStringpassed asTYPE_VOIDPis 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 throughrb_nogvl, so by default the GVL is released while the C function runs). Calling with the wrong number of arguments raisesArgumentError(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
VOIDParguments arrive asFiddle::Pointerobjects (checked), sopointer[offset, length]reads raw bytes, andString#unpack1("l")decodes a native 32-bit signed integer. Byte order and the width ofintare 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::Closureobject must stay referenced while C may call it; if it is collected, a later call jumps into freed memory. -
The README documents
Fiddle::Importerstruct 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
Source: ffi README: Synopsis
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 |
|
|
Structs, callbacks, memory |
Struct declarations via the importer, |
|
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); andwasm32-unknown-emscripten, for JavaScript environments including Node.js and browsers. -
Profiles:
minimal, with no standard extension libraries such asjson,yamlorstringio; andfull, 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-wasiand@ruby/head-wasm-wasi; the Emscripten build (@ruby/head-wasm-emscripten) is described as "not well tested". The helper package@ruby/wasm-wasiprovidesDefaultRubyVM.
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>
Source: ruby.wasm documentation: README
Facts the documentation gives about that example:
-
require "js"loads the bridge.JS.globalis the JavaScript global object, and[]/[]=read and write its properties, soJS.global[:document]is the page’sdocument. -
By default
putsgoes to the JavaScript console (console.log); the FAQ shows replacing$stdoutwith an object that has awritemethod to send output into the page instead. -
The
data-envattribute 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 setsRUBY_BOXandRUBY_FIBER_MACHINE_STACK_SIZE. -
To control the VM from JavaScript instead, load
@ruby/wasm-wasi, compile theruby+stdlib.wasmbinary, callDefaultRubyVM(module)and usevm.eval("..."). In Node.js the same API readsruby.wasmfromnode_modulesand runsvm.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
benchmarkandloggeras parts of the standard library. In Ruby 4.0 they are bundled gems, and so isfiddle(promoted from a default gem in 4.0, per the NEWS), so under Bundler each needs aGemfileentry. -
A C extension written to older guides can use APIs that Ruby 4.0 removed or changed:
rb_path_checkis gone andrb_thread_fd_closeis 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 |
|---|---|
|
The |
|
A |
Random crashes or corrupt strings in an extension that "works" |
A |
|
The extension did not call |
|
The object’s class has not registered a Memory View entry; check |
|
Wrong path or file extension for the platform; build the path from |
|
The name is misspelled, C++ name-mangled, or not exported by the library. |
A segmentation fault from |
A wrong declared type or a non-pointer passed as a pointer; the foreign library cannot be asked to check. |
|
|
References
-
Ruby 4.0 documentation: Extension (creating extension libraries for Ruby), with its sections on C struct to Ruby object, Prepare extconf.rb, Appendix D, generational GC, Appendix E, RB_GC_GUARD and Appendix F, Ractor support
-
Ruby 4.0.0 NEWS (C API updates, bundled gems, Set C API)
-
ruby.wasm documentation and ruby.wasm (ruby/ruby.wasm), with its cheat sheet and FAQ