Skip to main content

Julia Evans

My first Rust macro

Last night I wrote a Rust macro for the first time!! The most striking thing to me about this was how easy it was – I kind of expected it to be a weird hard finicky thing, and instead I found that I could go from “I don’t know how macros work but I think I could do this with a macro” to “wow I’m done” in less than an hour.

I used these examples to figure out how to write my macro.

what’s a macro?

There’s more than one kind of macro in Rust –

  • macros defined using macro_rules (they have an exclamation mark and you call them like functions – my_macro!())
  • “syntax extensions” / “procedural macros” like #[derive(Debug)] (you put these like annotations on your functions)
  • built-in macros like println!

Macros in Rust and Macros in Rust part II seems like a nice overview of the different kinds with examples

I’m not actually going to try to explain what a macro is, instead I will just show you what I used a macro for yesterday and hopefully that will be interesting. I’m going to be talking about macro_rules!, I don’t understand syntax extension/procedural macros yet.

compiling the get_stack_trace function for 30 different Ruby versions

I’d written some functions that got the stack trace out of a running Ruby program (get_stack_trace). But the function I wrote only worked for Ruby 2.2.0 – here’s what it looked like. Basically it imported some structs from bindings::ruby_2_2_0 and then used them.

use bindings::ruby_2_2_0::{rb_control_frame_struct, rb_thread_t, RString};
fn get_stack_trace(pid: pid_t) -> Vec<String> {
    // some code using rb_control_frame_struct, rb_thread_t, RString
}

Let’s say I wanted to instead have a version of get_stack_trace that worked for Ruby 2.1.6. bindings::ruby_2_2_0 and bindings::ruby_2_1_6 had basically all the same structs in them. But bindings::ruby_2_1_6::rb_thread_t wasn’t the same as bindings::ruby_2_2_0::rb_thread_t, it just had the same name and most of the same struct members.

So I could implement a working function for Ruby 2.1.6 really easily! I just need to basically replace 2_2_0 for 2_1_6, and then the compiler would generate different code (because rb_thread_t is different). Here’s a sketch of what the Ruby 2.1.6 version would look like:

use bindings::ruby_2_1_6::{rb_control_frame_struct, rb_thread_t, RString};
fn get_stack_trace(pid: pid_t) -> Vec<String> {
    // some code using rb_control_frame_struct, rb_thread_t, RString
}

what I wanted to do

I basically wanted to write code like this, to generate a get_stack_trace function for every Ruby version. The code inside get_stack_trace would be the same in every case, it’s just the use bindings::ruby_2_1_3 that needed to be different

pub mod ruby_2_1_3 {
    use bindings::ruby_2_1_3::{rb_control_frame_struct, rb_thread_t, RString};
    fn get_stack_trace(pid: pid_t) -> Vec<String> {
        // insert code here
    }
}
pub mod ruby_2_1_4 {
    use bindings::ruby_2_1_4::{rb_control_frame_struct, rb_thread_t, RString};
    fn get_stack_trace(pid: pid_t) -> Vec<String> {
        // same code
    }
}
pub mod ruby_2_1_5 {
    use bindings::ruby_2_1_5::{rb_control_frame_struct, rb_thread_t, RString};
    fn get_stack_trace(pid: pid_t) -> Vec<String> {
        // same code
    }
}
pub mod ruby_2_1_6 {
    use bindings::ruby_2_1_6::{rb_control_frame_struct, rb_thread_t, RString};
    fn get_stack_trace(pid: pid_t) -> Vec<String> {
        // same code
    }
}

macros to the rescue!

This really repetitive thing was I wanted to do was a GREAT fit for macros. Here’s what using macro_rules! to do this looked like!

macro_rules! ruby_bindings(
    ($ruby_version:ident) => (
    pub mod $ruby_version {
        use bindings::$ruby_version::{rb_control_frame_struct, rb_thread_t, RString};
        fn get_stack_trace(pid: pid_t) -> Vec<String> {
            // insert code here
        }
    }
));

I basically just needed to put my code in and insert $ruby_version in the places I wanted it to go in. So simple! I literally just looked at an example, tried the first thing I thought would work, and it worked pretty much right away.

(the actual code is more lines and messier but the usage of macros is exactly as simple in this example)

I was SO HAPPY about this because I’d been worried getting this to work would be hard but instead it was so easy!!

dispatching to the right code

Then I wrote some super simple dispatch code to call the right code depending on which Ruby version was running!

    let version = get_api_version(pid);
    let stack_trace_function = match version.as_ref() {
        "2.1.1" => stack_trace::ruby_2_1_1::get_stack_trace,
        "2.1.2" => stack_trace::ruby_2_1_2::get_stack_trace,
        "2.1.3" => stack_trace::ruby_2_1_3::get_stack_trace,
        "2.1.4" => stack_trace::ruby_2_1_4::get_stack_trace,
        "2.1.5" => stack_trace::ruby_2_1_5::get_stack_trace,
        "2.1.6" => stack_trace::ruby_2_1_6::get_stack_trace,
        "2.1.7" => stack_trace::ruby_2_1_7::get_stack_trace,
        "2.1.8" => stack_trace::ruby_2_1_8::get_stack_trace,
        // and like 20 more versions
        _ => panic!("OH NO OH NO OH NO"),
    };

it works!

I tried out my prototype, and it totally worked! The same program could get stack traces out the running Ruby program for all of the ~10 different Ruby versions I tried – it figured which Ruby version was running, called the right code, and got me stack traces!!

Previously I’d compile a version for Ruby 2.2.0 but then if I tried to use it for any other Ruby version it would crash, so this was a huge improvement.

There are still more issues with this approach that I need to sort out. The two main ones right now are: firstly the ruby binary that ships with Debian doesn’t have symbols and I need the address of the current thread, and secondly it’s still possible that #ifdefs will ruin my day.

Debugging a segfault in my Rust program A perf cheat sheet