Frequently Asked Questions
Find answers to common questions about building Ruby extensions with Rust.
General Questions
Q: When should I use Rust for Ruby extensions?
A: Consider Rust when you need:
- CPU-intensive algorithms (parsing, compression, cryptography)
- Memory-intensive operations (image processing, data transformation)
- System-level integration (OS APIs, hardware access)
- Concurrent/parallel processing
- Security-critical code
- Wrapping existing Rust libraries
Q: Is Rust harder to learn than C?
A: Different, not necessarily harder:
Rust challenges:
- Ownership and borrowing concepts
- Stricter compiler
- More complex type system
Rust advantages:
- No segfaults or memory leaks
- Better error messages
- Modern tooling (Cargo, rustfmt, clippy)
- Comprehensive standard library
- Good documentation
Most developers find Rust's learning curve worth it for the safety guarantees.
Q: How much faster will my code be?
A: It depends on what you're doing.
- CPU-bound operations: Often 10-100x faster
- Memory allocation heavy: 5-20x faster
- String processing: 5-50x faster
- Simple operations: May be slower due to FFI overhead
With Ruby's YJIT, pure Ruby is faster than ever. Profile first.
Q: Will this work with Rails?
A: Yes, rb-sys extensions work with Rails.
# Gemfile
gem 'your_rust_extension'
# In your Rails code
class DataProcessor
def process(data)
# Calls your Rust extension
YourRustExtension.process(data)
end
end
Many production Rails apps use Rust extensions for performance-critical paths.
Technical Questions
Q: How do I debug Rust extensions?
A: There are several approaches:
1. Print debugging:
use magnus::{Ruby, Value, value::ReprValue};
fn debug_function(value: String) {
eprintln!("Debug: value = {:?}", value);
// Or use Ruby's puts
let ruby = unsafe { Ruby::get_unchecked() };
let _: Value = ruby.eval(&format!("puts 'Debug: {}'", value))
.unwrap_or_else(|_| ruby.qnil().as_value());
}
2. Use a debugger:
# With LLDB on macOS
lldb -- ruby -r ./lib/my_extension.rb -e "MyExtension.method"
# With GDB on Linux
gdb --args ruby -r ./lib/my_extension.rb -e "MyExtension.method"
3. Logging:
// Add to Cargo.toml
// [dependencies]
// log = "0.4"
// env_logger = "0.10"
// In your code
// log::debug!("Processing {} items", items.len());
Q: How do I handle Ruby version compatibility?
A: rb-sys provides version detection.
// In build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
// rb-sys automatically handles Ruby version detection during build
Ok(())
}
// In your code
// rb-sys-env automatically sets Ruby version cfg flags
#[cfg(ruby_gte_3_0)]
fn ruby3_only_feature() {
// Ruby 3.0+ only code
}
#[cfg(not(ruby_gte_3_0))]
fn ruby2_compat() {
// Ruby 2.x compatible code
}
Q: Can I use async/await in Ruby extensions?
A: Yes, but you need to properly handle async runtimes. Since Ruby extensions are synchronous, you'll need to block on async code.
use magnus::{Error, Ruby};
use std::thread;
use std::sync::mpsc;
// For CPU-intensive operations, release the GVL
fn parallel_operation(data: Vec<u8>) -> Result<Vec<u8>, Error> {
use rb_sys::rb_thread_call_without_gvl;
use std::os::raw::c_void;
struct ProcessData {
input: Vec<u8>,
output: Option<Vec<u8>>,
}
extern "C" fn process_without_gvl(data: *mut c_void) -> *mut c_void {
unsafe {
let process_data = &mut *(data as *mut ProcessData);
// Perform CPU-intensive work here
let processed = process_data.input.iter()
.map(|&b| b.wrapping_add(1))
.collect();
process_data.output = Some(processed);
}
std::ptr::null_mut()
}
let mut process_data = ProcessData {
input: data,
output: None,
};
unsafe {
rb_thread_call_without_gvl(
Some(process_without_gvl),
&mut process_data as *mut _ as *mut c_void,
None,
std::ptr::null_mut(),
);
}
process_data.output.ok_or_else(|| {
Error::new(magnus::exception::runtime_error(), "Processing failed")
})
}
Q: How do I distribute my gem?
A: There are multiple strategies:
1. Source gems (easiest):
# In gemspec
# spec.extensions = ["ext/my_gem/extconf.rb"]
# spec.files = Dir["ext/**/*.{rs,toml,rb}", "lib/**/*.rb"]
# Users need Rust installed
2. Pre-compiled gems (best UX):
# Use rake-compiler-dock
rake gem:native
# Creates platform-specific gems:
# - my_gem-1.0.0-x86_64-linux.gem
# - my_gem-1.0.0-x86_64-darwin.gem
# - my_gem-1.0.0-arm64-darwin.gem
3. GitHub Actions (automated):
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: oxidize-rb/actions/setup-ruby-and-rust@v1
- uses: oxidize-rb/actions/cross-gem@v1
Performance Questions
Q: Why is my extension slower than expected?
A: There are common causes:
1. Too many conversions:
use magnus::{Error, RArray, TryConvert};
// Bad: Converts entire array upfront
fn sum(numbers: Vec<i64>) -> i64 {
numbers.iter().sum()
}
// Good: Iterates without conversion
fn sum_good(array: RArray) -> Result<i64, Error> {
let mut total = 0;
for item in array.each() {
total += i64::try_convert(item?)?;
}
Ok(total)
}
2. Not releasing the GVL:
fn release_gvl_example(data: &[u8]) {
// Release GVL for operations > 100ms
if data.len() > 1_000_000 {
// Use rb_thread_call_without_gvl
}
}
3. Excessive allocations:
fn excessive_allocations() {
// Bad: Allocates repeatedly
for i in 0..1000 {
let mut vec: Vec<i32> = Vec::new();
// ...
}
// Good: Reuse allocation
let mut vec: Vec<i32> = Vec::with_capacity(1000);
for i in 0..1000 {
vec.clear();
// ...
}
}
Q: How do I profile my extension?
A: Use cargo's built-in profiling.
# Add to Cargo.toml
# [profile.release]
# debug = true
# Profile with flamegraph
cargo install flamegraph
# sudo cargo flamegraph --bin my_extension
# Or use Ruby's profiling tools
# ruby -r profile -r ./lib/my_extension.rb -e "MyExtension.slow_method"
Memory Questions
Q: How do I prevent memory leaks?
A: Follow these patterns:
1. Always mark Ruby references:
use magnus::{RString};
#[magnus::wrap(class = "MyClass")]
struct MyClass {
// Store the string data instead of Ruby Value
string_data: String,
}
impl MyClass {
fn new(ruby_string: RString) -> Result<Self, magnus::Error> {
Ok(MyClass {
string_data: ruby_string.to_string()?,
})
}
}
2. Use RAII patterns:
use std::path::PathBuf;
// Resources are automatically cleaned up
struct TempFile {
path: PathBuf,
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
3. Test with GC stress:
# In your tests
# GC.stress = true
# YourExtension.method_that_allocates
# GC.stress = false
Q: Can I share memory between Ruby and Rust?
A: Yes, but carefully.
use magnus::{Error, RString};
// Zero-copy string access
fn process_string(ruby_str: RString) -> Result<(), Error> {
let bytes = unsafe { ruby_str.as_slice() };
// Process bytes without copying
Ok(())
}
// Shared memory with TypedData
struct SharedBuffer {
data: Vec<u8>,
}
impl SharedBuffer {
fn get_slice(&self, start: usize, len: usize) -> &[u8] {
&self.data[start..start + len]
}
}
Error Handling Questions
Q: How should I handle panics?
A: Avoid them entirely.
use magnus::{Error, exception};
// Bad: Panics crash Ruby
fn bad_function(index: usize, data: Vec<String>) -> String {
data[index].clone() // Panics on out of bounds
}
// Good: Return errors
fn good_function(index: usize, data: Vec<String>) -> Result<String, Error> {
data.get(index)
.cloned()
.ok_or_else(|| Error::new(
exception::index_error(),
format!("index {} out of bounds", index)
))
}
fn potentially_panicking_code() {}
// If you must use code that might panic
use std::panic;
fn safe_wrapper() -> Result<(), Error> {
panic::catch_unwind(|| {
potentially_panicking_code()
}).map_err(|_| Error::new(
exception::runtime_error(),
"operation failed"
))
}
Deployment Questions
Q: Do users need Rust installed?
A: It depends on the distribution method:
- Source gems: Yes, users need Rust
- Pre-compiled gems: No, includes compiled binary
- Bundled with vendored: No, but larger gem size
Q: How do I support multiple platforms?
A: Use rb-sys's cross-compilation.
# Install cross-compilation tool
gem install rb_sys
# Build for multiple platforms
# bundle exec rb-sys-dock --platforms x86_64-linux,aarch64-linux
# Or use GitHub Actions
# See oxidize-rb/cross-gem-action
Q: What about Ruby version compatibility?
A: rb-sys handles most differences.
# Cargo.toml
# [dependencies]
# rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
# Supports Ruby 2.6 through 3.3+
Troubleshooting
Q: "undefined symbol" errors?
A: There are common causes:
- Missing rb-sys dependency
- Wrong library type in Cargo.toml - use
crate-type = ["cdylib"]
- Name mismatch between Rust and Ruby
Q: "libclang not found" during installation?
A: Install libclang.
# macOS
brew install llvm
# Ubuntu/Debian
apt-get install libclang-dev
# Or use the gem
gem install libclang
Q: Extension works locally but fails in production?
A: Check the following:
- Platform compatibility - build for production architecture
- Ruby version - ensure compatibility
- Dependencies - system libraries available?
- Compilation flags - release vs debug build
Getting Help
Q: Where can I get help?
A: There are multiple resources:
- Oxidize Slack - Active community
- GitHub Discussions - Q&A forum
- GitHub Issues - Bug reports
- Examples Repository - Working code
Q: How can I contribute?
A: We welcome contributions.
- Documentation: Fix typos, add examples
- Code: Fix bugs, add features
- Examples: Share your extensions
- Community: Help others, share knowledge
See our Contributing Guide for details.