RCEE::System
This gem is part of the Ruby C Extensions Explained project at https://github.com/flavorjones/ruby-c-extensions-explained
Context
In the isolated
gem, I mentioned that one goal of a C extension might be to optimize performance. This is the case for BCrypt.
But there's another, more common, reason to write a C extension, which is to talk to a third-party library. Many Ruby gems use C extensions solely to integrate with a third-party library. Some examples:
- nokogiri → libxml2, libxslt, libgumbo
- psych → libyaml
- sqlite3 → libsqlite3
- rmagick → libMagick
- grpc → libgrpc
These gems have a thin-ish wrapper of Ruby and C that work together to make the library's features available as idiomatic Ruby.
Summary
This gem, as well as all the following gems, will call libyaml
as an example third-party integration, and will require that libyaml
be installed ahead of time on the target system.
Some real-world gems that use this "system" strategy are ruby-sqlite3 and rmagick.
Details
This gem's C code is located in ext/system/system.c
:
static VALUE
rb_system_extension_class_do_something(VALUE self)
{
int major, minor, patch;
yaml_get_version(&major, &minor, &patch);
return rb_sprintf("libyaml version %d.%d.%d", major, minor, patch);
}
That's pretty simple, but is enough to demonstrate the integration with libyaml
works.
The extconf.rb
is still simple (and similar to isolated/ext/isolated/extconf.rb
but contains this additional block:
unless find_header("yaml.h") && find_library("yaml", "yaml_get_version")
abort("\nERROR: *** could not find libyaml development environment ***\n\n")
end
find_header
and find_library
are MakeMakefile
helper methods which will search your system's standard directories looking for files. If it finds them, it makes sure the compile step will be able to find yaml.h
, and the link step will be able to find the libyaml
library file.
We ask find_header
to look for the yaml.h
header file because that's what our C code needs (see ext/system/system.h
). We ask find_library
to look for a library named libyaml
and check that it has the function yaml_get_version()
defined in it.
(We don't need to call find_library
for every function we intend to use; we just need to provide one function from the library so that MakeMakefile
can verify that linking will succeed.)
If these methods succeed, the Makefile
recipe looks something like this. Note the include directory is added for the compile step, and the library directory and name are added to the link step.
# `create_makefile` recipe is something like this
# compile phase:
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include system.c -o system.o
# link phase:
gcc -shared \
-L/path/to/ruby/lib -lruby \
-L/path/to/libyaml/lib -lyaml \
-lc -lm \
system.o -o system.so
If you run ldd
on the generated system.so
you should see libyaml
listed, something like:
libyaml-0.so.2 => /usr/lib/x86_64-linux-gnu/libyaml-0.so.2 (0x00007f345a3dc000)
Testing
See .github/workflows/system.yml
Key things to note:
- matrix across all supported Rubies and platforms
- use the github action
MSP-Greg/setup-ruby-pkgs@v1
to install system libraries on each platform
What Can Go Wrong
In addition to what's enumerated in isolated
's README ...
If MakeMakefile
methods fail to find the third-party library (or fail to compile and link against it), then the user will see an error message, and have to go figure out how to install libyaml
on their system.
If the third-party library is installed into non-standard directories by the package manager, your extconf.rb
may need special logic. rmagick
needs to do a lot of this.
If the third-party library has compile-time flags to control whether features are turned on or off, then your extconf.rb
may need to test for that with have_func
and your C code will need to handle the case where those methods aren't implemented.
The version of the third-party library may be older or newer than you expected, and either contain bugs or be missing new features, which also require additional code complexity.