Ruby Logo

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.

Home Ruby C Extensions & FFI - Native Integration

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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn c extensions & ffi - native integration

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!