Classes and Modules
This guide explains how to define and structure your code using Ruby's object model, including modules, classes, and inheritance.
Defining Modules
Key Points
- Use modules to namespace related functionality.
- Define constants and module-level methods.
- Use
staticvariables withAtomictypes to maintain thread-safe state within a module.
Example: Module with State
This example creates an Lz4Flex module that uses Lazy static variables
to safely initialize shared data like the module itself and custom error classes.
This is a common and robust pattern for managing global state in a Rust extension.
use magnus::{
function, prelude::*, value::{InnerValue, Lazy}, Error, ExceptionClass, RModule,
Ruby,
};
// Using a `Lazy` static to define a module is a great way to ensure that
// the module is only defined once, and that it is thread-safe.
static LZ4_FLEX: Lazy<RModule> =
Lazy::new(|ruby| match ruby.define_module("Lz4Flex") {
Ok(m) => m,
Err(e) => panic!("Failed to define Lz4Flex module: {}", e),
});
// We can also use `Lazy` to define error classes, which can be used
// across our extension.
fn base_error_class() -> ExceptionClass {
static BASE_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
let parent_module = LZ4_FLEX.get_inner_with(ruby);
let standard_error = ruby.exception_standard_error();
match parent_module.define_error("Error", standard_error) {
Ok(e) => e,
Err(err) => panic!("Failed to define Error class: {}", err),
}
});
// The `unsafe` block is necessary because `get_unchecked` is the only way
// to get a `&Ruby` handle in this context. It is safe because we know
// the Ruby VM is running when this code is called.
unsafe { BASE_ERROR_CLASS.get_inner_with(&Ruby::get_unchecked()) }
}
// A placeholder function for our example.
fn compress(input: String) -> Result<String, Error> {
// In a real-world scenario, you would have your compression logic here.
Ok(format!("compressed:{}", input))
}
// Another placeholder function.
fn decompress(input: String) -> Result<String, Error> {
// In a real-world scenario, you would have your decompression logic here.
if let Some(original) = input.strip_prefix("compressed:") {
Ok(original.to_string())
} else {
Err(Error::new(base_error_class(), "invalid input".to_string()))
}
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = LZ4_FLEX.get_inner_with(ruby);
// This call ensures the error class is defined and available to Ruby
// when the module is loaded.
base_error_class();
module.define_singleton_method("compress", function!(compress, 1))?;
module.define_singleton_method("decompress", function!(decompress, 1))?;
Ok(())
}
Creating Classes from Rust Structs
Key Points
- Use
#[magnus::wrap]to expose a Ruststructas a Ruby class. - The wrapped
structholds the instance data for each Ruby object. - Use
RefCellfor interior mutability when instance data needs to be modified through an immutable (&self) reference.
Example: A Wrapped Struct with Mutable State
This example defines a Tokenizer class in Rust that can be instantiated and modified from Ruby. It's inspired by real-world tokenizers but simplified for clarity.
It uses #[magnus::wrap] to expose the Rust Tokenizer struct as a Ruby class. The struct holds the instance data for each Ruby object, and we use RefCell for interior mutability, allowing us to modify data from &self methods (which is how Magnus exposes Ruby instance methods).
use magnus::{class, function, method, prelude::*, Error, Ruby};
use std::cell::RefCell;
use std::collections::HashMap;
// This struct is wrapped and exposed to Ruby as the `Tokenizer` class.
#[magnus::wrap(class = "Tokenizer")]
struct Tokenizer {
// In a real-world scenario, this would be a more complex struct.
// Here, we use a `RefCell` around a `HashMap` to represent a vocabulary
// that can be modified.
vocab: RefCell<HashMap<String, i64>>,
next_id: RefCell<i64>,
}
impl Tokenizer {
// The constructor, exposed to Ruby as `Tokenizer.new`.
fn new() -> Self {
Self {
vocab: RefCell::new(HashMap::new()),
next_id: RefCell::new(0),
}
}
// An instance method, exposed as `tokenizer.add_token("hello")`.
// It modifies the internal state.
fn add_token(&self, token: String) -> i64 {
let mut vocab = self.vocab.borrow_mut();
let mut next_id = self.next_id.borrow_mut();
// Return existing token ID if it's already in the vocab.
if let Some(id) = vocab.get(&token) {
return *id;
}
let id = *next_id;
vocab.insert(token, id);
*next_id += 1;
id
}
// An accessor method to get the vocabulary size.
fn vocab_size(&self) -> usize {
self.vocab.borrow().len()
}
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let class = ruby.define_class("Tokenizer", ruby.class_object())?;
class.define_singleton_method("new", function!(Tokenizer::new, 0))?;
class.define_method("add_token", method!(Tokenizer::add_token, 1))?;
class.define_method("vocab_size", method!(Tokenizer::vocab_size, 0))?;
Ok(())
}
Class Inheritance and Mixins
Key Points
- You can subclass existing Ruby classes (e.g.,
Array,String). - Define a module in Rust and include it into a class as a mixin.
Example: Subclassing and Including a Module
This example creates a custom SortedArray that subclasses Ruby's Array.
use magnus::{method, prelude::*, Error, Ruby, RArray, Value};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
// 1. Get the parent class (Array).
let array_class = ruby.class_array();
// 2. Define a subclass of Array.
let sorted_array = ruby.define_class("SortedArray", array_class)?;
// 3. Override a method.
sorted_array.define_method("<<", method!(|rb_self: RArray, item: Value| {
rb_self.push(item)?;
rb_self.funcall::<_, _, Value>("sort!", ())?; // Call sort! on self
Ok::<magnus::RArray, magnus::Error>(rb_self)
}, 1))?;
Ok(())
}
Singleton Methods
Key Points
- "Class methods" in Ruby are singleton methods on the class object.
- Use
define_singleton_methodto define class methods. - Singleton methods can also be defined on any specific object instance.
Example: Class and Instance Methods
This example defines a Logger class with both instance methods (log) and class methods (default_level).
use magnus::{function, method, prelude::*, Error, Ruby};
#[magnus::wrap(class = "Logger")]
struct Logger {
level: String,
}
impl Logger {
fn new(level: String) -> Self { Self { level } }
fn log(&self, message: String) { println!("[{}] {}", self.level, message); }
fn default_level() -> &'static str { "INFO" }
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let class = ruby.define_class("Logger", ruby.class_object())?;
class.define_singleton_method("new", function!(Logger::new, 1))?; // Constructor
class.define_singleton_method("default_level", function!(Logger::default_level, 0))?; // Class method
class.define_method("log", method!(Logger::log, 1))?; // Instance method
Ok(())
}