rb-sys
website·Indexed 32 days ago
https://oxidize-rb.orgLow-level Rust bindings for the Ruby C API used to build safe and high-performance Ruby extensions. The documentation covers memory management with Ruby's GC, type conversions, error mapping between Rust and Ruby, project structure for gems, and the use of rb-sys-test-helpers for integration testing.
What's inside rb-sys
- rb-sys is a toolchain designed to integrate Rust's performance, memory safety, and modern tooling into the Ruby ecosystem. It simplifies the FFI (Foreign Function Interface) integration between Ruby and Rust by providing simplified bindings to the Ruby C API, allowing developers to write CPU-intensive bottlenecks (such as parsing, image processing, and complex computations) in Rust while maintaining a Ruby application structure.
Understand the Ruby-Rust bridge architecture
The connection between Ruby and Rust is implemented in layers:rb-sysprovides the low-level,unsafebindings to Ruby's C API, while higher-level libraries like Magnus provide a safe and ergonomic wrapper for developers.Choose between rb-sys and Magnus for Ruby extensions
When building Ruby extensions with Rust, you can choose between low-level control via
rb-sysor high-level convenience viaMagnus.Aspect Direct rb-sys Magnus (High-Level) Control Maximum control over Ruby VM Abstracts away VM details Safety Requires unsafeblocksMostly safe API Verbosity Verbose; manual type conversion Concise; automatic type conversion Error Handling Manual check of Ruby error state Idiomatic Result<T, Error>Best For Performance-critical code, internal APIs Most general-purpose gems For most projects, Magnus is recommended. Use
rb-sysonly for critical performance requirements or when accessing Ruby internals not covered by wrappers.Structure a Ruby gem with Rust extensions
A production rb-sys project follows a specific directory structure to separate Ruby logic, Rust extension code, and build configurations. The root contains the workspace configuration and gem specifications, while theext/directory houses the Rust crates.my_gem/ ├── 📦 Cargo.toml # Workspace configuration ├── 💎 Gemfile # Ruby dependencies ├── 🔨 Rakefile # Build automation ├── 📋 my_gem.gemspec # Gem specification ├── 📝 README.md # Documentation ├── 🔒 Gemfile.lock # Locked dependencies ├── 📂 ext/ │ └── my_gem/ │ ├── 🦀 Cargo.toml # Extension dependencies │ ├── 🔧 extconf.rb # Build configuration │ ├── 🏗️ build.rs # Build script (optional) │ └── 📂 src/ │ ├── 📄 lib.rs # Rust entry point │ └── 📄 *.rs # Additional modules ├── 📂 lib/ │ ├── 💎 my_gem.rb # Ruby entry point │ └── 📂 my_gem/ │ ├── 📄 version.rb # Version constant │ └── 📄 *.rb # Ruby modules ├── 🧪 test/ # Test files ├── 📊 benchmark/ # Performance tests └── 📚 examples/ # Usage examplesUnderstand the oxidize-rb technology stack
The oxidize-rb ecosystem provides a layered approach to building Ruby extensions:
- Ruby Code: The end-user interface calling the library.
- Magnus: A high-level, ergonomic Rust API that handles Ruby objects safely.
- rb-sys: Low-level bindings to the Ruby C API.
- Rust Ecosystem: Access to any crate available on crates.io.
Understand rb-sys and Magnus core concepts
rb-sys provides low-level Rust bindings to Ruby's C API, serving as the foundation for building Ruby extensions in Rust. Magnus is a high-level Rust library built on top of rb-sys that provides a safer and more ergonomic API for developers.Understand the Rust-based Ruby gem compilation process
When running
bundle exec rake compile, the build process follows these four steps:extconf.rbexecution: Ruby'smkmflibrary executes the extension'sextconf.rbscript.- Makefile generation: The
create_rust_makefilehelper fromrb-sysgenerates aMakefileconfigured for the Rust code. cargo buildinvocation: TheMakefiletriggerscargo buildto compile the Rust crate into a dynamic shared library.- Library copying: The resulting compiled library is moved into the gem's
libdirectory for Ruby to load.
Core principles for building Ruby extensions with Magnus
When developing Ruby extensions in Rust using Magnus, follow these critical guidelines:
- Error Handling: Use
Result<T, magnus::Error>for any function that can fail. This ensures Rust errors are automatically mapped to Ruby exceptions. - Type Conversions: Use Magnus's automatic type conversions for simple types (e.g.,
String,i64,Vec<T>). - Performance/Complexity: For performance-critical code or complex types, use Ruby-specific objects like
RString,RArray, andRHashinstead of standard Rust types. - Stability: Never use
panic!. A panic in Rust will crash the entire Ruby process.
- Error Handling: Use
Build a Ruby extension using Magnus
Magnus provides a high-level, ergonomic API that handles automatic type conversion and uses Rust'sResultfor error handling, significantly reducing the need forunsafecode. It is suitable for most general-purpose Ruby gems.use magnus::{function, prelude::*, Error, Ruby}; fn hello(subject: String) -> String { format!("Hello from Rust, {subject}!") } #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error> { let module = ruby.define_module("StringUtils")?; module.define_singleton_method("hello", function!(hello, 1))?; Ok(()) }Choose the right Ruby-Rust integration approach
Depending on your goal, use different tools for interacting with Ruby from Rust:
- Raw C API access: Use
rb-sysdirectly forunsafeaccess tolibruby. - Gem Development: Prefer high-level libraries like Magnus (with the
rb-sysfeature) for a safer API. - Embedding Ruby: Use
rb-syswith thelink-rubyfeature to embed the Ruby VM in a Rust binary.
- Raw C API access: Use
Debug Rust extensions
To debug a Rust extension, compile the code with debug symbols and use standard debuggers such asgdborlldb.Handle Ruby nil values in Rust
Ruby'snilis a special value. When converting amagnus::Valueto a Rust type,TryConvert::try_convert(val).ok()will returnNoneif the value isnilor of an incompatible type. To returnnilto Ruby, useruby.qnil().as_value().use magnus::{Value, Ruby, RString, TryConvert, Error}; fn handle_nil(_ruby: &Ruby, val: Value) -> Result<(), Error> { // Returns None if val is nil or not a string let maybe_string: Option<RString> = TryConvert::try_convert(val).ok(); Ok(()) } fn returns_nil(ruby: &Ruby) -> Value { ruby.qnil().as_value() }