magnus

repository·main·Indexed 21 days ago

https://github.com/matsadler/magnus

A high-level library for creating Ruby bindings in Rust. It enables developers to write Ruby extension gems in Rust or embed a Ruby runtime within a Rust application. Magnus provides automatic type conversion between Ruby and Rust, tools for wrapping Rust structs and enums as Ruby objects, and macros like #[magnus::init] and #[magnus::wrap] to simplify the binding process.

Tokens
21.7K
Snippets
74
Records
81
Agent score
74%

What's inside magnus

  1. Wrap Rust types in Ruby objects

    main

    Magnus allows you to expose Rust structs and enums as Ruby objects. This enables Ruby to interact with Rust logic seamlessly. You can return these wrapped objects to Ruby or pass them back to Rust, where they are automatically unwrapped to native Rust references.

    There are two primary ways to wrap a type:

    1. Use the #[magnus::wrap] convenience macro.
    2. Implement the magnus::TypedData trait for more customization.

    Handling Mutability

    Because Ruby's GC manages the memory of the wrapped object, Magnus cannot bind functions using mutable references (&mut T). To allow mutation of fields within a wrapped struct, use the newtype pattern with RefCell.

    use magnus::{function, method, prelude::*, Error, Ruby};
    
    #[magnus::wrap(class = "Point")]
    struct Point {
        x: isize,
        y: isize,
    }
    
    impl Point {
        fn new(x: isize, y: isize) -> Self {
            Self { x, y }
        }
    
        fn x(&self) -> isize {
            self.x
        }
    
        fn y(&self) -> isize {
            self.y
        }
    
        fn distance(&self, other: &Point) -> f64 {
            (((other.x - self.x).pow(2) + (other.y - self.y).pow(2)) as f64).sqrt()
        }
    }
    
    #[magnus::init]
    fn init(ruby: &Ruby) -> Result<(), Error> {
        let class = ruby.define_class("Point", ruby.class_object())?;
        class.define_singleton_method("new", function!(Point::new, 2))?;
        class.define_method("x", method!(Point::x, 0))?;
        class.define_method("y", method!(Point::y, 0))?;
        class.define_method("distance", method!(Point::distance, 1))?;
        Ok(())
    }
  2. Safety: Managing Ruby objects in Rust

    main

    When using Magnus, Ruby objects must be kept on the stack.

    If you move a Ruby object to the heap (e.g., by storing it in a Vec, HashMap, or Box), the Ruby Garbage Collector (GC) will not be able to reach it. This can lead to the object being garbage collected while Rust still holds a reference, causing memory safety issues.

    Magnus does not enforce this via the Rust type system; you must manage this manually.

  3. Type conversions between Rust and Ruby

    main

    Magnus provides automatic type conversion between Rust and Ruby. Conversions follow Ruby's core pattern: if an object is not of the requested type, Magnus will attempt to call the corresponding #to_<type> method (e.g., #to_int, #to_str, #to_sym, #to_ary, #to_hash).

    For more granular control, you can use magnus::TryConvert for arguments accepted from Ruby, or magnus::IntoValue for values being returned to Ruby. If you need to bypass automatic conversions (for example, to enforce specific encodings or avoid allocations), use the magnus::Value type and perform manual type checking or conversion.

    fn example(ruby: &Ruby, val: magnus::Value) -> Result<(), magnus::Error> {
        // checks value is a String, does not call #to_str
        let r_string = RString::from_value(val)
            .ok_or_else(|| magnus::Error::new(ruby.exception_type_error(), "expected string"))?;
        
        if !r_string.is_utf8_compatible_encoding() {
            return Err(magnus::Error::new(
                ruby.exception_encoding_error(),
                "string must be utf-8",
            ));
        }
    
        unsafe {
            let s = r_string.as_str()?;
            // ...
        }
        Ok(())
    }
  4. Define Ruby methods from Rust functions

    main

    You can bind regular Rust functions to Ruby as either global functions or instance methods. Magnus handles automatic type conversion between Ruby and Rust types. If the arguments passed from Ruby are incompatible, Magnus will raise standard Ruby errors like ArgumentError or TypeError.

    // Defining a global function (no Ruby `self` argument)
    fn fib(n: usize) -> usize {
        match n {
            0 => 0,
            1 | 2 => 1,
            _ => fib(n - 1) + fib(n - 2),
        }
    }
    
    #[magnus::init]
    fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
        ruby.define_global_function("fib", magnus::function!(fib, 1));
        Ok(())
    }
    
    // Defining an instance method (requires a Ruby `self` argument)
    fn is_blank(rb_self: String) -> bool {
        !rb_self.contains(|c: char| !c.is_whitespace())
    }
    
    #[magnus::init]
    fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
        let class = ruby.define_class("String", ruby.class_object())?;
        // 0 indicates the number of arguments excluding `self`
        class.define_method("blank?", magnus::method!(is_blank, 0))?;
        Ok(())
    }
  5. Embed Ruby in a Rust binary

    main

    To call Ruby code from a standalone Rust program, enable the embed feature in your Cargo.toml:

    [dependencies]
    magnus = { version = "0.8", features = ["embed"] }

    Use magnus::Ruby::init to initialize the Ruby runtime. The closure passed to init provides a Ruby instance. You must ensure the value returned by init is not dropped until you are finished with Ruby. init can only be called once.

    use magnus::eval;
    
    fn main() {
        magnus::Ruby::init(|ruby| {
            let val: f64 = eval!(ruby, "a + rand", a = 1)?;
    
            println!("{}", val);
    
            Ok(())
        }).unwrap();
    }
  6. Write a Ruby extension gem in Rust

    main

    To create a Ruby extension, follow these steps:

    1. Configure Cargo.toml: Set the crate-type to ["cdylib"] so it builds as a dynamic system library.
    2. Define the entry point: Use the #[magnus::init] attribute on a function that takes &magnus::Ruby to define your classes and methods.
    3. Build and Package:
      • For development/packaging, use the rb_sys gem and rake-compiler to automate the build process.
      • Create an extconf.rb file in your extension directory that calls create_rust_makefile.
      • Run rake compile to generate the .so or .bundle file.
    4. Load in Ruby: Use require_relative to load the compiled extension.

    Example Cargo.toml

    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    magnus = "0.8"

    Example ext/my_example_gem/extconf.rb

    require "mkmf"
    require "rb_sys/mkmf"
    
    create_rust_makefile("my_example_gem/my_example_gem")
  7. Configure static linking for Ruby

    main

    To support statically linking Ruby, add rb-sys to your Cargo.toml. Ensure you select the same version used by Magnus.

    rb-sys = { version = "*", default-features = false, features = ["ruby-static"] }
  8. Call Ruby methods from Rust

    main

    To call Ruby methods from Rust, use the funcall method provided by the magnus::ReprValue trait. All Magnus Ruby wrapper types implement this trait.

    • For methods with direct counterparts in the Ruby C API (like Object#frozen? or Array#[]), use specialized methods like magnus::ReprValue::check_frozen or magnus::RArray::aref for better performance.
    • For all other Ruby methods, use funcall.

    funcall automatically converts the return value to the specified Rust type. If conversion fails or the Ruby method raises an error, it returns Err(magnus::Error). To skip type conversion, set the return type to magnus::Value.

    // 0 arguments
    let s: String = value.funcall("test", ())?;
    
    // 1 argument
    let x: bool = value.funcall("example", ("foo",))?;
    
    // 2 arguments
    let i: i64 = value.funcall("other", (42, false))?;
  9. Enable Ruby subclassing for wrapped Rust types

    main

    To allow Ruby to subclass a type wrapped in Magnus, the Rust type must satisfy three requirements:

    1. Implement the Default trait.
    2. Define an allocator.
    3. Define an initializer.

    You must also register the allocator and the initializer method in your init function using class.define_alloc_func::<T>() and class.define_method("initialize", ...).

    #[derive(Default)]
    struct Point {
        x: isize,
        y: isize,
    }
    
    #[derive(Default)]
    #[wrap(class = "Point")]
    struct MutPoint(RefCell<Point>);
    
    impl MutPoint {
        fn initialize(&self, x: isize, y: isize) {
            let mut this = self.0.borrow_mut();
            this.x = x;
            this.y = y;
        }
    }
    
    #[magnus::init]
    fn init(ruby: &Ruby) -> Result<(), Error> {
        let class = ruby.define_class("Point", ruby.class_object()).unwrap();
        class.define_alloc_func::<MutPoint>();
        class.define_method("initialize", method!(MutPoint::initialize, 2))?;
        Ok(())
    }
  10. What is an RFloat?

    main

    An RFloat is a pointer to a Ruby RFloat struct, which is Ruby's internal representation for high-precision floating-point numbers.

    In Ruby, most floats are stored as Flonum (immediate values), but numbers requiring higher precision are stored as heap-allocated RFloat objects. Magnus provides the RFloat type to allow Rust code to interact specifically with these high-precision objects.

    RFloat implements Numeric, IntoValue, and ReprValue, and can be converted to/from f64 or extracted from a magnus::Value.

  11. Mapping Ruby classes to Magnus Rust types

    main

    Magnus provides wrapper types to make working with specific Ruby classes easier. All these types, including the generic Value, implement the ReprValue trait and share many methods. Use these wrappers when you need to interact with specific Ruby data types in Rust.

    | Ruby Class | Magnus Type |
    |------------|-------------|
    | `String`   | [`RString`] |
    | `Integer`  | [`Integer`] |
    | `Float`    | [`Float`]   |
    | `Array`    | [`RArray`]  |
    | `Hash`     | [`RHash`]   |
    | `Symbol`   | [`Symbol`]  |
    | `Class`    | [`RClass`]  |
    | `Module`   | [`RModule`] |
  12. Use the Numeric trait for coerced arithmetic and comparisons

    main

    The Numeric trait provides specialized methods for performing operations on Ruby's Numeric subclasses (like Integer, Float, or Rational) while following Ruby's coercion protocol. This is more robust than a standard funcall because it correctly handles how Ruby objects interact when they are of different numeric types.

    There are four primary coercion methods:

    • coerce_bin: For binary operators like +, -, *, /.
    • coerce_cmp: For comparison operators like <=>.
    • coerce_relop: For relationship operators like <=, >, ==.
    • coerce_bit: For bitwise operators like |, ^, &.

    Note on coerce_cmp: If coercion fails, this method returns nil. To detect a nil result, set the expected return type to Option<U>. Other errors will still return an Err.

    All methods return Result<U, Error>, where U is the type you wish to convert the result into.

    use magnus::{Error, Numeric, Ruby};
    
    fn example(ruby: &Ruby) -> Result<(), Error> {
        let a = ruby.integer_from_i64(2);
        let b = ruby.float_from_f64(3.5);
        // Using coerce_bin for addition
        let c: Value = a.coerce_bin(b, "+")?;
        let c = Float::from_value(c);
        assert!(c.is_some());
        assert_eq!(c.unwrap().to_f64(), 5.5);
    
        Ok(())
    }