C Extensions & FFI - Native Integration
Master writing Ruby C extensions and using Foreign Function Interface (FFI) to integrate with native libraries and optimize performance-critical code.
Native Integration Options
Ruby provides two main approaches for native integration: C Extensions for maximum performance and FFI for flexibility and cross-platform compatibility.
C Extensions
- Maximum Performance: Direct C code integration
- Ruby API Access: Full access to Ruby internals
- Compilation Required: Platform-specific builds
- MRI Specific: May not work with other Ruby implementations
Foreign Function Interface (FFI)
- Cross-Platform: Works across Ruby implementations
- Dynamic Loading: Load libraries at runtime
- No Compilation: No need to compile C code
- Safety: Better error handling and memory safety
Writing C Extensions
Basic C Extension Structure
extconf.rb - Build Configuration
# extconf.rb
require 'mkmf'
# Check for required headers/libraries
have_header('stdio.h')
have_library('m', 'sqrt') # Check for math library
# Create Makefile
create_makefile('my_extension/my_extension')
my_extension.c - C Implementation
#include
#include
// Ruby value representing our module
static VALUE mMyExtension;
// C function that will be callable from Ruby
static VALUE
my_sqrt(VALUE self, VALUE number)
{
double value = NUM2DBL(number);
double result = sqrt(value);
return DBL2NUM(result);
}
// C function for string manipulation example
static VALUE
reverse_string(VALUE self, VALUE str)
{
// Convert Ruby string to C string
char *c_str = StringValuePtr(str);
long len = RSTRING_LEN(str);
// Create new string and reverse it
char *reversed = malloc(len + 1);
for (long i = 0; i < len; i++) {
reversed[i] = c_str[len - 1 - i];
}
reversed[len] = '\0';
// Convert back to Ruby string
VALUE result = rb_str_new_cstr(reversed);
free(reversed);
return result;
}
// Array processing example
static VALUE
sum_array(VALUE self, VALUE array)
{
Check_Type(array, T_ARRAY);
long len = RARRAY_LEN(array);
double sum = 0.0;
for (long i = 0; i < len; i++) {
VALUE element = RARRAY_AREF(array, i);
sum += NUM2DBL(element);
}
return DBL2NUM(sum);
}
// Initialization function
void
Init_my_extension(void)
{
// Define the module
mMyExtension = rb_define_module("MyExtension");
// Define module methods
rb_define_module_function(mMyExtension, "sqrt", my_sqrt, 1);
rb_define_module_function(mMyExtension, "reverse_string", reverse_string, 1);
rb_define_module_function(mMyExtension, "sum_array", sum_array, 1);
}
Building and Using
# Build the extension
ruby extconf.rb
make
# Use in Ruby
require_relative 'my_extension'
puts MyExtension.sqrt(16) # => 4.0
puts MyExtension.reverse_string("hello") # => "olleh"
puts MyExtension.sum_array([1, 2, 3, 4, 5]) # => 15.0
Ruby C API Essentials
Data Type Conversions
// Ruby to C conversions
int c_int = NUM2INT(ruby_value); // Ruby number to C int
long c_long = NUM2LONG(ruby_value); // Ruby number to C long
double c_double = NUM2DBL(ruby_value); // Ruby number to C double
char *c_string = StringValuePtr(ruby_value); // Ruby string to C string
// C to Ruby conversions
VALUE ruby_int = INT2NUM(c_int); // C int to Ruby number
VALUE ruby_long = LONG2NUM(c_long); // C long to Ruby number
VALUE ruby_double = DBL2NUM(c_double); // C double to Ruby number
VALUE ruby_string = rb_str_new_cstr(c_string); // C string to Ruby string
// Special values
VALUE ruby_true = Qtrue;
VALUE ruby_false = Qfalse;
VALUE ruby_nil = Qnil;
Working with Ruby Objects
// Check object types
Check_Type(value, T_STRING); // Ensure it's a string
Check_Type(value, T_ARRAY); // Ensure it's an array
Check_Type(value, T_HASH); // Ensure it's a hash
// Array operations
long len = RARRAY_LEN(array);
VALUE element = RARRAY_AREF(array, index);
rb_ary_store(array, index, value);
rb_ary_push(array, value);
// Hash operations
VALUE val = rb_hash_aref(hash, key);
rb_hash_aset(hash, key, value);
// String operations
long str_len = RSTRING_LEN(string);
char *str_ptr = RSTRING_PTR(string);
VALUE new_str = rb_str_buf_new(100); // Pre-allocate buffer
Error Handling
// Raise exceptions
rb_raise(rb_eStandardError, "Something went wrong");
rb_raise(rb_eArgError, "Wrong number of arguments");
rb_raise(rb_eTypeError, "Expected a string");
// Exception handling with rb_protect
static VALUE
protected_operation(VALUE arg)
{
// Potentially dangerous operation
return rb_funcall((VALUE)arg, rb_intern("dangerous_method"), 0);
}
int state;
VALUE result = rb_protect(protected_operation, data, &state);
if (state) {
// Exception occurred
return Qnil;
}
Advanced C Extension Techniques
Creating Ruby Classes in C
// Define a C-backed Ruby class
static VALUE cMyClass;
// Data structure for our class
typedef struct {
int value;
char *name;
} my_data_t;
// Memory management
static void
my_data_free(void *ptr)
{
my_data_t *data = (my_data_t *)ptr;
if (data->name) free(data->name);
free(data);
}
static const rb_data_type_t my_data_type = {
"MyClass",
{ 0, my_data_free, 0 },
0, 0,
RUBY_TYPED_FREE_IMMEDIATELY
};
// Constructor
static VALUE
my_class_initialize(VALUE self, VALUE value, VALUE name)
{
my_data_t *data;
data = malloc(sizeof(my_data_t));
data->value = NUM2INT(value);
data->name = strdup(StringValuePtr(name));
// Associate data with Ruby object
rb_ivar_set(self, rb_intern("@data"),
TypedData_Wrap_Struct(cMyClass, &my_data_type, data));
return self;
}
// Instance method
static VALUE
my_class_get_value(VALUE self)
{
my_data_t *data;
VALUE data_obj = rb_ivar_get(self, rb_intern("@data"));
TypedData_Get_Struct(data_obj, my_data_t, &my_data_type, data);
return INT2NUM(data->value);
}
// Initialize the class
void
Init_my_extension(void)
{
cMyClass = rb_define_class("MyClass", rb_cObject);
rb_define_method(cMyClass, "initialize", my_class_initialize, 2);
rb_define_method(cMyClass, "get_value", my_class_get_value, 0);
}
GIL Management
// Release GIL for long-running operations
static VALUE
cpu_intensive_without_gil(VALUE self, VALUE iterations)
{
long count = NUM2LONG(iterations);
// Function to run without GIL
void *(*func)(void *) = cpu_intensive_work;
// Release GIL and run
return rb_thread_call_without_gvl(func, &count, NULL, NULL);
}
// Function that runs without GIL
static void *
cpu_intensive_work(void *arg)
{
long count = *(long *)arg;
long result = 0;
// Perform CPU-intensive work
for (long i = 0; i < count; i++) {
result += i * i;
}
return (void *)result;
}
Memory Management Best Practices
// Use Ruby's memory allocation functions
char *buffer = ruby_xmalloc(size);
buffer = ruby_xrealloc(buffer, new_size);
ruby_xfree(buffer);
// For Ruby objects that need cleanup
static VALUE
ensure_cleanup(VALUE arg)
{
// Cleanup code
return Qnil;
}
static VALUE
allocate_resource(VALUE arg)
{
// Allocate resource
return resource;
}
// Ensure cleanup happens even if exception occurs
VALUE resource = rb_ensure(allocate_resource, data, ensure_cleanup, data);
// Mark objects for GC (if storing Ruby objects in C)
static void
my_mark_function(void *ptr)
{
my_data_t *data = (my_data_t *)ptr;
if (data->ruby_object) {
rb_gc_mark(data->ruby_object);
}
}
Foreign Function Interface (FFI)
FFI provides a safer, more portable way to call native libraries without writing C code or dealing with Ruby's C API.
Basic FFI Usage
require 'ffi'
module MathLib
extend FFI::Library
# Load the math library
ffi_lib 'm' # libm on Unix, msvcrt on Windows
# Declare functions
attach_function :sin, [:double], :double
attach_function :cos, [:double], :double
attach_function :sqrt, [:double], :double
attach_function :pow, [:double, :double], :double
end
# Use the functions
puts MathLib.sin(Math::PI / 2) # => 1.0
puts MathLib.sqrt(16) # => 4.0
puts MathLib.pow(2, 8) # => 256.0
Working with Structures
require 'ffi'
module TimeLib
extend FFI::Library
ffi_lib FFI::Library::LIBC
# Define C struct
class TimeStruct < FFI::Struct
layout :tm_sec, :int,
:tm_min, :int,
:tm_hour, :int,
:tm_mday, :int,
:tm_mon, :int,
:tm_year, :int,
:tm_wday, :int,
:tm_yday, :int,
:tm_isdst, :int
end
# Function that takes a pointer to struct
attach_function :localtime, [:pointer], :pointer
attach_function :time, [:pointer], :long
end
# Get current time
current_time = TimeLib.time(nil)
time_ptr = FFI::MemoryPointer.new(:long)
time_ptr.write_long(current_time)
# Convert to struct
tm_ptr = TimeLib.localtime(time_ptr)
tm = TimeLib::TimeStruct.new(tm_ptr)
puts "Hour: #{tm[:tm_hour]}"
puts "Minute: #{tm[:tm_min]}"
Custom Library Integration
# Example: Interfacing with custom C library
# First, create a simple C library (libexample.so)
require 'ffi'
module ExampleLib
extend FFI::Library
# Load custom library
ffi_lib './libexample.so' # or 'example' for system library
# Define callback function type
callback :progress_callback, [:int], :void
# Declare functions
attach_function :process_data, [:pointer, :int, :progress_callback], :int
attach_function :create_buffer, [:int], :pointer
attach_function :free_buffer, [:pointer], :void
# Define struct for complex data
class DataStruct < FFI::Struct
layout :id, :int,
:value, :double,
:name, :string,
:active, :bool
end
end
# Use the library
buffer = ExampleLib.create_buffer(1024)
# Define callback
progress_proc = proc do |percent|
puts "Progress: #{percent}%"
end
# Process data with callback
result = ExampleLib.process_data(buffer, 1024, progress_proc)
# Clean up
ExampleLib.free_buffer(buffer)
FFI Memory Management
Memory Pointers and Buffers
# Allocate memory
buffer = FFI::MemoryPointer.new(:int, 10) # Array of 10 integers
str_ptr = FFI::MemoryPointer.new(:char, 256) # 256-byte buffer
# Write data
buffer[0] = 42
buffer.write_array_of_int([1, 2, 3, 4, 5])
# Write string
str_ptr.write_string("Hello, FFI!")
# Read data
first_int = buffer[0]
int_array = buffer.read_array_of_int(5)
string_data = str_ptr.read_string
# Auto-release with block
FFI::MemoryPointer.new(:char, 1024) do |ptr|
ptr.write_string("Automatic cleanup")
# Memory automatically freed when block exits
end
String Handling
# Different ways to handle strings
module StringLib
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :strlen, [:string], :size_t
attach_function :strcpy, [:pointer, :string], :pointer
attach_function :malloc, [:size_t], :pointer
attach_function :free, [:pointer], :void
end
# Using :string type (automatic conversion)
length = StringLib.strlen("Hello")
# Manual string handling
source = "Hello, World!"
dest_ptr = StringLib.malloc(source.length + 1)
StringLib.strcpy(dest_ptr, source)
# Read the copied string
copied = dest_ptr.read_string
puts copied # "Hello, World!"
# Don't forget to free!
StringLib.free(dest_ptr)
Error Handling and Safety
# Safe FFI practices
module SafeLib
extend FFI::Library
# Set library
begin
ffi_lib 'mylib'
rescue LoadError
raise "Required library 'mylib' not found"
end
# Attach function with error checking
attach_function :risky_function, [:pointer, :size_t], :int
def self.safe_call(data)
return nil if data.nil? || data.empty?
# Allocate buffer safely
FFI::MemoryPointer.new(:char, data.size) do |ptr|
ptr.write_string(data)
result = risky_function(ptr, data.size)
# Check return code
case result
when 0
return ptr.read_string
when -1
raise "Function failed with error code #{result}"
else
warn "Unexpected return code: #{result}"
return nil
end
end
rescue FFI::NullPointerError
raise "Null pointer error - library function failed"
end
end
Performance Considerations
C Extensions Advantages
- Maximum Speed: Direct native code execution
- Ruby Integration: Direct access to Ruby internals
- Memory Control: Fine-grained memory management
- GIL Release: Can release GIL for parallel processing
FFI Advantages
- Portability: Works across Ruby implementations
- Safety: Better error handling and memory safety
- Maintainability: No C compilation required
- Development Speed: Faster iteration cycle
Performance Comparison
require 'benchmark'
# Pure Ruby
def pure_ruby_sum(array)
array.sum
end
# FFI version (hypothetical)
def ffi_sum(array)
# FFI call overhead + native sum
end
# C extension version
def c_extension_sum(array)
# Direct C implementation
end
array = (1..1_000_000).to_a
Benchmark.bm(20) do |x|
x.report("Pure Ruby:") { pure_ruby_sum(array) }
x.report("FFI:") { ffi_sum(array) }
x.report("C Extension:") { c_extension_sum(array) }
end
# Typical results:
# Pure Ruby: ~100ms
# FFI: ~50ms (overhead from FFI calls)
# C Extension: ~10ms (direct native execution)
Best Practices & Guidelines
When to Use C Extensions
- CPU-Intensive Operations: Mathematical computations, algorithms
- Performance Critical: Hot paths in high-traffic applications
- Ruby API Access: Need direct access to Ruby internals
- Existing C Libraries: Wrapping complex C libraries
When to Use FFI
- Simple Library Calls: Straightforward native library integration
- Cross-Platform Code: Need to support multiple Ruby implementations
- Rapid Prototyping: Quick integration without compilation
- System Libraries: Calling OS or standard library functions
Security Considerations
- Input Validation: Always validate inputs before passing to native code
- Buffer Overflows: Be careful with string and array operations
- Memory Leaks: Ensure proper cleanup of allocated memory
- Library Trust: Only use trusted native libraries
Common Pitfalls
- GC Issues: Ruby objects can be garbage collected while in use
- String Encoding: Be aware of string encoding differences
- Thread Safety: Native code must be thread-safe if called from threads
- Platform Dependencies: C extensions are platform-specific
Testing & Debugging
Testing C Extensions
# spec/my_extension_spec.rb
require 'spec_helper'
require_relative '../ext/my_extension/my_extension'
RSpec.describe MyExtension do
describe '.sqrt' do
it 'calculates square root correctly' do
expect(MyExtension.sqrt(16)).to be_within(0.001).of(4.0)
expect(MyExtension.sqrt(2)).to be_within(0.001).of(Math.sqrt(2))
end
it 'handles edge cases' do
expect(MyExtension.sqrt(0)).to eq(0.0)
expect { MyExtension.sqrt(-1) }.to raise_error(Math::DomainError)
end
end
describe '.reverse_string' do
it 'reverses strings correctly' do
expect(MyExtension.reverse_string("hello")).to eq("olleh")
expect(MyExtension.reverse_string("")).to eq("")
end
end
end
Debugging Tools
# Compile with debug symbols
ruby extconf.rb
make CFLAGS="-g -O0"
# Use gdb for debugging
gdb ruby
(gdb) run -e "require_relative 'my_extension'; MyExtension.problematic_method"
(gdb) bt # backtrace when it crashes
# Valgrind for memory issues
valgrind --tool=memcheck --leak-check=full \
ruby -e "require_relative 'my_extension'; MyExtension.test_method"
# AddressSanitizer (if available)
CFLAGS="-fsanitize=address -g" make
./ruby -e "require_relative 'my_extension'; MyExtension.test_method"
CI/CD for Native Extensions
# .github/workflows/test.yml
name: Test Native Extensions
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
ruby: ['3.0', '3.1', '3.2']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Compile extension
run: |
cd ext/my_extension
ruby extconf.rb
make
- name: Run tests
run: bundle exec rspec