Having renewed my love for my Raspberry Pi, I wanted to make a quick library to access a temperature sensor through Ruby. This sensor uses a single-bus format, and one communication lasts about 4ms. I felt like this was good enough of an excuse to play with the native layer of Ruby. Here is how I ended up using rake-compiler to build native Ruby extensions.
Quick disclaimer: most of what I did was heavily inspired by a RubyGems guide. I’ll try to point out things that I had most issues with or that I found particularly interesting. All the code associated with this post is hosted on my GitHub project.
Adding a dependency on an external library
Adding a dependency on an external library was one of the things that were not completely clear. In my case, I needed to use libgpiod. In order to do so, I had to modify extconf.rb to add the following lines:
abort('missing "gpiod.h"') unless find_header('gpiod.h', ['/usr/include'])
abort('missing libgpiod') unless find_library('gpiod', 'gpiod_chip_open_lookup')
These two lines ensure that the generated Makefile will include the right search directories for the headers and link to the right library.
Controlling the native classes’ modules
I ended up with a really simple solution for handling my modules: the module is created in Ruby, and then fetched in C and used there. In the sample project, the module creation is done in the main Ruby file and then used when initializing the C module.
Getting the module was done easily: rb_const_get(rb_cObject, rb_intern("NCI")). Once this was done, creating the class was done using the handy rb_define_class_under: rb_define_class_under(mNCI, "NCINativeDevice", rb_cObject). The third parameter to this last function is the superclass, in this case the Object class.
Associating a C structure with the Ruby instance
The first function (_alloc) is used to allocate an empty C structure. This function is not the initializer, it will not get any of the arguments passed on initialization. The important part of the function is the call to Data_Make_Struct. This function will allocate the given C structure, but won’t initialize it for you.
The second function (_init) is the true initializer. It will receive the arguments that are passed in the Ruby code. In order to store these values in the C structure, you need to extract the C pointer from the instance. This is done using Data_Get_Struct. Weirdly enough, this won’t return the pointer to the C structure, it stores it in the last argument.
The last function (_free) is the most important of all. It is responsible for deallocating whatever you allocated. The C structure (allocated for you by Data_Make_Struct) does not need to be deallocated, the system will do it for you.