Introduction

Welcome to the rb-sys guide. This book will show you how to build Ruby extensions in Rust that are both powerful and reliable.

The primary goal of rb-sys is to make building native Ruby extensions in Rust easier than it would be in C. If it's not easy, it's a bug.

Key Features

  • Battle-tested Rust bindings for the Ruby C API
  • Support for Ruby 2.6+
  • Support for all major platforms (Linux, macOS, Windows)
  • Cross-compilation support for gems
  • Integration with rake-compiler
  • Test helpers for Ruby extensions

Why Rust for Ruby Extensions?

Ruby extensions have traditionally been written in C, requiring manual memory management and careful handling of Ruby's internals. This approach is error-prone and often results in security vulnerabilities, memory leaks, and crashes.

While C extensions offer flexibility and minimal dependencies, Rust extensions provide a superior developer experience with improved safety guarantees and access to a rich ecosystem of libraries.

Rust offers a compelling alternative with several advantages:

  • Memory safety without garbage collection
  • Strong type system that catches errors at compile time
  • Modern language features like pattern matching and traits
  • Access to the vast Rust ecosystem via crates.io
  • Strong tooling for testing, documentation, and deployment

Importantly, performance should not be the sole motivation for using Rust. With Ruby's YJIT compiler, pure Ruby code is now faster than ever. Instead, consider Rust when you need memory safety, type safety, or want to leverage the rich Rust ecosystem's capabilities.

The rb-sys Project Ecosystem

rb-sys consists of several components working together:

  1. rb-sys crate: Provides low-level Rust bindings to Ruby's C API
  2. rb_sys gem: Handles the Ruby side of extension compilation
  3. Magnus: A higher-level, ergonomic API for Rust/Ruby interoperability
  4. rb-sys-dock: Docker-based cross-compilation tooling
  5. GitHub Actions: Setup and cross-compilation automation for CI workflows

Most developers will use the Magnus API when building their extensions, as it provides a much more ergonomic interface than using rb-sys directly.

Here's how these components interact when building a typical Ruby gem with Rust:

Your Ruby Code (.rb files)
       ↓
  Your Rust Code (.rs files)
       ↓
   Magnus API
  rb-sys crate
Ruby C API Bindings
  Ruby VM

During compilation:

  Your gem's extconf.rb
  rb_sys gem's create_rust_makefile
  Cargo build process using rb-sys crate
  Native extension (.so/.bundle/.dll)

You can click the eye icon () to see the hidden details in these diagrams.

Comparison with Traditional C Extensions

Let's compare writing extensions in Rust versus C:

AspectC ExtensionsRust Extensions
Memory SafetyManual memory managementGuaranteed memory safety at compile time
Type SafetyWeak typing, runtime errorsStrong static typing, compile-time checks
API ErgonomicsLow-level C APIHigh-level Magnus API
Development SpeedSlower, more error-proneFaster, safer development cycle
Ecosystem AccessLimited to C librariesFull access to Rust crates
DebuggingHarder to debug memory issuesEasier to debug with Rust's safety guarantees
Cross-CompilationComplex manual configurationSimplified with rb-sys-dock

While C extensions offer flexibility and minimal dependencies, Rust extensions provide a superior developer experience with improved safety guarantees and access to a rich ecosystem of libraries.

Real-World Examples

These gems demonstrate rb-sys in action:

  • lz4-ruby - LZ4 compression library with rb-sys
  • wasmtime-rb - WebAssembly runtime with rb-sys and Magnus
  • oxi-test - Canonical example of how to use rb-sys (minimal, fully tested, cross-compiled)
  • blake3-ruby - Fast cryptographic hash function with full cross-platform support

Supported Toolchains

  • Ruby: 2.6+ (for full compatibility with Rubygems)
  • Rust: 1.65+

Dependencies

To build a Ruby extension in Rust, you'll need:

  • Ruby development headers (usually part of ruby-dev packages)
  • Rust (via rustup)
  • libclang (for bindgen)
    • On macOS: brew install llvm
    • On Linux: apt-get install libclang-dev

Getting Help

If you have questions, please join our Slack channel or open an issue on GitHub.

Contributing to this book

This book is open source! Find a typo? Did we overlook something? Send us a pull request!. Help wanted!

License

rb-sys is licensed under either:

  • Apache License, Version 2.0
  • MIT license

at your option.

Next Steps

Prerequisites and Installation

This chapter provides a streamlined setup guide for building Ruby extensions with Rust.

TL;DR

1. Install Prerequisites

# Install Ruby (3.0+ recommended)
# Using your preferred manager: rbenv, rvm, asdf, etc.

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install rb_sys gem
gem install rb_sys

2. Create a New Gem with Rust Extension

# Generate a new gem with Rust extension support
bundle gem --ext=rust mygem
cd mygem

# Build the extension
bundle install
bundle exec rake compile

# Try it out
bundle exec rake

That's it! You now have a working Ruby gem with Rust extension.

Detailed Installation

If you encounter issues with the quick start above, here are the detailed requirements:

Ruby Requirements

  • Ruby 3.0+ recommended (2.6+ supported)
  • Ruby development headers (usually part of official packages)
  • Bundler (gem install bundler)

Rust Requirements

  • Rust 1.65.0+ via rustup
  • Make sure Cargo is in your PATH (typically ~/.cargo/bin)

C Compiler Requirements

  • macOS: Xcode Command Line Tools (xcode-select --install)
  • Linux: build-essential (Debian/Ubuntu) or Development Tools (Fedora/RHEL)
  • Windows: Microsoft Visual Studio C++ Build Tools

libclang (for Ruby/Rust FFI bindings)

Simplest approach: add to your Gemfile

gem "libclang", "~> 14.0"

Verifying Your Setup

The simplest way to verify your setup is to create a test gem:

bundle gem --ext=rust hello_rusty
cd hello_rusty
bundle install
bundle exec rake compile
bundle exec rake test

If everything runs without errors, your environment is correctly set up.

Troubleshooting

If you encounter issues:

  1. Missing libclang: Add the libclang gem to your Gemfile
  2. Missing C compiler: Install appropriate build tools for your platform
  3. Ruby headers not found: Install Ruby development package

For detailed troubleshooting, consult the rb-sys wiki.

Next Steps

Quick Start: Your First Extension

This chapter shows you how to create a Ruby gem with a Rust extension using Bundler's built-in Rust support.

Creating a Gem with Bundler

The easiest way to create a new gem with a Rust extension is with Bundler:

# Create a new gem with a Rust extension
bundle gem --ext=rust hello_rusty
cd hello_rusty

This command generates everything you need to build a Ruby gem with a Rust extension.

Understanding the Generated Files

Let's examine the key files Bundler created:

hello_rusty/
├── ext/hello_rusty/          # Rust extension directory
│   ├── Cargo.toml            # Rust dependencies
│   ├── extconf.rb            # Ruby extension config
│   └── src/lib.rs            # Rust code
├── lib/hello_rusty.rb        # Main Ruby file
└── hello_rusty.gemspec       # Gem specification

The Rust Code (lib.rs)

Bundler generates a simple "Hello World" implementation:

#![allow(unused)]
fn main() {
// ext/hello_rusty/src/lib.rs
use magnus::{define_module, function, prelude::*, Error};

#[magnus::init]
fn init() -> Result<(), Error> {
    let module = define_module("HelloRusty")?;
    module.define_singleton_method("hello", function!(|| "Hello from Rust!", 0))?;
    Ok(())
}
}

You can click the "play" button on code blocks to try them out in the Rust Playground where appropriate. For code that depends on the Ruby API, you won't be able to run it directly, but you can experiment with Rust syntax and standard library functions.

The Extension Configuration (extconf.rb)

# ext/hello_rusty/extconf.rb
require "mkmf"
require "rb_sys/mkmf"

create_rust_makefile("hello_rusty/hello_rusty")

This file connects Ruby's build system to Cargo.

Enhancing the Default Implementation

Let's improve the default implementation by adding a simple class:

// This is our enhanced implementation
use magnus::{define_module, define_class, function, method, prelude::*, Error, Ruby};

// Define a struct to hold state
struct Greeter {
    name: String,
}

// Implement Ruby wrapper for the struct
#[magnus::wrap(class = "HelloRusty::Greeter")]
impl Greeter {
    // Constructor
    fn new(name: String) -> Self {
        Greeter { name }
    }

    // Instance method
    fn greet(&self) -> String {
        format!("Hello, {}!", self.name)
    }
}

// Let's also add a method that takes a parameter
impl Greeter {
    fn greet_with_prefix(&self, prefix: String) -> String {
        format!("{} Hello, {}!", prefix, self.name)
    }
}

// Module initialization function
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("HelloRusty")?;

    // Define and configure the Greeter class
    let class = module.define_class("Greeter", ruby.class_object())?;
    class.define_singleton_method("new", function!(Greeter::new, 1))?;
    class.define_method("greet", method!(Greeter::greet, 0))?;

    // We could also expose the additional method
    // class.define_method("greet_with_prefix", method!(Greeter::greet_with_prefix, 1))?;

    Ok(())
}

Click the eye icon () to reveal commented lines with additional functionality that you could add to your implementation.

Building and Testing

Compile the Extension

# Install dependencies and compile
bundle install
bundle exec rake compile

What happens during compilation:

  1. Ruby's mkmf reads your extconf.rb
  2. create_rust_makefile generates a Makefile with Cargo commands
  3. Cargo compiles your Rust code to a dynamic library
  4. The binary is copied to lib/hello_rusty/hello_rusty.{so,bundle,dll}

Run the Tests

Bundler generates a basic test file. Let's update it:

 test/test_hello_rusty.rb
require "test_helper"

class TestHelloRusty < Minitest::Test
  def test_that_it_has_a_version_number
    refute_nil ::HelloRusty::VERSION
  end

  def test_greeter
    greeter = HelloRusty::Greeter.new("Rustacean")
    assert_equal "Hello, Rustacean!", greeter.greet
  end

   # If we implemented the additional method, we could test it
   def test_greeter_with_prefix
     greeter = HelloRusty::Greeter.new("Rustacean")
     assert_equal "Howdy! Hello, Rustacean!", greeter.greet_with_prefix("Howdy!")
   end
end

Run the tests:

 Run the standard test suite
bundle exec rake test

 You can also run specific tests
 bundle exec ruby -Ilib:test test/test_hello_rusty.rb -n test_greeter

Try It in the Console

 Start the console
bundle exec bin/console

 You can also use irb directly
 bundle exec irb -Ilib -rhello_rusty

Once in the console, you can interact with your extension:

 Create a new greeter object
greeter = HelloRusty::Greeter.new("World")

 Call the greet method
puts greeter.greet  # => "Hello, World!"

 # If you added the additional method, you could call it
 puts greeter.greet_with_prefix("Howdy!")  # => "Howdy! Hello, World!"

Customizing the Build

You can customize the build process with environment variables:

 Release build (optimized)
RB_SYS_CARGO_PROFILE=release bundle exec rake compile

 With specific Cargo features
RB_SYS_CARGO_FEATURES=feature1,feature2 bundle exec rake compile

 You can also combine variables
 RB_SYS_CARGO_PROFILE=release RB_SYS_CARGO_FEATURES=feature1 bundle exec rake compile

 For more verbose output
 RB_SYS_CARGO_VERBOSE=1 bundle exec rake compile

Remember that building in release mode will produce optimized, faster code but will increase compilation time.

Next Steps

Congratulations! You've created a Ruby gem with a Rust extension. In the next chapters, we'll explore:

  • Better project organization
  • Working with Ruby objects in Rust
  • Memory management and safety
  • Performance optimization

Project Structure

In this chapter, we'll explore how to set up and organize a Ruby gem with a Rust extension. We'll focus on practical patterns and highlight how to leverage valuable Rust libraries without introducing unnecessary complexity.

Enhanced Project Structure

Building on the structure created by bundle gem --ext=rust, a well-organized rb-sys project typically looks like this:

my_gem/
├── Cargo.toml                # Rust workspace configuration
├── Gemfile                   # Ruby dependencies
├── Rakefile                  # Build tasks
├── my_gem.gemspec            # Gem specification
├── ext/
│   └── my_gem/
│       ├── Cargo.toml        # Rust crate configuration
│       ├── extconf.rb        # Ruby extension configuration
│       └── src/
│           └── lib.rs        # Main Rust entry point
├── lib/
│   ├── my_gem.rb             # Main Ruby file
│   └── my_gem/
│       └── version.rb        # Version information
└── test/                     # Tests

Let's examine a practical example using a useful but simple Rust library.

Example: URL Parsing with the url crate

The url crate, developed by the Servo team, is a robust implementation of the URL Standard. It provides accurate URL parsing that would be complex to implement from scratch. Here's a simple example:

1. Extension Cargo.toml

# ext/url_parser/Cargo.toml
[package]
name = "url_parser"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
license = "MIT"
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
# High-level Ruby bindings with rb-sys feature
magnus = { version = "0.7", features = ["rb-sys"] }

# The main Rust library we're wrapping
url = "2.4"

[build-dependencies]
rb-sys-env = "0.1"

2. Main Rust Implementation

#![allow(unused)]
fn main() {
// ext/url_parser/src/lib.rs
use magnus::{define_module, define_class, function, method, prelude::*, Error, Ruby};
use url::Url;

// Simple URL wrapper class
struct UrlWrapper {
    inner: Url,
}

#[magnus::wrap(class = "UrlParser::URL")]
impl UrlWrapper {
    // Parse a URL string
    fn parse(url_str: String) -> Result<Self, Error> {
        match Url::parse(&url_str) {
            Ok(url) => Ok(UrlWrapper { inner: url }),
            Err(err) => {
                Err(Error::new(magnus::exception::arg_error(), format!("Invalid URL: {}", err)))
            }
        }
    }

    // Basic getters
    fn scheme(&self) -> String {
        self.inner.scheme().to_string()
    }

    fn host(&self) -> Option<String> {
        self.inner.host_str().map(|s| s.to_string())
    }

    fn path(&self) -> String {
        self.inner.path().to_string()
    }

    fn query(&self) -> Option<String> {
        self.inner.query().map(|s| s.to_string())
    }

    // String representation of the URL
    fn to_string(&self) -> String {
        self.inner.to_string()
    }
}

// Module-level utilities
fn is_valid_url(url_str: String) -> bool {
    Url::parse(&url_str).is_ok()
}

// Module init function - Ruby extension entry point
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Define the main module
    let module = ruby.define_module("UrlParser")?;

    // Add utility function at module level
    module.define_singleton_method("valid_url?", function!(is_valid_url, 1))?;

    // Define and configure the URL class
    let class = module.define_class("URL", ruby.class_object())?;
    class.define_singleton_method("parse", function!(UrlWrapper::parse, 1))?;

    // Instance methods
    class.define_method("scheme", method!(UrlWrapper::scheme, 0))?;
    class.define_method("host", method!(UrlWrapper::host, 0))?;
    class.define_method("path", method!(UrlWrapper::path, 0))?;
    class.define_method("query", method!(UrlWrapper::query, 0))?;
    class.define_method("to_s", method!(UrlWrapper::to_string, 0))?;

    Ok(())
}
}

3. Ruby Integration

# lib/url_parser.rb
require_relative "url_parser/version"
require_relative "url_parser/url_parser"

module UrlParser
  class Error < StandardError; end

  # Parse a URL string and return a URL object
  def self.parse(url_string)
    URL.parse(url_string)
  rescue => e
    raise Error, "Failed to parse URL: #{e.message}"
  end

  # Check if a URL has an HTTPS scheme
  def self.https?(url_string)
    return false unless valid_url?(url_string)
    url = parse(url_string)
    url.scheme == "https"
  end
end

4. Simple Tests

# test/test_url_parser.rb
require "test_helper"

class TestUrlParser < Minitest::Test
  def test_basic_url_parsing
    url = UrlParser::URL.parse("https://example.com/path?query=value")

    assert_equal "https", url.scheme
    assert_equal "example.com", url.host
    assert_equal "/path", url.path
    assert_equal "query=value", url.query
  end

  def test_url_validation
    assert UrlParser.valid_url?("https://example.com")
    refute UrlParser.valid_url?("not a url")
  end

  def test_https_check
    assert UrlParser.https?("https://example.com")
    refute UrlParser.https?("http://example.com")
  end

  def test_invalid_url_raises_error
    assert_raises UrlParser::Error do
      UrlParser.parse("not://a.valid/url")
    end
  end
end

Key Aspects of this Project

1. Simplicity with Value

This example demonstrates how to:

  • Wrap a useful Rust library (url) with minimal code
  • Expose only the most essential functionality
  • Handle errors properly
  • Integrate with Ruby idiomatically

2. Why Use Rust for URL Parsing?

Ruby has URI handling in its standard library, but the Rust url crate offers advantages:

  • Full compliance with the URL standard used by browsers
  • Better handling of internationalized domain names (IDNs)
  • Robust error detection
  • Significant performance benefits for URL-heavy applications

3. Project Organization Principles

  • Keep dependencies minimal: Just what you need, nothing more
  • Clean public API: Expose only what users need
  • Proper error handling: Map Rust errors to meaningful Ruby exceptions
  • Simple tests: Verify both functionality and edge cases

Anatomy of a Rusty Ruby Gem: hello_rusty

This documentation provides a comprehensive walkthrough of the hello_rusty gem, a simple but complete Ruby gem that uses Rust for its native extension. This example demonstrates the key components of creating a Ruby gem with Rust using rb-sys and magnus.

Project Structure

A properly structured Rusty Ruby Gem follows the standard Ruby gem conventions with the addition of Rust-specific elements. Here's the structure of the hello_rusty gem:

hello_rusty/
├── bin/                      # Executable files
├── ext/                      # Native extension code
│   └── hello_rusty/          # The Rust extension directory
│       ├── Cargo.toml        # Rust package manifest
│       ├── extconf.rb        # Ruby extension configuration
│       └── src/
│           └── lib.rs        # Rust implementation
├── lib/                      # Ruby code
│   ├── hello_rusty.rb        # Main Ruby file
│   └── hello_rusty/
│       └── version.rb        # Version definition
├── sig/                      # RBS type signatures
│   └── hello_rusty.rbs       # Type definitions
├── test/                     # Test files
│   ├── test_hello_rusty.rb   # Test for the gem
│   └── test_helper.rb        # Test setup
├── Cargo.lock                # Rust dependency lock file
├── Cargo.toml                # Workspace-level Rust config (optional)
├── Gemfile                   # Ruby dependencies
├── LICENSE.txt               # License file
├── Rakefile                  # Build tasks
├── README.md                 # Documentation
└── hello_rusty.gemspec       # Gem specification

Key Components

1. Ruby Components

Gemspec (hello_rusty.gemspec)

The gemspec defines metadata about the gem and specifies build requirements:

# frozen_string_literal: true

require_relative "lib/hello_rusty/version"

Gem::Specification.new do |spec|
  spec.name = "hello_rusty"
  spec.version = HelloRusty::VERSION
  spec.authors = ["Ian Ker-Seymer"]
  spec.email = ["hello@ianks.com"]

  # ... metadata ...

  spec.required_ruby_version = ">= 3.0.0"

  # Files to include in the gem
  spec.files = [...]

  # IMPORTANT: This line tells RubyGems that this gem has a native extension
  # and where to find the build configuration
  spec.extensions = ["ext/hello_rusty/Cargo.toml"]

  spec.require_paths = ["lib"]
end

Key points:

  • The extensions field points to the Cargo.toml file
  • Version is defined in a separate Ruby file
  • Required Ruby version is specified

Main Ruby file (lib/hello_rusty.rb)

# frozen_string_literal: true

require_relative "hello_rusty/version"
require_relative "hello_rusty/hello_rusty"  # Loads the compiled Rust extension

module HelloRusty
  class Error < StandardError; end
  # Additional Ruby code can go here
end

Key points:

  • Requires the version file
  • Requires the compiled native extension
  • Defines a module matching the Rust module

Version file (lib/hello_rusty/version.rb)

# frozen_string_literal: true

module HelloRusty
  VERSION = "0.1.0"
end

Type Definitions (sig/hello_rusty.rbs)

RBS type definitions for better IDE support and type checking:

module HelloRusty
  VERSION: String
  # Add type signatures for your methods here
end

2. Rust Components

Cargo Configuration (ext/hello_rusty/Cargo.toml)

[package]
name = "hello_rusty"
version = "0.1.0"
edition = "2021"
authors = ["Ian Ker-Seymer <hello@ianks.com>"]
license = "MIT"
publish = false

[lib]
crate-type = ["cdylib"]  # Outputs a dynamic library

[dependencies]
magnus = { version = "0.6.2" }  # High-level Ruby bindings

Key points:

  • Uses cdylib crate type to build a dynamic library
  • Depends on magnus for high-level Ruby bindings
  • Version should match the Ruby gem version

Rust Implementation (ext/hello_rusty/src/lib.rs)

Here's the actual implementation from our example code:

#![allow(unused)]
fn main() {
use magnus::{function, prelude::*, Error, Ruby};

fn hello(subject: String) -> String {
    format!("Hello from Rust, {subject}!")
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("HelloRusty")?;
    module.define_singleton_method("hello", function!(hello, 1))?;
    Ok(())
}
}

This code is included directly from the example project file. When the source file is updated, this documentation will automatically reflect those changes.

Key points:

  • Uses the magnus crate for Ruby integration
  • The #[magnus::init] macro marks the entry point for the extension
  • Defines a Ruby module matching the gem name
  • Exposes the hello Rust function as a Ruby method

3. Build System

Extension Configuration (ext/hello_rusty/extconf.rb)

# frozen_string_literal: true

require "mkmf"
require "rb_sys/mkmf"

create_rust_makefile("hello_rusty/hello_rusty")

Key points:

  • Uses rb_sys/mkmf to handle Rust compilation
  • Creates a makefile for the native extension

Rakefile (Rakefile)

Here's the actual Rakefile from our example project:

 frozen_string_literal: true

require "bundler/gem_tasks"
require "minitest/test_task"

Minitest::TestTask.create

require "rubocop/rake_task"

RuboCop::RakeTask.new

require "rb_sys/extensiontask"

task build: :compile

GEMSPEC = Gem::Specification.load("hello_rusty.gemspec")

RbSys::ExtensionTask.new("hello_rusty", GEMSPEC) do |ext|
  ext.lib_dir = "lib/hello_rusty"
end

task default: %i[compile test rubocop]

 You can customize the build further:
 RbSys::ExtensionTask.new("hello_rusty", GEMSPEC) do |ext|
   ext.lib_dir = "lib/hello_rusty"
   ext.cross_compile = true  # Enable cross-compilation
   ext.cross_platform = ['x86_64-linux', 'x86_64-darwin']  # Platforms to target
 end

The eye icon () reveals additional configuration options you can use in your Rakefile.

Key points:

  • Uses RbSys::ExtensionTask to manage Rust compilation
  • Sets the output directory to lib/hello_rusty
  • Defines standard tasks for building, testing, and linting

4. Testing

Test File (test/test_hello_rusty.rb)

# frozen_string_literal: true

require "test_helper"

class TestHelloRusty < Minitest::Test
  def test_that_it_has_a_version_number
    refute_nil ::HelloRusty::VERSION
  end

  def test_hello
    result = HelloRusty.hello("World")
    assert_equal "Hello from Rust, World!", result
  end
end

Key points:

  • Tests basic functionality of the gem
  • Verifies the version is defined
  • Tests the Rust-implemented hello method

Build Process

When building a Rusty Ruby Gem, the following steps occur:

  1. rake compile is run (either directly or through rake build)
  2. The RbSys::ExtensionTask processes the extension:
    • It reads the ext/hello_rusty/Cargo.toml file
    • It sets up the appropriate build environment
    • It runs cargo build with the appropriate options
    • It copies the resulting .so/.bundle/.dll to lib/hello_rusty/
  3. The compiled binary is then packaged into the gem

Usage

Once installed, this gem can be used in Ruby code as follows:

require "hello_rusty"

# Call the Rust-implemented method
greeting = HelloRusty.hello("Rusty Rubyist")
puts greeting  # => "Hello from Rust, Rusty Rubyist!"

Key Concepts Demonstrated

  1. Module Structure: The gem defines a Ruby module that's implemented in Rust
  2. Function Exposure: Rust functions are exposed as Ruby methods
  3. Type Conversion: Rust handles string conversion automatically through magnus
  4. Error Handling: The Rust code uses Result<T, Error> for Ruby-compatible error handling
  5. Build Integration: The gem uses rb-sys to integrate with Ruby's build system
  6. Testing: Standard Ruby testing tools work with the Rust-implemented functionality

Next Steps for Expansion

To expand this basic example, you could:

  1. Add Ruby classes backed by Rust structs using TypedData
  2. Implement more complex methods with various argument types
  3. Add error handling with custom Ruby exceptions
  4. Use the Ruby GVL (Global VM Lock) for thread safety
  5. Implement memory management through proper object marking
  6. Add benchmarks to demonstrate performance characteristics

This example provides a solid foundation for understanding the structure and implementation of Rusty Ruby Gems with rb-sys.

Development Approaches

When building Ruby extensions with Rust and rb-sys, you have two main approaches to choose from:

  1. Direct rb-sys usage: Working directly with Ruby's C API through the rb-sys bindings
  2. Higher-level wrappers: Using libraries like Magnus that build on top of rb-sys

This chapter will help you understand when to use each approach and how to mix them when needed.

Direct rb-sys Usage

The rb-sys crate provides low-level bindings to Ruby's C API. This approach gives you complete control over how your Rust code interacts with Ruby.

When to Use Direct rb-sys

  • When you need maximum control over Ruby VM interaction
  • For specialized extensions that need access to low-level Ruby internals
  • When performance is absolutely critical and you need to eliminate any overhead
  • When implementing functionality not yet covered by higher-level wrappers

Example: Simple Extension with Direct rb-sys

Here's a simple example of a Ruby extension using direct rb-sys:

#![allow(unused)]
fn main() {
use rb_sys::{
    rb_define_module, rb_define_module_function, rb_str_new_cstr,
    rb_string_value_cstr, VALUE
};
use std::ffi::CString;
use std::os::raw::c_char;

// Helper macro for creating C strings
macro_rules! cstr {
    ($s:expr) => {
        concat!($s, "\0").as_ptr() as *const c_char
    };
}

// Reverse a string
unsafe extern "C" fn reverse(_: VALUE, s: VALUE) -> VALUE {
    let c_str = rb_string_value_cstr(&s);
    let rust_str = std::ffi::CStr::from_ptr(c_str).to_str().unwrap();
    let reversed = rust_str.chars().rev().collect::<String>();

    let c_string = CString::new(reversed).unwrap();
    rb_str_new_cstr(c_string.as_ptr())
}

// Module initialization function
#[no_mangle]
pub extern "C" fn Init_string_utils() {
    unsafe {
        let module = rb_define_module(cstr!("StringUtils"));

        rb_define_module_function(
            module,
            cstr!("reverse"),
            Some(reverse as unsafe extern "C" fn(VALUE, VALUE) -> VALUE),
            1,
        );
    }
}
}

Using rb_thread_call_without_gvl for Performance

When performing computationally intensive operations, it's important to release Ruby's Global VM Lock (GVL) to allow other threads to run. The rb_thread_call_without_gvl function provides this capability:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby, RString};
use rb_sys::rb_thread_call_without_gvl;
use std::{ffi::c_void, panic::{self, AssertUnwindSafe}, ptr::null_mut};

/// Execute a function without holding the Global VM Lock (GVL).
/// This allows other Ruby threads to run while performing CPU-intensive tasks.
///
/// # Safety
///
/// The passed function must not interact with the Ruby VM or Ruby objects
/// as it runs without the GVL, which is required for safe Ruby operations.
///
/// # Returns
///
/// Returns the result of the function or a magnus::Error if the function panics.
pub fn nogvl<F, R>(func: F) -> Result<R, Error>
where
    F: FnOnce() -> R,
    R: Send + 'static,
{
    struct CallbackData<F, R> {
        func: Option<F>,
        result: Option<Result<R, String>>, // Store either the result or a panic message
    }

    extern "C" fn call_without_gvl<F, R>(data: *mut c_void) -> *mut c_void
    where
        F: FnOnce() -> R,
        R: Send + 'static,
    {
        // Safety: We know this pointer is valid because we just created it below
        let data = unsafe { &mut *(data as *mut CallbackData<F, R>) };

        // Use take() to move out of the Option, ensuring we don't try to run the function twice
        if let Some(func) = data.func.take() {
            // Use panic::catch_unwind to prevent Ruby process termination if the Rust code panics
            match panic::catch_unwind(AssertUnwindSafe(func)) {
                Ok(result) => data.result = Some(Ok(result)),
                Err(panic_info) => {
                    // Convert panic info to a string message
                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&'static str>() {
                        s.to_string()
                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
                        s.clone()
                    } else {
                        "Unknown panic occurred in Rust code".to_string()
                    };

                    data.result = Some(Err(panic_msg));
                }
            }
        }

        null_mut()
    }

    // Create a data structure to pass the function and receive the result
    let mut data = CallbackData {
        func: Some(func),
        result: None,
    };

    unsafe {
        // Release the GVL and call our function
        rb_thread_call_without_gvl(
            Some(call_without_gvl::<F, R>),
            &mut data as *mut _ as *mut c_void,
            None,  // No unblock function
            null_mut(),
        );
    }

    // Extract the result or create an error if the function failed
    match data.result {
        Some(Ok(result)) => Ok(result),
        Some(Err(panic_msg)) => {
            // Convert the panic message to a Ruby RuntimeError
            let ruby = unsafe { Ruby::get_unchecked() };
            Err(Error::new(
                ruby.exception_runtime_error(),
                format!("Rust panic in nogvl: {}", panic_msg)
            ))
        },
        None => {
            // This should never happen if the callback runs, but handle it anyway
            let ruby = unsafe { Ruby::get_unchecked() };
            Err(Error::new(
                ruby.exception_runtime_error(),
                "nogvl function was not executed"
            ))
        }
    }
}

// For checking large inputs
pub fn nogvl_if_large<F, R>(input_len: usize, func: F) -> Result<R, Error>
where
    F: FnOnce() -> R,
    R: Send + 'static,
{
    const MAX_INPUT_LEN: usize = 8192; // Threshold for using GVL release

    if input_len > MAX_INPUT_LEN {
        nogvl(func)
    } else {
        // If the input is small, just run the function directly
        // but still wrap the result in a Result for consistency
        match panic::catch_unwind(AssertUnwindSafe(func)) {
            Ok(result) => Ok(result),
            Err(_) => {
                let ruby = unsafe { Ruby::get_unchecked() };
                Err(Error::new(
                    ruby.exception_runtime_error(),
                    "Rust panic in small input path"
                ))
            }
        }
    }
}

// Example: Using with Magnus API
fn compress(ruby: &Ruby, data: RString) -> Result<RString, Error> {
    let data_bytes = data.as_bytes();
    let data_len = data_bytes.len();

    // Use nogvl_if_large with proper error handling
    let compressed_bytes = nogvl_if_large(data_len, || {
        // CPU-intensive operation here that returns a Vec<u8>
        compression_algorithm(data_bytes)
    })?; // Propagate any errors

    // Create new Ruby string with compressed data
    let result = RString::from_slice(ruby, &compressed_bytes);
    Ok(result)
}

// Example: Registering the method
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("Compression")?;

    // Using method! for defining instance methods
    module.define_singleton_method("compress", function!(compress, 1))?;

    Ok(())
}
}

How Direct rb-sys Works

When using rb-sys directly:

  1. You define C-compatible functions with the extern "C" calling convention
  2. You manually convert between Ruby's VALUE type and Rust types
  3. You're responsible for memory management and type safety
  4. You must use the #[no_mangle] attribute on the initialization function so Ruby can find it
  5. All interactions with Ruby data happen through raw pointers and unsafe code

Higher-level Wrappers (Magnus)

Magnus provides a more ergonomic, Rust-like API on top of rb-sys. It handles many of the unsafe aspects of Ruby integration for you.

When to Use Magnus

  • For most standard Ruby extensions where ease of development is important
  • When you want to avoid writing unsafe code
  • When you want idiomatic Rust error handling
  • For extensions with complex type conversions
  • When working with Ruby classes and objects in an object-oriented way

Example: Simple Extension with Magnus

Let's look at a simple example using Magnus, based on real-world usage patterns:

#![allow(unused)]
fn main() {
use magnus::{function, prelude::*, Error, Ruby};

fn hello(subject: String) -> String {
    format!("Hello from Rust, {subject}!")
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("StringUtils")?;
    module.define_singleton_method("hello", function!(hello, 1))?;
    Ok(())
}
}

Looking at a more complex example from a real-world project (lz4-flex-rb):

#![allow(unused)]
fn main() {
use magnus::{function, prelude::*, Error, RModule, Ruby};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("Lz4Flex")?;

    // Define error classes
    let base_error = module.define_error("Error", magnus::exception::standard_error())?;
    let _ = module.define_error("EncodeError", base_error)?;
    let _ = module.define_error("DecodeError", base_error)?;

    // Define methods
    module.define_singleton_method("compress", function!(compress, 1))?;
    module.define_singleton_method("decompress", function!(decompress, 1))?;

    // Define aliases
    module.singleton_class()?.define_alias("deflate", "compress")?;
    module.singleton_class()?.define_alias("inflate", "decompress")?;

    // Define nested module
    let varint_module = module.define_module("VarInt")?;
    varint_module.define_singleton_method("compress", function!(compress_varint, 1))?;
    varint_module.define_singleton_method("decompress", function!(decompress_varint, 1))?;

    Ok(())
}
}

How Magnus Works

Magnus builds on top of rb-sys and provides:

  1. Automatic type conversions between Ruby and Rust
  2. Rust-like error handling with Result types
  3. Memory safety through RAII patterns
  4. More ergonomic APIs for defining modules, classes, and methods
  5. A more familiar development experience for Rust programmers

When to Choose Each Approach

Choose Direct rb-sys When:

  • Performance is absolutely critical: You need to eliminate every bit of overhead
  • You need low-level control: Your extension needs to do things not possible with Magnus
  • GVL management is important: You need fine-grained control over when to release the GVL
  • Compatibility with older Ruby versions: You need version-specific behavior

Choose Magnus When:

  • Developer productivity is important: You want to write less code
  • Memory safety is a priority: You want Rust's safety guarantees
  • You're working with complex Ruby objects: You need convenient methods for Ruby class integration
  • Error handling is complex: You want to leverage Rust's error handling

Mixing Approaches

You can also mix the two approaches when appropriate. Magnus provides access to the underlying rb-sys functionality when needed:

#![allow(unused)]
fn main() {
use magnus::{function, prelude::*, Error, Ruby};
use rb_sys;
use std::os::raw::c_char;

fn high_level() -> String {
    "High level".to_string()
}

unsafe extern "C" fn low_level(_: rb_sys::VALUE) -> rb_sys::VALUE {
    // Direct rb-sys implementation
    let c_string = std::ffi::CString::new("Low level").unwrap();
    rb_sys::rb_str_new_cstr(c_string.as_ptr())
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MixedExample")?;

    // Use Magnus for most things
    module.define_singleton_method("high_level", function!(high_level, 0))?;

    // Use rb-sys directly for special cases
    unsafe {
        rb_sys::rb_define_module_function(
            module.as_raw(),
            cstr!("low_level"),
            Some(low_level as unsafe extern "C" fn(rb_sys::VALUE) -> rb_sys::VALUE),
            0,
        );
    }

    Ok(())
}

// Helper macro for C strings
macro_rules! cstr {
    ($s:expr) => {
        concat!($s, "\0").as_ptr() as *const c_char
    };
}
}

Enabling rb-sys Feature in Magnus

To access rb-sys through Magnus, enable the rb-sys feature:

# Cargo.toml
[dependencies]
magnus = { version = "0.7", features = ["rb-sys"] }

Common Mixing Patterns

  1. Use Magnus for most functionality, rb-sys for specific optimizations:

    • Define your public API using Magnus for safety and ease
    • Drop down to rb-sys in critical performance paths, especially when using nogvl
  2. Use rb-sys for core functionality, Magnus for complex conversions:

    • Build core functionality with rb-sys for maximum control
    • Use Magnus for handling complex Ruby objects or collections
  3. Start with Magnus, optimize with rb-sys over time:

    • Begin development with Magnus for rapid progress
    • Profile your code and replace hot paths with direct rb-sys

Real-World Examples

Let's look at how real projects decide between these approaches:

Blake3-Ruby (Direct rb-sys)

Blake3-Ruby is a cryptographic hashing library that uses direct rb-sys to achieve maximum performance:

#![allow(unused)]
fn main() {
// Based on blake3-ruby
use rb_sys::{
    rb_define_module, rb_define_module_function, rb_string_value_cstr,
    rb_str_new_cstr, VALUE,
};

#[no_mangle]
pub extern "C" fn Init_blake3_ext() {
    unsafe {
        // Create module and class hierarchy
        let digest_module = /* ... */;
        let blake3_class = /* ... */;

        // Define methods directly using rb-sys for maximum performance
        rb_define_module_function(
            blake3_class,
            cstr!("digest"),
            Some(rb_blake3_digest as unsafe extern "C" fn(VALUE, VALUE) -> VALUE),
            1,
        );

        // More method definitions...
    }
}

unsafe extern "C" fn rb_blake3_digest(_klass: VALUE, string: VALUE) -> VALUE {
    // Extract data from Ruby VALUE
    let data_ptr = rb_string_value_cstr(&string);
    let data_len = /* ... */;

    // Release GVL for CPU-intensive operation
    let hash = nogvl(|| {
        blake3::hash(/* ... */)
    });

    // Return result as Ruby string
    rb_str_new_cstr(/* ... */)
}
}

LZ4-Flex-RB (Mixed Approach)

The LZ4-Flex-RB gem demonstrates a more sophisticated approach mixing Magnus with direct rb-sys calls:

#![allow(unused)]
fn main() {
// Based on lz4-flex-rb
use magnus::{function, prelude::*, Error, RModule, Ruby};
use rb_sys::{rb_str_locktmp, rb_str_unlocktmp, rb_thread_call_without_gvl};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("Lz4Flex")?;

    // High-level API using Magnus
    module.define_singleton_method("compress", function!(compress, 1))?;
    module.define_singleton_method("decompress", function!(decompress, 1))?;

    Ok(())
}

// Functions that mix high-level Magnus with low-level rb-sys
fn compress(input: LockedRString) -> Result<RString, Error> {
    let bufsize = get_maximum_output_size(input.len());
    let mut output = RStringMut::buf_new(bufsize);

    // Use nogvl_if_large to release GVL for large inputs
    let outsize = nogvl_if_large(input.len(), || {
        lz4_flex::block::compress_into(input.as_slice(), output.as_mut_slice())
    }).map_err(|e| Error::new(encode_error_class(), e.to_string()))?;

    output.set_len(outsize);
    Ok(output.into_inner())
}

// Helper for locked RString (uses rb-sys directly)
struct LockedRString(RString);

impl LockedRString {
    fn new(string: RString) -> Self {
        unsafe { rb_str_locktmp(string.as_raw()) };
        Self(string)
    }

    fn as_slice(&self) -> &[u8] {
        // Implementation using rb-sys functions
    }
}

impl Drop for LockedRString {
    fn drop(&mut self) {
        unsafe { rb_str_unlocktmp(self.0.as_raw()) };
    }
}
}

Working with Ruby Objects

Basic Type Conversions

When writing Ruby extensions in Rust, one of the most common tasks is converting between Ruby and Rust types. The magnus crate provides a comprehensive set of conversion functions for this purpose.

Primitive Types

#![allow(unused)]
fn main() {
use magnus::{RString, Ruby, Value, Integer, Float, Boolean};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Convert Rust types to Ruby
    let rb_string: RString = RString::new(ruby, "Hello, Ruby!");  // Rust &str to Ruby String
    let rb_int: Integer = Integer::from_i64(42);                 // Rust i64 to Ruby Integer
    let rb_float: Float = Float::from_f64(3.14159);             // Rust f64 to Ruby Float
    let rb_bool: Boolean = Boolean::from(true);                 // Rust bool to Ruby true/false

    // Convert Ruby types to Rust
    let rust_string: String = rb_string.to_string()?;           // Ruby String to Rust String
    let rust_int: i64 = rb_int.to_i64()?;                       // Ruby Integer to Rust i64
    let rust_float: f64 = rb_float.to_f64()?;                   // Ruby Float to Rust f64
    let rust_bool: bool = rb_bool.to_bool();                    // Ruby true/false to Rust bool

    Ok(())
}
}

Checking Types

When working with Ruby objects, you often need to check their types:

#![allow(unused)]
fn main() {
use magnus::{RString, Ruby, Value, check_type};

fn process_value(ruby: &Ruby, val: Value) -> Result<(), magnus::Error> {
    if val.is_nil() {
        println!("Got nil");
    } else if let Ok(s) = RString::try_convert(val) {
        println!("Got string: {}", s.to_string()?);
    } else if check_type::<Integer>(val) {
        println!("Got integer: {}", Integer::from_value(val)?.to_i64()?);
    } else {
        println!("Got some other type");
    }

    Ok(())
}
}

Strings, Arrays, and Hashes

Working with Ruby Strings

Ruby strings are encoded and have more complex behavior than Rust strings:

#![allow(unused)]
fn main() {
use magnus::{RString, Ruby, Encoding};

fn string_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Create a new Ruby string
    let hello = RString::new(ruby, "Hello");

    // Concatenate strings
    let world = RString::new(ruby, " World!");
    let message = hello.concat(ruby, world)?;

    // Get the encoding
    let encoding = message.encoding();
    println!("String encoding: {}", encoding.name());

    // Convert to different encoding
    let utf16 = Encoding::find("UTF-16BE").unwrap();
    let utf16_str = message.encode(ruby, utf16, None)?;

    // Get bytes
    let bytes = message.as_bytes();
    println!("Bytes: {:?}", bytes);

    // Create from bytes with specific encoding
    let latin1 = Encoding::find("ISO-8859-1").unwrap();
    let bytes = [72, 101, 108, 108, 111]; // "Hello" in ASCII/Latin1
    let latin1_str = RString::from_slice(ruby, &bytes, Some(latin1));

    Ok(())
}
}

Working with Ruby Arrays

Ruby arrays can hold any kind of Ruby object:

#![allow(unused)]
fn main() {
use magnus::{RArray, Ruby, Value};

fn array_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Create a new empty array
    let array = RArray::new(ruby);

    // Push elements
    array.push(ruby, 1)?;
    array.push(ruby, "two")?;
    array.push(ruby, 3.0)?;

    // Get length
    let length = array.len();
    println!("Array length: {}", length);

    // Access elements
    let first: i64 = array.get(0)?;
    let second: String = array.get(1)?;
    let third: f64 = array.get(2)?;

    // Iterate through elements
    for i in 0..array.len() {
        let item: Value = array.get(i)?;
        println!("Item {}: {:?}", i, item);
    }

    // Another way to iterate
    array.each(|val| {
        println!("Item: {:?}", val);
        Ok(())
    })?;

    // Create an array from Rust Vec
    let numbers = vec![1, 2, 3, 4, 5];
    let rb_array = RArray::from_iter(ruby, numbers);

    // Convert to a Rust Vec
    let vec: Vec<i64> = rb_array.to_vec()?;

    Ok(())
}
}

Working with Ruby Hashes

Ruby hashes are similar to Rust's HashMap but can use any Ruby object as keys:

#![allow(unused)]
fn main() {
use magnus::{RHash, Value, Symbol, Ruby};

fn hash_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Create a new hash
    let hash = RHash::new(ruby);

    // Add key-value pairs
    hash.aset(ruby, "name", "Alice")?;
    hash.aset(ruby, Symbol::new("age"), 30)?;
    hash.aset(ruby, 1, "one")?;

    // Get values
    let name: String = hash.get(ruby, "name")?;
    let age: i64 = hash.get(ruby, Symbol::new("age"))?;
    let one: String = hash.get(ruby, 1)?;

    // Check if key exists
    if hash.has_key(ruby, "name")? {
        println!("Has key 'name'");
    }

    // Delete a key
    hash.delete(ruby, 1)?;

    // Iterate over key-value pairs
    hash.foreach(|k, v| {
        println!("Key: {:?}, Value: {:?}", k, v);
        Ok(())
    })?;

    // Convert to a Rust HashMap (if keys and values are convertible)
    let map: std::collections::HashMap<String, String> = hash.to_hash()?;

    Ok(())
}
}

Handling nil Values

Ruby's nil is a special value that requires careful handling:

#![allow(unused)]
fn main() {
use magnus::{Value, Ruby, RNil};

fn handle_nil(ruby: &Ruby, val: Value) -> Result<(), magnus::Error> {
    // Check if a value is nil
    if val.is_nil() {
        println!("Value is nil");
    }

    // Get nil
    let nil = ruby.nil();

    // Options and nil
    let maybe_string: Option<String> = val.try_convert()?;
    match maybe_string {
        Some(s) => println!("Got string: {}", s),
        None => println!("No string (was nil or couldn't convert)"),
    }

    // Explicitly return nil from a function
    fn returns_nil() -> RNil {
        RNil::get()
    }

    Ok(())
}
}

Converting Between Ruby and Rust Types

Magnus provides powerful type conversion traits that make it easy to convert between Ruby and Rust types.

From Rust to Ruby (TryConvert)

#![allow(unused)]
fn main() {
use magnus::{Value, Ruby, TryConvert, Error};

// Convert custom Rust types to Ruby objects
struct Person {
    name: String,
    age: u32,
}

impl TryConvert for Person {
    fn try_convert(val: Value) -> Result<Self, Error> {
        let ruby = unsafe { Ruby::get_unchecked() };
        let hash = RHash::try_convert(val)?;

        let name: String = hash.get(ruby, "name")?;
        let age: u32 = hash.get(ruby, "age")?;

        Ok(Person { name, age })
    }
}

// Usage
fn process_person(val: Value) -> Result<(), Error> {
    let person: Person = val.try_convert()?;
    println!("Person: {} ({})", person.name, person.age);
    Ok(())
}
}

From Ruby to Rust (IntoValue)

#![allow(unused)]
fn main() {
use magnus::{Value, Ruby, IntoValue, Error};

struct Point {
    x: f64,
    y: f64,
}

impl IntoValue for Point {
    fn into_value_with(self, ruby: &Ruby) -> Result<Value, Error> {
        let hash = RHash::new(ruby);
        hash.aset(ruby, "x", self.x)?;
        hash.aset(ruby, "y", self.y)?;
        Ok(hash.as_value())
    }
}

// Usage
fn create_point(ruby: &Ruby) -> Result<Value, Error> {
    let point = Point { x: 10.5, y: 20.7 };
    point.into_value_with(ruby)
}
}

Creating Ruby Objects from Rust

Creating Simple Objects

#![allow(unused)]
fn main() {
use magnus::{RObject, Ruby, Value, class, method};

fn create_objects(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Create a basic Ruby Object
    let obj = RObject::new(ruby, ruby.class_object())?;

    // Instantiate a specific class
    let time_class = ruby.class_object::<Time>()?;
    let now = time_class.funcall(ruby, "now", ())?;

    // Create a Date object
    let date_class = class::object("Date")?;
    let today = date_class.funcall(ruby, "today", ())?;

    // Call methods on the object
    let formatted: String = today.funcall(ruby, "strftime", ("%Y-%m-%d",))?;

    Ok(())
}
}

Creating Objects with Instance Variables

#![allow(unused)]
fn main() {
use magnus::{RObject, Ruby, Symbol};

fn create_with_ivars(ruby: &Ruby) -> Result<(), magnus::Error> {
    // Create a Ruby object
    let obj = RObject::new(ruby, ruby.class_object())?;

    // Set instance variables
    obj.ivar_set(ruby, "@name", "Alice")?;
    obj.ivar_set(ruby, "@age", 30)?;

    // Get instance variables
    let name: String = obj.ivar_get(ruby, "@name")?;
    let age: i64 = obj.ivar_get(ruby, "@age")?;

    // Alternatively, use symbols
    let name_sym = Symbol::new("@name");
    let name_value = obj.ivar_get(ruby, name_sym)?;

    Ok(())
}
}

Working with Ruby Methods

#![allow(unused)]
fn main() {
use magnus::{RObject, Ruby, prelude::*};

fn call_methods(ruby: &Ruby) -> Result<(), magnus::Error> {
    let array_class = ruby.class_object::<RArray>()?;

    // Creating an array with methods
    let array = array_class.funcall(ruby, "new", (5, "hello"))?;

    // Call methods with different argument patterns
    array.funcall(ruby, "<<", ("world",))?; // One argument
    array.funcall(ruby, "insert", (1, "inserted"))?; // Multiple arguments

    // Call with a block using a closure
    let mapped = array.funcall_with_block(ruby, "map", (), |arg| {
        if let Ok(s) = String::try_convert(arg) {
            Ok(s.len())
        } else {
            Ok(0)
        }
    })?;

    // Methods with keyword arguments
    let hash_class = ruby.class_object::<RHash>()?;
    let merge_opts = [(
        Symbol::new("overwrite"),
        true
    )];
    let hash = RHash::new(ruby);
    let other = RHash::new(ruby);
    hash.funcall_kw(ruby, "merge", (other,), merge_opts)?;

    Ok(())
}
}

Advanced Techniques

Handling Arbitrary Ruby Values

Sometimes you need to work with Ruby values without knowing their type in advance:

#![allow(unused)]
fn main() {
use magnus::{Value, Ruby, CheckType, Error};

fn describe_value(val: Value) -> Result<String, Error> {
    let ruby = unsafe { Ruby::get_unchecked() };

    if val.is_nil() {
        return Ok("nil".to_string());
    }

    if let Ok(s) = String::try_convert(val) {
        return Ok(format!("String: {}", s));
    }

    if let Ok(i) = i64::try_convert(val) {
        return Ok(format!("Integer: {}", i));
    }

    if let Ok(f) = f64::try_convert(val) {
        return Ok(format!("Float: {}", f));
    }

    if val.respond_to(ruby, "each")? {
        return Ok("Enumerable object".to_string());
    }

    // Get the class name
    let class_name: String = val.class().name();
    Ok(format!("Object of class: {}", class_name))
}
}

Working with Duck Types

Ruby often uses duck typing rather than relying on concrete classes:

#![allow(unused)]
fn main() {
use magnus::{Error, Value, Ruby};

fn process_enumerable(ruby: &Ruby, val: Value) -> Result<Value, Error> {
    // Check if the object responds to 'each'
    if !val.respond_to(ruby, "each")? {
        return Err(Error::new(
            ruby.exception_type_error(),
            "Expected an object that responds to 'each'"
        ));
    }

    // We can now safely call 'map' which most enumerables support
    val.funcall_with_block(ruby, "map", (), |item| {
        if let Ok(n) = i64::try_convert(item) {
            Ok(n * 2)
        } else {
            Ok(item)  // Pass through unchanged if not a number
        }
    })
}
}

Best Practices

  1. Always Handle Errors: Type conversions can fail, wrap them in proper error handling.

  2. Use try_convert: Prefer try_convert over direct conversions to safely handle type mismatches.

  3. Remember Boxing Rules: All Ruby objects are reference types, while many Rust types are value types.

  4. Be Careful with Magic Methods: Some Ruby methods like method_missing might not behave as expected when called from Rust.

  5. Cache Ruby Objects: If you're repeatedly using the same Ruby objects (like classes or symbols), consider caching them using Lazy or similar mechanisms.

  6. Check for nil: Always check for nil values before attempting conversions that don't handle nil.

  7. Use Type Annotations: Explicitly specifying types when converting Ruby values to Rust can make your code clearer and avoid potential runtime errors.

  8. Pass Ruby State: Always pass the Ruby instance through your functions when needed rather than using Ruby::get() repeatedly, as this is more performant and clearer about dependencies.

Ruby Classes and Modules

This chapter covers how to define and work with Ruby classes and modules from Rust. It explains different approaches for creating Ruby objects, defining methods, and organizing your code.

Defining Modules

Modules in Ruby are used to namespace functionality and define mixins. Here's how to create and use modules in your Rust extension:

Creating a Basic Module

#![allow(unused)]
fn main() {
use magnus::{define_module, prelude::*, Error, Ruby};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Create a top-level module
    let module = ruby.define_module("MyExtension")?;

    // Define a method on the module
    module.define_singleton_method("version", function!(|| "1.0.0", 0))?;

    // Create a nested module
    let utils = module.define_module("Utils")?;
    utils.define_singleton_method("helper", function!(|| "Helper function", 0))?;

    Ok(())
}
}

This creates a module structure that would look like this in Ruby:

module MyExtension
  def self.version
    "1.0.0"
  end

  module Utils
    def self.helper
      "Helper function"
    end
  end
end

Module Constants

You can define constants in your modules:

#![allow(unused)]
fn main() {
use magnus::{define_module, Module, Ruby, Error, Value, Symbol};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("Config")?;

    // Define constants
    module.const_set::<_, _, Value>(ruby, "VERSION", "1.0.0")?;
    module.const_set::<_, _, Value>(ruby, "MAX_CONNECTIONS", 100)?;
    module.const_set::<_, _, Value>(ruby, "DEFAULT_MODE", Symbol::new("production"))?;

    Ok(())
}
}

Using Module Attributes

To maintain module state, a common pattern is storing attributes in the module itself:

#![allow(unused)]
fn main() {
use magnus::{define_module, function, prelude::*, Error, Module, Ruby};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

// Store a counter in a static atomic
static REQUEST_COUNT: AtomicUsize = AtomicUsize::new(0);

// Store configuration in a mutex
static CONFIG: Mutex<Option<String>> = Mutex::new(None);

fn increment_counter() -> usize {
    REQUEST_COUNT.fetch_add(1, Ordering::SeqCst)
}

fn get_config() -> Result<String, Error> {
    match CONFIG.lock().unwrap().clone() {
        Some(config) => Ok(config),
        None => Ok("default".to_string()),
    }
}

fn set_config(value: String) -> Result<String, Error> {
    let mut config = CONFIG.lock().unwrap();
    *config = Some(value.clone());
    Ok(value)
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("Stats")?;

    module.define_singleton_method("increment", function!(increment_counter, 0))?;
    module.define_singleton_method("count", function!(|| REQUEST_COUNT.load(Ordering::SeqCst), 0))?;

    // Configuration methods
    module.define_singleton_method("config", function!(get_config, 0))?;
    module.define_singleton_method("config=", function!(set_config, 1))?;

    Ok(())
}
}

Creating Ruby Classes from Rust Structs

Magnus provides several ways to define Ruby classes that wrap Rust structures. The approach you choose depends on your specific needs.

Using the TypedData Trait (Full Control)

For full control over memory management and Ruby integration:

#![allow(unused)]
fn main() {
use magnus::{class, define_class, method, prelude::*, DataTypeFunctions, TypedData, Error, Ruby};

// Define a Rust struct
#[derive(Debug, TypedData)]
#[magnus(class = "MyExtension::Point", free_immediately, size)]
struct Point {
    x: f64,
    y: f64,
}

// Implement required trait
impl DataTypeFunctions for Point {}

// Implement methods
impl Point {
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    fn x(&self) -> f64 {
        self.x
    }

    fn y(&self) -> f64 {
        self.y
    }

    fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }

    fn to_s(&self) -> String {
        format!("Point({}, {})", self.x, self.y)
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyExtension")?;
    let class = module.define_class("Point", ruby.class_object())?;

    // Define the constructor
    class.define_singleton_method("new", function!(|x: f64, y: f64| {
        Point::new(x, y)
    }, 2))?;

    // Define instance methods
    class.define_method("x", method!(Point::x, 0))?;
    class.define_method("y", method!(Point::y, 0))?;
    class.define_method("distance", method!(Point::distance, 1))?;
    class.define_method("to_s", method!(Point::to_s, 0))?;

    Ok(())
}
}

Using the Wrap Macro (Simplified Approach)

For a simpler approach with less boilerplate:

#![allow(unused)]
fn main() {
use magnus::{define_class, function, method, prelude::*, Error, Ruby};

// Define a Rust struct
struct Rectangle {
    width: f64,
    height: f64,
}

// Use the wrap macro to handle the Ruby class mapping
#[magnus::wrap(class = "MyExtension::Rectangle")]
impl Rectangle {
    // Constructor
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }

    // Instance methods
    fn width(&self) -> f64 {
        self.width
    }

    fn height(&self) -> f64 {
        self.height
    }

    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyExtension")?;
    let class = module.define_class("Rectangle", ruby.class_object())?;

    // Register class methods and instance methods
    class.define_singleton_method("new", function!(Rectangle::new, 2))?;
    class.define_method("width", method!(Rectangle::width, 0))?;
    class.define_method("height", method!(Rectangle::height, 0))?;
    class.define_method("area", method!(Rectangle::area, 0))?;
    class.define_method("perimeter", method!(Rectangle::perimeter, 0))?;

    Ok(())
}
}

Using RefCell for Mutable Rust Objects

For Ruby objects that need interior mutability:

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use magnus::{define_class, function, method, prelude::*, Error, Ruby};

struct Counter {
    count: usize,
}

#[magnus::wrap(class = "MyExtension::Counter")]
struct MutCounter(RefCell<Counter>);

impl MutCounter {
    fn new(initial: usize) -> Self {
        MutCounter(RefCell::new(Counter { count: initial }))
    }

    fn count(&self) -> usize {
        self.0.borrow().count
    }

    fn increment(&self) -> usize {
        let mut counter = self.0.borrow_mut();
        counter.count += 1;
        counter.count
    }

    fn increment_by(&self, n: usize) -> usize {
        let mut counter = self.0.borrow_mut();
        counter.count += n;
        counter.count
    }

    // AVOID this pattern which can cause BorrowMutError
    fn bad_increment_method(&self) -> Result<usize, Error> {
        // Don't do this - it keeps the borrowing active while trying to borrow_mut
        if self.0.borrow().count > 10 {
            // This will panic with "already borrowed: BorrowMutError"
            self.0.borrow_mut().count += 100;
        } else {
            self.0.borrow_mut().count += 1;
        }

        Ok(self.0.borrow().count)
    }

    // CORRECT pattern - complete the first borrow before starting the second
    fn good_increment_method(&self) -> Result<usize, Error> {
        // Copy the value first
        let current_count = self.0.borrow().count;

        // Then the first borrow is dropped and we can borrow_mut safely
        if current_count > 10 {
            self.0.borrow_mut().count += 100;
        } else {
            self.0.borrow_mut().count += 1;
        }

        Ok(self.0.borrow().count)
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyExtension")?;
    let class = module.define_class("Counter", ruby.class_object())?;

    class.define_singleton_method("new", function!(MutCounter::new, 1))?;
    class.define_method("count", method!(MutCounter::count, 0))?;
    class.define_method("increment", method!(MutCounter::increment, 0))?;
    class.define_method("increment_by", method!(MutCounter::increment_by, 1))?;
    class.define_method("good_increment", method!(MutCounter::good_increment_method, 0))?;

    Ok(())
}
}

Implementing Ruby Methods

Magnus provides flexible macros to help define methods with various signatures.

Function vs Method Macros

Magnus provides two primary macros for defining callable Ruby code:

  1. function! - For singleton/class methods and module functions
  2. method! - For instance methods when you need access to the Rust object (&self)

Here's how to use each:

#![allow(unused)]
fn main() {
use magnus::{function, method, define_class, prelude::*, Error, Ruby};

struct Calculator {}

#[magnus::wrap(class = "Calculator")]
impl Calculator {
    // Constructor - a class method
    fn new() -> Self {
        Calculator {}
    }

    // Regular instance method that doesn't raise exceptions
    fn add(&self, a: i64, b: i64) -> i64 {
        a + b
    }

    // Method that needs the Ruby interpreter to raise an exception
    fn divide(ruby: &Ruby, _rb_self: &Self, a: i64, b: i64) -> Result<i64, Error> {
        if b == 0 {
            return Err(Error::new(
                ruby.exception_zero_div_error(),
                "Division by zero"
            ));
        }
        Ok(a / b)
    }

    // Class method that doesn't need a Calculator instance
    fn version() -> &'static str {
        "1.0.0"
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Calculator", ruby.class_object())?;

    // Register the constructor with function!
    class.define_singleton_method("new", function!(Calculator::new, 0))?;

    // Register a class method with function!
    class.define_singleton_method("version", function!(Calculator::version, 0))?;

    // Register instance methods with method!
    class.define_method("add", method!(Calculator::add, 2))?;
    class.define_method("divide", method!(Calculator::divide, 2))?;

    Ok(())
}
}

Method Signature Patterns

There are several common method signature patterns depending on what your method needs to do:

Basic Method (no Ruby access, no exceptions)

#![allow(unused)]
fn main() {
fn add(&self, a: i64, b: i64) -> i64 {
    a + b
}
}

Method that Raises Exceptions

#![allow(unused)]
fn main() {
fn divide(ruby: &Ruby, _rb_self: &Self, a: i64, b: i64) -> Result<i64, Error> {
    if b == 0 {
        return Err(Error::new(
            ruby.exception_zero_div_error(),
            "Division by zero"
        ));
    }
    Ok(a / b)
}
}

Method that Needs to Access Self by Value

#![allow(unused)]
fn main() {
// Usually for cloning or consuming self
fn clone_and_modify(rb_self: Value) -> Result<Value, Error> {
    let ruby = unsafe { Ruby::get_unchecked() };
    let obj = ruby.class_object::<Calculator>()?.new_instance(())?;
    // Modify obj...
    Ok(obj)
}
}

Method with Ruby Block

#![allow(unused)]
fn main() {
fn with_retries(ruby: &Ruby, _rb_self: &Self, max_retries: usize, block: Proc) -> Result<Value, Error> {
    let mut retries = 0;
    loop {
        match block.call(ruby, ()) {
            Ok(result) => return Ok(result),
            Err(e) if retries < max_retries => {
                retries += 1;
                // Maybe backoff or log error
            },
            Err(e) => return Err(e),
        }
    }
}
}

Class Inheritance and Mixins

Ruby supports a rich object model with single inheritance and multiple module inclusion. Magnus allows you to replicate this model in your Rust extension.

Creating a Subclass

#![allow(unused)]
fn main() {
use magnus::{Module, class, define_class, method, prelude::*, Error, Ruby};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Get the parent class (Ruby's built-in Array)
    let array_class = ruby.class_object::<RArray>()?;

    // Create a subclass
    let sorted_array = ruby.define_class("SortedArray", array_class)?;

    // Override the << (push) method to keep the array sorted
    sorted_array.define_method("<<", method!(|ruby, rb_self: Value, item: Value| {
        let array = RArray::from_value(rb_self)?;
        array.push(ruby, item)?;

        // Call sort! to keep the array sorted
        array.funcall(ruby, "sort!", ())?;

        Ok(rb_self) // Return self for method chaining
    }, 1))?;

    Ok(())
}
}

Including Modules (Mixins)

#![allow(unused)]
fn main() {
use magnus::{Module, class, define_class, define_module, method, prelude::*, Error, Ruby};

fn make_comparable(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyComparable")?;

    // Define methods for the module
    module.define_method("<=>", method!(|_ruby, rb_self: Value, other: Value| {
        // Implementation of the spaceship operator for comparison
        let self_num: Result<i64, _> = rb_self.try_convert();
        let other_num: Result<i64, _> = other.try_convert();

        match (self_num, other_num) {
            (Ok(a), Ok(b)) => Ok(a.cmp(&b) as i8),
            _ => Ok(nil()),
        }
    }, 1))?;

    // Define methods that depend on <=>
    module.define_method("==", method!(|ruby, rb_self: Value, other: Value| {
        let result: i8 = rb_self.funcall(ruby, "<=>", (other,))?;
        Ok(result == 0)
    }, 1))?;

    module.define_method(">", method!(|ruby, rb_self: Value, other: Value| {
        let result: i8 = rb_self.funcall(ruby, "<=>", (other,))?;
        Ok(result > 0)
    }, 1))?;

    module.define_method("<", method!(|ruby, rb_self: Value, other: Value| {
        let result: i8 = rb_self.funcall(ruby, "<=>", (other,))?;
        Ok(result < 0)
    }, 1))?;

    Ok(())
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Create our module
    make_comparable(ruby)?;

    // Create a class
    let score = ruby.define_class("Score", ruby.class_object())?;

    // Define methods
    score.define_singleton_method("new", function!(|value: i64| {
        let obj = RObject::new(ruby.class_object::<Score>())?;
        obj.ivar_set(ruby, "@value", value)?;
        Ok(obj)
    }, 1))?;

    score.define_method("value", method!(|ruby, rb_self: Value| {
        rb_self.ivar_get::<_, i64>(ruby, "@value")
    }, 0))?;

    // Include our module
    let comparable = ruby.define_module("MyComparable")?;
    score.include_module(ruby, comparable)?;

    Ok(())
}
}

Working with Singleton Methods

Singleton methods in Ruby are methods attached to individual objects, not to their class. The most common use is defining class methods, but they can be applied to any object.

Defining a Class with Both Instance and Singleton Methods

#![allow(unused)]
fn main() {
use magnus::{class, define_class, function, method, prelude::*, Error, Ruby, Value};

#[magnus::wrap(class = "Logger")]
struct Logger {
    level: String,
}

impl Logger {
    fn new(level: String) -> Self {
        Logger { level }
    }

    fn log(&self, message: String) -> String {
        format!("[{}] {}", self.level, message)
    }

    // Class methods (singleton methods)
    fn default_level() -> &'static str {
        "INFO"
    }

    fn create_default(ruby: &Ruby) -> Result<Value, Error> {
        let class = ruby.class_object::<Logger>()?;
        let default_level = Self::default_level();
        class.new_instance((default_level,))
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Logger", ruby.class_object())?;

    // Instance methods
    class.define_singleton_method("new", function!(Logger::new, 1))?;
    class.define_method("log", method!(Logger::log, 1))?;

    // Class methods using function! macro
    class.define_singleton_method("default_level", function!(Logger::default_level, 0))?;
    class.define_singleton_method("create_default", function!(Logger::create_default, 0))?;

    Ok(())
}
}

Attaching Methods to a Specific Object (True Singleton Methods)

#![allow(unused)]
fn main() {
use magnus::{module, function, prelude::*, Error, Ruby, Value};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Create a single object
    let config = ruby.eval::<Value>("Object.new")?;

    // Define singleton methods directly on that object
    config.define_singleton_method(ruby, "get", function!(|| {
        "Configuration value"
    }, 0))?;

    config.define_singleton_method(ruby, "enabled?", function!(|| {
        true
    }, 0))?;

    // Make it globally accessible
    ruby.define_global_const("CONFIG", config)?;

    Ok(())
}
}

This creates an object that can be used in Ruby like:

CONFIG.get          # => "Configuration value"
CONFIG.enabled?     # => true
CONFIG.class        # => Object

Best Practices

  1. Use magnus macros for class definition: The wrap and TypedData macros simplify class definition significantly.

  2. Consistent naming: Keep Ruby and Rust naming conventions consistent within their domains (snake_case for Ruby methods, CamelCase for Ruby classes).

  3. Layer your API: Consider providing both low-level and high-level APIs for complex functionality.

  4. Document method signatures: When using methods that can raise exceptions, document which exceptions can be raised.

  5. RefCell borrowing pattern: Always release a borrow() before calling borrow_mut() by copying any needed values.

  6. Method macro selection: Use function! for singleton methods and method! for instance methods.

  7. Include the Ruby parameter: Always include ruby: &Ruby in your method signature if your method might raise exceptions or interact with the Ruby runtime.

  8. Reuse existing Ruby patterns: When designing your API, follow existing Ruby conventions that users will already understand.

  9. Cache Ruby classes and modules: Use Lazy to cache frequently accessed classes and modules.

  10. Maintain object hierarchy: Properly use Ruby's inheritance and module system to organize your code."

Error Handling in Rust Ruby Extensions

Proper error handling is critical for robust Ruby extensions. This guide covers how to handle errors in Rust and map them to appropriate Ruby exceptions.

Improper error handling can lead to crashes that take down the entire Ruby VM. This chapter shows you how to properly raise and handle exceptions in your Rust extensions.

Overview of Error Handling Approaches

When building Ruby extensions with Rust, you'll typically use one of these error handling patterns:

  1. Result-based error handling: Using Rust's Result<T, E> type to return errors
  2. Ruby exception raising: Converting Rust errors into Ruby exceptions
  3. Panic catching: Handling unexpected Rust panics and converting them to Ruby exceptions

The most common pattern in rb-sys extensions is to use Rust's Result<T, magnus::Error> type, where the Error type represents a Ruby exception that can be raised.

The Result Type and Magnus::Error

Magnus uses Result<T, Error> as the standard way to handle errors. The Error type represents a Ruby exception that can be raised:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby};

fn might_fail(ruby: &Ruby, value: i64) -> Result<i64, Error> {
    if value < 0 {
        return Err(Error::new(
            ruby.exception_arg_error(),
            "Value must be positive"
        ));
    }
    Ok(value * 2)
}
}

The Error type:

  • Contains a reference to a Ruby exception class
  • Includes an error message
  • Can be created from an existing Ruby exception

Mapping Rust Errors to Ruby Exceptions

Standard Ruby Exception Types

Magnus provides access to all standard Ruby exception types:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby};

fn divide(ruby: &Ruby, a: f64, b: f64) -> Result<f64, Error> {
    if b == 0.0 {
        return Err(Error::new(
            ruby.exception_zero_div_error(),
            "Division by zero"
        ));
    }
    Ok(a / b)
}

fn process_array(ruby: &Ruby, index: isize, array: RArray) -> Result<Value, Error> {
    if index < 0 || index >= array.len() as isize {
        return Err(Error::new(
            ruby.exception_index_error(),
            format!("Index {} out of bounds (0..{})", index, array.len() - 1)
        ));
    }
    array.get(index as usize)
}

fn parse_number(ruby: &Ruby, input: &str) -> Result<i64, Error> {
    match input.parse::<i64>() {
        Ok(num) => Ok(num),
        Err(_) => Err(Error::new(
            ruby.exception_arg_error(),
            format!("Cannot parse '{}' as a number", input)
        )),
    }
}
}

Common Ruby exception types available through the Ruby API:

MethodException ClassTypical Use Case
ruby.exception_arg_error()ArgumentErrorInvalid argument value or type
ruby.exception_index_error()IndexErrorArray/string index out of bounds
ruby.exception_key_error()KeyErrorHash key not found
ruby.exception_name_error()NameErrorReference to undefined name
ruby.exception_no_memory_error()NoMemoryErrorMemory allocation failure
ruby.exception_not_imp_error()NotImplementedErrorFeature not implemented
ruby.exception_range_error()RangeErrorValue outside valid range
ruby.exception_regexp_error()RegexpErrorInvalid regular expression
ruby.exception_runtime_error()RuntimeErrorGeneral runtime error
ruby.exception_script_error()ScriptErrorProblem in script execution
ruby.exception_syntax_error()SyntaxErrorInvalid syntax
ruby.exception_type_error()TypeErrorType mismatch
ruby.exception_zero_div_error()ZeroDivisionErrorDivision by zero

Creating Custom Exception Classes

You can define custom exception classes for your extension:

#![allow(unused)]
fn main() {
use magnus::{class, Error, Ruby};

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyExtension")?;

    // Create custom exception classes
    let std_error = ruby.exception_standard_error();
    let custom_error = module.define_class("CustomError", std_error)?;
    let validation_error = module.define_class("ValidationError", custom_error)?;

    // Register them as constants for easier access
    ruby.define_global_const("MY_CUSTOM_ERROR", custom_error)?;

    Ok(())
}

// Using the custom exception
fn validate(ruby: &Ruby, value: &str) -> Result<(), Error> {
    if value.is_empty() {
        return Err(Error::new(
            ruby.class_path_to_value("MyExtension::ValidationError"),
            "Validation failed: value cannot be empty"
        ));
    }
    Ok(())
}
}

Passing and Re-raising Ruby Exceptions

You can pass along existing Ruby exceptions:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby, Value};

fn process_data(ruby: &Ruby, input: Value) -> Result<Value, Error> {
    // Call a method that might raise
    let result = match input.funcall(ruby, "process", ()) {
        Ok(val) => val,
        Err(err) => return Err(err), // Pass along the original error
    };

    // Or with the ? operator
    let result = input.funcall(ruby, "process", ())?;

    Ok(result)
}
}

For wrapping and adding context to errors:

#![allow(unused)]
fn main() {
fn compute_with_context(ruby: &Ruby, input: Value) -> Result<Value, Error> {
    match complex_operation(ruby, input) {
        Ok(result) => Ok(result),
        Err(err) => {
            // Create a new error with additional context
            Err(Error::new(
                ruby.exception_runtime_error(),
                format!("Computation failed: {}", err.message(ruby).unwrap_or_default())
            ))
        }
    }
}
}

Handling Rust Panics

Rust panics should be caught and converted to Ruby exceptions to prevent crashing the Ruby VM:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby};
use std::panic::{self, catch_unwind};

fn dangerous_operation(ruby: &Ruby, input: i64) -> Result<i64, Error> {
    // Catch any potential panics
    let result = catch_unwind(|| {
        // Code that might panic
        if input == 0 {
            panic!("Unexpected zero value");
        }
        input * 2
    });

    match result {
        Ok(value) => Ok(value),
        Err(_) => Err(Error::new(
            ruby.exception_runtime_error(),
            "Internal error: Rust panic occurred"
        )),
    }
}
}

Error Handling Patterns

The Question Mark Operator

The ? operator simplifies error handling by automatically propagating errors:

#![allow(unused)]
fn main() {
fn multi_step_operation(ruby: &Ruby, value: i64) -> Result<i64, Error> {
    // Each operation can fail, ? will return early on error
    let step1 = validate_input(ruby, value)?;
    let step2 = transform_data(ruby, step1)?;
    let step3 = final_calculation(ruby, step2)?;

    Ok(step3)
}
}

Pattern Matching on Errors

For more sophisticated error handling, pattern match on error types:

#![allow(unused)]
fn main() {
fn handle_specific_errors(ruby: &Ruby, value: Value) -> Result<Value, Error> {
    let result = value.funcall(ruby, "some_method", ());

    match result {
        Ok(val) => Ok(val),
        Err(err) if err.is_kind_of(ruby, ruby.exception_zero_div_error()) => {
            // Handle division by zero specially
            Ok(ruby.integer_from_i64(0))
        },
        Err(err) if err.is_kind_of(ruby, ruby.exception_arg_error()) => {
            // Convert ArgumentError to a different type
            Err(Error::new(
                ruby.exception_runtime_error(),
                format!("Invalid argument: {}", err.message(ruby).unwrap_or_default())
            ))
        },
        Err(err) => Err(err), // Pass through other errors
    }
}
}

Context Managers / RAII Pattern

Use Rust's RAII (Resource Acquisition Is Initialization) pattern for cleanup operations:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby};
use std::fs::File;
use std::io::{self, Read};

struct TempResource {
    data: Vec<u8>,
}

impl TempResource {
    fn new() -> Self {
        // Allocate resource
        TempResource { data: Vec::new() }
    }
}

impl Drop for TempResource {
    fn drop(&mut self) {
        // Clean up will happen automatically, even if an error occurs
        println!("Cleaning up resource");
    }
}

fn process_with_resource(ruby: &Ruby) -> Result<Value, Error> {
    // Resource is created
    let mut resource = TempResource::new();

    // If an error occurs here, resource will still be cleaned up
    let file_result = File::open("data.txt");
    let mut file = match file_result {
        Ok(f) => f,
        Err(e) => return Err(Error::new(
            ruby.exception_io_error(),
            format!("Could not open file: {}", e)
        )),
    };

    // Resource will be dropped at the end of this scope
    Ok(ruby.ary_new_from_ary(&[1, 2, 3]))
}
}

Best Practices for Error Handling

Following these practices will make your extensions more robust and provide a better experience for users.

1. Be Specific with Exception Types

Choose the most appropriate Ruby exception type:

#![allow(unused)]
fn main() {
use magnus::{Error, Ruby};

fn example(ruby: &Ruby, index: usize, array: &[i32]) -> Result<i32, Error> {
// ✅ GOOD: Specific exception type
if index >= array.len() {
    return Err(Error::new(
        ruby.exception_index_error(),
        format!("Index {} out of bounds (0..{})", index, array.len() - 1)
    ));
}

// Example with bad practice commented out
/*
// ❌ BAD: Generic RuntimeError for specific issue
if index >= array.len() {
    return Err(Error::new(
        ruby.exception_runtime_error(),
        "Invalid index"
    ));
}
*/
    Ok(array[index])
}
}

Ruby has a rich hierarchy of exception types. Using the specific exception type helps users handle errors properly in their Ruby code.

2. Provide Clear Error Messages

Include relevant details in error messages:

#![allow(unused)]
fn main() {
// ✅ GOOD: Descriptive error with context
let err_msg = format!(
    "Cannot parse '{}' as a number in range {}-{}",
    input, min, max
);

// ❌ BAD: Vague error message
let err_msg = "Invalid input";
}

3. Maintain Ruby Error Hierarchies

Respect Ruby's exception hierarchy:

#![allow(unused)]
fn main() {
// ✅ GOOD: Proper exception hierarchy
let io_error = ruby.exception_io_error();
let file_error = module.define_class("FileError", io_error)?;
let format_error = module.define_class("FormatError", file_error)?;

// ❌ BAD: Improper exception hierarchy
let format_error = module.define_class("FormatError", ruby.class_object())?; // Not inheriting from StandardError
}

4. Avoid Panicking

Use Result instead of panic:

#![allow(unused)]
fn main() {
// ✅ GOOD: Return Result for expected error conditions
fn process(value: i64) -> Result<i64, Error> {
    if value < 0 {
        return Err(Error::new(
            ruby.exception_arg_error(),
            "Value must be positive"
        ));
    }
    Ok(value * 2)
}

// ❌ BAD: Panicking on expected error condition
fn process(value: i64) -> i64 {
    if value < 0 {
        panic!("Value must be positive"); // Will crash the Ruby VM!
    }
    value * 2
}
}

5. Catch All Ruby Exceptions

When calling Ruby methods, always handle exceptions:

#![allow(unused)]
fn main() {
// ✅ GOOD: Catch exceptions from Ruby method calls
let result = match obj.funcall(ruby, "some_method", ()) {
    Ok(val) => val,
    Err(err) => {
        // Handle or re-raise the error
        return Err(err);
    }
};

// ❌ BAD: Not handling potential Ruby exceptions
let result = obj.funcall(ruby, "some_method", ()).unwrap(); // May panic!
}

Error Handling with RefCell

When using RefCell for interior mutability, handle borrow errors gracefully:

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use magnus::{Error, Ruby};

#[magnus::wrap(class = "Counter")]
struct MutCounter(RefCell<u64>);

impl MutCounter {
    fn new() -> Self {
        MutCounter(RefCell::new(0))
    }

    fn increment(ruby: &Ruby, self_: &Self) -> Result<u64, Error> {
        match self_.0.try_borrow_mut() {
            Ok(mut value) => {
                *value += 1;
                Ok(*value)
            },
            Err(_) => Err(Error::new(
                ruby.exception_runtime_error(),
                "Cannot modify counter: already borrowed"
            )),
        }
    }

    // Better approach: complete borrows before starting new ones
    fn safe_increment(&self) -> u64 {
        let mut value = self.0.borrow_mut();
        *value += 1;
        *value
    }
}
}

Conclusion

Never use unwrap() or expect() in production code for your Ruby extensions. These can cause panics that will crash the Ruby VM. Always use proper error handling with Result and Error types.

Effective error handling makes your Ruby extensions more robust and user-friendly. By using the right exception types and providing clear error messages, you create a better experience for users of your extension.

Remember these key points:

  • Use Result<T, Error> for functions that can fail
  • Choose appropriate Ruby exception types
  • Provide clear, detailed error messages
  • Handle Rust panics to prevent VM crashes
  • Respect Ruby's exception hierarchy

After you've handled errors in your Rust code, try to test your extension with invalid inputs to ensure it fails gracefully with appropriate Ruby exceptions rather than crashing.

Memory Management & Safety

One of the most important aspects of writing Ruby extensions is proper memory management. This chapter covers how Ruby's garbage collector interacts with Rust objects and how to ensure your extensions don't leak memory or cause segmentation faults.

Improper memory management is the leading cause of crashes and security vulnerabilities in native extensions. Rust's safety guarantees help prevent many common issues, but you still need to carefully manage the boundary between Ruby and Rust memory.

Ruby's Garbage Collection System

Ruby uses a mark-and-sweep garbage collector to manage memory. Understanding how it works is essential for writing safe extensions:

  1. Marking Phase: Ruby traverses all visible objects, marking them as "in use"
  2. Sweeping Phase: Objects that weren't marked are considered garbage and freed

When you create Rust objects that reference Ruby objects, you need to tell Ruby's GC about these references to prevent premature garbage collection. The TypedData trait and mark method provide the mechanism to do this.

TypedData and DataTypeFunctions

Magnus provides a TypedData trait and DataTypeFunctions trait for managing Ruby objects that wrap Rust structs. This is the recommended way to handle complex objects in Rust.

Basic TypedData Example

Here's how to define a simple Ruby object that wraps a Rust struct:

#![allow(unused)]
fn main() {
use magnus::{prelude::*, Error, Ruby, TypedData, DataTypeFunctions};

// Define your Rust struct
#[derive(TypedData)]
#[magnus(class = "MyExtension::Counter", free_immediately)]
struct Counter {
    count: i64,
}

// Implement required functions
impl DataTypeFunctions for Counter {}

// Implement methods for your struct
impl Counter {
    fn new(initial_value: i64) -> Self {
        Counter { count: initial_value }
    }

    fn increment(&mut self, amount: i64) -> i64 {
        self.count += amount;
        self.count
    }

    fn value(&self) -> i64 {
        self.count
    }
}

// Register with Ruby
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Counter", ruby.class_object())?;

    class.define_singleton_method("new", function!(|initial: i64| {
        Ok(class.wrap(Counter::new(initial)))
    }, 1))?;

    class.define_method("increment", method!(Counter::increment, 1))?;
    class.define_method("value", method!(Counter::value, 0))?;

    Ok(())
}
}

Implementing GC Marking

When your Rust struct holds references to Ruby objects, you need to implement the mark method to tell Ruby's GC about those references. Here's a simple example:

#![allow(unused)]
fn main() {
use magnus::{
    prelude::*, Error, Ruby, Value, TypedData, DataTypeFunctions,
    gc::Marker, typed_data::Obj
};

// A struct that holds references to Ruby objects
#[derive(TypedData)]
#[magnus(class = "MyExtension::Person", free_immediately, mark)]
struct Person {
    // Reference to a Ruby string (their name)
    name: Value,
    // Reference to Ruby array (their hobbies)
    hobbies: Value,
    // Optional reference to another Person (their friend)
    friend: Option<Obj<Person>>,
}

// Implement DataTypeFunctions with mark method
impl DataTypeFunctions for Person {
    // This is called during GC mark phase
    fn mark(&self, marker: &Marker) {
        // Mark the Ruby objects we reference
        marker.mark(self.name);
        marker.mark(self.hobbies);

        // If we have a friend, mark them too
        if let Some(ref friend) = self.friend {
            marker.mark(*friend);
        }
    }
}

impl Person {
    fn new(name: Value, hobbies: Value) -> Self {
        Self {
            name,
            hobbies,
            friend: None,
        }
    }

    fn add_friend(&mut self, friend: Obj<Person>) {
        self.friend = Some(friend);
    }

    fn name(&self) -> Value {
        self.name
    }

    fn hobbies(&self) -> Value {
        self.hobbies
    }

    fn friend(&self) -> Option<Obj<Person>> {
        self.friend.clone()
    }
}

// Register with Ruby
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Person", ruby.class_object())?;

    class.define_singleton_method("new", function!(|name: Value, hobbies: Value| {
        Ok(class.wrap(Person::new(name, hobbies)))
    }, 2))?;

    class.define_method("name", method!(Person::name, 0))?;
    class.define_method("hobbies", method!(Person::hobbies, 0))?;
    class.define_method("friend", method!(Person::friend, 0))?;
    class.define_method("add_friend", method!(Person::add_friend, 1))?;

    Ok(())
}
}

In this example:

  1. The Person struct holds references to Ruby objects (name and hobbies) and another wrapped Rust object (friend)
  2. We implement the mark method to tell Ruby's GC about all these references
  3. During garbage collection, Ruby will know not to collect these objects as long as the Person is alive

A Real-World Example: Trap from wasmtime-rb

Here's a slightly simplified version of a real-world example from the wasmtime-rb project:

#![allow(unused)]
fn main() {
use magnus::{
    prelude::*, method, Error, Ruby, TypedData, DataTypeFunctions,
    typed_data::Obj, Symbol
};

// A struct representing a WebAssembly trap (error)
#[derive(TypedData)]
#[magnus(class = "Wasmtime::Trap", size, free_immediately)]
pub struct Trap {
    trap: wasmtime::Trap,
    wasm_backtrace: Option<wasmtime::WasmBacktrace>,
}

// No references to Ruby objects, so mark is empty
impl DataTypeFunctions for Trap {}

impl Trap {
    pub fn new(trap: wasmtime::Trap, wasm_backtrace: Option<wasmtime::WasmBacktrace>) -> Self {
        Self {
            trap,
            wasm_backtrace,
        }
    }

    // Return a text description of the trap error
    pub fn message(&self) -> String {
        self.trap.to_string()
    }

    // Return the wasm backtrace if available
    pub fn wasm_backtrace_message(&self) -> Option<String> {
        self.wasm_backtrace.as_ref().map(|bt| format!("{bt}"))
    }

    // Return the trap code as a Ruby symbol
    pub fn code(&self) -> Result<Option<Symbol>, Error> {
        match self.trap {
            wasmtime::Trap::StackOverflow => Ok(Some(Symbol::new("STACK_OVERFLOW"))),
            wasmtime::Trap::MemoryOutOfBounds => Ok(Some(Symbol::new("MEMORY_OUT_OF_BOUNDS"))),
            // More cases...
            _ => Ok(Some(Symbol::new("UNKNOWN"))),
        }
    }

    // Custom inspect method
    pub fn inspect(rb_self: Obj<Self>) -> Result<String, Error> {
        Ok(format!(
            "#<Wasmtime::Trap:0x{:016x} @trap_code={}>",
            rb_self.as_raw(),
            rb_self.code()?.map_or("nil".to_string(), |s| s.to_string())
        ))
    }
}

// Register with Ruby
pub fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Trap", ruby.class_object())?;

    class.define_method("message", method!(Trap::message, 0))?;
    class.define_method("wasm_backtrace_message", method!(Trap::wasm_backtrace_message, 0))?;
    class.define_method("code", method!(Trap::code, 0))?;
    class.define_method("inspect", method!(Trap::inspect, 0))?;

    Ok(())
}
}

This example shows:

  1. A Rust struct that wraps WebAssembly-specific types
  2. Methods that convert Rust values to Ruby-friendly types
  3. A simple implementation of DataTypeFunctions (since there are no Ruby object references to mark)

More Complex Example: Memory References

Let's look at a more complex scenario involving memory management:

#![allow(unused)]
fn main() {
use magnus::{
    prelude::*, gc::Marker, Error, Ruby, TypedData, DataTypeFunctions,
    typed_data::Obj, value::Opaque, RString
};

// Represents WebAssembly memory
#[derive(TypedData)]
#[magnus(class = "Wasmtime::Memory", free_immediately, mark)]
struct Memory {
    // Reference to a store (context) object
    store: StoreContext,
    // The actual WebAssembly memory
    memory: WasmMemory,
}

impl DataTypeFunctions for Memory {
    fn mark(&self, marker: &Marker) {
        // Mark the store so it stays alive
        self.store.mark(marker);
    }
}

// A guard that ensures memory access is safe
struct MemoryGuard {
    // Reference to Memory object
    memory: Opaque<Obj<Memory>>,
    // Size when created, to detect resizing
    original_size: u64,
}

impl MemoryGuard {
    fn new(memory: Obj<Memory>) -> Result<Self, Error> {
        let original_size = memory.size()?;

        Ok(Self {
            memory: memory.into(),
            original_size,
        })
    }

    fn get(&self) -> Result<&Memory, Error> {
        let ruby = Ruby::get().unwrap();
        let mem = ruby.get_inner_ref(&self.memory);

        // Check that memory size hasn't changed
        if mem.size()? != self.original_size {
            return Err(Error::new(
                magnus::exception::runtime_error(),
                "memory was resized, reference is no longer valid"
            ));
        }

        Ok(mem)
    }

    fn mark(&self, marker: &Marker) {
        marker.mark(self.memory);
    }
}

// A slice of WebAssembly memory
#[derive(TypedData)]
#[magnus(class = "Wasmtime::MemorySlice", free_immediately, mark)]
struct MemorySlice {
    guard: MemoryGuard,
    offset: usize,
    size: usize,
}

impl DataTypeFunctions for MemorySlice {
    fn mark(&self, marker: &Marker) {
        // Mark the memory guard, which marks the memory object
        self.guard.mark(marker);
    }
}

impl MemorySlice {
    fn new(memory: Obj<Memory>, offset: usize, size: usize) -> Result<Self, Error> {
        let guard = MemoryGuard::new(memory)?;

        // Validate the slice is in bounds
        let mem = guard.get()?;
        if offset + size > mem.data_size()? {
            return Err(Error::new(
                magnus::exception::range_error(),
                "memory slice out of bounds"
            ));
        }

        Ok(Self {
            guard,
            offset,
            size,
        })
    }

    // Read the slice as a Ruby string (efficiently, without copying)
    fn to_str(&self) -> Result<RString, Error> {
        let ruby = unsafe { Ruby::get_unchecked() };
        let mem = self.guard.get()?;
        let data = mem.data()?;

        // Extract the relevant slice
        let slice = &data[self.offset..self.offset + self.size];

        // Create a Ruby string directly from the slice (zero-copy)
        Ok(ruby.str_from_slice(slice))
    }

    // Read the slice as a UTF-8 string (with validation)
    fn to_utf8_str(&self) -> Result<RString, Error> {
        let ruby = unsafe { Ruby::get_unchecked() };
        let mem = self.guard.get()?;
        let data = mem.data()?;

        // Extract the relevant slice
        let slice = &data[self.offset..self.offset + self.size];

        // Validate UTF-8 and create a Ruby string
        match std::str::from_utf8(slice) {
            Ok(s) => Ok(RString::new(s)),
            Err(e) => Err(Error::new(
                magnus::exception::encoding_error(),
                format!("invalid UTF-8: {}", e)
            ))
        }
    }
}
}

This more advanced example demonstrates:

  1. Guarded Resource Access: The MemoryGuard ensures memory operations are safe by checking for resizing
  2. Proper GC Integration: Both structs implement marking to ensure referenced objects aren't collected
  3. Efficient String Creation: Using str_from_slice to create strings directly from memory without extra copying
  4. Error Handling: All operations that might fail return meaningful errors
  5. Resource Validation: The code validates bounds before accessing memory

Common Memory Management Pitfalls

These pitfalls can lead to crashes, memory leaks, or undefined behavior in your Ruby extensions. Understanding and avoiding them is crucial for writing reliable code.

1. Forgetting to Mark References

If your Rust struct holds Ruby objects but doesn't implement marking, those objects might be collected while still in use:

#![allow(unused)]
fn main() {
use magnus::{gc::Marker, DataTypeFunctions, Value};

// BAD: No marking implementation
struct BadExample {
    ruby_object: Value,  // This reference won't be marked by GC
}

impl DataTypeFunctions for BadExample {
    // Missing mark implementation!
}

// GOOD: Proper marking
struct GoodExample {
    ruby_object: Value,
}

impl DataTypeFunctions for GoodExample {
    fn mark(&self, marker: &Marker) {
        marker.mark(self.ruby_object);
    }
}

// A more complex example with multiple references
struct ComplexExample {
    ruby_strings: Vec<Value>,
    ruby_hash: Value,
    ruby_object: Value,
}

impl DataTypeFunctions for ComplexExample {
    fn mark(&self, marker: &Marker) {
        // Mark each string in the vector
        for string in &self.ruby_strings {
            marker.mark(*string);
        }

        // Mark the hash and object
        marker.mark(self.ruby_hash);
        marker.mark(self.ruby_object);
    }
}
}

Click the eye icon () to see an additional example of marking multiple references in a more complex struct.

2. Creating Cyclic References

Cyclic references (A references B, which references A) can lead to memory leaks. Consider using weak references or redesigning your object graph.

3. Inefficient String Creation

String handling is often a performance bottleneck in Ruby extensions. Using the right APIs can significantly improve performance.

Creating strings inefficiently can significantly impact performance:

#![allow(unused)]
fn main() {
use magnus::{Error, RString, Ruby};

// BAD: Creates unnecessary temporary Rust String
fn inefficient_string(data: &[u8]) -> Result<RString, Error> {
    let temp_string = String::from_utf8(data.to_vec())?; // Unnecessary allocation
    Ok(RString::new(&temp_string))  // Another copy
}

// GOOD: Direct creation from slice
fn efficient_string(ruby: &Ruby, data: &[u8]) -> RString {
    ruby.str_from_slice(data)  // No extra copies
}

// ALSO GOOD: Creating from string slice when UTF-8 is confirmed
fn from_str(ruby: &Ruby, s: &str) -> RString {
    RString::new(s)
}

// ALSO GOOD: Creating binary string with capacity then filling
fn build_string(ruby: &Ruby, size: usize) -> RString {
    let mut string = RString::with_capacity(size);
    // Fill the string directly...
    string
}
}

Both memory usage and performance are significantly improved by avoiding unnecessary allocations and copies. The eye icon () reveals additional efficient string handling examples.

4. Not Handling Exceptions Properly

Ruby exceptions can disrupt the normal flow of your code. Ensure resources are cleaned up even when exceptions occur.

RefCell and Interior Mutability

When creating Ruby objects with Rust, you'll often need to use interior mutability patterns. The most common approach is using RefCell to allow your Ruby objects to be mutated even when users hold immutable references to them.

Understanding RefCell and Borrowing

Rust's RefCell allows mutable access to data through shared references, but enforces Rust's borrowing rules at runtime. This is perfect for Ruby extension objects, where Ruby owns the object and we interact with it via method calls.

A common pattern is to wrap your Rust struct in a RefCell:

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use magnus::{prelude::*, Error, Ruby};

struct Counter {
    count: i64,
}

#[magnus::wrap(class = "MyExtension::Counter")]
struct MutCounter(RefCell<Counter>);

impl MutCounter {
    fn new(initial: i64) -> Self {
        Self(RefCell::new(Counter { count: initial }))
    }

    fn count(&self) -> i64 {
        self.0.borrow().count
    }

    fn increment(&self) -> i64 {
        let mut counter = self.0.borrow_mut();
        counter.count += 1;
        counter.count
    }
}
}

The BorrowMutError Problem

A common mistake when using RefCell is trying to borrow mutably when you already have an active immutable borrow. This leads to a BorrowMutError panic:

#![allow(unused)]
fn main() {
// BAD - will panic with "already borrowed: BorrowMutError"
fn buggy_add(&self, val: i64) -> Result<i64, Error> {
    // First borrow is still active when we try to borrow_mut below
    if let Some(sum) = self.0.borrow().count.checked_add(val) {
        self.0.borrow_mut().count = sum; // ERROR - already borrowed above
        Ok(sum)
    } else {
        Err(Error::new(
            ruby.exception_range_error(),
            "result too large"
        ))
    }
}
}

The problem is that the borrow() in the if condition is still active when we try to use borrow_mut() in the body. Rust's borrow checker would catch this at compile time for normal references, but RefCell defers this check to runtime, resulting in a panic.

The Solution: Complete Borrows Before Mutating

The solution is to complete all immutable borrows before starting mutable ones:

#![allow(unused)]
fn main() {
// GOOD - copy the value first to complete the borrow
fn safe_add(&self, val: i64) -> Result<i64, Error> {
    // Get the current count, completing this borrow
    let current_count = self.0.borrow().count;

    // Now we can safely borrow mutably
    if let Some(sum) = current_count.checked_add(val) {
        self.0.borrow_mut().count = sum; // Safe now
        Ok(sum)
    } else {
        Err(Error::new(
            ruby.exception_range_error(),
            "result too large"
        ))
    }
}
}

By copying count to a local variable, we complete the immutable borrow before starting the mutable one, avoiding the runtime panic.

Complex Example with Multiple Operations

When working with more complex data structures:

#![allow(unused)]
fn main() {
struct Game {
    players: Vec<String>,
    current_player: usize,
    score: i64,
}

#[magnus::wrap(class = "MyGame")]
struct MutGame(RefCell<Game>);

impl MutGame {
    fn new() -> Self {
        Self(RefCell::new(Game {
            players: Vec::new(),
            current_player: 0,
            score: 0,
        }))
    }

    // INCORRECT: Multiple borrows that will cause issues
    fn buggy_next_player_scores(&self, points: i64) -> Result<String, Error> {
        let game = self.0.borrow();
        if game.players.is_empty() {
            return Err(Error::new(
                magnus::exception::runtime_error(),
                "No players in game"
            ));
        }

        // This would panic - we're still borrowing game
        let mut game_mut = self.0.borrow_mut();
        game_mut.score += points;
        let player = game_mut.current_player;
        game_mut.current_player = (player + 1) % game_mut.players.len();

        Ok(format!("{} scored {} points! New total: {}",
            game_mut.players[player], points, game_mut.score))
    }

    // CORRECT: Copy all needed data before releasing the borrow
    fn safe_next_player_scores(&self, points: i64) -> Result<String, Error> {
        // Read all the data we need first
        let player_name: String;
        let new_player_index: usize;
        let new_score: i64;

        {
            // Create a block scope to ensure the borrow is dropped
            let game = self.0.borrow();
            if game.players.is_empty() {
                return Err(Error::new(
                    magnus::exception::runtime_error(),
                    "No players in game"
                ));
            }

            player_name = game.players[game.current_player].clone();
            new_player_index = (game.current_player + 1) % game.players.len();
            new_score = game.score + points;
        } // borrow is dropped here

        // Now we can borrow mutably
        let mut game = self.0.borrow_mut();
        game.score = new_score;
        game.current_player = new_player_index;

        Ok(format!("{} scored {} points! New total: {}",
            player_name, points, new_score))
    }
}
}

Using Temporary Variables Instead of Block Scopes

If you prefer, you can use temporary variables instead of block scopes:

#![allow(unused)]
fn main() {
fn add_player(&self, player: String) -> Result<usize, Error> {
    // Get the current number of players first
    let player_count = self.0.borrow().players.len();

    // Now we can mutate
    let mut game = self.0.borrow_mut();
    game.players.push(player);

    Ok(player_count + 1) // Return new count
}
}

RefCell Best Practices

  1. Complete All Borrows: Always complete immutable borrows before starting mutable borrows.

  2. Use Block Scopes or Variables: Either use block scopes to limit borrow lifetimes or copy needed values to local variables.

  3. Minimize Borrow Scope: Keep the scope of borrows as small as possible.

  4. Clone When Necessary: If you need to keep references to data while mutating other parts, clone the data you need to keep.

  5. Consider Data Design: Structure your data to minimize the need for complex borrowing patterns.

  6. Error When Conflicting: If you can't resolve a borrowing conflict cleanly, make the operation an error rather than trying to force it.

Best Practices

  1. Use TypedData and DataTypeFunctions: They provide a safe framework for memory management
  2. Always Implement Mark Methods: Mark all Ruby objects your struct references
  3. Validate Assumptions: Check that resources are valid before using them
  4. Use Zero-Copy APIs: Leverage APIs like str_from_slice to avoid unnecessary copying
  5. Use Guards for Changing Data: Validate assumptions before accessing data that might change
  6. Test Thoroughly with GC Stress: Run tests with GC.stress = true to expose memory issues
  7. Handle RefCell Borrowing Carefully: Complete all immutable borrows before starting mutable ones to avoid runtime panics

By following these practices, you can write Ruby extensions in Rust that are both memory-safe and efficient.

Next Steps

In the next chapter, we'll explore performance optimization techniques that leverage Rust's strengths while maintaining memory safety.

The Build Process

Overview

This chapter explains what happens behind the scenes when rb-sys compiles your Rust extension, helping you debug issues and optimize builds.

This chapter explains what happens behind the scenes when rb-sys compiles your Rust extension. Understanding this process will help you debug issues and optimize your extension.

How rb-sys Compiles Your Code

When you run bundle exec rake compile, several steps happen in sequence:

  1. Ruby's mkmf system reads your extconf.rb file
  2. The create_rust_makefile function generates a Makefile
  3. Cargo builds your Rust code as a dynamic library
  4. The resulting binary is copied to your gem's lib directory

Let's examine each step in detail.

The Role of extconf.rb

The extconf.rb file is the entry point for Ruby's native extension system. For rb-sys projects, it typically looks like this:

# extconf.rb
require "mkmf"
require "rb_sys/mkmf"

create_rust_makefile("my_gem/my_gem")

The create_rust_makefile function:

  1. Sets up the environment for compiling Rust code
  2. Generates a Makefile with appropriate Cargo commands
  3. Configures where the compiled library should be placed

Configuration Options

You can customize the build process by passing a block to create_rust_makefile:

create_rust_makefile("my_gem/my_gem") do |config|
  # Set cargo profile (defaults to ENV["RB_SYS_CARGO_PROFILE"] or :dev)
  config.profile = ENV.fetch("MY_GEM_PROFILE", :dev).to_sym

  # Enable specific cargo features
  config.features = ["feature1", "feature2"]

  # Set environment variables for cargo
  config.env = { "SOME_VAR" => "value" }

  # Specify extra Rust flags
  config.extra_rustflags = ["--cfg=feature=\"custom_feature\""]

  # Clean up target directory after installation to reduce gem size
  config.clean_after_install = true

  # Force installation of Rust toolchain if not present
  config.force_install_rust_toolchain = "stable"

  # Auto-install Rust toolchain during gem installation
  config.auto_install_rust_toolchain = true
end

For a complete reference of all available configuration options, see the rb_sys Gem Configuration documentation.

Environment Variables

Several environment variables affect the build process:

VariableDescriptionDefault
RB_SYS_CARGO_PROFILECargo profile to use (dev or release)dev
RB_SYS_CARGO_FEATURESComma-separated list of features to enableNone
RB_SYS_CARGO_ARGSAdditional arguments to pass to cargoNone

For example:

RB_SYS_CARGO_PROFILE=release bundle exec rake compile

Debugging the Build Process

When things go wrong, you can debug the build process:

1. Enable Verbose Output

bundle exec rake compile VERBOSE=1

2. Inspect Generated Files

Look at the generated Makefile and Cargo configuration:

cat ext/my_gem/Makefile

3. Run Cargo Directly

You can run Cargo commands directly in the extension directory:

cd ext/my_gem
cargo build -v

Optimizing the Build

Development vs. Release Builds

During development, use the default dev profile for faster compilation:

RB_SYS_CARGO_PROFILE=dev bundle exec rake compile

For production releases, use the release profile for optimized performance:

RB_SYS_CARGO_PROFILE=release bundle exec rake compile

Cargo Configuration

In your Cargo.toml, you can customize optimization levels:

[profile.release]
lto = true             # Link-time optimization
opt-level = 3          # Maximum optimization
codegen-units = 1      # Optimize for size at the expense of compile time

Build Scripts (build.rs)

For advanced customization, you can use Rust's build script feature:

// ext/my_gem/build.rs
fn main() {
    // Detect features at build time
    if std::env::var("TARGET").unwrap().contains("windows") {
        println!("cargo:rustc-cfg=feature=\"windows\"");
    }

    // Link to system libraries if needed
    println!("cargo:rustc-link-lib=dylib=ssl");

    // Rerun if specific files change
    println!("cargo:rerun-if-changed=src/native_code.h");
}

Remember to add this to your Cargo.toml:

# ext/my_gem/Cargo.toml
[package]
# ...
build = "build.rs"

Cross-Compilation with rb-sys-dock

The real power of rb-sys is its ability to cross-compile extensions using rb-sys-dock. This tool runs your build in Docker containers configured for different platforms.

Basic Cross-Compilation

To set up cross-compilation with the RbSys::ExtensionTask:

# Rakefile
RbSys::ExtensionTask.new("my_gem", GEMSPEC) do |ext|
  ext.lib_dir = "lib/my_gem"
  ext.cross_compile = true
  ext.cross_platform = ['x86_64-linux', 'x86_64-darwin', 'arm64-darwin']
end

Then you can cross-compile with:

bundle exec rake native:my_gem:x86_64-linux

Using rb-sys-dock Directly

You can also use rb-sys-dock directly:

bundle exec rb-sys-dock --platform x86_64-linux --build

Supported Platforms

rb-sys supports many platforms, including:

  • x86_64-linux (Linux on Intel/AMD 64-bit)
  • x86_64-linux-musl (Static Linux builds)
  • aarch64-linux (Linux on ARM64)
  • x86_64-darwin (macOS on Intel)
  • arm64-darwin (macOS on Apple Silicon)
  • x64-mingw-ucrt (Windows 64-bit UCRT)

CI/CD with oxidize-rb/actions

The oxidize-rb/actions repository provides GitHub Actions specifically designed for rb-sys projects:

setup-ruby-and-rust

# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
        ruby: [3.0, 3.1, 3.2, 3.3]
    steps:
      - uses: actions/checkout@v4
      - uses: oxidize-rb/actions/setup-ruby-and-rust@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true
          cargo-cache: true
      - name: Compile
        run: bundle exec rake compile
      - name: Test
        run: bundle exec rake test

cross-gem

# .github/workflows/cross-gem.yml
jobs:
  cross_gems:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform: ["x86_64-linux", "x86_64-darwin", "arm64-darwin"]
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - uses: oxidize-rb/actions/cross-gem@v1
        with:
          platform: ${{ matrix.platform }}

For a complete CI/CD setup, combine these actions to test your extension on multiple Ruby versions and platforms, then cross-compile for release.

Next Steps

Cross-Platform Development

Overview

One of rb-sys's greatest strengths is its support for cross-platform Ruby extensions. This chapter covers how to develop, test, and distribute extensions across multiple platforms.

Supported Platforms

rb-sys supports cross-compilation to the following platforms:

PlatformSupportedDocker Image
x86_64-linuxrbsys/x86_64-linux
x86_64-linux-muslrbsys/x86_64-linux-musl
aarch64-linuxrbsys/aarch64-linux
aarch64-linux-muslrbsys/aarch64-linux-musl
arm-linuxrbsys/arm-linux
arm64-darwinrbsys/arm64-darwin
x64-mingw32rbsys/x64-mingw32
x64-mingw-ucrtrbsys/x64-mingw-ucrt
mswinnot available on Docker
trufflerubynot available on Docker

The Docker images are available on Docker Hub and are automatically updated with each rb-sys release.

Platform Considerations

Ruby extensions face several cross-platform challenges:

  • Different operating systems (Linux, macOS, Windows)
  • Different CPU architectures (x86_64, ARM64)
  • Different Ruby implementations
  • Different compilers and linkers
  • System libraries and dependencies

rb-sys provides tools to handle these differences effectively.

Understanding Platform Targets

Ruby identifies platforms with standardized strings:

Platform StringDescription
x86_64-linux64-bit Linux on Intel/AMD
aarch64-linux64-bit Linux on ARM
x86_64-darwin64-bit macOS on Intel
arm64-darwin64-bit macOS on Apple Silicon
x64-mingw-ucrt64-bit Windows (UCRT)
x64-mingw3264-bit Windows (older)

These platform strings are used by:

  • RubyGems to select the correct pre-built binary
  • rake-compiler for cross-compilation
  • rb-sys-dock to build for different platforms

Conditional Compilation

Rust's conditional compilation features allow you to write platform-specific code:

#![allow(unused)]
fn main() {
// Platform-specific code
#[cfg(target_os = "windows")]
fn platform_specific() {
    // Windows-specific implementation
}

#[cfg(target_os = "macos")]
fn platform_specific() {
    // macOS-specific implementation
}

#[cfg(target_os = "linux")]
fn platform_specific() {
    // Linux-specific implementation
}
}

For architectures:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
fn arch_specific() {
    // x86_64 implementation
}

#[cfg(target_arch = "aarch64")]
fn arch_specific() {
    // ARM64 implementation
}
}

Complete Example: File Path Handling

Here's a real-world example of handling paths differently across platforms:

#![allow(unused)]
fn main() {
use std::path::PathBuf;

fn get_config_path() -> PathBuf {
    #[cfg(target_os = "windows")]
    {
        let mut path = PathBuf::new();
        if let Some(profile) = std::env::var_os("USERPROFILE") {
            path.push(profile);
            path.push("AppData");
            path.push("Roaming");
            path.push("MyApp");
            path.push("config.toml");
        }
        path
    }

    #[cfg(target_os = "macos")]
    {
        let mut path = PathBuf::new();
        if let Some(home) = std::env::var_os("HOME") {
            path.push(home);
            path.push("Library");
            path.push("Application Support");
            path.push("MyApp");
            path.push("config.toml");
        }
        path
    }

    #[cfg(target_os = "linux")]
    {
        let mut path = PathBuf::new();
        if let Some(config_dir) = std::env::var_os("XDG_CONFIG_HOME") {
            path.push(config_dir);
        } else if let Some(home) = std::env::var_os("HOME") {
            path.push(home);
            path.push(".config");
        }
        path.push("myapp");
        path.push("config.toml");
        path
    }

    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
    {
        // Default for other platforms
        PathBuf::from("config.toml")
    }
}
}

Platform-Specific Dependencies

Cargo.toml supports platform-specific dependencies:

[dependencies]
# Common dependencies...

[target.'cfg(target_os = "linux")'.dependencies]
jemallocator = { version = "0.5", features = ["disable_initial_exec_tls"] }

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winbase"] }

[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

Example: System-specific Memory Allocation

#![allow(unused)]
fn main() {
#[cfg(target_os = "linux")]
use jemallocator::Jemalloc;

#[cfg(target_os = "linux")]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

// Rest of your code...
}

Using build.rs for Platform Detection

The Rust build script (build.rs) can be used to detect platforms and configure builds:

// ext/my_gem/build.rs
fn main() {
    // Detect OS
    let target = std::env::var("TARGET").unwrap_or_default();

    if target.contains("windows") {
        println!("cargo:rustc-link-lib=dylib=user32");
        println!("cargo:rustc-cfg=feature=\"windows_specific\"");
    } else if target.contains("darwin") {
        println!("cargo:rustc-link-lib=framework=CoreFoundation");
        println!("cargo:rustc-cfg=feature=\"macos_specific\"");
    } else if target.contains("linux") {
        println!("cargo:rustc-link-lib=dylib=dl");
        println!("cargo:rustc-cfg=feature=\"linux_specific\"");
    }

    // Tell Cargo to invalidate the built crate whenever the build script changes
    println!("cargo:rerun-if-changed=build.rs");
}

Then in your code:

#![allow(unused)]
fn main() {
#[cfg(feature = "windows_specific")]
fn platform_init() {
    // Windows initialization code
}

#[cfg(feature = "macos_specific")]
fn platform_init() {
    // macOS initialization code
}

#[cfg(feature = "linux_specific")]
fn platform_init() {
    // Linux initialization code
}
}

Cross-Compilation with rb-sys-dock

rb-sys-dock is a Docker-based tool that simplifies cross-compilation:

Setting Up rb-sys-dock in Your Gem

  1. Add rb-sys-dock to your Gemfile:
# Gemfile
group :development do
  gem "rb-sys-dock", "~> 0.1"
end
  1. Configure your Rakefile for cross-compilation:
# Rakefile
require "rb_sys/extensiontask"

GEMSPEC = Gem::Specification.load("my_gem.gemspec")

RbSys::ExtensionTask.new("my_gem", GEMSPEC) do |ext|
  ext.lib_dir = "lib/my_gem"
  ext.cross_compile = true
  ext.cross_platform = [
    "x86_64-linux",
    "aarch64-linux",
    "x86_64-darwin",
    "arm64-darwin",
    "x64-mingw-ucrt"
  ]
end

Building for a Specific Platform

To build for a specific platform:

bundle exec rake native:my_gem:x86_64-linux

This creates a platform-specific gem in the pkg directory.

Building for All Platforms

To build for all configured platforms:

bundle exec rake native

Using rb-sys-dock Directly

For more control, use rb-sys-dock directly:

# Build for a specific platform
bundle exec rb-sys-dock --platform x86_64-linux --build

# Start a shell in the Docker container
bundle exec rb-sys-dock --platform x86_64-linux --shell

Testing Cross-Platform Builds

Local Testing with Docker

You can test your cross-compiled Linux extensions locally:

# Run tests inside a Docker container
bundle exec rb-sys-dock --platform x86_64-linux --command "bundle exec rake test"

Local Testing on macOS

If you're on macOS with Apple Silicon, you can test both architectures:

# Test arm64-darwin build (native)
bundle exec rake test

# Test x86_64-darwin build (cross-compiled)
arch -x86_64 bundle exec rake test

CI/CD for Multiple Platforms

GitHub Actions is ideal for testing across platforms:

Testing on Multiple Platforms

# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        ruby: ["3.0", "3.1", "3.2", "3.3"]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: oxidize-rb/actions/setup-ruby-and-rust@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true
      - run: bundle exec rake compile
      - run: bundle exec rake test

Ruby-Head Compatibility

When supporting ruby-head or development versions of Ruby, you must publish a source gem alongside your precompiled gems. This is necessary because:

  1. The Ruby ABI (Application Binary Interface) can change between development versions
  2. Precompiled binary gems built against one ruby-head version may be incompatible with newer ruby-head versions
  3. Source gems allow users to compile the extension against their specific ruby-head version

To ensure compatibility, add a source gem to your release process:

# Rakefile
RbSys::ExtensionTask.new("my_gem", GEMSPEC) do |ext|
  # Configure cross-platform gems as usual
  ext.cross_compile = true
  ext.cross_platform = ['x86_64-linux', 'arm64-darwin', ...]

  # The default platform will build the source gem
end

Then in your CI/CD pipeline, include both platform-specific and source gem builds:

# .github/workflows/release.yml
jobs:
  # First build all platform-specific gems
  cross_compile:
    # ...

  # Then build the source gem
  source_gem:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - run: bundle install
      - run: bundle exec rake build # Builds the source gem
      - uses: actions/upload-artifact@v3
        with:
          name: source-gem
          path: pkg/*.gem # Include source gem without platform suffix

Cross-Compiling for Release

# .github/workflows/release.yml
name: Release

on:
  push:
    tags: ["v*"]

jobs:
  cross_compile:
    strategy:
      fail-fast: false
      matrix:
        platform: ["x86_64-linux", "aarch64-linux", "x86_64-darwin", "arm64-darwin", "x64-mingw-ucrt"]

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.1"
      - uses: oxidize-rb/actions/cross-gem@v1
        with:
          platform: ${{ matrix.platform }}
      - uses: actions/upload-artifact@v3
        with:
          name: gem-${{ matrix.platform }}
          path: pkg/*-${{ matrix.platform }}.gem

Complete CI Workflow Example

Here's a more complete workflow showing an automated release process with tests and cross-compilation:

# .github/workflows/gem-release.yml
name: Gem Release

on:
  push:
    tags:
      - "v*"

jobs:
  fetch-data:
    runs-on: ubuntu-latest
    outputs:
      platforms: ${{ steps.fetch.outputs.supported-ruby-platforms }}
    steps:
      - id: fetch
        uses: oxidize-rb/actions/fetch-ci-data@v1
        with:
          supported-ruby-platforms: |
            exclude: [x86-linux, x86-darwin, arm-linux]

  test:
    needs: fetch-data
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        ruby: ["3.0", "3.1", "3.2", "3.3"]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: oxidize-rb/actions/setup-ruby-and-rust@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true
      - run: bundle exec rake compile
      - run: bundle exec rake test

  cross-compile:
    needs: [fetch-data, test]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        platform: ${{ fromJSON(needs.fetch-data.outputs.platforms) }}
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.1"
      - uses: oxidize-rb/actions/cross-gem@v1
        with:
          platform: ${{ matrix.platform }}
      - uses: actions/upload-artifact@v3
        with:
          name: gem-${{ matrix.platform }}
          path: pkg/*-${{ matrix.platform }}.gem

  release:
    needs: cross-compile
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.1"
      - uses: actions/download-artifact@v3
        with:
          path: artifacts
      - name: Move gems to pkg directory
        run: |
          mkdir -p pkg
          find artifacts -name "*.gem" -exec mv {} pkg/ \;
      - name: Publish to RubyGems
        run: |
          mkdir -p ~/.gem
          echo -e "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}" > ~/.gem/credentials
          chmod 0600 ~/.gem/credentials
          gem push pkg/*.gem
        env:
          RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}

Platform-Specific Issues and Solutions

Windows

Windows presents unique challenges for Ruby extensions:

  • Path Handling: Use forward slashes (/) in paths, not backslashes (\)

    #![allow(unused)]
    fn main() {
    // Instead of this:
    let path = "C:\\Users\\Name\\file.txt";
    
    // Do this:
    let path = "C:/Users/Name/file.txt";
    }
  • DLL Loading: Handle DLL loading carefully

    #![allow(unused)]
    fn main() {
    #[cfg(target_os = "windows")]
    fn load_library(name: &str) -> Result<(), Error> {
        use std::os::windows::ffi::OsStrExt;
        use std::ffi::OsStr;
        use winapi::um::libloaderapi::LoadLibraryW;
    
        let name_wide: Vec<u16> = OsStr::new(name)
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();
    
        let handle = unsafe { LoadLibraryW(name_wide.as_ptr()) };
        if handle.is_null() {
            return Err(Error::new("Failed to load library"));
        }
    
        Ok(())
    }
    }
  • Asynchronous I/O: Windows has different async I/O APIs

    #![allow(unused)]
    fn main() {
    #[cfg(target_os = "windows")]
    use windows_specific_io::read_file;
    
    #[cfg(not(target_os = "windows"))]
    use posix_specific_io::read_file;
    }

macOS

  • Architectures: Support both Intel and Apple Silicon

    # Rakefile
    RbSys::ExtensionTask.new("my_gem", GEMSPEC) do |ext|
      ext.cross_platform = ["x86_64-darwin", "arm64-darwin"]
    end
    
  • Framework Linking: Link against macOS frameworks

    #![allow(unused)]
    fn main() {
    // build.rs
    #[cfg(target_os = "macos")]
    {
        println!("cargo:rustc-link-lib=framework=Security");
        println!("cargo:rustc-link-lib=framework=CoreFoundation");
    }
    }
  • Universal Binary: Consider building universal binaries

    # extconf.rb
    if RUBY_PLATFORM =~ /darwin/
      ENV['RUSTFLAGS'] = "-C link-arg=-arch -C link-arg=arm64 -C link-arg=-arch -C link-arg=x86_64"
    end
    

Linux

  • glibc vs musl: Consider both glibc and musl for maximum compatibility

    # Rakefile
    RbSys::ExtensionTask.new("my_gem", GEMSPEC) do |ext|
      ext.cross_platform = ["x86_64-linux", "x86_64-linux-musl"]
    end
    
  • Static Linking: Increase portability with static linking

    # Cargo.toml
    [target.'cfg(target_os = "linux")'.dependencies]
    openssl-sys = { version = "0.9", features = ["vendored"] }
    
  • Multiple Distributions: Test on different distributions in CI

    # .github/workflows/linux-test.yml
    jobs:
      test:
        strategy:
          matrix:
            container: ["ubuntu:20.04", "debian:bullseye", "alpine:3.15"]
        container: ${{ matrix.container }}
    

Best Practices

  1. Start with cross-compilation early - Don't wait until release time
  2. Test on all target platforms - Ideally in CI
  3. Use platform-specific code sparingly - Abstract platform differences when possible
  4. Prefer conditional compilation over runtime checks - Better performance and safer code
  5. Document platform requirements - Make dependencies clear to users
  6. Use feature flags for optional platform support - Allow users to opt-in to platform-specific features

Example: Good Platform Abstraction

#![allow(unused)]
fn main() {
// Platform abstraction module
mod platform {
    pub struct FileHandle(PlatformSpecificHandle);

    impl FileHandle {
        pub fn open(path: &str) -> Result<Self, Error> {
            #[cfg(target_os = "windows")]
            {
                // Windows-specific implementation
                // ...
            }

            #[cfg(unix)]
            {
                // Unix-based implementation (Linux, macOS, etc.)
                // ...
            }

            #[cfg(not(any(target_os = "windows", unix)))]
            {
                return Err(Error::new("Unsupported platform"));
            }
        }

        pub fn read(&self, buf: &mut [u8]) -> Result<usize, Error> {
            // Platform-specific reading implementation
            // ...
        }

        pub fn write(&self, buf: &[u8]) -> Result<usize, Error> {
            // Platform-specific writing implementation
            // ...
        }
    }
}

// User code just uses the abstraction
use platform::FileHandle;

fn process_file(path: &str) -> Result<(), Error> {
    let file = FileHandle::open(path)?;
    // Common code without platform-specific details
    Ok(())
}
}

Complete Example: Cross-Platform Release Workflow

Here's a complete example for releasing a cross-platform gem:

  1. Develop locally on your preferred platform
  2. Test your changes locally with bundle exec rake test
  3. Verify cross-platform builds with bundle exec rb-sys-dock --platform x86_64-linux --command "bundle exec rake test"
  4. Commit and push your changes
  5. CI tests run on all supported platforms
  6. Create a release tag when ready (git tag v1.0.0 && git push --tags)
  7. Cross-compilation workflow builds platform-specific gems
  8. Publish gems to RubyGems or your private repository

By following this workflow, you can be confident your extension works consistently across platforms.

Real-World Examples

Many real-world gems use rb-sys for cross-platform development:

  • blake3-ruby - Fast cryptographic hash function implementation with full cross-platform support
  • lz4-ruby - LZ4 compression library with rb-sys
  • wasmtime-rb - WebAssembly runtime

These projects demonstrate successful cross-platform strategies and can serve as references for your own extensions.

Example from wasmtime-rb

wasmtime-rb wraps platform-specific functionality while presenting a consistent API:

#![allow(unused)]
fn main() {
#[cfg(unix)]
mod unix {
    pub unsafe fn map_memory(addr: *mut u8, len: usize) -> Result<(), Error> {
        // Unix-specific memory mapping
    }
}

#[cfg(windows)]
mod windows {
    pub unsafe fn map_memory(addr: *mut u8, len: usize) -> Result<(), Error> {
        // Windows-specific memory mapping
    }
}

// Public API uses the platform-specific implementation
pub unsafe fn map_memory(addr: *mut u8, len: usize) -> Result<(), Error> {
    #[cfg(unix)]
    {
        return unix::map_memory(addr, len);
    }

    #[cfg(windows)]
    {
        return windows::map_memory(addr, len);
    }

    #[cfg(not(any(unix, windows)))]
    {
        return Err(Error::new("Unsupported platform"));
    }
}
}

Summary

Cross-platform development with rb-sys leverages Rust's excellent platform-specific features:

  1. Conditional compilation provides platform-specific code paths
  2. Platform-specific dependencies allow different libraries per platform
  3. rb-sys-dock enables easy cross-compilation for multiple platforms
  4. GitHub Actions integration automates testing and releases

By following the patterns in this chapter, your Ruby extensions can work seamlessly across all major platforms while minimizing platform-specific code and maintenance burden.

Next Steps

Testing Extensions

Testing is a critical part of developing Ruby extensions. This chapter covers strategies for testing your Rust code that interfaces with Ruby, from unit tests to integration tests and CI workflows.

Testing is particularly important for Ruby extensions because segmentation faults, memory leaks, and other low-level issues can crash the entire Ruby VM. Untested extensions can lead to hard-to-debug production crashes.

rb-sys-test-helpers Overview

The rb-sys-test-helpers crate provides specialized utilities for testing Ruby extensions in Rust. It solves many of the challenges associated with testing code that interacts with the Ruby VM:

  • Automating Ruby VM initialization and teardown
  • Managing thread safety for Ruby VM operations
  • Handling Ruby exceptions in tests
  • Providing GC stress testing to catch memory issues
  • Offering conversion helpers for common Ruby types

For detailed API documentation, see the Test Helpers API Reference.

Unit Testing Rust Code

The Challenge of Testing Ruby Extensions

Testing Rust code that interacts with Ruby presents unique challenges:

  1. Ruby VM Initialization: The Ruby VM must be properly initialized before tests run.
  2. Thread Safety: Ruby's VM has thread-specific state that must be managed.
  3. Exception Handling: Ruby exceptions need to be properly caught and converted to Rust errors.
  4. Memory Management: Memory allocated by Ruby needs to be protected from garbage collection during tests.

rb-sys provides specialized tools to overcome these challenges, particularly the #[ruby_test] macro which handles Ruby VM initialization and thread management automatically.

Complete Test Setup Guide

Setting up proper testing for Ruby extensions requires several components working together. This guide provides a comprehensive setup that you can adapt to your project.

Required Dependencies

Your Cargo.toml needs to be configured with the appropriate dependencies:

[package]
name = "my_extension"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

# Main dependencies
[dependencies]
magnus = "0.6" # For high-level Ruby API
rb-sys = "0.9"  # Required for rb_sys_test_helpers to work

# Test dependencies
[dev-dependencies]
rb-sys-env = "0.1"          # For Ruby environment detection
rb-sys-test-helpers = "0.2" # For Ruby VM test helpers

The key points:

  • Include rb-sys as a regular dependency (not just a dev-dependency)
  • Both rb-sys-env and rb-sys-test-helpers are needed for tests

Setting Up build.rs

Create a build.rs file in your project root with the following content:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // This activates rb-sys-env for both normal builds and tests
    let _ = rb_sys_env::activate()?;

    // Any additional build configuration can go here

    Ok(())
}

The rb_sys_env::activate() function:

  • Sets up Cargo configuration based on the detected Ruby environment
  • Exposes Ruby version information as feature flags (e.g., ruby_gte_3_0, ruby_use_flonum)
  • Ensures proper linking to the Ruby library

Importing Test Helpers

In your test module, import the necessary components:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use rb_sys_test_helpers::ruby_test;
    use magnus::{Ruby, Error};

    // Your test functions go here...
}
}

The #[ruby_test] Macro

The #[ruby_test] macro is the simplest and most reliable way to test Ruby extensions in Rust. It handles all the complexities of VM initialization and thread management.

The simplest way to test Ruby extensions is with the #[ruby_test] macro, which wraps your test functions to ensure they run within a properly initialized Ruby VM:

#![allow(unused)]
fn main() {
// Complete example of using the ruby_test macro
use rb_sys::*;
use rb_sys_test_helpers::ruby_test;

#[ruby_test]
fn test_string_manipulation() {
    unsafe {
        // Create a Ruby string
        let rb_str = rb_utf8_str_new_cstr("hello\0".as_ptr() as _);

        // Append to the string
        let rb_str = rb_str_cat(rb_str, " world\0".as_ptr() as _, 6);

        // Convert to Rust string for assertion
        let mut rb_str_val = rb_str;
        let result_ptr = rb_string_value_cstr(&mut rb_str_val);
        let result = std::ffi::CStr::from_ptr(result_ptr)
            .to_string_lossy()
            .to_string();

        assert_eq!(result, "hello world");
    }
}

// You can add options to the macro
#[ruby_test(gc_stress)]
fn test_with_gc_stress() {
    // This test will run with GC stress enabled
    // Ruby's garbage collector will run more frequently
    // to help catch memory management issues
    unsafe {
        let rb_str = rb_utf8_str_new_cstr("test\0".as_ptr() as _);
        rb_gc_guard!(rb_str); // Protect from GC
        rb_gc(); // Force garbage collection
        // If rb_str was not protected, it might be collected here
    }
}

// Version-specific tests using rb-sys-env features
#[ruby_test]
fn test_with_version_conditionals() {
    // This block only runs on Ruby 3.0 or newer
    #[cfg(ruby_gte_3_0)]
    {
        // Test Ruby 3.0+ specific features
    }

    // This block only runs on Ruby 2.7
    #[cfg(all(ruby_gte_2_7, ruby_lt_3_0))]
    {
        // Test Ruby 2.7 specific features
    }

    // This block runs if float values are stored
    // as immediate values (Ruby implementation detail)
    #[cfg(ruby_use_flonum)]
    {
        // Test flonum implementation
    }
}
}

The #[ruby_test] macro:

  1. Ensures the Ruby VM is initialized once and only once
  2. Runs all tests on the same OS thread
  3. Catches and propagates Ruby exceptions as Rust errors
  4. Performs GC after each test to catch memory management issues

Click the eye icon () to view additional examples of the macro options and version-specific testing.

The #[ruby_test] macro:

  1. Ensures the Ruby VM is initialized once and only once
  2. Runs all tests on the same OS thread
  3. Catches and propagates Ruby exceptions as Rust errors
  4. Performs GC after each test to catch memory management issues

Using Magnus with #[ruby_test]

Magnus provides a much more ergonomic Rust API for working with Ruby. Combined with the #[ruby_test] macro, it makes testing Ruby extensions much simpler and safer.

One of the great advantages of the #[ruby_test] macro is that it works seamlessly with Magnus, providing a much more ergonomic way to test Ruby integrations:

#![allow(unused)]
fn main() {
// Complete example of using Magnus with ruby_test
use magnus::{RString, Ruby};
use rb_sys_test_helpers::ruby_test;

#[ruby_test]
fn test_with_magnus() {
    // Get the Ruby interpreter - no unsafe required when using Magnus!
    let ruby = Ruby::get().unwrap();

    // Create a Ruby string with Magnus
    let hello = RString::new(ruby, "Hello, ");

    // Append to the string
    let message = hello.concat(ruby, "World!");

    // Convert to Rust string for assertion - easy with Magnus
    let result = message.to_string().unwrap();

    assert_eq!(result, "Hello, World!");
}

// Testing more complex Ruby interactions
#[ruby_test]
fn test_ruby_class_interaction() {
    let ruby = Ruby::get().unwrap();

    // Define a Ruby class for testing
    let test_class = ruby.define_class("TestClass", ruby.class_object()).unwrap();

    // Define a method on the class
    test_class.define_method("double",
        magnus::method!(|_rb_self, num: i64| -> i64 { num * 2 }, 1)
    ).unwrap();

    // Use Ruby's eval to test the class
    let result: i64 = ruby.eval("TestClass.new.double(21)").unwrap();

    assert_eq!(result, 42);
}
}

Magnus makes it much easier to interact with Ruby objects in a safe and idiomatic way. Using Magnus with the #[ruby_test] macro gives you the best of both worlds:

  • Magnus's safe, high-level API
  • The #[ruby_test] macro's robust Ruby VM management

Click the eye icon () to see examples of more complex Ruby class interactions.

Magnus makes it much easier to interact with Ruby objects in a safe and idiomatic way. Using Magnus with the #[ruby_test] macro gives you the best of both worlds:

  • Magnus's safe, high-level API
  • The #[ruby_test] macro's robust Ruby VM management

Here's another example showing how to work with Ruby classes and methods using Magnus:

#![allow(unused)]
fn main() {
use magnus::{class, eval, method, prelude::*, Module, RClass, Ruby};
use rb_sys_test_helpers::ruby_test;

#[ruby_test]
fn test_ruby_class_interaction() {
    let ruby = Ruby::get().unwrap();

    // Define a Ruby class for testing
    let test_class = ruby.define_class("TestClass", ruby.class_object()).unwrap();

    // Define a method on the class
    test_class.define_method("double", method!(|ruby, num: i64| -> i64 {
        num * 2
    })).unwrap();

    // Create an instance and call the method
    let result: i64 = eval!(ruby, "TestClass.new.double(21)").unwrap();

    assert_eq!(result, 42);
}
}

Testing with GC Stress

To detect subtle memory management issues, you can enable GC stress testing:

#![allow(unused)]
fn main() {
#[ruby_test(gc_stress)]
fn test_gc_interactions() {
    unsafe {
        // Create a Ruby string
        let s = rb_str_new_cstr("hello world\0".as_ptr() as _);

        // Get a pointer to the string's contents
        let s_ptr = RSTRING_PTR(s);

        // Protect s from garbage collection
        rb_gc_guard!(s);

        // Now we can safely use s_ptr, even though GC might run
        let t = rb_str_new_cstr("prefix: \0".as_ptr() as _);
        let result = rb_str_cat_cstr(t, s_ptr);

        // More code...
    }
}
}

With Magnus, the same test is more straightforward:

#![allow(unused)]
fn main() {
use magnus::{RString, Ruby};
use rb_sys_test_helpers::ruby_test;

#[ruby_test(gc_stress)]
fn test_gc_interactions_with_magnus() {
    let ruby = Ruby::get().unwrap();

    // Create first string
    let s = RString::new(ruby, "hello world");

    // Magnus handles GC protection automatically!

    // Create second string and concatenate
    let t = RString::new(ruby, "prefix: ");
    let result = t.concat(ruby, &s);

    assert_eq!(result.to_string().unwrap(), "prefix: hello world");
}
}

The gc_stress option forces Ruby's garbage collector to run frequently during the test, which helps expose bugs related to:

  • Objects not being properly protected from GC
  • Dangling pointers
  • Invalid memory access

Handling Ruby Exceptions

Ruby exceptions can be caught and converted to Rust errors using the protect function:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_exception_handling() {
    use rb_sys_test_helpers::protect;

    // This code will raise a Ruby exception
    let result = unsafe {
        protect(|| {
            rb_sys::rb_raise(rb_sys::rb_eRuntimeError, "Test error\0".as_ptr() as _);
            // This will never be reached
            "success"
        })
    };

    // Verify we got an error
    assert!(result.is_err());

    // Check the error message
    let error = result.unwrap_err();
    assert!(error.message().unwrap().contains("Test error"));
}
}

With Magnus, exception handling is more natural:

#![allow(unused)]
fn main() {
use magnus::{eval, Ruby, Error};
use rb_sys_test_helpers::ruby_test;

#[ruby_test]
fn test_exception_handling_with_magnus() {
    let ruby = Ruby::get().unwrap();

    // Evaluate Ruby code that raises an exception
    let result: Result<String, Error> = eval!(ruby, "raise 'Test error'");

    // Verify we got an error
    assert!(result.is_err());

    // Magnus errors contain the Ruby exception
    let error = result.unwrap_err();
    assert!(error.to_string().contains("Test error"));
}
}

Version-Specific Tests

rb-sys-env provides feature flags that allow you to write version-specific tests:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_version_specific_features() {
    // This test will only run on Ruby 3.0 or higher
    #[cfg(ruby_gte_3_0)]
    {
        // Test Ruby 3.0+ specific features
        unsafe {
            // Example: using Ractor API which is only available in Ruby 3.0+
            #[cfg(ruby_have_ruby_ractor_h)]
            let is_ractor_supported = rb_sys::rb_ractor_main_p() != 0;

            // ...
        }
    }

    // This block will only run on Ruby 2.7
    #[cfg(all(ruby_gte_2_7, ruby_lt_3_0))]
    {
        // Test Ruby 2.7 specific features
        // ...
    }
}
}

With Magnus:

#![allow(unused)]
fn main() {
use magnus::{Ruby, eval};
use rb_sys_test_helpers::ruby_test;

#[ruby_test]
fn test_version_specific_features_with_magnus() {
    let ruby = Ruby::get().unwrap();

    // This test will only run on Ruby 3.0 or higher
    #[cfg(ruby_gte_3_0)]
    {
        // Test Ruby 3.0+ specific features
        #[cfg(ruby_have_ruby_ractor_h)]
        let is_ractor_supported: bool = eval!(ruby, "defined?(Ractor) != nil").unwrap();

        #[cfg(ruby_have_ruby_ractor_h)]
        assert!(is_ractor_supported);
    }
}
}

Available version flags include:

  • ruby_gte_X_Y: Ruby version >= X.Y
  • ruby_lt_X_Y: Ruby version < X.Y
  • ruby_eq_X_Y: Ruby version == X.Y
  • ruby_have_FEATURE: Specific Ruby API feature is available

Test Helpers and Macros

rb-sys-test-helpers includes several macros to simplify common testing patterns:

#![allow(unused)]
fn main() {
// Convert a Ruby string to a Rust String for testing
#[ruby_test]
fn test_with_helper_macros() {
    use rb_sys_test_helpers::rstring_to_string;

    unsafe {
        let rb_str = rb_utf8_str_new_cstr("hello world\0".as_ptr() as _);
        let rust_str = rstring_to_string!(rb_str);

        assert_eq!(rust_str, "hello world");
    }
}
}

Manual Ruby VM Setup

For more complex test scenarios, you can manually initialize the Ruby VM:

#![allow(unused)]
fn main() {
use rb_sys_test_helpers::{with_ruby_vm, protect};

#[test]
fn test_complex_scenario() {
    with_ruby_vm(|| {
        // Multiple operations that need a Ruby VM
        let result1 = unsafe {
            protect(|| {
                // First operation...
                42
            })
        };

        let result2 = unsafe {
            protect(|| {
                // Second operation...
                "success"
            })
        };

        assert_eq!(result1.unwrap(), 42);
        assert_eq!(result2.unwrap(), "success");
    }).unwrap();
}
}

With Magnus, the same approach but more ergonomically:

#![allow(unused)]
fn main() {
use magnus::{eval, Ruby};
use rb_sys_test_helpers::with_ruby_vm;

#[test]
fn test_complex_scenario_with_magnus() {
    with_ruby_vm(|| {
        let ruby = Ruby::get().unwrap();

        // First operation
        let result1: i64 = eval!(ruby, "21 * 2").unwrap();

        // Second operation
        let result2: String = eval!(ruby, "'suc' + 'cess'").unwrap();

        assert_eq!(result1, 42);
        assert_eq!(result2, "success");
    }).unwrap();
}
}

Debugging Failed Tests

When your tests fail, debugging tools can help identify the root cause. LLDB is particularly useful for debugging memory issues, segmentation faults, and other low-level problems.

Using LLDB to Debug Tests

LLDB is a powerful debugger that works well with Rust and Ruby code. Here's how to use it with your tests:

  1. First, compile your extension with debug symbols:

    RUSTFLAGS="-g" bundle exec rake compile
    
  2. Run your test with LLDB:

    lldb -- ruby -Ilib -e "require 'my_extension'; require_relative 'test/test_my_extension.rb'"
    
  3. At the LLDB prompt, set breakpoints in your Rust code:

    (lldb) breakpoint set --name MutCalculator::divide
    
  4. Run the program:

    (lldb) run
    
  5. When the breakpoint is hit, you can:

    • Examine variables: frame variable
    • Print expressions: p self or p val
    • Step through code: next (over) or step (into)
    • Continue execution: continue
    • Show backtrace: bt

LLDB Commands for Ruby Extensions

Some LLDB commands that are particularly useful for Ruby extensions:

# To print a Ruby string VALUE
(lldb) p rb_string_value_cstr(&my_rb_string_val)

# To check if a VALUE is nil
(lldb) p RB_NIL_P(my_value)

# To get the Ruby class name of an object
(lldb) p rb_class2name(rb_class_of(my_value))

# To check Ruby exception information
(lldb) p rb_errinfo()

Debugging Memory Issues

For memory-related issues:

  1. Set a breakpoint around where objects are created
  2. Set a breakpoint where the crash occurs
  3. When hitting the first breakpoint, note memory addresses
  4. When hitting the second breakpoint, check if those addresses are still valid
# Example debugging session for memory issues
$ lldb -- ruby -Ilib -e "require 'my_extension'; MyExtension.test_method"
(lldb) breakpoint set --name MutPoint::new
(lldb) breakpoint set --name MutPoint::add_x
(lldb) run

# When first breakpoint hits
(lldb) frame variable
(lldb) p self
(lldb) continue

# When second breakpoint hits
(lldb) frame variable
(lldb) p self

Debugging RefCell Borrow Errors

For diagnosing BorrowMutError panics:

  1. Set a breakpoint right before the borrow operation:

    (lldb) breakpoint set --file lib.rs --line 123
    
  2. When it hits, check the status of the RefCell:

    (lldb) p self.0
    
  3. Step through the code and watch when borrows occur:

    (lldb) next
    

Further Information

For more comprehensive debugging setup including VSCode integration and debugging the Ruby C API, see the Debugging & Troubleshooting chapter.

Common Testing Patterns and Anti-Patterns

When testing Ruby extensions, several patterns emerge that can help you write more effective tests, along with anti-patterns to avoid.

Pattern: Proper Method Invocation

#![allow(unused)]
fn main() {
// ✅ GOOD: Using associated function syntax for methods with Ruby/self parameters
let result = MutCalculator::divide(&ruby, &calc, 6.0, 2.0);

// ❌ BAD: This won't compile - can't call as instance method
// let result = calc.divide(&ruby, 6.0, 2.0);
}

Pattern: Complete RefCell Borrows

#![allow(unused)]
fn main() {
// ✅ GOOD: Complete the borrow before attempting to borrow mutably
let current_x = self.0.borrow().x;  // First borrow completes here
if let Some(sum) = current_x.checked_add(val) {
    self.0.borrow_mut().x = sum;    // Safe to borrow mutably now
}

// ❌ BAD: Will panic with "already borrowed: BorrowMutError"
// if let Some(sum) = self.0.borrow().x.checked_add(val) {
//     self.0.borrow_mut().x = sum;  // Error: still borrowed from the if condition
// }
}

Pattern: Ruby Error Checking

Testing error handling is crucial for Ruby extensions. Here's how to properly test different exception scenarios:

#![allow(unused)]
fn main() {
// ✅ GOOD: Verify specific Ruby exception types
let result = MutCalculator::divide(&ruby, &calc, 6.0, 0.0);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_kind_of(ruby, ruby.exception_zero_div_error()));
assert!(err.message().unwrap().contains("Division by zero"));

// ❌ BAD: Just checking for any error without specific type
// assert!(result.is_err());
}
Testing Different Ruby Exception Types
#![allow(unused)]
fn main() {
// Testing for ArgumentError
fn test_argument_error() -> Result<(), Error> {
    let ruby = Ruby::get()?;
    let calc = Calculator::new();

    // Function that raises ArgumentError on negative input
    let result = Calculator::sqrt(&ruby, &calc, -1.0);
    assert!(result.is_err());

    let err = result.unwrap_err();
    assert!(err.is_kind_of(ruby, ruby.exception_arg_error()));
    assert!(err.message().unwrap().contains("must be positive"));

    Ok(())
}

// Testing for RangeError
fn test_range_error() -> Result<(), Error> {
    let ruby = Ruby::get()?;
    let calc = Calculator::new();

    // Function that raises RangeError on large values
    let result = Calculator::factorial(&ruby, &calc, 100);
    assert!(result.is_err());

    let err = result.unwrap_err();
    assert!(err.is_kind_of(ruby, ruby.exception_range_error()));

    Ok(())
}

// Testing for TypeError
fn test_type_error() -> Result<(), Error> {
    let ruby = Ruby::get()?;

    // Use eval to create a type error situation
    let result: Result<i64, Error> = ruby.eval("'string' + 5");
    assert!(result.is_err());

    let err = result.unwrap_err();
    assert!(err.is_kind_of(ruby, ruby.exception_type_error()));

    Ok(())
}
}
Testing Ruby Exceptions Using eval

You can also test how Ruby exceptions are raised and handled using eval:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_ruby_exceptions_with_eval() -> Result<(), Error> {
    let ruby = Ruby::get()?;

    // Set up our extension
    let module = ruby.define_module("MyModule")?;
    let calc_class = module.define_class("Calculator", ruby.class_object())?;
    calc_class.define_singleton_method("new", function!(Calculator::new, 0))?;
    calc_class.define_method("divide", method!(Calculator::divide, 2))?;

    // Test division by zero from Ruby code
    let result: Result<f64, Error> = ruby.eval("MyModule::Calculator.new.divide(10, 0)");
    assert!(result.is_err());

    let err = result.unwrap_err();
    assert!(err.is_kind_of(ruby, ruby.exception_zero_div_error()));

    Ok(())
}
}
Verifying Custom Exception Types

For custom exception classes:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_custom_exception() -> Result<(), Error> {
    let ruby = Ruby::get()?;

    // Create a custom exception class
    let module = ruby.define_module("MyModule")?;
    let custom_error = module.define_class("CustomError", ruby.exception_standard_error())?;

    // Define a method that raises our custom error
    let obj = ruby.eval::<Value>("Object.new")?;
    obj.define_singleton_method(ruby, "raise_custom",
        function!(|ruby: &Ruby| -> Result<(), Error> {
            Err(Error::new(
                ruby.class_path_to_value("MyModule::CustomError"),
                "Custom error message"
            ))
        }, 0)
    )?;

    // Call the method and verify the exception
    let result: Result<(), Error> = ruby.eval("Object.new.raise_custom");
    assert!(result.is_err());

    let err = result.unwrap_err();
    assert!(err.is_kind_of(ruby, custom_error));
    assert!(err.message().unwrap().contains("Custom error"));

    Ok(())
}
}

Pattern: Proper Memory Management

#![allow(unused)]
fn main() {
// ✅ GOOD: Test with GC stress to catch memory issues
#[ruby_test(gc_stress)]
fn test_memory_management() {
    // Test code here will run with GC stress enabled
}

// ✅ GOOD: Ensure objects used in raw C API are protected
unsafe {
    let rb_str = rb_utf8_str_new_cstr("hello\0".as_ptr() as _);
    let rb_str = rb_gc_guard!(rb_str);  // Protected from GC
}

// ❌ BAD: Using raw pointers without protection
// unsafe {
//     let rb_str = rb_utf8_str_new_cstr("hello\0".as_ptr() as _);
//     // rb_str could be collected here if GC runs
// }
}

Pattern: Version-Specific Testing

#![allow(unused)]
fn main() {
// ✅ GOOD: Conditional tests based on Ruby version
#[ruby_test]
fn test_features() {
    #[cfg(ruby_gte_3_0)]
    {
        // Test Ruby 3.0+ specific features
    }

    #[cfg(not(ruby_gte_3_0))]
    {
        // Test for older Ruby versions
    }
}

// ❌ BAD: Runtime checks for version
// if ruby_version() >= (3, 0, 0) {
//     // Test Ruby 3.0+ specific features
// }
}

Testing Best Practices

Failing to follow these practices can result in segmentation faults, memory leaks, and other serious issues that may only appear in production environments with specific data or Ruby versions.

  1. Use #[ruby_test] for most tests: This macro handles Ruby VM setup automatically.
  2. Consider Magnus for cleaner tests: Magnus offers a much more ergonomic API than raw rb-sys.
  3. Enable gc_stress for memory management tests: This helps catch GC-related bugs early.
  4. Always protect raw Ruby pointers: Use rb_gc_guard! when you need to use raw pointers.
  5. Catch exceptions properly: Don't let Ruby exceptions crash your tests.
  6. Use conditional compilation for version-specific tests: Leverage the version flags from rb-sys-env.
  7. Test edge cases: Nil values, empty strings, large numbers, etc.
  8. Use helper macros: Convert between Ruby and Rust types using provided helpers.

Code Example: Testing With Best Practices

#![allow(unused)]
fn main() {
use magnus::{class, eval, function, method, prelude::*, Error, Ruby, Value};
use rb_sys_test_helpers::ruby_test;
use std::cell::RefCell;

// Define a struct with interior mutability
struct Counter {
    count: i64,
}

#[magnus::wrap(class = "MyExtension::Counter")]
struct MutCounter(RefCell<Counter>);

impl MutCounter {
    fn new(initial: i64) -> Self {
        Self(RefCell::new(Counter { count: initial }))
    }

    fn count(&self) -> i64 {
        self.0.borrow().count
    }

    fn increment(&self) -> i64 {
        let mut counter = self.0.borrow_mut();
        counter.count += 1;
        counter.count
    }

    // Method that uses Ruby VM to potentially raise exceptions
    fn add_checked(ruby: &Ruby, rb_self: &Self, val: i64) -> Result<i64, Error> {
        // ✅ GOOD: Complete borrow before starting a new one
        let current = rb_self.0.borrow().count;

        if let Some(sum) = current.checked_add(val) {
            rb_self.0.borrow_mut().count = sum;
            Ok(sum)
        } else {
            Err(Error::new(
                ruby.exception_range_error(),
                "Addition would overflow"
            ))
        }
    }
}

// Comprehensive test suite following best practices
#[cfg(test)]
mod tests {
    use super::*;

    // ✅ GOOD: Basic functionality test
    #[ruby_test]
    fn test_counter_basic() {
        let counter = MutCounter::new(0);
        assert_eq!(counter.count(), 0);
        assert_eq!(counter.increment(), 1);
        assert_eq!(counter.increment(), 2);
    }

    // ✅ GOOD: Test with Ruby exceptions
    #[ruby_test]
    fn test_counter_overflow() {
        let ruby = Ruby::get().unwrap();
        let counter = MutCounter::new(i64::MAX);

        // Test method that might raise Ruby exception
        let result = MutCounter::add_checked(&ruby, &counter, 1);
        assert!(result.is_err());

        // ✅ GOOD: Check specific exception type
        let err = result.unwrap_err();
        assert!(err.is_kind_of(ruby, ruby.exception_range_error()));
    }

    // ✅ GOOD: GC stress testing to catch memory issues
    #[ruby_test(gc_stress)]
    fn test_with_gc_stress() {
        let ruby = Ruby::get().unwrap();
        let counter = MutCounter::new(0);

        // Register Ruby class for testing from Ruby
        let class = ruby.define_class("Counter", ruby.class_object()).unwrap();
        class.define_singleton_method("new", function!(MutCounter::new, 1)).unwrap();
        class.define_method("increment", method!(MutCounter::increment, 0)).unwrap();

        // Access from Ruby (with GC stress active)
        let result: i64 = ruby.eval(
            "counter = Counter.new(5); counter.increment; counter.increment"
        ).unwrap();

        assert_eq!(result, 7);
    }

    // ✅ GOOD: Version-specific tests
    #[ruby_test]
    fn test_version_specific() {
        #[cfg(ruby_gte_3_0)]
        {
            // Test Ruby 3.0+ specific features
        }

        #[cfg(all(ruby_gte_2_7, ruby_lt_3_0))]
        {
            // Test Ruby 2.7 specific features
        }
    }
}
}

This example illustrates proper handling of RefCell borrowing, Ruby exceptions, GC stress testing, and version-specific tests.

Example: Complete Test Module

Here's a complete end-to-end example based on the rusty_calculator extension. This includes the project structure, required files, and comprehensive test module:

Project Setup

First, ensure your project has the correct file structure:

my_extension/
├── Cargo.toml
├── build.rs
├── src/
│   └── lib.rs

Cargo.toml

[package]
name = "my_extension"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
magnus = "0.6"
rb-sys = "0.9"

[dev-dependencies]
rb-sys-env = "0.1"
rb-sys-test-helpers = "0.2"

build.rs

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // Activate rb-sys-env to set up Ruby environment for both builds and tests
    let _ = rb_sys_env::activate()?;

    Ok(())
}

lib.rs

This example includes a calculator class with a method that can potentially raise a Ruby exception:

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use magnus::{function, method, prelude::*, wrap, Error, Ruby};

// Calculator struct with memory
struct Calculator {
    memory: f64,
}

#[wrap(class = "MyExtension::Calculator")]
struct MutCalculator(RefCell<Calculator>);

impl MutCalculator {
    // Constructor
    fn new() -> Self {
        Self(RefCell::new(Calculator { memory: 0.0 }))
    }

    // Basic arithmetic that returns a Result which can generate Ruby exceptions
    fn divide(ruby: &Ruby, _rb_self: &Self, a: f64, b: f64) -> Result<f64, Error> {
        if b == 0.0 {
            return Err(Error::new(
                ruby.exception_zero_div_error(),
                "Division by zero"
            ));
        }
        Ok(a / b)
    }

    // Regular instance method
    fn add(&self, a: f64, b: f64) -> f64 {
        a + b
    }

    // Memory operations using RefCell
    fn store(&self, value: f64) -> f64 {
        self.0.borrow_mut().memory = value;
        value
    }

    fn recall(&self) -> f64 {
        self.0.borrow().memory
    }
}

// Module initialization
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("MyExtension")?;

    // Set up the Calculator class
    let calc_class = module.define_class("Calculator", ruby.class_object())?;
    calc_class.define_singleton_method("new", function!(MutCalculator::new, 0))?;
    calc_class.define_method("divide", method!(MutCalculator::divide, 2))?;
    calc_class.define_method("add", method!(MutCalculator::add, 2))?;
    calc_class.define_method("store", method!(MutCalculator::store, 1))?;
    calc_class.define_method("recall", method!(MutCalculator::recall, 0))?;

    Ok(())
}

// Complete test module
#[cfg(test)]
mod tests {
    use super::*;
    use rb_sys_test_helpers::ruby_test;

    // Basic functionality test
    #[ruby_test]
    fn test_calculator_basic_operations() {
        let calc = MutCalculator::new();

        // Test regular instance method
        assert_eq!(calc.add(2.0, 3.0), 5.0);

        // Test memory operations
        assert_eq!(calc.store(42.0), 42.0);
        assert_eq!(calc.recall(), 42.0);
    }

    // Test method that raises Ruby exceptions
    #[ruby_test]
    fn test_calculator_divide() {
        let ruby = Ruby::get().unwrap();
        let calc = MutCalculator::new();

        // Test normal division - note the function syntax for methods
        // that take ruby and rb_self parameters
        let result = MutCalculator::divide(&ruby, &calc, 10.0, 2.0);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 5.0);

        // Test division by zero
        let result = MutCalculator::divide(&ruby, &calc, 10.0, 0.0);
        assert!(result.is_err());

        // Verify specific exception type
        let err = result.unwrap_err();
        assert!(err.is_kind_of(ruby, ruby.exception_zero_div_error()));
        assert!(err.message().unwrap().contains("Division by zero"));
    }

    // Test with GC stress for memory issues
    #[ruby_test(gc_stress)]
    fn test_calculator_with_gc_stress() {
        let calc = MutCalculator::new();

        // Store and recall with GC stress active
        for i in 0..100 {
            calc.store(i as f64);
            assert_eq!(calc.recall(), i as f64);
        }

        // No segfaults or panics means test passed
    }

    // Test for Ruby integration using eval
    #[ruby_test]
    fn test_ruby_integration() {
        let ruby = Ruby::get().unwrap();

        // Define the calculator class - this simulates what init() does
        let module = ruby.define_module("MyExtension").unwrap();
        let calc_class = module.define_class("Calculator", ruby.class_object()).unwrap();
        calc_class.define_singleton_method("new", function!(MutCalculator::new, 0)).unwrap();
        calc_class.define_method("add", method!(MutCalculator::add, 2)).unwrap();

        // Call methods via Ruby's eval
        let result: f64 = ruby.eval("MyExtension::Calculator.new.add(2, 3)").unwrap();
        assert_eq!(result, 5.0);
    }
}
}

This complete example demonstrates:

  1. Proper project setup with required dependencies
  2. A realistic implementation with potential error conditions
  3. Testing various method types (regular instance methods and methods with Ruby state)
  4. Testing Ruby exceptions with proper type checking
  5. Memory safety testing with GC stress
  6. Ruby integration testing via eval

You can adapt this template to your own extension, adding the specific functionality your project requires.


## Integration Testing Ruby API

<div class="tip">

Integration tests verify that your extension works correctly when called from Ruby code. Testing both in Rust and Ruby provides the most complete coverage.

</div>

Integration tests verify that your Ruby extension's API works correctly when called from Ruby code. These tests are typically written in Ruby and run using Ruby's test frameworks.

### Setting Up Ruby Tests

Most Ruby gems use Minitest or RSpec for testing. Here's how to set up integration tests with Minitest (which bundler creates by default):

```ruby,hidelines=#
# test/test_my_extension.rb
require "test_helper"

class TestMyExtension < Minitest::Test
  def setup
    # Set up test fixtures
    @calculator = MyExtension::Calculator.new
  end

  def test_basic_addition
    assert_equal 5, @calculator.add(2, 3)
  end

  def test_division_by_zero
    error = assert_raises(ZeroDivisionError) do
      @calculator.divide(10, 0)
    end
    assert_match /division by zero/i, error.message
  end

  def test_nil_handling
    # Test that nil values are properly handled
    assert_nil @calculator.process(nil)
  end

# # Test memory management
# def test_gc_safety
#   # Create many objects and force garbage collection
#   1000.times do |i|
#     obj = MyExtension::Calculator.new
#     obj.add(i, i)
#
#     # Force garbage collection periodically
#     GC.start if i % 100 == 0
#   end
#
#   # If we reach here without segfaults, the test passes
#   assert true
# end
#
# # Test edge cases
# def test_edge_cases
#   # Test with extreme values
#   max = (2**60)
#   assert_equal max * 2, @calculator.multiply(max, 2)
#
#   # Test with different types
#   assert_raises(TypeError) do
#     @calculator.add("string", 1)
#   end
# end
end

Click the eye icon () to see additional tests for memory management and edge cases.

Testing Error Handling

It's particularly important to test how your extension handles error conditions:

def test_error_propagation
  # Test that Rust errors properly convert to Ruby exceptions
  error = assert_raises(RangeError) do
    @calculator.factorial(100) # Too large, should raise RangeError
  end
  assert_match /too large/i, error.message
end

def test_invalid_arguments
  # Test type validation
  error = assert_raises(TypeError) do
    @calculator.add("string", 3) # Should raise TypeError
  end
  assert_match /expected numeric/i, error.message
end

Testing Memory Management

Test memory management by creating objects and forcing garbage collection:

def test_gc_safety
  # Create many objects and force garbage collection
  1000.times do |i|
    obj = MyExtension::Point.new(i, i)

    # Force garbage collection periodically
    GC.start if i % 100 == 0
  end

  # If we reach here without segfaults or leaks, the test passes
  assert true
end

def test_object_references
  # Test that nested objects maintain correct references
  parent = MyExtension::Node.new("parent")
  child = MyExtension::Node.new("child")

  # Create relationship
  parent.add_child(child)

  # Force garbage collection
  GC.start

  # Both objects should still be valid
  assert_equal "parent", parent.name
  assert_equal "child", parent.children.first.name
end

Common Testing Patterns

When testing Ruby extensions written in Rust, several patterns emerge that can help ensure correctness and stability.

Testing Type Conversions

Type conversions between Rust and Ruby are common sources of bugs:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_type_conversions() {
    let ruby = Ruby::get().unwrap();

    // Test Ruby to Rust conversions
    let rb_str = RString::new(ruby, "test");
    let rb_int = Integer::from_i64(42);
    let rb_array = RArray::from_iter(ruby, vec![1, 2, 3]);

    // Convert to Rust types
    let rust_str: String = rb_str.to_string().unwrap();
    let rust_int: i64 = rb_int.to_i64().unwrap();
    let rust_vec: Vec<i64> = rb_array.to_vec().unwrap();

    // Verify conversions
    assert_eq!(rust_str, "test");
    assert_eq!(rust_int, 42);
    assert_eq!(rust_vec, vec![1, 2, 3]);

    // Test Rust to Ruby conversions
    let rust_str = "reverse";
    let rb_str = RString::new(ruby, rust_str);
    assert_eq!(rb_str.to_string().unwrap(), rust_str);
}
}

Method Invocation Syntax in Tests

When testing Rust methods exposed to Ruby, it's important to understand the different invocation patterns based on the method's signature:

Regular Instance Methods

For methods that only take &self and don't interact with the Ruby VM:

#![allow(unused)]
fn main() {
// Method definition
fn count(&self) -> isize {
    self.0.borrow().count
}

// In tests - use instance method syntax
#[ruby_test]
fn test_count() {
    let counter = MutCounter::new(0);
    assert_eq!(counter.count(), 0);
}
}

Methods with Ruby State

For methods that require the Ruby interpreter (to raise exceptions or interact with Ruby objects):

#![allow(unused)]
fn main() {
// Method definition
fn divide(ruby: &Ruby, _rb_self: &Self, a: f64, b: f64) -> Result<f64, Error> {
    if b == 0.0 {
        return Err(Error::new(
            ruby.exception_zero_div_error(),
            "Division by zero"
        ));
    }
    Ok(a / b)
}

// In tests - use associated function syntax with explicit self parameter
#[ruby_test]
fn test_divide() {
    let ruby = Ruby::get().unwrap();
    let calc = MutCalculator::new();

    // CORRECT: Associated function syntax with all parameters
    let result = MutCalculator::divide(&ruby, &calc, 6.0, 2.0);
    assert!(result.is_ok());
    assert_eq!(result.unwrap(), 3.0);

    // INCORRECT: This will not compile
    // let result = calc.divide(&ruby, 6.0, 2.0);
}
}

The key difference is that when a method takes rb_self: &Self as a parameter (as many methods do that interact with Ruby), it's not a true instance method from Rust's perspective. In tests, you must call these using the associated function syntax, passing in the Ruby interpreter and the self reference explicitly.

Testing RefCell Borrowing

For extensions that use RefCell for interior mutability, test these patterns thoroughly:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_refcell_borrowing() {
    let ruby = Ruby::get().unwrap();
    let counter = MutCounter::new(0);

    // Test regular instance methods
    assert_eq!(counter.count(), 0);
    assert_eq!(counter.increment(), 1);
    assert_eq!(counter.increment(), 2);

    // Test methods that use checked operations with the Ruby VM
    // Note the use of associated function syntax here
    let result = MutCounter::add_checked(&ruby, &counter, 10);
    assert!(result.is_ok());
    assert_eq!(result.unwrap(), 13);
    assert_eq!(counter.count(), 13);
}
}

GC Stress Testing

Testing with Ruby's garbage collector is essential to ensure your extension doesn't leak memory or access deallocated objects. The #[ruby_test(gc_stress)] option helps identify these issues early by running the garbage collector more frequently.

Basic GC Stress Testing

#![allow(unused)]
fn main() {
#[ruby_test(gc_stress)]
fn test_gc_integration() {
    let ruby = Ruby::get().unwrap();

    // Create objects that should be properly managed
    for i in 0..100 {
        let obj = SomeObject::new(i);
        // obj goes out of scope here, should be collected
    }

    // Force garbage collection explicitly
    ruby.gc_start();

    // No panics or segfaults means the test passes
}
}

Testing with TypedData and Mark Methods

For custom classes that hold Ruby object references, test the mark method implementation:

#![allow(unused)]
fn main() {
use magnus::{gc::Marker, TypedData, DataTypeFunctions, Value};

// A struct that holds references to Ruby objects
#[derive(TypedData)]
#[magnus(class = "MyExtension::Container", free_immediately, mark)]
struct Container {
    item: Value,
    metadata: Value,
}

impl DataTypeFunctions for Container {
    fn mark(&self, marker: &Marker) {
        marker.mark(self.item);
        marker.mark(self.metadata);
    }
}

impl Container {
    fn new(item: Value, metadata: Value) -> Self {
        Self { item, metadata }
    }

    fn item(&self) -> Value {
        self.item
    }
}

// Test with GC stress
#[ruby_test(gc_stress)]
fn test_container_mark_method() {
    let ruby = Ruby::get().unwrap();

    // Create Ruby strings
    let item = RString::new(ruby, "Test Item");
    let metadata = RString::new(ruby, "Item Description");

    // Create our container
    let container = Container::new(item.as_value(), metadata.as_value());

    // Force garbage collection
    ruby.gc_start();

    // The items should still be accessible and not garbage collected
    let retrieved_item = container.item();
    let item_str: String = RString::from_value(retrieved_item).unwrap().to_string().unwrap();

    assert_eq!(item_str, "Test Item");
}
}

Testing Object References After GC

This test ensures objects referenced by your extension aren't prematurely collected:

#![allow(unused)]
fn main() {
#[ruby_test(gc_stress)]
fn test_object_references_survive_gc() {
    let ruby = Ruby::get().unwrap();

    // Create a struct holding references to other objects
    #[derive(TypedData)]
    #[magnus(class = "Node", free_immediately, mark)]
    struct Node {
        value: Value,
        children: Vec<Value>,
    }

    impl DataTypeFunctions for Node {
        fn mark(&self, marker: &Marker) {
            marker.mark(self.value);
            for child in &self.children {
                marker.mark(*child);
            }
        }
    }

    impl Node {
        fn new(value: Value) -> Self {
            Self { value, children: Vec::new() }
        }

        fn add_child(&mut self, child: Value) {
            self.children.push(child);
        }

        fn child_values(&self, ruby: &Ruby) -> Result<Vec<String>, Error> {
            let mut result = Vec::new();
            for child in &self.children {
                let str = RString::from_value(*child)?;
                result.push(str.to_string()?);
            }
            Ok(result)
        }
    }

    // Create the parent node
    let parent_value = RString::new(ruby, "Parent");
    let mut parent = Node::new(parent_value.as_value());

    // Add many child nodes
    for i in 0..20 {
        let child = RString::new(ruby, format!("Child {}", i));
        parent.add_child(child.as_value());
    }

    // Run garbage collection multiple times
    for _ in 0..5 {
        ruby.gc_start();
    }

    // Verify all children are still accessible
    let child_values = parent.child_values(ruby).unwrap();
    assert_eq!(child_values.len(), 20);
    assert_eq!(child_values[0], "Child 0");
    assert_eq!(child_values[19], "Child 19");
}
}

Testing Memory Safety with Raw Pointers

If your extension uses raw C API functions, test with gc_stress and use rb_gc_guard!:

#![allow(unused)]
fn main() {
use rb_sys::*;

#[ruby_test(gc_stress)]
fn test_raw_pointer_safety() {
    unsafe {
        // Create Ruby values
        let rb_ary = rb_ary_new();

        // IMPORTANT: Protect from GC
        let rb_ary = rb_gc_guard!(rb_ary);

        // Add items to the array
        for i in 0..10 {
            let rb_str = rb_utf8_str_new_cstr(format!("item {}\0", i).as_ptr() as _);

            // IMPORTANT: Protect each string from GC
            let rb_str = rb_gc_guard!(rb_str);

            rb_ary_push(rb_ary, rb_str);
        }

        // Force GC
        rb_gc();

        // Array should still have 10 elements
        assert_eq!(rb_ary_len(rb_ary), 10);
    }
}
}

Test Helpers and Utilities

rb-sys-test-helpers provides various utilities to make testing easier.

Value Conversion Helpers

These macros help with common conversions when testing:

#![allow(unused)]
fn main() {
use rb_sys_test_helpers::{rstring_to_string, rarray_to_vec};

#[ruby_test]
fn test_with_conversion_helpers() {
    unsafe {
        // Create Ruby objects
        let rb_str = rb_utf8_str_new_cstr("hello\0".as_ptr() as _);
        let rb_ary = rb_ary_new();
        rb_ary_push(rb_ary, rb_utf8_str_new_cstr("one\0".as_ptr() as _));
        rb_ary_push(rb_ary, rb_utf8_str_new_cstr("two\0".as_ptr() as _));

        // Convert to Rust using helpers
        let rust_str = rstring_to_string!(rb_str);
        let rust_vec = rarray_to_vec!(rb_ary, String);

        // Verify conversions
        assert_eq!(rust_str, "hello");
        assert_eq!(rust_vec, vec!["one".to_string(), "two".to_string()]);
    }
}
}

Exception Handling Helpers

The protect function simplifies handling Ruby exceptions:

#![allow(unused)]
fn main() {
use rb_sys_test_helpers::protect;

#[ruby_test]
fn test_exception_handling() {
    // Try an operation that might raise an exception
    let result = unsafe {
        protect(|| {
            // Ruby operation that might raise
            rb_sys::rb_funcall(
                rb_sys::rb_cObject,
                rb_sys::rb_intern("nonexistent_method\0".as_ptr() as _),
                0
            )
        })
    };

    // Verify we got an exception
    assert!(result.is_err());
    let error = result.unwrap_err();
    assert!(error.message().unwrap().contains("undefined method"));
}
}

CI Testing Workflow

CI testing is essential for extensions that will be distributed as gems. Without it, you risk publishing binaries that crash on specific Ruby versions or platforms.

Setting up continuous integration (CI) testing is crucial for Ruby extension gems. This section covers best practices for testing your extensions in CI environments.

Basic GitHub Actions Setup

A simple GitHub Actions workflow for a Rust Ruby extension typically includes:

  1. Setting up Ruby and Rust environments
  2. Running compilation
  3. Executing tests
  4. Linting the code
 .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
        ruby: ['3.0', '3.1', '3.2']

    steps:
    - uses: actions/checkout@v3

     Use the setup-ruby-and-rust action from oxidize-rb
    - name: Set up Ruby and Rust
      uses: oxidize-rb/actions/setup-ruby-and-rust@v1
      with:
        ruby-version: ${{ matrix.ruby }}
        bundler-cache: true
        cargo-cache: true

     Run tests
    - name: Compile and test
      run: |
        bundle exec rake compile
        bundle exec rake test

     Run Rust tests
    - name: Run Rust tests
      run: cargo test --workspace

 # Windows testing job
 windows:
   runs-on: windows-latest
   strategy:
     matrix:
       ruby: ['3.1']
   steps:
   - uses: actions/checkout@v3
   - name: Set up Ruby and Rust (Windows)
     uses: oxidize-rb/actions/setup-ruby-and-rust@v1
     with:
       ruby-version: ${{ matrix.ruby }}
       bundler-cache: true
       cargo-cache: true
   - name: Run tests
     run: |
       bundle exec rake compile
       bundle exec rake test

Click the eye icon () to see a Windows-specific job configuration.

The oxidize-rb/actions repository provides specialized GitHub Actions for Ruby extensions written in Rust, making setup much simpler.

Memory Testing with ruby_memcheck

Memory leaks can be particularly difficult to detect in Ruby extensions. Tools like ruby_memcheck help catch these issues early.

The ruby_memcheck gem provides a powerful way to detect memory leaks in Ruby extensions. It uses Valgrind under the hood but filters out false positives that are common when running Valgrind on Ruby code.

To use ruby_memcheck, add it to your test workflow:

 Add to your Gemfile
gem 'ruby_memcheck', group: :development

 In your Rakefile:
require 'ruby_memcheck'

test_config = lambda do |t|
  t.libs << "test"
  t.test_files = FileList["test/**/*_test.rb"]
end

namespace :test do
  RubyMemcheck::TestTask.new(valgrind: test_config)
end

 # Advanced configuration
 RubyMemcheck.config do |config|
   # Adjust valgrind options
   config.valgrind_options += ["--leak-check=full", "--show-leak-kinds=all"]

   # Specify custom suppression files
   config.valgrind_suppression_files << "my_suppressions.supp"

   # Skip specific Ruby functions
   config.skipped_ruby_functions << /my_custom_allocator/
 end

To run memory tests:

# Install valgrind first if needed
# sudo apt-get install valgrind  # On Debian/Ubuntu

# Run the tests with memory checking
bundle exec rake test:valgrind

Click the eye icon () to see advanced configuration options for ruby_memcheck.

For more detailed instructions and configuration options, refer to the ruby_memcheck documentation.

Cross-Platform Testing with rb-sys-dock

For testing across different platforms, rb-sys-dock provides Docker images pre-configured for cross-platform compilation and testing of Rust Ruby extensions.

Best Practices for CI Testing

Without thorough CI testing across all supported platforms and Ruby versions, your extension may work perfectly in your development environment but crash for users with different setups.

  1. Test Matrix: Test against multiple Ruby versions, Rust versions, and platforms
  2. Memory Testing: Include memory leak detection with ruby_memcheck
  3. Linting: Validate code formatting and catch Rust warnings
  4. Cross-Platform: Test on all platforms you aim to support
  5. Documentation Verification: Test code examples in documentation

The oxidize-rb/actions repository provides ready-to-use GitHub Actions for:

  • Setting up Ruby and Rust environments
  • Building native gems
  • Cross-compiling for multiple platforms
  • Running tests and linting checks

Using these specialized actions will save you time and ensure your tests follow best practices.

Debugging & Troubleshooting

This chapter covers techniques for debugging Rust-based Ruby extensions, common error patterns, and approaches to solving the most frequent issues.

Overview

To debug Rust extensions, you can use either LLDB or GDB. First, you will need to compile with the dev Cargo profile, so debug symbols are available.

To do that you can run: RB_SYS_CARGO_PROFILE=dev rake compile. Alternatively, you can add a helper Rake task to make this easier:

# Rakefile

desc "Compile the extension with debug symbols"
task "compile:debug" do
  ENV["RB_SYS_CARGO_PROFILE"] = "dev"
  Rake::Task["compile"].invoke
end

💡 Tip: Join the Slack channel to ask questions and get help from the community!

Common Errors and Solutions

Compilation Errors

Missing Ruby Headers

Error:

fatal error: ruby.h: No such file or directory
#include <ruby.h>
         ^~~~~~~~
compilation terminated.

Solution:

  • Ensure Ruby development headers are installed
  • Check that rb_sys_env::activate() is being called in your build.rs
  • Verify that your Ruby installation is accessible to your build environment

Incompatible Ruby Version

Error:

error: failed to run custom build command for `rb-sys v0.9.78`

With details mentioning Ruby version compatibility issues.

Solution:

  • Ensure your rb-sys version is compatible with your Ruby version
  • Update rb-sys to the latest version
  • Check your build environment's Ruby version with ruby -v

Linking Errors

Error:

error: linking with `cc` failed: exit status: 1
... undefined reference to `rb_define_module` ...

Solution:

  • Ensure proper linking configuration in build.rs
  • Make sure you've called rb_sys_env::activate()
  • Verify that your Ruby installation is correctly detected

Runtime Errors

Segmentation Faults

Segmentation faults typically occur when accessing memory improperly:

Common Causes:

  1. Accessing Ruby objects after they've been garbage collected
  2. Not protecting Ruby values from garbage collection during C API calls
  3. Incorrect use of raw pointers

Solutions:

  • Use TypedData and implement the mark method to protect Ruby objects
  • Use rb_gc_guard! macro when working with raw C API
  • Prefer the higher-level Magnus API over raw rb-sys

Already Borrowed: BorrowMutError

When using RefCell for interior mutability:

Error:

thread '<unnamed>' panicked at 'already borrowed: BorrowMutError', ...

Solution:

  • Complete all immutable borrows before attempting mutable borrows
  • Copy required data out of immutable borrows before borrowing mutably
  • See the RefCell and Interior Mutability section in the Memory Management chapter

Method Argument Mismatch

Error:

ArgumentError: wrong number of arguments (given 2, expected 1)

Solution:

  • Check method definitions in your Rust code
  • Ensure function! and method! macros have the correct arity
  • Verify Ruby method calls match the defined signatures

Type Conversion Failures

Error:

TypeError: no implicit conversion of Integer into String

Solution:

  • Add proper type checking and conversions in Rust
  • Use try_convert and handle conversion errors gracefully
  • Add explicit type annotations to clarify intent

Debugging Techniques

Using Backtraces

Ruby's built-in backtraces can help identify where problems originate:

begin
  # Code that might raise an exception
  MyExtension.problematic_method
rescue => e
  puts e.message
  puts e.backtrace
end

You can enhance backtraces with the backtrace gem:

require 'backtrace'
Backtrace.enable_ruby_source_inspect!

begin
  MyExtension.problematic_method
rescue => e
  puts Backtrace.for(e)
end

VSCode + LLDB

The code-lldb extension for VSCode is a great way to debug Rust code. Here is an example configuration file:

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "name": "Debug",
      "preLaunchTask": {
        "task": "compile:debug",
        "type": "rake"
      },
      "program": "~/.asdf/installs/ruby/3.1.1/bin/ruby",
      "args": ["-Ilib", "test/test_helper.rb"],
      "cwd": "${workspaceFolder}",
      "sourceLanguages": ["rust"]
    }
  ]
}

Debugging the Ruby C API

With this basic setup, you can set breakpoints and interactively debug your Rust code. However, if Ruby is not built with debug symbols, any calls into the Ruby C API become a black box. Luckily, it's straight-forward to fix this.

Compiling Ruby with debug symbols and source code

Using chruby or ruby-build
  1. First, compile Ruby like so:

    $ RUBY_CFLAGS="-Og -ggdb" ruby-build --keep 3.1.2 /opt/rubies/3.1.2-debug
    
  2. Make sure your .vscode/launch.json file is configured to use /opt/rubies/3.1.2-debug/bin/ruby.

Using rbenv
  1. First, compile Ruby like so:

    $ RUBY_CFLAGS="-Og -ggdb" rbenv install --keep 3.1.2
    
  2. Make sure your .vscode/launch.json file is configured to use $RBENV_ROOT/versions/3.1.2/bin/ruby.

LLDB from the Command Line

LLDB is an excellent tool for debugging Rust extensions from the command line:

  1. Compile with debug symbols:

    RUSTFLAGS="-g" bundle exec rake compile
    
  2. Run Ruby with LLDB:

    lldb -- ruby -I lib -e 'require "my_extension"; MyExtension.method_to_debug'
    
  3. Set breakpoints and run:

    (lldb) breakpoint set --name rb_my_method
    (lldb) run
    
  4. Common LLDB commands:

    • bt - Display backtrace
    • frame variable - Show local variables
    • p expression - Evaluate expression
    • n - Step over
    • s - Step into
    • c - Continue

GDB for Linux

GDB offers similar capabilities to LLDB on Linux systems:

  1. Compile with debug symbols:

    RUSTFLAGS="-g" bundle exec rake compile
    
  2. Run Ruby with GDB:

    gdb --args ruby -I lib -e 'require "my_extension"; MyExtension.method_to_debug'
    
  3. Set breakpoints and run:

    (gdb) break rb_my_method
    (gdb) run
    
  4. Common GDB commands:

    • bt - Display backtrace
    • info locals - Show local variables
    • p expression - Evaluate expression
    • n - Step over
    • s - Step into
    • c - Continue

Rust Debugging Statements

Strategic use of Rust's debug facilities can help identify issues:

#![allow(unused)]
fn main() {
// Debug prints only included in debug builds
#[cfg(debug_assertions)]
println!("Debug: counter value = {}", counter);

// More structured logging
use log::{debug, error, info};

fn some_function() -> Result<(), Error> {
    debug!("Entering some_function");

    if let Err(e) = fallible_operation() {
        error!("Operation failed: {}", e);
        return Err(e.into());
    }

    info!("Operation succeeded");
    Ok(())
}
}

To enable logging output, add a logger like env_logger:

#![allow(unused)]
fn main() {
fn init(ruby: &Ruby) -> Result<(), Error> {
    env_logger::init();
    // Rest of initialization...
    Ok(())
}
}

And set the log level when running Ruby:

RUST_LOG=debug ruby -I lib -e 'require "my_extension"'

Memory Leak Detection

Using ruby_memcheck

The ruby_memcheck gem helps identify memory leaks in Ruby extensions by filtering out Ruby's internal memory management noise when running Valgrind.

  1. Install dependencies:

    gem install ruby_memcheck
    # On Debian/Ubuntu
    apt-get install valgrind
    
  2. Set up in your Rakefile:

    require 'ruby_memcheck'
    
    test_config = lambda do |t|
      t.libs << "test"
      t.test_files = FileList["test/**/*_test.rb"]
    end
    
    namespace :test do
      RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
    end
    
  3. Run memory leak detection:

    bundle exec rake test:valgrind
    

For more detailed instructions and configuration options, refer to the ruby_memcheck documentation.

Best Practices

  1. Add Meaningful Error Messages: Make your error messages descriptive and helpful
  2. Test Edge Cases: Thoroughly test edge cases like nil values, empty strings, etc.
  3. Maintain a Test Suite: Comprehensive tests catch issues early
  4. Use Memory Safety Features: Leverage Rust's safety features rather than bypassing them
  5. Provide Debugging Symbols: Always include debug symbol builds for better debugging
  6. Document Troubleshooting: Add a troubleshooting section to your extension's documentation
  7. Log Appropriately: Include contextual information in log messages

Next Steps

  • Build your extension with RB_SYS_CARGO_PROFILE=dev and practice setting breakpoints.
  • Explore GDB as an alternative to LLDB for low-level debugging.
  • See the Memory Management & Safety chapter for GC-related troubleshooting.
  • If you're still stuck, join the Slack channel to ask questions and get help from the community!

Troubleshooting Guide

This chapter provides solutions to common issues encountered when developing Ruby extensions with rb-sys and magnus.

Getting Started Issues

Installation Problems

Missing libclang

Problem: Build fails with errors related to missing libclang:

error: failed to run custom build command for `bindgen v0.64.0`
...
could not find libclang: "couldn't find any valid shared libraries matching: ['libclang.so', ...]"

Solutions:

  1. Add libclang to your Gemfile:

    gem "libclang", "~> 14.0"
    
  2. On Linux, install libclang through your package manager:

    # Debian/Ubuntu
    apt-get install libclang-dev
    
    # Fedora/RHEL
    dnf install clang-devel
    
  3. On macOS:

    brew install llvm
    export LLVM_CONFIG=$(brew --prefix llvm)/bin/llvm-config
    

Ruby Headers Not Found

Problem: Build fails with error about missing Ruby headers:

error: failed to run custom build command for `rb-sys v0.9.78`
...
fatal error: ruby.h: No such file or directory

Solutions:

  1. Ensure you have Ruby development headers installed:

    # Debian/Ubuntu
    apt-get install ruby-dev
    
    # Fedora/RHEL
    dnf install ruby-devel
    
  2. If using rbenv/rvm/asdf, make sure you've installed the development headers:

    # For rbenv
    RUBY_CONFIGURE_OPTS=--enable-shared rbenv install 3.0.0
    
    # For rvm
    rvm install 3.0.0 --with-openssl-dir=$(brew --prefix openssl)
    
  3. Check your build.rs file includes:

    #![allow(unused)]
    fn main() {
    let _ = rb_sys_env::activate()?;
    }

Compilation Issues

Cargo Build Errors

Version Compatibility

Problem: Build fails with version incompatibility errors:

error: failed to select a version for the requirement `rb-sys = "^0.9.80"`

Solution:

  1. Check your magnus and rb-sys versions are compatible:

    # Cargo.toml
    [dependencies]
    magnus = "0.7"     # Check latest compatible version
    rb-sys = "0.9.80"  # Check latest version
    
  2. Update rb-sys in your Gemfile:

    gem 'rb_sys', '~> 0.9.80'
    

Linking Issues

Problem: Build fails with undefined references:

error: linking with `cc` failed: exit status: 1
...
undefined reference to `rb_define_module`

Solutions:

  1. Ensure your build.rs is correctly set up:

    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let _ = rb_sys_env::activate()?;
        Ok(())
    }
  2. Verify Ruby version compatibility with rb-sys version

  3. Ensure you have Ruby development headers installed

Build Script Errors

Problem: Build script execution fails:

error: failed to run custom build command for `rb-sys v0.9.78`

Solutions:

  1. Check permissions and environment variables:

    # Set necessary environment variables
    export RUBY_ROOT=$(rbenv prefix)
    export PATH=$RUBY_ROOT/bin:$PATH
    
  2. If using Docker, ensure the build environment has Ruby installed and configured

Runtime Issues

Ruby Object Management

Segmentation Faults

Problem: Ruby process crashes with a segmentation fault:

[BUG] Segmentation fault
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]

Solutions:

  1. Ensure Ruby objects are correctly protected from garbage collection:

    #![allow(unused)]
    fn main() {
    // Using Magnus (preferred)
    let obj = RObject::new(ruby, ruby.class_object())?;
    
    // With raw rb-sys
    unsafe {
        let obj = rb_sys::rb_obj_alloc(rb_sys::rb_cObject);
        rb_sys::rb_gc_register_mark_object(obj);
        // Use obj...
        rb_sys::rb_gc_unregister_mark_object(obj);
    }
    }
  2. Check all pointers are valid before dereferencing

  3. Use TypedData with proper mark implementation

BorrowMutError Panics

Problem: Your extension panics with a "already borrowed" error when using RefCell:

thread '<unnamed>' panicked at 'already borrowed: BorrowMutError'

Solutions:

  1. Fix borrow order - always complete immutable borrows before mutable borrows:

    #![allow(unused)]
    fn main() {
    // WRONG - causes BorrowMutError
    if self.0.borrow().value > 10 {
        self.0.borrow_mut().value = 0; // Error: still borrowed from the if condition
    }
    
    // RIGHT - copy values then borrow mutably
    let current = self.0.borrow().value; // Complete this borrow
    if current > 10 {
        self.0.borrow_mut().value = 0; // Now safe to borrow mutably
    }
    }
  2. Consider restructuring your data to avoid nested borrows

  3. Use separate methods for reading and writing

Ruby/Rust Type Conversion Issues

Unexpected Nil Values

Problem: Your extension crashes when encountering nil:

TypeError: no implicit conversion of nil into String

Solutions:

  1. Always check for nil before conversion:

    #![allow(unused)]
    fn main() {
    fn process_string(val: Value) -> Result<String, Error> {
        if val.is_nil() {
            // Handle nil case
            return Ok("default".to_string());
        }
    
        let string = RString::try_convert(val)?;
        Ok(string.to_string()?)
    }
    }
  2. Use Option for conversions that might return nil:

    #![allow(unused)]
    fn main() {
    let maybe_string: Option<String> = val.try_convert()?;
    match maybe_string {
        Some(s) => process_string(s),
        None => handle_nil_case(),
    }
    }

Type Errors

Problem: Function fails with type mismatch errors:

TypeError: wrong argument type Integer (expected String)

Solutions:

  1. Add explicit type checking before conversion:

    #![allow(unused)]
    fn main() {
    fn process_value(ruby: &Ruby, val: Value) -> Result<(), Error> {
        if !val.is_kind_of(ruby, ruby.class_object::<RString>()?) {
            return Err(Error::new(
                ruby.exception_type_error(),
                format!("Expected String, got {}", val.class().name())
            ));
        }
    
        let string = RString::try_convert(val)?;
        // Process string...
        Ok(())
    }
    }
  2. Use try_convert with proper error handling:

    #![allow(unused)]
    fn main() {
    match RString::try_convert(val) {
        Ok(string) => {
            // Process string...
            Ok(())
        },
        Err(_) => {
            Err(Error::new(
                ruby.exception_type_error(),
                "Expected String argument"
            ))
        }
    }
    }

Memory and Performance Issues

Memory Leaks

Problem: Your extension gradually consumes more memory over time.

Solutions:

  1. Ensure Ruby objects are properly released when using raw rb-sys:

    #![allow(unused)]
    fn main() {
    unsafe {
        // Register the object to protect it from GC
        rb_sys::rb_gc_register_mark_object(obj);
    
        // Use the object...
    
        // Unregister when done (important!)
        rb_sys::rb_gc_unregister_mark_object(obj);
    }
    }
  2. Implement proper mark methods for TypedData:

    #![allow(unused)]
    fn main() {
    #[derive(TypedData)]
    #[magnus(class = "MyClass", free_immediately, mark)]
    struct MyObject {
        references: Vec<Value>,
    }
    
    impl DataTypeFunctions for MyObject {
        fn mark(&self, marker: &Marker) {
            for reference in &self.references {
                marker.mark(*reference);
            }
        }
    }
    }
  3. Use ruby_memcheck to detect leaks (see Debugging chapter)

Global VM Lock (GVL) Issues

Problem: CPU-intensive operations block the Ruby VM.

Solutions:

  1. Release the GVL during CPU-intensive work:

    #![allow(unused)]
    fn main() {
    use rb_sys::rb_thread_call_without_gvl;
    use std::{ffi::c_void, ptr::null_mut};
    
    pub fn nogvl<F, R>(func: F) -> R
    where
        F: FnOnce() -> R,
        R: Send + 'static,
    {
        struct CallbackData<F, R> {
            func: Option<F>,
            result: Option<R>,
        }
    
        extern "C" fn callback<F, R>(data: *mut c_void) -> *mut c_void
        where
            F: FnOnce() -> R,
            R: Send + 'static,
        {
            let data = unsafe { &mut *(data as *mut CallbackData<F, R>) };
            if let Some(func) = data.func.take() {
                data.result = Some(func());
            }
            null_mut()
        }
    
        let mut data = CallbackData {
            func: Some(func),
            result: None,
        };
    
        unsafe {
            rb_thread_call_without_gvl(
                Some(callback::<F, R>),
                &mut data as *mut _ as *mut c_void,
                None,
                null_mut(),
            );
        }
    
        data.result.unwrap()
    }
    }
  2. Only release the GVL for operations that don't interact with Ruby objects:

    #![allow(unused)]
    fn main() {
    // Safe to run without GVL - pure computation
    let result = nogvl(|| {
        compute_intensive_function(input_data)
    });
    
    // NOT safe to run without GVL - interacts with Ruby
    // let result = nogvl(|| {
    //     ruby_object.some_method() // WRONG - will crash
    // });
    }

Cross-Platform Issues

Ruby-Head Compatibility Issues

Problem: Gems don't work with ruby-head or development versions of Ruby.

Solutions:

  1. Always publish a source gem alongside platform-specific gems:

    # In your release workflow, build both platform-specific and source gems
    # The source gem allows ruby-head users to compile against their exact version
    bundle exec rake build  # For source gem
    bundle exec rake native # For platform-specific gems
    
  2. For users, install the gem with compilation enabled:

    gem install my_gem --platform=ruby
    
  3. For gem maintainers, update your CI to test against ruby-head:

    # .github/workflows/test.yml
    strategy:
      matrix:
        ruby: ["3.1", "3.2", "3.3", "head"]
    

Platform-Specific Build Problems

Windows Issues

Problem: Build fails on Windows with linking errors.

Solutions:

  1. Ensure you have the correct toolchain installed:

    rustup target add x86_64-pc-windows-msvc
    
  2. Add platform-specific configuration in Cargo.toml:

    [target.'cfg(windows)'.dependencies]
    winapi = { version = "0.3", features = ["everything"] }
    
  3. Use conditional compilation for platform-specific code:

    #![allow(unused)]
    fn main() {
    #[cfg(windows)]
    fn platform_specific() {
        // Windows-specific code
    }
    
    #[cfg(unix)]
    fn platform_specific() {
        // Unix-specific code
    }
    }

macOS Issues

Problem: Build fails on macOS with architecture or linking issues.

Solutions:

  1. Specify architecture when needed:

    RUBY_CONFIGURE_OPTS="--with-arch=x86_64,arm64" rbenv install 3.0.0
    
  2. Fix linking for universal binaries:

    #![allow(unused)]
    fn main() {
    // build.rs
    #[cfg(target_os = "macos")]
    {
        println!("cargo:rustc-link-arg=-arch");
        println!("cargo:rustc-link-arg=arm64");
        println!("cargo:rustc-link-arg=-arch");
        println!("cargo:rustc-link-arg=x86_64");
    }
    }

Cargo.toml Configuration Issues

Feature Flag Problems

Problem: Build fails because of conflicting or missing feature flags.

Solutions:

  1. Check for feature flag issues in dependencies:

    [dependencies]
    magnus = { version = "0.7", features = ["rb-sys"] }
    rb-sys = { version = "0.9.80", features = ["stable-api"] }
    
  2. Use debug prints in build.rs to check feature detection:

    fn main() {
        println!("cargo:warning=Ruby version: {}", rb_sys_env::ruby_major_version());
    
        #[cfg(feature = "some-feature")]
        println!("cargo:warning=some-feature is enabled");
    }

Ruby Integration Issues

Method Definition Problems

Problem: Ruby method definitions don't work as expected.

Solutions:

  1. Check method arity and macro usage:

    #![allow(unused)]
    fn main() {
    // For instance methods (that use &self)
    class.define_method("instance_method", method!(MyClass::instance_method, 1))?;
    
    // For class/module methods (no &self)
    class.define_singleton_method("class_method", function!(class_method, 1))?;
    }
  2. Verify method signatures:

    #![allow(unused)]
    fn main() {
    // Instance method
    fn instance_method(&self, arg: Value) -> Result<Value, Error> {
        // Method implementation...
    }
    
    // Class method
    fn class_method(arg: Value) -> Result<Value, Error> {
        // Method implementation...
    }
    
    // Method with ruby
    fn method_with_ruby(ruby: &Ruby, arg: Value) -> Result<Value, Error> {
        // Method implementation...
    }
    }

Module/Class Hierarchy Issues

Problem: Ruby modules or classes aren't defined correctly.

Solutions:

  1. Check the correct nesting of defines:

    #![allow(unused)]
    fn main() {
    // Define a module and a nested class
    let module = ruby.define_module("MyModule")?;
    let class = module.define_class("MyClass", ruby.class_object())?;
    
    // Define a nested module
    let nested = module.define_module("Nested")?;
    }
  2. Verify class inheritance:

    #![allow(unused)]
    fn main() {
    // Get the correct superclass
    let superclass = ruby.class_object::<RObject>()?;
    
    // Define a class with the superclass
    let class = ruby.define_class("MyClass", superclass)?;
    }

Debugging Ruby Exceptions

Custom Exception Handling

Problem: Ruby exceptions aren't properly caught or raised.

Solutions:

  1. Define and use custom exception classes:

    #![allow(unused)]
    fn main() {
    fn init(ruby: &Ruby) -> Result<(), Error> {
        let module = ruby.define_module("MyModule")?;
    
        // Define custom exceptions
        let std_error = ruby.exception_standard_error();
        let custom_error = module.define_class("CustomError", std_error)?;
    
        Ok(())
    }
    
    fn raise_custom_error(ruby: &Ruby) -> Result<(), Error> {
        Err(Error::new(
            ruby.class_path_to_value("MyModule::CustomError"),
            "Something went wrong"
        ))
    }
    }
  2. Catch specific exception types:

    #![allow(unused)]
    fn main() {
    fn handle_exceptions(ruby: &Ruby, val: Value) -> Result<Value, Error> {
        let result = val.funcall(ruby, "may_raise", ());
    
        match result {
            Ok(v) => Ok(v),
            Err(e) if e.is_kind_of(ruby, ruby.exception_zero_div_error()) => {
                // Handle division by zero
                Ok(ruby.integer_from_i64(0))
            },
            Err(e) => Err(e), // Re-raise other exceptions
        }
    }
    }

Additional Resources

rb-sys Crate Features

The rb-sys crate provides battle-tested Rust bindings for the Ruby C API. It uses the rust-bindgen crate to generate bindings from the ruby.h header.

Usage Notice

This is a very low-level library. If you are looking to write a gem in Rust, you should probably use the Magnus crate with the rb-sys-interop feature, which provides a higher-level, more ergonomic API.

If you need raw/unsafe bindings to libruby, then this crate is for you!

Writing a Ruby Gem

Ruby gems require boilerplate to be defined to be usable from Ruby. rb-sys makes this process painless by doing the work for you. Simply enable the gem feature:

[dependencies]
rb-sys = "0.9"

Under the hood, this ensures the crate does not link libruby (unless on Windows) and defines a ruby_abi_version function for Ruby 3.2+.

Embedding libruby in Your Rust App

IMPORTANT: If you are authoring a Ruby gem, you do not need to enable this feature.

If you need to link libruby (i.e., you are initializing a Ruby VM in your Rust code), you can enable the link-ruby feature:

[dependencies]
rb-sys = { version = "0.9", features = ["link-ruby"] }

Static libruby

You can also force static linking of libruby:

[dependencies]
rb-sys = { version = "0.9", features = ["ruby-static"] }

Alternatively, you can set the RUBY_STATIC=true environment variable.

Available Features

The rb-sys crate provides several features that can be enabled in your Cargo.toml:

FeatureDescription
global-allocatorReport Rust memory allocations to the Ruby GC (recommended)
ruby-staticLink the static version of libruby
link-rubyLink libruby (typically used for embedding, not for extensions)
bindgen-rbimplsInclude the Ruby impl types in bindings
bindgen-deprecated-typesInclude deprecated Ruby methods in bindings
gemSet up the crate for use in a Ruby gem (default feature)
stable-apiUse the stable API (C level) if available for your Ruby version

Example Cargo.toml

[dependencies]
rb-sys = { version = "0.9", features = ["global-allocator", "stable-api"] }

Ruby Version Compatibility

rb-sys is compatible with Ruby 2.6 and later. The crate detects the Ruby version at compile time and adapts the bindings accordingly.

For Ruby 3.2 and later, rb-sys provides a ruby_abi_version function that is required for native extensions.

Integration with Magnus

If you're building a Ruby extension, it's recommended to use the Magnus crate on top of rb-sys. Magnus provides a high-level, safe API for interacting with Ruby:

[dependencies]
magnus = { version = "0.7", features = ["rb-sys"] }
rb-sys = "0.9"

rb_sys Gem Configuration

The rb_sys gem makes it easy to build native Ruby extensions in Rust. It interoperates with existing Ruby native extension toolchains (i.e., rake-compiler) to make testing, building, and cross-compilation of gems easy.

RbSys::ExtensionTask

This gem provides a RbSys::ExtensionTask class that can be used to build a Ruby extension in Rust. It's a thin wrapper around Rake::ExtensionTask that provides sane defaults for building Rust extensions.

# Rakefile

require "rb_sys/extensiontask"

GEMSPEC = Gem::Specification.load("my_gem.gemspec")

RbSys::ExtensionTask.new("my-crate-name", GEMSPEC) do |ext|
  ext.lib_dir = "lib/my_gem"

  # If you want to use `rb-sys-dock` for cross-compilation:
  ext.cross_compile = true
end

create_rust_makefile

The gem provides a simple helper to build a Ruby-compatible Makefile for your Rust extension:

# ext/rust_reverse/extconf.rb

# We need to require mkmf *first* since `rake-compiler` injects code here for cross compilation
require "mkmf"
require "rb_sys/mkmf"

create_rust_makefile("rust_reverse") do |r|
  # Create debug builds in dev. Make sure that release gems are compiled with
  # `RB_SYS_CARGO_PROFILE=release` (optional)
  r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :dev).to_sym

  # Can be overridden with `RB_SYS_CARGO_FEATURES` env var (optional)
  r.features = ["test-feature"]

  # You can add whatever env vars you want to the env hash (optional)
  r.env = {"FOO" => "BAR"}

  # If your Cargo.toml is in a different directory, you can specify it here (optional)
  r.ext_dir = "."

  # Extra flags to pass to the $RUSTFLAGS environment variable (optional)
  r.extra_rustflags = ["--cfg=some_nested_config_var_for_crate"]

  # Force a rust toolchain to be installed via rustup (optional)
  # You can also set the env var `RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN=true`
  r.force_install_rust_toolchain = "stable"

  # Clean up the target/ dir after `gem install` to reduce bloat (optional)
  r.clean_after_install = false # default: true if invoked by rubygems

  # Auto-install Rust toolchain if not present on "gem install" (optional)
  r.auto_install_rust_toolchain = false # default: true if invoked by rubygems
end

Environment Variables

The rb_sys gem respects several environment variables that can modify its behavior:

Environment VariableDescription
RB_SYS_CARGO_PROFILESet the Cargo profile (i.e., release or dev)
RB_SYS_CARGO_FEATURESComma-separated list of Cargo features to enable
RB_SYS_FORCE_INSTALL_RUST_TOOLCHAINForce installation of a Rust toolchain
RUBY_STATICForce static linking of libruby if set to true
LIBCLANG_PATHPath to libclang if it can't be found automatically

Tips and Tricks

  • When using rake-compiler to build your gem, you can use the RB_SYS_CARGO_PROFILE environment variable to set the Cargo profile (i.e., release or dev).

  • You can pass Cargo arguments to rake-compiler like so: rake compile -- --verbose

  • It's possible to force an installation of a Rust toolchain by setting the RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN environment variable. This will install rustup and cargo in the build directory, so the end user does not have to have Rust pre-installed. Ideally, this should be a last resort, as it's better to already have the toolchain installed on your system.

Troubleshooting

Libclang Issues

If you see an error like this:

thread 'main' panicked at 'Unable to find libclang: "couldn't find any valid shared libraries matching: \['libclang.so', 'libclang-*.so', 'libclang.so.*', 'libclang-*.so.*'\], set the `LIBCLANG_PATH` environment variable to a path where one of these files can be found (invalid: \[\])"'

This means that bindgen is having issues finding a usable version of libclang. An easy way to fix this is to install the libclang gem, which will install a pre-built version of libclang for you. rb_sys will automatically detect this gem and use it.

# Gemfile
gem "libclang", "~> 14.0.6"

rb-sys-test-helpers

The rb-sys-test-helpers crate provides utilities for testing Ruby extensions from Rust. It makes it easy to run tests with a valid Ruby VM.

Usage

Add this to your Cargo.toml:

[dev-dependencies]
rb-sys-env = { version = "0.1" }
rb-sys-test-helpers = { version = "0.2" }

Then, in your crate's build.rs:

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = rb_sys_env::activate()?;

    Ok(())
}

Then, you can use the ruby_test attribute macro in your tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use rb_sys_test_helpers::ruby_test;
    use rb_sys::{rb_num2fix, rb_int2big, FIXNUM_P};

    #[ruby_test]
    fn test_something() {
        // Your test code here will have a valid Ruby VM (hint: this works with
        // the `magnus` crate, too!)
        //
        // ...

        let int = unsafe { rb_num2fix(1) };
        let big = unsafe { rb_int2big(9999999) };

        assert!(FIXNUM_P(int));
        assert!(!FIXNUM_P(big));
    }
}
}

How It Works

The ruby_test macro sets up a Ruby VM before running your test and tears it down afterward. This allows you to interact with Ruby from your Rust code during tests without having to set up the VM yourself.

The test helpers are compatible with both rb-sys for low-level C API access and magnus for higher-level Ruby interactions.

Common Testing Patterns

Testing Value Conversions

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_value_conversion() {
    use rb_sys::{rb_cObject, rb_funcall, rb_str_new_cstr, rb_iv_set};
    use std::ffi::CString;

    unsafe {
        let obj = rb_cObject;
        let name = CString::new("test").unwrap();
        let value = rb_str_new_cstr(name.as_ptr());

        rb_iv_set(obj, b"@name\0".as_ptr() as *const _, value);

        let result = rb_funcall(obj, b"instance_variable_get\0".as_ptr() as *const _, 1,
                                rb_str_new_cstr(b"@name\0".as_ptr() as *const _));

        assert_eq!(value, result);
    }
}
}

Testing with Magnus

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_with_magnus() {
    use magnus::{Ruby, RString, Value};

    let ruby = unsafe { Ruby::get().unwrap() };
    let string = RString::new(ruby, "Hello, world!").unwrap();

    assert_eq!(string.to_string().unwrap(), "Hello, world!");
}
}

Testing Multiple Ruby Versions

To test against multiple Ruby versions, you can use environment variables and CI configuration:

# .github/workflows/test.yml
jobs:
  test:
    strategy:
      matrix:
        ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
    steps:
      - uses: actions/checkout@v4
      - uses: oxidize-rb/actions/setup-ruby-and-rust@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - run: cargo test

Your tests will run against each Ruby version in the matrix, helping you ensure compatibility.

Integration with Other Test Frameworks

The ruby_test attribute works with common Rust test frameworks like proptest and quickcheck:

#![allow(unused)]
fn main() {
#[ruby_test]
fn test_with_proptest() {
    use proptest::prelude::*;

    proptest!(|(s in "[a-zA-Z0-9]*")| {
        let ruby = unsafe { Ruby::get().unwrap() };
        let ruby_string = RString::new(ruby, &s).unwrap();
        assert_eq!(ruby_string.to_string().unwrap(), s);
    });
}
}

Community and Support

The rb-sys project is maintained by the Oxidize Ruby community, a group of developers passionate about integrating Rust and Ruby. We're committed to helping you succeed with your Rust-powered Ruby extensions.

Getting Help

If you encounter issues or have questions about using rb-sys, there are several ways to get help:

Join Our Slack Community

The Oxidize Ruby community has an active Slack workspace where you can ask questions, share your projects, and get help from community members and maintainers.

GitHub Issues

If you think you've found a bug or want to request a new feature, please open an issue on GitHub:

Real-World Examples

Looking at real-world examples can be a great way to learn how to use rb-sys effectively:

Contributing to rb-sys

We welcome contributions from the community! Whether it's improving documentation, fixing bugs, or adding new features, your contributions are valuable.

See the Contributing Guide for detailed information about setting up a development environment and making contributions.

Resources

Documentation

Tools

Blog Posts and Talks

Contributing to rb-sys