A Rubyist's Walk Along the C-side (Part 1): Hello World!

This is an article in a multi-part series called “A Rubyist’s Walk Along the C-side”

In this article, we’ll explore how you can set up and build your very first Ruby C extension. At the end of this article, you will be able to write the following Ruby script, in C!

puts "Hello world!"

This probably doesn’t look very exciting to you, but there’s quite a bit of boilerplate involved before we can actually write this simple script. Let’s get started!

Prerequisites

This article expects that you are familiar with Ruby, and have a basic understanding of C.

You should also have make and gcc (or clang for macOS) installed. For systems with the APT package manager, you can run the following command.

$ sudo apt install -y make gcc

And, of course, you should have a recent Ruby version (e.g. 2.7 or 3.0) installed.

Why do we write C extensions?

It’s no secret that Ruby is not very fast. Ruby’s C APIs are a bridge between the Ruby world and C world. That way, we can write high-performing C code while still being able to do everything we could in Ruby. Examples of gems that require high performance include puma, liquid-c, and bcrypt.

Another reason to write a C extension is when we need to call C libraries. Example gems are nokogiri and mysql2. We could also use ffi to call C functions from Ruby, but I won’t be covering that in this guide.

Should I write my gem as a C extension?

If you’re asking yourself this question, chances are the answer is “no”. C extensions are 10x harder to write, maintain, and understand than regular Ruby code. In a C extension, there are so many more issues we have to consider, such as memory management and type safety, that we usually don’t think about when writing Ruby code. Additionally, the Ruby C API is largely undocumented, making it difficult to use and hard to onboard new developers or contributors.

Writing our “Hello world!” script

You can find the accompanying source code in the GitHub repository peterzhu2118/ruby-c-ext-code

Let’s start writing our C program. Let’s call our program my_c_ext. Create and open the file ext/my_c_ext.c where we will write the code.

The resulting code will look like the following. I’ll explain it line-by-line below.

#include <ruby.h>

void Init_my_c_ext(void)
{
    ID id_puts = rb_intern("puts");

    VALUE hello_world_str = rb_str_new_cstr("Hello world!");
    rb_funcall(rb_mKernel, id_puts, 1, hello_world_str);
}

We start by including the header file for Ruby.

#include <ruby.h>

We can then define the function that will be called when we load this C extension. It must be called Init_<name of file> (Ruby will call this function when we require this C extension) and accept no arguments and returns void.

void Init_my_c_ext(void)

We then call rb_intern, which converts the C string "puts" to the corresponding Ruby symbol :puts. The data type we use here is ID, which is the type in Ruby used for symbols (it’s a type definition for an unsigned long).

ID id_puts = rb_intern("puts");

We then create a new Ruby string out of a C string. Note the type VALUE used here. We’ll be using this type throughout our code as it’s the type used to represent Ruby objects. Just like the type ID, the underlying data type is also an unsigned long.

VALUE hello_world_str = rb_str_new_cstr("Hello world!");

This is where all the magic happens. rb_funcall works like Object.send, we pass in an object instance in the first argument, the symbol for the method we want to call, the number of arguments, and a list of arguments. In our case, the object is the module Kernel (Ruby has builtin variables for various classes). We pass in the symbol :puts that we created a few lines above as the method name. We have just a single argument, and it’s the Ruby string "Hello world!".

rb_funcall(rb_mKernel, id_puts, 1, hello_world_str);

Compilation

Now that we’ve written our C extension, let’s look at how we can compile it. First, we’ll create a file ext/extconf.rb that helps us generate the Makefile that we need.

require "mkmf"

create_makefile "my_c_ext"

This will require the mkmf (make Makefile) library built into Ruby that will generate the Makefile we need to correctly build and link our C extension.

$ ruby ext/extconf.rb
creating Makefile

$ ls ext
extconf.rb  Makefile  my_c_ext.c

We see that after running this script we get a Makefile generated. We can now use this Makefile to build our C extension.

$ cd ext

$ make
compiling my_c_ext.c
linking shared-object my_c_ext.so

$ ls
extconf.rb  Makefile  my_c_ext.c  my_c_ext.o  my_c_ext.so

Using our C extension

In order to use our C extension, we need to create a Ruby script. Let’s create my_c_ext.rb and place the following contents in it.

require_relative "ext/my_c_ext"

You might not know this, require and require_relative can not only load other Ruby files, but can also load shared objects (in our case, it will load ext/my_c_ext.so)!

$ ruby my_c_ext.rb
Hello world!

We can run this script, and we see the expected output!

Summary

In this article, we looked at why Ruby supports C extensions, and how to get started on building your very first C extension. In the next article, we’ll look at how to define methods using the C API.