Skip to main content

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 the Init_... function. It provides a safe, managed Ruby context.
  • Ruby Handle: The init function receives a &Ruby handle, 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 init function returns a Result<(), magnus::Error>. If any of the define_module or define_singleton_method calls fail, the error is propagated and converted into a Ruby exception. This is much safer than unsafe calls that could crash the interpreter.
  • function! macro: This macro from magnus wraps 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.