Skip to main content

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:

  1. Missing rb-sys dependency
  2. Wrong library type in Cargo.toml - use crate-type = ["cdylib"]
  3. 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:

  1. Platform compatibility - build for production architecture
  2. Ruby version - ensure compatibility
  3. Dependencies - system libraries available?
  4. Compilation flags - release vs debug build

Getting Help

Q: Where can I get help?

A: There are multiple resources:

  1. Oxidize Slack - Active community
  2. GitHub Discussions - Q&A forum
  3. GitHub Issues - Bug reports
  4. 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.


Still Have Questions?

Join our Slack community - we are happy to help.