rosy 0.0.6

Ruby bindings for Rust.
Documentation

Rosy

Build status Lines of code crates.io downloads docs.rs MIT or Apache 2.0

High-level, zero (or low) cost bindings of Ruby's C API for Rust.

Index

Features

  • Zero or very-low cost abstractions over Ruby's C API.

    If speed is of the utmost importance, Rosy has functionality that cannot be beaten performance-wise when using the C API directly. However, this may require carefully writing some unsafe code.

    • For example, Object::call will catch any raised Ruby exceptions via the protected family of functions. On the other hand, Object::call_unchecked will allow any thrown exception propagate (which causes a segmentation fault in Rust-land) unless protected.

      Checking for exceptions via protected has a cost associated with it and so it may be best to wrap multiple instances of exception-throwing code with it rather than just one.

    • If it is known that no panic! will occur anywhere within exception-checked code, then calling protected_no_panic will emit fewer instructions at the cost of safety. The FnOnce passed into this function is called within an FFI context, and panicking here is undefined behavior. Panics in a normal protected call are safely caught with the stack unwinding properly.

  • Bindings that leverage Rust's type system to the fullest:

    • Rosy makes certain Ruby types generic over enclosing types:

    • When defining methods via Class::def_method or def_method!:

      • The receiver is statically typed as the generic Object type that the Class is meant for.

      • Arguments (excluding the receiver) are generic up to and including 15 AnyObjects. It may also take either an Array or an AnyObject pointer paired with a length. These allow for passing in a variable number of arguments.

  • Safety wherever possible.

    Unfortunately, due to the inherent nature of Ruby's C API, this isn't easily achievable without a few compromises in performance. A few factors that cause this are:

    • Ruby's garbage collector de-allocating objects whose references don't live on the stack if they are not marked. This may lead to a possible use after free.

    • Many Ruby functions can throw exceptions. 😓

      These cause a segmentation fault in Rust code that's not being called originally from a Ruby context. Functions that may throw an exception are marked as unsafe or have a safe exception-checking equivalent.

Installation

This crate is available on crates.io and can be used by adding the following to your project's Cargo.toml:

[dependencies]
rosy = "0.0.6"

Rosy has functionality that is only available for certain Ruby versions. The following features can currently be enabled:

  • ruby_2_6

For example:

[dependencies.rosy]
version = "0.0.6"
features = ["ruby_2_6"]

Finally add this to your crate root (main.rs or lib.rs):

extern crate rosy;

Usage

Rosy allows you to perform many operations over Ruby objects in a way that feels very natural in Rust.

use rosy::String;

// The VM must be initialized before doing anything
rosy::vm::init().expect("Could not initialize Ruby");

let string = String::from("hello\r\n");
string.call("chomp!").unwrap();

assert_eq!(string, "hello");

Defining Ruby Methods

To define a UTF-8-aware method blank? on Ruby's String class, one can very simply use the def_method! macro. This allows for defining a function that takes the typed object (in this case String) for the class as its receiver.

use rosy::prelude::*;

let class = Class::of::<String>();

rosy::def_method!(class, "blank?", |this: String| {
    this.is_whitespace()
}).unwrap();

let string = String::from(" \r\n");
let result = string.call("blank?");

assert_eq!(result.unwrap(), true);

Although the macro may feel somewhat magical, it's actually just a zero-cost wrapper around Class::def_method, which itself is a low-cost abstraction over rb_define_method_id. To bring the abstraction cost down to absolute zero, use def_method_unchecked!.

Defining Ruby Classes

Defining a new class is rather straightforward:

let my_object = Class::def("MyObject").unwrap();

Attempting to define an existing class will result in an error:

let array = Class::def("Array")
    .unwrap_err()
    .existing_class()
    .unwrap();

assert_eq!(array, Class::array());

To get an existing named class if it's not a built-in class, one should call Class::get:

let my_object = Class::get("MyObject").unwrap();

And if it's ambiguous as to whether the class already exists, there's the best of both worlds: Class::get_or_def. This will define a class with the given name if it doesn't already exist.

let my_object = Class::get_or_def("MyObject").unwrap();

To define a class within the namespace of a class or module, use Mixin::def_class.

Defining Ruby Subclasses

The Class::subclass method allows for creating a new class that inherits from the method receiver's class.

let sub_object = my_object.subclass("MyObjectChild").unwrap();

assert!(sub_object < my_object);

To define a subclass within the namespace of a class or module, use Mixin::def_subclass.

Catching Ruby Exceptions

Rust code can be protected from Ruby exceptions very easily.

use rosy::{Object, String, protected};

let string = String::from("¡Hola!");

let result = protected(|| unsafe {
    string.call_unchecked("likes_pie?")
});

assert!(result.unwrap_err().is_no_method_error());

Authors

License

This project is made available under either the conditions of the MIT License or Apache License 2.0 at your choosing.

See LICENSE.md.


Back to top