Ruby Classes and Modules
This guide explains how to define and work with Ruby classes and modules from Rust. It covers approaches for creating Ruby objects, defining methods, and organizing 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
use magnus::{function, 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:
use magnus::{Module, Ruby, Error, Symbol};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("Config")?;
// Define constants
module.const_set("VERSION", "1.0.0")?;
module.const_set("MAX_CONNECTIONS", 100)?;
module.const_set("DEFAULT_MODE", Symbol::new("production"))?;
Ok(())
}
Using Module Attributes
To maintain module state, a common pattern is storing attributes in the module itself:
use magnus::{function, prelude::*, Error, 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()
.map_err(|_| Error::new(magnus::exception::runtime_error(), "Failed to acquire lock"))?
.clone() {
Some(config) => Ok(config),
None => Ok("default".to_string()),
}
}
fn set_config(value: String) -> Result<String, Error> {
let mut config = CONFIG.lock()
.map_err(|_| Error::new(magnus::exception::runtime_error(), "Failed to acquire lock"))?;
*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:
use magnus::{function, 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:
use magnus::{function, method, prelude::*, Error, Ruby};
// Define a Rust struct
#[magnus::wrap(class = "MyExtension::Rectangle")]
struct Rectangle {
width: f64,
height: f64,
}
// Use the wrap macro to handle the Ruby class mapping
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:
use std::cell::RefCell;
use magnus::{Error};
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
}
// 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)
}
}
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:
function!
- For singleton/class methods and module functionsmethod!
- For instance methods when you need access to the Rust object (&self
)
Here's how to use each:
use magnus::{function, method, prelude::*, Error, Ruby};
#[magnus::wrap(class = "Calculator")]
struct 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:
```ruby
# Basic method with no exceptions
def add(a, b)
a + b
end
# Method that raises exceptions
def divide(a, b)
raise ZeroDivisionError, "Division by zero" if b == 0
a / b
end
# Method that takes a block
def with_retries(max_retries)
retries = 0
begin
yield
rescue => e
if retries < max_retries
retries += 1
retry
else
raise e
end
end
end
```
```rust
// Basic Method (no Ruby access, no exceptions)
fn add(&self, a: i64, b: i64) -> i64 {
a + b
}
// Method that Raises Exceptions
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 with Ruby Block
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
use magnus::{method, prelude::*, Error, Ruby, RArray, Value};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
// Get the parent class (Ruby's built-in Array)
let array_class = ruby.class_array();
// 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!(|rb_self: Value, item: Value| {
let array = RArray::from_value(rb_self)
.ok_or_else(|| Error::new(magnus::exception::type_error(), "not an array"))?;
array.push(item)?;
// Call sort! to keep the array sorted
let _: Value = array.funcall("sort!", ())?;
Ok(rb_self) // Return self for method chaining
}, 1))?;
Ok(())
}
Including Modules (Mixins)
use magnus::{RModule, method, function, prelude::*, Error, Ruby, Value, RObject, TryConvert, value::ReprValue};
use std::cmp::Ordering;
fn make_comparable(ruby: &Ruby) -> Result<RModule, Error> {
let module = ruby.define_module("MyComparable")?;
// Define methods for the module
module.define_method("<=>", method!(|ruby: &Ruby, rb_self: Value, other: Value| -> Result<Value, Error> {
// Implementation of the spaceship operator for comparison
let self_num: Result<i64, _> = TryConvert::try_convert(rb_self);
let other_num: Result<i64, _> = TryConvert::try_convert(other);
match (self_num, other_num) {
(Ok(a), Ok(b)) => Ok(ruby.integer_from_i64(match a.cmp(&b) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}).as_value()),
_ => Ok(ruby.qnil().as_value()),
}
}, 1))?;
// Define methods that depend on <=>
module.define_method("==", method!(|_ruby: &Ruby, rb_self: Value, other: Value| -> Result<bool, Error> {
let result: Value = rb_self.funcall("<=>", (other,))?;
if result.is_nil() {
Ok(false)
} else {
let num: i64 = TryConvert::try_convert(result)?;
Ok(num == 0)
}
}, 1))?;
module.define_method(">", method!(|_ruby: &Ruby, rb_self: Value, other: Value| -> Result<bool, Error> {
let result: Value = rb_self.funcall("<=>", (other,))?;
if result.is_nil() {
Ok(false)
} else {
let num: i64 = TryConvert::try_convert(result)?;
Ok(num > 0)
}
}, 1))?;
module.define_method("<", method!(|_ruby: &Ruby, rb_self: Value, other: Value| -> Result<bool, Error> {
let result: Value = rb_self.funcall("<=>", (other,))?;
if result.is_nil() {
Ok(false)
} else {
let num: i64 = TryConvert::try_convert(result)?;
Ok(num < 0)
}
}, 1))?;
Ok(module)
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
// Create our module
let module = make_comparable(ruby)?;
// Create a class
let score = ruby.define_class("Score", ruby.class_object())?;
// Define methods
score.define_singleton_method("new", function!(|ruby: &Ruby, value: i64| -> Result<Value, Error> {
let score_class = ruby.class_object().const_get::<_, Value>("Score")?;
let obj = RObject::try_convert(score_class.funcall("new", ())?)?;
obj.ivar_set("@value", value)?;
Ok(obj.as_value())
}, 1))?;
score.define_method("value", method!(|ruby: &Ruby, rb_self: Value| -> Result<i64, Error> {
let obj = RObject::try_convert(rb_self)?;
obj.ivar_get::<_, i64>("@value")
}, 0))?;
// Define <=> for Score instances
score.define_method("<=>", method!(|ruby: &Ruby, rb_self: Value, other: Value| -> Result<Value, Error> {
let self_obj = RObject::try_convert(rb_self)?;
let self_value: i64 = self_obj.ivar_get::<_, i64>("@value")?;
// Try to get value from other if it's a Score
if let Ok(other_score) = RObject::try_convert(other) {
if let Ok(other_value) = other_score.ivar_get::<_, i64>("@value") {
return Ok(ruby.integer_from_i64(match self_value.cmp(&other_value) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}).as_value());
}
}
Ok(ruby.qnil().as_value())
}, 1))?;
// Include our module
score.include_module(module)?;
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
use magnus::{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().const_get::<_, Value>("Logger")?;
let default_level = Self::default_level();
class.funcall("new", (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)
use magnus::{function, prelude::*, Error, Ruby, RObject};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
// Create a single object
let config = ruby.eval::<RObject>("Object.new")?;
// Get the singleton class of the object
let singleton_class = config.singleton_class()?;
// Define singleton methods on the singleton class
singleton_class.define_method("get", function!(|| {
"Configuration value"
}, 0))?;
singleton_class.define_method("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
-
Use magnus macros for class definition: The
wrap
andTypedData
macros simplify class definition significantly. -
Consistent naming: Keep Ruby and Rust naming conventions consistent within their domains (snake_case for Ruby methods, CamelCase for Ruby classes).
-
Layer your API: Consider providing both low-level and high-level APIs for complex functionality.
-
Document method signatures: When using methods that can raise exceptions, document which exceptions can be raised.
-
RefCell borrowing pattern: Always release a
borrow()
before callingborrow_mut()
by copying any needed values. -
Method macro selection: Use
function!
for singleton methods andmethod!
for instance methods. -
Include the Ruby parameter: Always include
ruby: &Ruby
in your method signature if your method might raise exceptions or interact with the Ruby runtime. -
Reuse existing Ruby patterns: When designing your API, follow existing Ruby conventions that users will already understand.
-
Cache Ruby classes and modules: Use
Lazy
to cache frequently accessed classes and modules. -
Maintain object hierarchy: Properly use Ruby's inheritance and module system to organize your code.