Project Structure

In this chapter, we'll explore how to set up and organize a Ruby gem with a Rust extension. We'll focus on practical patterns and highlight how to leverage valuable Rust libraries without introducing unnecessary complexity.

Enhanced Project Structure

Building on the structure created by bundle gem --ext=rust, a well-organized rb-sys project typically looks like this:

my_gem/
├── Cargo.toml                # Rust workspace configuration
├── Gemfile                   # Ruby dependencies
├── Rakefile                  # Build tasks
├── my_gem.gemspec            # Gem specification
├── ext/
│   └── my_gem/
│       ├── Cargo.toml        # Rust crate configuration
│       ├── extconf.rb        # Ruby extension configuration
│       └── src/
│           └── lib.rs        # Main Rust entry point
├── lib/
│   ├── my_gem.rb             # Main Ruby file
│   └── my_gem/
│       └── version.rb        # Version information
└── test/                     # Tests

Let's examine a practical example using a useful but simple Rust library.

Example: URL Parsing with the url crate

The url crate, developed by the Servo team, is a robust implementation of the URL Standard. It provides accurate URL parsing that would be complex to implement from scratch. Here's a simple example:

1. Extension Cargo.toml

# ext/url_parser/Cargo.toml
[package]
name = "url_parser"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
license = "MIT"
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
# High-level Ruby bindings with rb-sys feature
magnus = { version = "0.7", features = ["rb-sys"] }

# The main Rust library we're wrapping
url = "2.4"

[build-dependencies]
rb-sys-env = "0.1"

2. Main Rust Implementation

#![allow(unused)]
fn main() {
// ext/url_parser/src/lib.rs
use magnus::{define_module, define_class, function, method, prelude::*, Error, Ruby};
use url::Url;

// Simple URL wrapper class
struct UrlWrapper {
    inner: Url,
}

#[magnus::wrap(class = "UrlParser::URL")]
impl UrlWrapper {
    // Parse a URL string
    fn parse(url_str: String) -> Result<Self, Error> {
        match Url::parse(&url_str) {
            Ok(url) => Ok(UrlWrapper { inner: url }),
            Err(err) => {
                Err(Error::new(magnus::exception::arg_error(), format!("Invalid URL: {}", err)))
            }
        }
    }

    // Basic getters
    fn scheme(&self) -> String {
        self.inner.scheme().to_string()
    }

    fn host(&self) -> Option<String> {
        self.inner.host_str().map(|s| s.to_string())
    }

    fn path(&self) -> String {
        self.inner.path().to_string()
    }

    fn query(&self) -> Option<String> {
        self.inner.query().map(|s| s.to_string())
    }

    // String representation of the URL
    fn to_string(&self) -> String {
        self.inner.to_string()
    }
}

// Module-level utilities
fn is_valid_url(url_str: String) -> bool {
    Url::parse(&url_str).is_ok()
}

// Module init function - Ruby extension entry point
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    // Define the main module
    let module = ruby.define_module("UrlParser")?;

    // Add utility function at module level
    module.define_singleton_method("valid_url?", function!(is_valid_url, 1))?;

    // Define and configure the URL class
    let class = module.define_class("URL", ruby.class_object())?;
    class.define_singleton_method("parse", function!(UrlWrapper::parse, 1))?;

    // Instance methods
    class.define_method("scheme", method!(UrlWrapper::scheme, 0))?;
    class.define_method("host", method!(UrlWrapper::host, 0))?;
    class.define_method("path", method!(UrlWrapper::path, 0))?;
    class.define_method("query", method!(UrlWrapper::query, 0))?;
    class.define_method("to_s", method!(UrlWrapper::to_string, 0))?;

    Ok(())
}
}

3. Ruby Integration

# lib/url_parser.rb
require_relative "url_parser/version"
require_relative "url_parser/url_parser"

module UrlParser
  class Error < StandardError; end

  # Parse a URL string and return a URL object
  def self.parse(url_string)
    URL.parse(url_string)
  rescue => e
    raise Error, "Failed to parse URL: #{e.message}"
  end

  # Check if a URL has an HTTPS scheme
  def self.https?(url_string)
    return false unless valid_url?(url_string)
    url = parse(url_string)
    url.scheme == "https"
  end
end

4. Simple Tests

# test/test_url_parser.rb
require "test_helper"

class TestUrlParser < Minitest::Test
  def test_basic_url_parsing
    url = UrlParser::URL.parse("https://example.com/path?query=value")

    assert_equal "https", url.scheme
    assert_equal "example.com", url.host
    assert_equal "/path", url.path
    assert_equal "query=value", url.query
  end

  def test_url_validation
    assert UrlParser.valid_url?("https://example.com")
    refute UrlParser.valid_url?("not a url")
  end

  def test_https_check
    assert UrlParser.https?("https://example.com")
    refute UrlParser.https?("http://example.com")
  end

  def test_invalid_url_raises_error
    assert_raises UrlParser::Error do
      UrlParser.parse("not://a.valid/url")
    end
  end
end

Key Aspects of this Project

1. Simplicity with Value

This example demonstrates how to:

  • Wrap a useful Rust library (url) with minimal code
  • Expose only the most essential functionality
  • Handle errors properly
  • Integrate with Ruby idiomatically

2. Why Use Rust for URL Parsing?

Ruby has URI handling in its standard library, but the Rust url crate offers advantages:

  • Full compliance with the URL standard used by browsers
  • Better handling of internationalized domain names (IDNs)
  • Robust error detection
  • Significant performance benefits for URL-heavy applications

3. Project Organization Principles

  • Keep dependencies minimal: Just what you need, nothing more
  • Clean public API: Expose only what users need
  • Proper error handling: Map Rust errors to meaningful Ruby exceptions
  • Simple tests: Verify both functionality and edge cases