Hello Rusty!
This section serves as a companion to the "Hello, Rusty!" example, a fundamental demonstration of building a Ruby gem with a Rust extension. We'll explore a more robust, production-oriented approach using the magnus library, which provides high-level, safe bindings to the Ruby C API.
What We're Building
A simple gem named HelloRusty with a single method, greet, that returns a string from Rust. We'll focus on writing safe, idiomatic code that is easy to maintain.
Project Structure
The project structure remains standard for a gem with a native extension:
hello_rusty/
├── Cargo.toml # Rust package manifest
├── Gemfile # Ruby dependencies
├── Rakefile # Build automation
├── ext/ # Native extension directory
│ └── hello_rusty/ # Extension implementation
│ ├── Cargo.toml # Rust crate configuration
│ ├── extconf.rb # Ruby extension configuration
│ └── src/ # Rust source code
│ └── lib.rs # Main implementation
├── hello_rusty.gemspec # Gem specification
└── lib/ # Ruby library code
└── hello_rusty.rb # Main Ruby module
Implementation Details
Ruby Extension Configuration (extconf.rb)
We use rb-sys's ExtensionTask to set up the build process for our native extension. This file remains simple:
# ext/hello_rusty/extconf.rb
require 'rb_sys/extensiontask'
RbSys::ExtensionTask.new('hello_rusty') do |ext|
ext.lib_dir = File.expand_path('../../lib/hello_rusty', __dir__)
end
The Rust Implementation (lib.rs)
Here, instead of using the low-level rb-sys APIs directly, we'll use magnus to create a safer and more ergonomic implementation.
First, let's update our Cargo.toml for the extension:
# ext/hello_rusty/Cargo.toml
[package]
name = "hello_rusty"
version = "0.1.0"
edition = "2021"
[dependencies]
magnus = "0.7"
[lib]
crate-type = ["cdylib"]
Now, the Rust code in ext/hello_rusty/src/lib.rs:
use magnus::{function, prelude::*, Error, Ruby};
// The Ruby-visible function. It takes no arguments and returns a String.
fn greet() -> String {
"Hello from Rust!".to_string()
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("HelloRusty")?;
module.define_singleton_method("greet", function!(greet, 0))?;
Ok(())
}
Key Improvements:
#[magnus::init]: This attribute macro handles the boilerplate of setting up theInit_...function. It provides a safe, managedRubycontext.RubyHandle: Theinitfunction receives a&Rubyhandle, which is a token that ensures the Ruby VM is properly initialized. All interactions with Ruby happen through this handle.- Result-based Error Handling: The
initfunction returns aResult<(), magnus::Error>. If any of thedefine_moduleordefine_singleton_methodcalls fail, the error is propagated and converted into a Ruby exception. This is much safer thanunsafecalls that could crash the interpreter. function!macro: This macro frommagnuswraps our Rust function (greet) and makes it callable from Ruby, automatically handling the conversion of arguments and return values.
The Ruby Wrapper (lib/hello_rusty.rb)
The Ruby side remains simple, loading the compiled extension:
require_relative "hello_rusty/version"
require_relative "hello_rusty/hello_rusty" # This loads the .so/.dylib file
module HelloRusty
class Error < StandardError; end
# Your Ruby code goes here...
end
Trying It Out
Once the gem is compiled (e.g., via bundle exec rake compile), you can use it in Ruby:
require 'hello_rusty'
puts HelloRusty.greet
# => "Hello from Rust!"
This refactored example provides a much stronger foundation for building real-world Rusty Ruby gems by emphasizing safety, error handling, and maintainability.