Working with ruby C extensions on Mac

Ulysse BUONOMO
Klaxit Tech Blog
Published in
6 min readJan 18, 2022

I’ve been working more and more with the Ruby C API at Klaxit, either to develop a simple gem that quickly encodes/decodes polylines or to maintain a well-established topology gem called RGeo (~PostGIS in Ruby).

One issue I’ve encountered is the lack of documentation, or at least of entry points to dig further (and you’ll have to dig at some point!). This blog post is an attempt to help you fill this gap. My goal is to provide pointers, not to write a complete guide on how to implement a simple C extension. To take the most of this article, I’d suggest you try compiling a very simple extension beforehand, tenderlove may guide you through that task.

Topics: getting ready | debugging | benchmarking | documenting | keep learning

Getting ready

First, you’ll need to download the ruby codebase : a lot of the C extension stuff is not that much documented. I’d also suggest you to read at least the Header and source files overview by the ruby core team.

You will have to frequently refer to that codebase while you’re coding, and that’s fine, because if you are reading this article, you are most certainly already working on something relevant. At this point, I’d also suggest you to read the great book ruby hacking guide which deep-dives into ruby 2.1 internals, which are slightly outdated, but enough to get the big picture.

One last thing before continuing. Since your C extensions will most likely end up on rubygems.org, I suggest reading their Gems with extensions guide to understanding how your codebase should be structured. For a simple example, you can have a look at github.com/klaxit/fast-polylines which is a repo I’ll mention a few more times in this article.

Debugging

I’ve not found yet a simple way to plug a gdb into my C extensions on my MacBook (/u/ulysse_bn if you know on that topic).

But I still find my way through nonetheless as I am a puts debuggerer. And this applies to C extensions as well:

// Quickly print a `VALUE`
VALUE my = some_factory();
rb_p(my);

If you want to do more than just rb_p or rb_obj_method, you should take a look at ruby/debug.h. It contains some nice debugging tools such as rb_tracepoint_{new,enable} for internal tracing. You'll have to #include <ruby/debug.h> to use it. This helped us implement the new compaction API on RGeo by checking whether we were GCing or not.

Another way to debug ruby C extensions, may be to debug solely the C part: including ruby.h is enough to compile some C, which can be debugged as you would for any C codebase.

Benchmarking

Here is what I have found so far…

Flamegraph

These are useful whenever you want to have the big picture of where a bottleneck can be. On a Mac, you can use dtrace to generate a flamegraph. But beware, you'll have to mess with csrutil (which logged me out of basically everything: be ready to type some passwords).

Here’s an example generation I made for fast_polylines:

#!/usr/bin/env zsh
# Run with sudo
die() {
>&2 echo $1
exit 1
}
case $1 in
encode)
ruby -Iext -Ilib -rfast_polylines -e '
POINTS = [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]
loop { FastPolylines.encode(POINTS) }
' &
;;
decode)
ruby -Iext -Ilib -rfast_polylines -e '
POLYLINE = "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
loop { FastPolylines.decode(POLYLINE) }
' &
;;
*) die '$1 must be encode or decode'
esac
ruby_pid=$!# BONUS: open instruments from your terminal!
# instruments -l 30000 -t Time\ Profiler -p $ruby_pid
sleep 2temp_dir=$(mktemp -d)# >>>>>>> dtrace call <<<<<<<<<
dtrace -x ustackframes=100 -n "profile-97 /pid == $ruby_pid && arg1/ { @[ustack()] = count(); } tick-10s { exit(0); }" -o $temp_dir/out.user_stacks
kill -term $ruby_pidpushd $temp_dirgit clone git@github.com:brendangregg/FlameGraph.git fg
./fg/stackcollapse.pl out.user_stacks > out.folded
./fg/flamegraph.pl out.folded > flamegraph.svg
popdmv $temp_dir/flamegraph.svg .
rm -rf $temp_dir

You’ll have a nice flamegraph. To learn how to interpret them, I would suggest you start here.

Also, note the dtrace call in my example: I've sampled the user (not kernel) trace at a frequency of 97 Hz. You may want to do things slightly differently. I've taken this example directly on the flamegraph repo, pick the one you prefer.

Instrument

On your Mac, go to Instruments > Time Profiler. If it is still close
to the Big Sur era, you should see a breadcrumb, near the record button. Click on that breadcrumb and select your running ruby program. Emphasis on running, it is way easier to debug an already running program than to find how to tell Instrument to start one (IMHO). Then, you'll have access to a timeline that is made of time samplings of your application. The bigger the weight, the more time it takes overall.

This is a bit faster to get than flamegraphs and can be sufficient. I’ve used it when switching fast-polylines to a C ext and it hinted me cleverly that I was using too much rb_str_new0 at that time.

Documenting

You can also write doc for a C extension. The main goal here will be to target
correct API documentation for rubydoc.info. This website uses yardoc which internally uses RDoc, and more specifically for us: RDoc::Parser::C.

There are some key points here, hence I’ve made an example by adding API doc to FastPolylines.

In a few steps:

  • Follow RDoc::Parser::C conventions, if you are unsure, check how it is done with examples from ruby source.
/*
* call-seq:
* Thread.start([args]*) {|args| block } -> thread
* Thread.fork([args]*) {|args| block } -> thread
*
* Basically the same as ::new. However, if class Thread is subclassed, then
* calling +start+ in that subclass will not invoke the subclass's
* +initialize+ method.
*/
static VALUE
thread_start(VALUE klass, VALUE args)
  • Make sure to include files you want, and have a .yardopts file that indicates paths you want to parse and the kind of formatting you want (I favor Markdown). This file must be included in your .gemspec file!
  • Test with gem install yard webrick; yard; ruby -run -e httpd path/to/doc -p 1234
  • Check after deploying if rubydoc.info renders as you wish, you can import
    a project directly from github, before publishing to rubygem. This will reduce the feedback loop: rubydoc.info/github (button Add project). This is important because rubydoc.info may use another version of yardoc.

Another way to document C methods is also to have simple wrapping in ruby, so that ruby readers can look under your lib/ folder and see your API, the convention we use in RGeo for that is to prefix C methods with an underscore and call them in ruby:

# Returns the resolution used by buffer calculations on geometries
# created by this factory
def buffer_resolution
_buffer_resolution
end

Keep learning

Ruby’s codebase is always evolving, and quite rapidly. You should always check what are the implications of new minors or majors on C extensions. For instance, Ruby 2.7 introduced GC Compaction, which led to a lot of segfaults in a lot of repositories for the brave ones using it (GC.compact). I won't go into details on how to avoid those issues, but here are some examples on how to fix segv, take advantage of compaction, and how to test this.

Another example is the compatibility break for keyword argument parameters that happened in ruby 2.7 as well, the core team gave C extension developers the required tools to have their code compile in every ruby versions, we should be wise to use that!

A good way to keep in touch with ruby’s C side for me is to look at the source when reviewing documentation, it is a good way to discover C counterpart of methods you usually use in ruby.

Wrapping up

I’ve tried to give a quick overview of the tools and methods I use to understand the MRI, and code robust C extensions. If you want to go a bit further, Peter Zhu is covering the same subject, but in a much more complete fashion: blog.peterzhu.ca/ruby-c-ext.

As a closing (and almost not trolling) note: Tenderlove stated that the best way to write C extensions that doesn’t fail is to not write those, using pure ruby instead. Of course, you cannot meet pure ruby performances with C, but keep in mind that using extensions is a costly tradeoff, use at your own risk.

Please drop a comment below if you still have questions, or if you think I missed important entry points. I’m also available on /u/ulysse_bn or https://ulysse.md if you want to talk C ext :)

--

--