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.

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

  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.