Working with Ruby Objects
When writing Ruby extensions in Rust, one of the most common tasks is converting between Ruby and Rust types. The magnus crate provides a comprehensive set of conversion functions for this purpose.
Basic Type Conversions
Primitive Types
use magnus::{RString, Ruby, Integer, Float, Error};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
// Convert Rust types to Ruby
let rb_string: RString = ruby.str_new("Hello, Ruby!"); // Rust &str to Ruby String
let rb_int: Integer = ruby.integer_from_i64(42); // Rust i64 to Ruby Integer
let rb_float: Float = ruby.float_from_f64(std::f64::consts::PI); // Rust f64 to Ruby Float
let _rb_bool = ruby.qtrue(); // Rust bool to Ruby true/false
// Convert Ruby types to Rust
let _rust_string: String = rb_string.to_string()?; // Ruby String to Rust String
let _rust_int: i64 = rb_int.to_i64()?; // Ruby Integer to Rust i64
let _rust_float: f64 = rb_float.to_f64(); // Ruby Float to Rust f64
// To check if a value is true/false, compare with qtrue/qfalse
Ok(())
}
Checking Types
When working with a generic magnus::Value, you often need to determine what kind of object it is before you can work with it. The is_kind_of method, combined with TryConvert, provides a robust way to do this.
use magnus::{function, method, prelude::*, Error, Ruby, Value, TryConvert};
// An example of a simple wrapped struct.
#[magnus::wrap(class = "MyNumber")]
struct MyNumber(i64);
impl MyNumber {
fn new(val: i64) -> Self { Self(val) }
fn get(&self) -> i64 { self.0 }
}
// A function that can accept different types from Ruby.
fn process_value(ruby: &Ruby, val: Value) -> Result<(), Error> {
// We can use `is_kind_of` to check the class of a Ruby object before
// attempting a conversion.
if val.is_kind_of(ruby.class_integer()) {
let i = i64::try_convert(val)?;
println!("Got a built-in Integer: {}", i);
} else if val.is_kind_of(ruby.class_string()) {
let s = String::try_convert(val)?;
println!("Got a built-in String: '{}'", s);
} else if let Ok(n) = <&MyNumber>::try_convert(val) {
// For our own wrapped types, we can convert to a reference.
println!("Got our custom MyNumber with value: {}", n.get());
} else {
// As a fallback, print a generic message.
println!("Got some other type.");
}
Ok(())
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let my_number_class = ruby.define_class("MyNumber", ruby.class_object())?;
my_number_class.define_singleton_method("new", function!(MyNumber::new, 1))?;
my_number_class.define_method("get", method!(MyNumber::get, 0))?;
ruby.define_global_function("process_value", function!(process_value, 1));
Ok(())
}
Strings, Arrays, and Hashes
Working with Ruby Strings
Ruby strings are encoded and have more complex behavior than Rust strings:
use magnus::{RString, Ruby, value::ReprValue};
fn string_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
// Create a new Ruby string
let mut hello = ruby.str_new("Hello");
// Concatenate strings (using funcall)
let world = ruby.str_new(" World!");
let message: RString = hello.as_value().funcall("concat", (world,))?;
// Get bytes
let bytes = unsafe { message.as_slice() };
println!("Bytes: {:?}", bytes);
// Create from bytes
let bytes = [72, 101, 108, 108, 111]; // "Hello" in ASCII
let _new_str = ruby.str_from_slice(&bytes);
Ok(())
}
Working with Ruby Arrays
Ruby arrays can hold any kind of Ruby object:
use magnus::{RArray, Ruby, Value};
fn array_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
// Create a new empty array
let array: RArray = ruby.ary_new();
// Push elements
array.push(1)?;
array.push("two")?;
array.push(3.0)?;
// Get length
let _length = array.len();
// Access elements
let _first: i64 = array.entry(0)?;
let _second: String = array.entry(1)?;
let _third: f64 = array.entry(2)?;
// Iterate through elements
for i in 0..array.len() {
let item: Value = array.entry(i as isize)?;
println!("Item {}: {:?}", i, item);
}
// Another way to iterate
array.into_iter().for_each(|val| {
println!("Item: {:?}", val);
});
// Create an array from Rust Vec
let numbers = vec![1, 2, 3, 4, 5];
let rb_array = ruby.ary_from_vec(numbers);
// Convert to a Rust Vec
let _vec: Vec<i64> = rb_array.to_vec()?;
Ok(())
}
Working with Ruby Hashes
Ruby hashes are similar to Rust's HashMap but can use any Ruby object as keys:
use magnus::{RHash, Value, Ruby, r_hash::ForEach, TryConvert, Error};
fn hash_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
// Create a new hash
let hash: RHash = ruby.hash_new();
// Add key-value pairs
hash.aset("name", "Alice")?;
hash.aset(ruby.to_symbol("age"), 30)?;
hash.aset(1, "one")?;
// Get values
let _name: String = TryConvert::try_convert(
hash.get("name")
.ok_or_else(|| Error::new(ruby.exception_key_error(), "name not found"))?
)?;
let _age: i64 = TryConvert::try_convert(
hash.get(ruby.to_symbol("age"))
.ok_or_else(|| Error::new(ruby.exception_key_error(), "age not found"))?
)?;
let _one: String = TryConvert::try_convert(
hash.get(1)
.ok_or_else(|| Error::new(ruby.exception_key_error(), "1 not found"))?
)?;
// Check if key exists
if hash.get("name").is_some() {
println!("Has key 'name'");
}
// Delete a key
let _deleted: Value = hash.delete(1)?;
// Iterate over key-value pairs
hash.foreach(|k: Value, v: Value| {
println!("Key: {:?}, Value: {:?}", k, v);
Ok(ForEach::Continue)
})?;
// Convert to a Rust HashMap (if keys and values are convertible)
// Convert to Rust HashMap by iterating
let mut map = std::collections::HashMap::new();
hash.foreach(|k: Value, v: Value| {
if let (Ok(key), Ok(value)) = (String::try_convert(k), String::try_convert(v)) {
map.insert(key, value);
}
Ok(ForEach::Continue)
})?;
Ok(())
}
Handling nil Values
Ruby's nil is a special value that requires careful handling:
use magnus::{Value, Ruby, RString, value::ReprValue, TryConvert, Error};
fn handle_nil(_ruby: &Ruby, val: Value) -> Result<(), Error> {
// Try to convert value - returns None if nil or wrong type
let maybe_string: Option<RString> = TryConvert::try_convert(val).ok();
match maybe_string {
Some(s) => println!("Got string: {}", s.to_string()?),
None => println!("No string (was nil or couldn't convert)"),
}
// Return nil from a function
fn returns_nil(ruby: &Ruby) -> Value {
ruby.qnil().as_value()
}
let _ = returns_nil; // silence unused warning
Ok(())
}
Converting Between Ruby and Rust Types
Magnus provides powerful type conversion traits that make it easy to convert between Ruby and Rust types.
From Ruby to Rust (TryConvert)
use magnus::{Value, Ruby, TryConvert, Error, RHash};
// Convert custom Rust types to Ruby objects
struct Person {
name: String,
age: u32,
}
impl TryConvert for Person {
fn try_convert(val: Value) -> Result<Self, Error> {
let ruby = unsafe { Ruby::get_unchecked() };
let hash = RHash::try_convert(val)?;
let name: String = TryConvert::try_convert(hash.get("name").ok_or_else(|| Error::new(ruby.exception_key_error(), "name not found"))?)?;
let age: u32 = TryConvert::try_convert(hash.get("age").ok_or_else(|| Error::new(ruby.exception_key_error(), "age not found"))?)?;
Ok(Person { name, age })
}
}
// Usage
fn process_person(val: Value) -> Result<(), Error> {
let person: Person = Person::try_convert(val)?;
println!("Person: {} ({})", person.name, person.age);
Ok(())
}
From Rust to Ruby (IntoValue)
use magnus::{Value, Ruby, IntoValue, Error, value::ReprValue};
struct Point {
x: f64,
y: f64,
}
impl IntoValue for Point {
fn into_value_with(self, ruby: &Ruby) -> Value {
let hash = ruby.hash_new();
let _ = hash.aset("x", self.x);
let _ = hash.aset("y", self.y);
hash.as_value()
}
}
// Usage
fn create_point(ruby: &Ruby) -> Result<Value, Error> {
let point = Point { x: 10.5, y: 20.7 };
Ok(point.into_value_with(ruby))
}
Best Practices
-
Always Handle Errors: Type conversions can fail, wrap them in proper error handling.
-
Use try_convert: Prefer
try_convertover direct conversions to safely handle type mismatches. -
Remember Boxing Rules: All Ruby objects are reference types, while many Rust types are value types.
-
Be Careful with Magic Methods: Some Ruby methods like
method_missingmight not behave as expected when called from Rust. -
Cache Ruby Objects: If you're repeatedly using the same Ruby objects (like classes or symbols), consider caching them using
Lazyor similar mechanisms. -
Check for nil: Always check for nil values before attempting conversions that don't handle nil.
-
Use Type Annotations: Explicitly specifying types when converting Ruby values to Rust can make your code clearer and avoid potential runtime errors.
-
Pass Ruby State: Always pass the
Rubyinstance through your functions when needed rather than usingRuby::get()repeatedly, as this is more performant and clearer about dependencies.