Skip to main content

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.

Rust

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

  1. Always Handle Errors: Type conversions can fail, wrap them in proper error handling.

  2. Use try_convert: Prefer try_convert over direct conversions to safely handle type mismatches.

  3. Remember Boxing Rules: All Ruby objects are reference types, while many Rust types are value types.

  4. Be Careful with Magic Methods: Some Ruby methods like method_missing might not behave as expected when called from Rust.

  5. Cache Ruby Objects: If you're repeatedly using the same Ruby objects (like classes or symbols), consider caching them using Lazy or similar mechanisms.

  6. Check for nil: Always check for nil values before attempting conversions that don't handle nil.

  7. Use Type Annotations: Explicitly specifying types when converting Ruby values to Rust can make your code clearer and avoid potential runtime errors.

  8. Pass Ruby State: Always pass the Ruby instance through your functions when needed rather than using Ruby::get() repeatedly, as this is more performant and clearer about dependencies.