Skip to main content

Development Approaches

You can build Ruby extensions with Rust using rb-sys directly for low-level control, or use a higher-level library like Magnus for safety and convenience. This page helps you choose the right approach.

Comparison

AspectDirect rb-sysMagnus (High-Level)
ControlMaximum control over Ruby VM.Abstracts away VM details.
SafetyRequires unsafe code blocks.Provides a mostly safe API.
VerbosityMore verbose; manual type conversion.Concise; automatic type conversion.
Error HandlingManual check of Ruby error state.Idiomatic Result<T, Error>.
Best ForPerformance-critical code, accessing internal APIs.Most general-purpose gems.

Direct rb-sys Usage

The rb-sys crate provides low-level bindings to Ruby's C API, offering complete control over Rust-Ruby interaction.

When to Use Direct rb-sys

  • Maximum control over Ruby VM interaction.
  • Access to low-level Ruby internals.
  • Critical performance requirements, eliminating any overhead.
  • Implementing functionality not yet covered by higher-level wrappers.

Example: Simple Extension

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;

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

unsafe extern "C" fn reverse(_: VALUE, s: VALUE) -> VALUE {
let mut s_copy = s;
let c_str = rb_string_value_cstr(&mut s_copy);
let rust_str = match std::ffi::CStr::from_ptr(c_str).to_str() {
Ok(s) => s,
Err(_) => return rb_str_new_cstr(c"".as_ptr()),
};
let reversed = rust_str.chars().rev().collect::<String>();
let c_string = match CString::new(reversed) {
Ok(s) => s,
Err(_) => return rb_str_new_cstr(c"".as_ptr()),
};
rb_str_new_cstr(c_string.as_ptr())
}

#[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(std::mem::transmute(reverse as unsafe extern "C" fn(VALUE, VALUE) -> VALUE)),
1,
);
}
}

Releasing GVL with rb_thread_call_without_gvl

For computationally intensive operations, release Ruby's Global VM Lock (GVL) to allow other threads to run.

use magnus::{Error, Ruby};
use rb_sys::rb_thread_call_without_gvl;
use std::{ffi::c_void, panic::{self, AssertUnwindSafe}, ptr::null_mut};

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>>,
}

extern "C" fn call_without_gvl<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() {
match panic::catch_unwind(AssertUnwindSafe(func)) {
Ok(result) => data.result = Some(Ok(result)),
Err(panic_info) => {
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()
}

let mut data = CallbackData { func: Some(func), result: None };

unsafe {
rb_thread_call_without_gvl(
Some(call_without_gvl::<F, R>),
&mut data as *mut _ as *mut c_void,
None,
null_mut(),
);
}

match data.result {
Some(Ok(result)) => Ok(result),
Some(Err(panic_msg)) => {
let ruby = unsafe { Ruby::get_unchecked() };
Err(Error::new(
ruby.exception_runtime_error(),
format!("Rust panic in nogvl: {}", panic_msg),
))
}
None => {
let ruby = unsafe { Ruby::get_unchecked() };
Err(Error::new(
ruby.exception_runtime_error(),
"nogvl function was not executed",
))
}
}
}

How Direct rb-sys Works

  1. Defines C-compatible functions (extern "C").
  2. Manually converts between Ruby VALUE and Rust types.
  3. Requires manual memory management and type safety.
  4. Uses #[no_mangle] for Ruby visibility.
  5. All Ruby data interactions use raw pointers and unsafe code.

Higher-level Wrappers (Magnus)

Magnus provides an ergonomic, Rust-like API built on rb-sys, handling many unsafe Ruby integration aspects.

When to Use Magnus

  • Most standard Ruby extensions.
  • Avoiding unsafe code.
  • Idiomatic Rust error handling.
  • Complex type conversions.
  • Object-oriented interaction with Ruby classes and objects.

Example: Simple Extension with Magnus

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(())
}

Example: LZ4-Flex-RB (Complex)

use magnus::{function, prelude::*, Error, Ruby, RString};

fn compress(input: RString) -> Result<RString, Error> {
Ok(input) // Placeholder
}

fn decompress(input: RString) -> Result<RString, Error> {
Ok(input) // Placeholder
}

fn compress_varint(input: RString) -> Result<RString, Error> {
Ok(input) // Placeholder
}

fn decompress_varint(input: RString) -> Result<RString, Error> {
Ok(input) // Placeholder
}

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

module.define_singleton_method("compress", function!(compress, 1))?;
module.define_singleton_method("decompress", function!(decompress, 1))?;
module.singleton_class()?.define_alias("deflate", "compress")?;
module.singleton_class()?.define_alias("inflate", "decompress")?;

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

  1. Automatic type conversions.
  2. Rust-like error handling with Result.
  3. Memory safety via RAII patterns.
  4. Ergonomic APIs for modules, classes, and methods.

Mixing Approaches

Combine Magnus for general use with direct rb-sys for performance-critical sections.

use magnus::{function, prelude::*, Error, Ruby, value::ReprValue};
use std::os::raw::c_char;
use std::ffi::CString;
use magnus::rb_sys::AsRawValue;

fn high_level() -> String { "High level".to_string() }
macro_rules! cstr { ($s:expr) => { concat!($s, "\0").as_ptr() as *const c_char }; }

unsafe extern "C" fn low_level() -> rb_sys::VALUE { // Changed signature
let c_string = match CString::new("Low level") {
Ok(s) => s,
Err(_) => return rb_sys::rb_str_new_cstr(c"".as_ptr()),
};
rb_sys::rb_str_new_cstr(c_string.as_ptr())
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("MixedExample")?;
module.define_singleton_method("high_level", function!(high_level, 0))?;
unsafe {
rb_sys::rb_define_module_function(
module.as_value().as_raw(),
cstr!("low_level"),
Some(low_level as unsafe extern "C" fn() -> rb_sys::VALUE), // Explicit cast
0,
);
}
Ok(())
}

Enabling rb-sys Feature in Magnus

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

Common Mixing Patterns

  1. Magnus for API, rb-sys for optimization: Define public API with Magnus; drop to rb-sys for hot paths (e.g., with nogvl).
  2. rb-sys for core, Magnus for conversions: Build core with rb-sys; use Magnus for complex Ruby objects.
  3. Start Magnus, optimize rb-sys later: Rapid development with Magnus; profile and optimize hot paths with direct rb-sys.

Real-World Examples

Blake3-Ruby (Direct rb-sys)

Blake3-Ruby uses direct rb-sys for maximum cryptographic hashing performance.

use rb_sys::{rb_define_module, rb_define_module_function, rb_str_new, VALUE, RSTRING_LEN, RSTRING_PTR};
use std::os::raw::c_char;

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

#[no_mangle]
pub extern "C" fn Init_digest_ext() {
unsafe {
let digest_module = rb_define_module(cstr!("Digest"));
rb_define_module_function(
digest_module,
cstr!("simple_hash"),
Some(std::mem::transmute(rb_simple_hash as unsafe extern "C" fn(rb_sys::VALUE, rb_sys::VALUE) -> rb_sys::VALUE)),
1,
);
}
}

unsafe extern "C" fn rb_simple_hash(_klass: VALUE, string: VALUE) -> VALUE {
let data_ptr = RSTRING_PTR(string) as *const u8;
let data_len = RSTRING_LEN(string) as usize;
let data_slice = std::slice::from_raw_parts(data_ptr, data_len);

let mut hash: u32 = 0;
for &byte in data_slice { hash = hash.wrapping_mul(31).wrapping_add(byte as u32); }

let hash_str = format!("{:08x}", hash);
let hash_bytes = hash_str.as_bytes();
rb_str_new(hash_bytes.as_ptr() as *const c_char, hash_bytes.len() as i64)
}

LZ4-Flex-RB (Mixed Approach)

LZ4-Flex-RB mixes Magnus with direct rb-sys calls for compression.

use magnus::{function, prelude::*, Error, Ruby, RString};
use rb_sys::{rb_str_locktmp, rb_str_unlocktmp, RSTRING_PTR, RSTRING_LEN};
use magnus::rb_sys::AsRawValue;

fn compress(input: RString) -> Result<RString, Error> { Ok(input) } // Placeholder
fn decompress(input: RString) -> Result<RString, Error> { Ok(input) } // Placeholder

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

module.define_singleton_method("compress", function!(compress, 1))?;
module.define_singleton_method("decompress", function!(decompress, 1))?;
module.singleton_class()?.define_alias("deflate", "compress")?;
module.singleton_class()?.define_alias("inflate", "decompress")?;

let varint_module = module.define_module("VarInt")?;
varint_module.define_singleton_method("compress", function!(compress, 1))?;
varint_module.define_singleton_method("decompress", function!(decompress, 1))?;

Ok(())
}

struct LockedRString(RString);

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

fn as_slice(&self) -> &[u8] {
unsafe {
let value = self.0.as_value();
let ptr = RSTRING_PTR(value.as_raw()) as *const u8;
let len = RSTRING_LEN(value.as_raw()) as usize;
std::slice::from_raw_parts(ptr, len)
}
}
}

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