danielpclark/rutie: embedding a CRuby VM inside a Rust program, and the safety that costs
“The Tie Between Ruby and Rust.”
At a glance
- What is it?
- Rutie is an MIT licensed Rust crate that calls Ruby objects from Rust and exposes Rust functions to Ruby as a native extension. The API is small, the examples are in the README, and the price is an unsafe boundary where Rust's guarantees stop applying, with a documented troubleshooting list of segfaults and GC crashes that tells you where.
- Who is it for?
- Adopt Rutie when you have a Ruby library you need to call from a Rust service, or a Rust function you need to expose to a Ruby application, and you are prepared to hold the Ruby VM's rules rather than Rust's for the boundary code.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A tie with two ancestors
Rutie describes itself as the tie between Ruby and Rust, and it works in both directions. You can integrate Ruby with your Rust application, or integrate Rust with your Ruby application, and the project claims you can do either with relative ease.
The lineage explains a lot about the design. The README names its continuation of two earlier projects, ruru and ruby-sys, both MIT licensed, and the crate metadata lists three authors, Steve Klabnik, Dmitry Gritsay and Daniel P. Clark, which is the same lineage of people who wrote the Ruby bindings for Rust before it.
The crate itself is `rutie`, version 0.14.3, edition 2021, MIT licensed, with a `build.rs` build script and a `links = "ruby"` entry, which is the Cargo mechanism for declaring that the crate links against a native library, here libruby. Two features are declared. `no-link` tells the build script not to link to libruby, which is what you want if you are only using the macro surface, and `derive` pulls in the optional `rutie-derive` crate.
The project describes itself as being maintained, and the last push to the repository was on 2026-10-17. The repository has no GitHub releases, so versions move on crates.io rather than in the release list, and the version table in the README covers Ruby 3 separately from Ruby 4.0, which the badges claim support for.
Calling Ruby from Rust means starting the VM first
The dependency is added the usual way:
[dependencies]
rutie = "0.14.3"Then the Ruby VM has to exist before anything else happens. The instructions say to add `VM::init()` to the beginning of the execution path of the program, and the example in the README shows it inside the test function:
// Rust projects must start the Ruby VM
VM::init();With a VM running, the Ruby types are wrapped values. `RString::new_utf8` creates a Ruby string from Rust text, and calling a method on it goes through `send`:
extern crate rutie;
use rutie::{Object, RString, VM};
fn try_it(s: &str) -> String {
let a = RString::new_utf8(s);
// The `send` method returns an AnyObject type.
let b = unsafe { a.send("reverse", &[]) };The `unsafe` block is not decoration, and the comment explains why. `send` returns an `AnyObject` type, and the value has to be converted back into something Rust can use:
// We must try to convert the AnyObject
// type back to our usable type.
match b.try_convert_to::<RString>() {
Ok(ruby_string) => ruby_string.to_string(),
Err(_) => "Fail!".to_string(),
}
}That `try_convert_to` returning a `Result` is the shape of the whole library. Ruby is dynamically typed, so a call that looks like it returns a string can return anything, and the library asks you to prove the type before you use it.
One operational detail catches everyone on a fresh machine. On Linux you need to set `LD_LIBRARY_PATH` to point at the directory of your Ruby library, and on macOS you need `DYLD_LIBRARY_PATH` with the same information. The path comes from:
ruby -e "puts RbConfig::CONFIG['libdir']"With that set, `cargo test` and `cargo run` work, and the README's example test asserts that reversing `apples` gives `selppa`.
Exposing Rust to Ruby means building a cdylib
The other direction is the one a Ruby application needs. You start a Ruby project with `bundle gem rutie_ruby_example`, change into that directory, run `cargo init --lib`, remove the TODOs from the gemspec, add Rutie to the `Cargo.toml` and define the library type:
[dependencies]
rutie = {version="xxx"}
[lib]
name = "rutie_ruby_example"
crate-type = ["cdylib"]The `cdylib` crate type is the mechanism, since Ruby loads native extensions as shared libraries. The Rust side then defines a class and its methods through the `class!` and `methods!` macros, and exports an initialisation function that Ruby calls when the library is required:
#[no_mangle]
pub extern "C" fn Init_rutie_ruby_example() {
Class::new("RutieExample", None).define(|klass| {
klass.def_self("reverse", pub_reverse);
});
}The naming convention in that snippet is explained right after it. When using the `methods!` macro or `extern` functions, the method name must not clash with any others, which is why the example function is prefixed with `pub_`.
Ruby then loads it through the `rutie` gem, whose source is in the `gem` folder of this repository, added to the gemspec or the Gemfile:
# gemspec
spec.add_dependency 'rutie', '~> 0.0.5'
# Gemfile
gem 'rutie', '~> 0.0.5'The mismatch between the gem version and the crate version is worth noting rather than assuming a mistake. The gem and the crate are released on their own cadences, and the gem is the loader while the crate is the implementation.
The derive feature writes the boilerplate
The `derive` feature enables the `rutie-derive` crate, which the Cargo manifest describes as providing `#[rubyclass]`, `#[rubymethods]` and `#[rubyfunction]`. The library's own test configuration requires that feature, with a dedicated test target for the derive macros.
The value is the same as any proc macro in a bridge library: it removes the manual plumbing that the `class!` and `methods!` macros otherwise require for every type you want to expose. The `methods!` example in the README is a good illustration of how much ceremony the macro based path saves, since each method still has to declare its Ruby argument and return types and handle the conversion.
There is also a compile time test target for the derive macros, named `derive_ui`, whose purpose is stated in the manifest comment. The derive macros' compile errors are tested on stable Rust because the messages include rustc's own, which change between versions. That is a sign of how seriously the project takes the error messages, which matters when the alternative is a wall of type errors from a boundary where two type systems meet.
Where Rust's guarantees stop
The README has a section on safety that it frames as a comparison between the Rutie philosophy and the Rust philosophy on safety, and it is the part to read before adopting the crate for anything you cannot afford to crash.
The mechanism is visible in the code above. `a.send("reverse", &[])` is inside an `unsafe` block, and the value it produces is a dynamically typed Ruby object that has to be converted back with a `Result`. Ruby's garbage collector moves objects, its exceptions unwind through a stack that Rust does not model, and Ruby code can raise, throw or call `exit` at any point. None of that is visible to the Rust compiler, so the compiler cannot check it.
The troubleshooting index makes the practical consequences concrete. It lists panics for some Rubies on CI server tests, a Rust signal 11, SIGSEGV, invalid memory reference, an error loading shared libraries for `libruby.so`, the `methods!` macro failing to call methods from other methods within the same macro, handling exceptions raised from Ruby in Rust code, and a segfault during garbage collection when using a Ruby method written in C.
That last entry is the one that decides the fit. If your Ruby side calls into a C extension, the interaction between that C code and the garbage collector is a known crash path, and the project documents it rather than pretending it away. The alternative to Rutie for that case is a native extension in C, where the same hazard exists but is at least not mediated by a Rust wrapper.
Versioning, examples and where to start reading
The project is explicit about how to learn it. You are highly encouraged to read the source code, because every method that has been mapped from Ruby for public use in `src/class/*` is well documented with example code, and the documentation says this is the best way to get up and running. The `examples/` directory holds integration examples based on the README, including `eval.rs` and separate directories for the derive example, the Ruby example, a GVL example and a Rust example.
The SemVer section is what tells you how to read version numbers in this project, and it is worth checking against your own dependency policy before you pin a version, since the crate is pre-1.0 at 0.14.3 and the Ruby compatibility story is tied to the Ruby version table rather than to the crate version.
On the licence, everything is MIT, including the two ancestors the project continues, so there is no copyleft obligation in the bridge code. The obligations you take on are the ones that come with running Ruby inside your process at all, which is a different kind of risk and is not something a licence can address.
Editorial conclusion
Adopt Rutie when you have a Ruby library you need to call from a Rust service, or a Rust function you need to expose to a Ruby application, and you are prepared to hold the Ruby VM's rules rather than Rust's for the boundary code. Do not adopt it to replace a native extension written in C, because the project's own troubleshooting list includes segfaults during garbage collection when a Ruby method written in C is involved, and do not adopt it where a panic in the Ruby VM would take down a process you cannot afford to lose. Verify first that your Ruby installation exposes the shared library path you need, since on Linux you must set `LD_LIBRARY_PATH` and on macOS `DYLD_LIBRARY_PATH`, and that the version of Ruby you target matches the version table the project maintains, with Ruby 4.0 support claimed and Ruby 3 covered by a separate table.
Frequently asked questions
How do I call a Ruby method from Rust with rutie?
Start the Ruby VM with `VM::init()` first, then use the wrapped Ruby types. `RString::new_utf8` creates a Ruby string, `send` calls a method and returns an `AnyObject`, and you convert it back with `try_convert_to::<RString>()` and handle the `Result`.
Why does my rutie build fail to find libruby?
On Linux you must set `LD_LIBRARY_PATH` to the directory of your Ruby library, and on macOS `DYLD_LIBRARY_PATH` with the same information. The path comes from `ruby -e "puts RbConfig::CONFIG['libdir']"`, and the README notes that loading shared libraries such as `libruby.so` is a known troubleshooting item.
How do I expose Rust functions to Ruby with rutie?
Build a `cdylib` with `cargo init --lib`, set `crate-type = ["cdylib"]` in the Cargo.toml, define the class and methods with the `class!` and `methods!` macros, and export an `Init_<name>` function that Ruby calls on load. The library is then required from Ruby through the `rutie` gem.
What does the derive feature of rutie provide?
The optional `derive` feature enables the `rutie-derive` crate, which supplies the `#[rubyclass]`, `#[rubymethods]` and `#[rubyfunction]` attributes, so you do not have to write the class and method boilerplate by hand.
Which Ruby versions does rutie support?
The README states Ruby 4.0 compatibility in its badges and keeps a separate version table for Ruby 3, and the project is a continuation of the MIT licensed ruru and ruby-sys projects. The last push to the repository was on 2026-10-17.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/danielpclark-rutie)Was this useful?