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:
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:
#![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
-
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."