High-level, zero (or low) cost bindings of Ruby's C API for Rust.
-
Performance:
Rosy enables you to write the most performant code possible, such that using the C API directly would not improve performance. In other words, it presents zero-cost abstractions. However, not all of Rosy's safe abstractions are zero-cost. Sometimes this is only possible by writing some
unsafe
code since Rosy can't be made aware of certain aspects of the program state.-
For example,
Object::call
will catch any raised Ruby exceptions via theprotected
family of functions. On the other hand,Object::call_unchecked
will allow any thrown exception propagate (which causes a segmentation fault in Rust-land) unlessprotected
.Checking for exceptions via
protected
has a cost associated with it. It is best to wrap multiple instances of unchecked exception-throwing functions. This allows for reducing the number of speed bumps in your code. -
If it is known that no
panic!
will occur anywhere within exception-checked code, then callingprotected_no_panic
will emit fewer instructions at the cost of safety. TheFnOnce
passed into this function is called within an FFI context; because of that, panicking here is undefined behavior. Panics in a normalprotected
call are safely caught with the stack unwinding properly.
Note that
unsafe
functions suffixed with_unchecked
always have a safe counterpart. Before reaching forunsafe
functions, consider using these instead and profiling your code to find out whether it's actually necessary. -
-
Powerful Types:
Rosy leverages Rust's type system to the fullest.
-
Rosy makes certain Ruby types generic over enclosing types:
-
Array<O>
is generic overObject
types that it contains, defaulting toAnyObject
. -
Hash<K, V>
is generic overObject
keys and values, both defaulting toAnyObject
. -
Class<O>
is generic over anObject
type that it may instantiate viaClass::new_instance
.
-
-
When defining methods via
Class::def_method
ordef_method!
:-
The receiver is statically typed as the generic
Object
type that theClass
is meant for. -
Arguments (excluding the receiver) are generic up to and including 15
AnyObject
s. It may also take either anArray
or anAnyObject
pointer paired with a length. These allow for passing in a variable number of arguments.
-
-
-
Safety: *where possible
Rosy exposes safe abstractions over most of Ruby's C API. Wherever this isn't possible, such functionality is marked as
unsafe
with a documented explanation on safe usage.Unfortunately, due to the inherent nature of Ruby's C API, safety is often not easily achievable without a few compromises.
-
Many Ruby functions can raise exceptions, which trigger a segmentation fault in Rust-land. 😓
Functions that may raise an exception are marked as
unsafe
or have a safe exception-checking equivalent viaprotected
. However, checking for an exception has a cost in performance. -
Ruby's garbage collector de-deallocates objects whose references don't live on the stack, unless they are
mark
ed. This may lead to a possible use after free. When wrapping Rust data, it is important to implementRosy::mark
correctly.
-
Rosy requires cargo
and an existing Ruby installation:
-
cargo
can be installed viarustup
-
Ruby can be installed
rvm
,rbenv
, or your favorite package manager
The rosy
crate is available on crates.io and can be used by adding
the following to your project's Cargo.toml
:
[dependencies]
rosy = "0.0.9"
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.9"
features = ["ruby_2_6"]
Finally add this to your crate root (main.rs
or lib.rs
):
extern crate rosy;
Rosy can be compiled by simply running:
cargo build
It will automatically try to find the dynamic library via the current ruby
installation.
To enable static linking, specify the static
feature flag:
[dependencies.rosy]
version = "0.0.9"
features = ["static"]
To use a specific Ruby installation, you can do either of the following:
-
Set
ROSY_RUBY=path/to/ruby
This must point to an executable.
-
Set
ROSY_RUBY=client:version
. For example:-
ROSY_RUBY=rvm:2.6.0
-
ROSY_RUBY=rbenv:2.5.0
If the
:version
portion is not provided, thenROSY_RUBY_VERSION
is used to get the version number. For example:ROSY_RUBY=rvm ROSY_RUBY_VERSION=2.6.0 cargo build
-
Rosy allows you to perform many operations over Ruby objects in a way that feels very natural in Rust.
The virtual machine must be initialized via vm::init
before doing anything:
rosy::vm::init().expect("Could not initialize Ruby");
Once finished with Ruby, you can clean up its resources permanently via
vm::destroy
:
if let Err(code) = unsafe { rosy::vm::destroy() } {
std::process::exit(code);
}
Note that you can no longer use Ruby once the VM has been destroyed.
Using Object::call
, any method can be invoked on the receiving object:
use rosy::String;
let string = String::from("hello\r\n");
string.call("chomp!").unwrap();
assert_eq!(string, "hello");
To pass in arguments, use Object::call_with
:
use rosy::{Array, Integer, Object};
let array: Array<Integer> = (10..20).collect();
let args: &[Integer] = &[1.into(), 5.into(), 9.into()];
let values = array.call_with("values_at", args).unwrap();
assert_eq!(values, [11, 15, 19][..]);
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 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
.
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
.
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());
Rosy uses aloxide
to find and link Ruby during its build phase. Because of
that, Rosy's platform support is totally dependent on it. Changes that fix
issues with linking (or in the future, building) Ruby should be submitted to
that library for use in this one.
To work locally on aloxide
and Rosy in combination with each other, change
Rosy's Cargo.toml
like so:
[build-dependencies]
aloxide = { path = "path/to/aloxide", version = "0.0.8", default-features = false }
Like with most technologies, Rosy isn't the first of its kind.
Helix is a Rust library built on top of macros. Interaction with the Ruby
runtime is done via a ruby!
macro which features a DSL that's a mix between
Rust and Ruby syntax. To those coming from Ruby, they'll feel right at home.
However, those coming from Rust may feel that the macro is a little too
magical.
Unlike Helix, for each of Rosy's macros, there's an alternative approach that can be taken purely through types, traits, and functions. Rosy is designed to be convenient and high-level while trying not to hide the low-level details that can allow you to write better-optimized code. This is parallel to the way that Rust acts as a high-level language.
Rutie is a Rust library that tries to be less magical than Helix. It is a
continuation of the work done on ruru, which is no longer maintained as of the
end of 2017. Rutie takes an excellent approach to wrapping Ruby's C API in Rust
by exposing Ruby classes as Rust struct
s. This inspired the layout and design
of Rosy to some extent.
However, unlike Rutie, Rosy doesn't expose the lower-level C bindings. The reasoning is that if certain functionality is missing from Rosy, it should be added to the core library by either requesting it through an issue or submitting a pull request with an implementation.
Also, unlike Rutie, Rosy marks all exception-throwing functions as unsafe
. Not
handling a Ruby exception from Rust-land results in a segmentation fault. One
of the major reasons that some people choose to write Rust over C is to get away
from these. The Rust philosophy is that safe code should not be able to trigger
a segmentation fault. Just like with Rutie, Rosy allows Rust code to be
protected
against raised exceptions.
Rosy is a part of the Ocean package manager.
And it is developed by the following individual contributors:
-
Creator: Nikolai Vazquez
This project is made available under either the conditions of the MIT License or Apache License 2.0 at your choosing.
See LICENSE.md
.
Congrats on making it this far! ʕノ•ᴥ•ʔノ🌹