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};
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
// Convert Rust types to Ruby
let rb_string: RString = RString::new("Hello, Ruby!"); // Rust &str to Ruby String
let rb_int: Integer = Integer::from_i64(42); // Rust i64 to Ruby Integer
let rb_float: Float = 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 Ruby objects, you often need to check their types:
use magnus::{RString, Ruby, Value, Integer, TryConvert};
fn process_value(ruby: &Ruby, val: Value) -> Result<(), magnus::Error> {
// Try converting to different types
if let Ok(s) = TryConvert::try_convert(val) {
let s: RString = s;
println!("Got string: {}", s.to_string()?);
} else if let Ok(i) = TryConvert::try_convert(val) {
let i: Integer = i;
println!("Got integer: {}", i.to_i64()?);
} else {
println!("Got some other type (could be nil or another type)");
}
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, value::ReprValue};
fn string_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
// Create a new Ruby string
let mut hello = RString::new("Hello");
// Concatenate strings (using funcall)
let world = RString::new(" World!");
hello.as_value().funcall::<_, _, Value>("concat", (world,))?;
let message = hello;
// 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 = RString::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::new();
// Push elements
array.push(1)?;
array.push("two")?;
array.push(3.0)?;
// Get length
let length = array.len();
println!("Array length: {}", length);
// 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.each().for_each(|val| {
println!("Item: {:?}", val);
});
// Create an array from Rust Vec
let numbers = vec![1, 2, 3, 4, 5];
let rb_array = RArray::from_iter(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, Symbol, Ruby, r_hash::ForEach, TryConvert, Error};
fn hash_operations(ruby: &Ruby) -> Result<(), magnus::Error> {
// Create a new hash
let hash = RHash::new();
// Add key-value pairs
hash.aset("name", "Alice")?;
hash.aset(Symbol::new("age"), 30)?;
hash.aset(1, "one")?;
// Get values
let name: String = TryConvert::try_convert(
hash.get("name")
.ok_or_else(|| Error::new(magnus::exception::key_error(), "name not found"))?
)?;
let age: i64 = TryConvert::try_convert(
hash.get(Symbol::new("age"))
.ok_or_else(|| Error::new(magnus::exception::key_error(), "age not found"))?
)?;
let one: String = TryConvert::try_convert(
hash.get(1)
.ok_or_else(|| Error::new(magnus::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};
fn handle_nil(ruby: &Ruby, val: Value) -> Result<(), magnus::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()
}
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 Rust to Ruby (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 Ruby to Rust (IntoValue)
use magnus::{Value, Ruby, IntoValue, Error, RHash, value::ReprValue};
struct Point {
x: f64,
y: f64,
}
impl IntoValue for Point {
fn into_value_with(self, ruby: &Ruby) -> Value {
let hash = RHash::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_convert
over 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_missing
might 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
Lazy
or 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
Ruby
instance through your functions when needed rather than usingRuby::get()
repeatedly, as this is more performant and clearer about dependencies.