Basic Patterns
This page covers the essential patterns for defining functions, converting types, and handling errors when building a Ruby extension in Rust.
Core Principles
- Use
Result<T, magnus::Error>for any function that can fail. This automatically maps Rust errors to Ruby exceptions. - Leverage Magnus's automatic type conversions for simple types (
String,i64,Vec<T>, etc.). - For complex types or performance-critical code, work directly with Ruby objects like
RString,RArray, andRHash. - Never
panic!. A panic in your Rust code will crash the entire Ruby process.
Functions & Methods
Module Functions
Define standalone functions in a Ruby module. Magnus handles the conversion from Rust types to Ruby objects.
use magnus::{function, prelude::*, Error, Ruby};
// A function that can fail.
fn divide(ruby: &Ruby, a: f64, b: f64) -> Result<f64, Error> {
if b == 0.0 {
// This becomes a `ZeroDivisionError` in Ruby.
Err(Error::new(ruby.exception_zero_div_error(), "divided by 0"))
} else {
Ok(a / b)
}
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("MathUtils")?;
module.define_singleton_method("divide", function!(divide, 2))?;
Ok(())
}
Instance Methods
Define a class in Rust and attach instance methods to it. Use #[magnus::wrap] to expose a Rust struct as a Ruby object.
use magnus::{method, prelude::*, Error, Ruby};
use std::cell::RefCell;
#[magnus::wrap(class = "Counter")]
struct Counter(RefCell<i64>);
impl Counter {
fn new(initial: i64) -> Self {
Self(RefCell::new(initial))
}
fn increment(&self, amount: i64) {
*self.0.borrow_mut() += amount;
}
fn value(&self) -> i64 {
*self.0.borrow()
}
}
Type Conversions
Strings
Automatic conversion works for String. For more control or efficiency, use RString.
use magnus::{RString, Error};
// Automatic conversion from Ruby String to Rust String.
fn get_length(s: String) -> usize {
s.len()
}
// Direct manipulation of a Ruby string, avoiding a full copy.
fn is_empty(s: RString) -> bool {
s.is_empty()
}
Numbers
Automatic conversion works for primitives like i64 and f64. A common failure is integer overflow.
use magnus::{Integer, Error, exception, Ruby};
fn safe_multiply(ruby: &Ruby, a: i64, b: i64) -> Result<i64, Error> {
a.checked_mul(b).ok_or_else(|| {
Error::new(ruby.exception_range_error(), "integer overflow")
})
}
Arrays
Automatic conversion works for Vec<T>. For heterogeneous arrays or large datasets, use RArray.
use magnus::{RArray, Error, TryConvert};
// Automatic conversion.
fn sum_integers(numbers: Vec<i64>) -> i64 {
numbers.iter().sum()
}
// Manual conversion with error handling for mixed types.
fn first_integer(array: RArray) -> Result<i64, Error> {
let first_item = array.entry(0)?;
i64::try_convert(first_item)
}
Hashes
Automatic conversion works for HashMap<K, V>. For symbol keys or mixed value types, use RHash.
use magnus::{RHash, Symbol, Error, Ruby, TryConvert};
use std::collections::HashMap;
// Automatic conversion.
fn get_keys(hash: HashMap<String, i64>) -> Vec<String> {
hash.keys().cloned().collect()
}
// Manual lookup with a symbol key.
fn get_id(ruby: &Ruby, hash: RHash) -> Result<i64, Error> {
match hash.get(ruby.to_symbol("id")) {
Some(val) => i64::try_convert(val),
None => Err(Error::new(ruby.exception_arg_error(), "missing :id key")),
}
}
Error Handling
Define custom Rust error types and convert them into Ruby exceptions. See the Error Handling guide for more advanced patterns.
use magnus::{Error, prelude::*};
use std::fmt;
#[derive(Debug)]
struct MyError(String);
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
// Use it in a function.
fn might_fail(should_fail: bool) -> Result<(), MyError> {
if should_fail {
Err(MyError("Something went wrong".to_string()))
} else {
Ok(())
}
}
Optional Arguments & Blocks
Optional Arguments
Use Option<T> for optional arguments. For keyword arguments, accept an RHash.
use magnus::{RHash, Error, TryConvert};
// `greeting` will be `None` if the argument is omitted or `nil`.
fn greet(name: String, greeting: Option<String>) -> String {
format!("{}, {}!", greeting.unwrap_or_else(|| "Hello".to_string()), name)
}
Blocks
Accept a Ruby block as a Proc.
use magnus::{block::Proc, Value, Error};
fn call_the_block(block: Proc) -> Result<Value, Error> {
block.call(("Hello from Rust!",))
}