Skip to main content

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, and RHash.
  • 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!",))
}