Video transcript & code
In episode #386, we learned a sneaky way to discover the operating system's error message for a given error number, without actually triggering the error.
We have to initialize a new
SystemCallError object with an error number and no message.
Then we can plug in various error numbers, and read the object's automatically initialized
SystemCallError.new(2).message # => "No such file or directory" SystemCallError.new(3).message # => "No such process" SystemCallError.new(4).message # => "Interrupted system call"
This works because under the covers, the
SystemCallError initialization code asks the operating system for a message corresponding to the given number. Why can't we ask the operating for the error message directly? Simply because Ruby doesn't provide any method bindings that would enable us to do so.
Of course, if we really wanted to, we could write our own Ruby extension library in C, exposing the function we need. But that's a big hassle to access one little function.
Fortunately, there's another way. Ruby ships with a standard library which enables us to make calls directly to any dynamically-loadable C library.
It's called "fiddle".
In order to get at the function we need, we first need a handle on the library that contains it. On my system that library is
libc, the standard C library. In order to load the library, we call
require "fiddle" libc = Fiddle.dlopen
We need to provide a library file name to the
dlopen method. How can we find out the filename that we need?
Well, we know that Ruby itself has access to the function in question. So it must have a link to the requisite dynamic library.
To find out what dynamic libraries the Ruby executable links to, we can go to the terminal. First, we find out the path of the current Ruby executable. Then, we feed that path into the
This utility spits out a list of libraries linked into the
ruby executable. As we can see, one of them is
We can copy the filename, and use that as our library to loadl.
require "fiddle" libc = Fiddle.dlopen("/lib/x86_64-linux-gnu/libc.so.6")
As it turns out, we don't actually need this whole filename, since this library is within the system library search path. We can get away with just using the library name with its version.
require "fiddle" libc = Fiddle.dlopen("libc.so.6")
Now that we have a handle on the whole library, we need to get a handle for just the function we want.
We can do this with the subscript operator, providing the name of the function we are interested in, which is
In return, we get back a number.
require "fiddle" libc = Fiddle.dlopen("libc.so.6") libc["strerror"] # => 139997737567408
This number is simply the memory address of the function we want to call. By itself, this isn't very useful.
Our next step is to construct a handle object for the function. For this, we use the
Fiddle::Function class. As arguments, we provide first the function address.
Unlike Ruby methods, there is no way to ask a function address anything about what types of argument it takes. Instead, we have to specify an argument signature.
We do that in the form of an array.
If we call up the man page for
strerror, we can see that it expects just one argument: an integer.
So in the array, we specify a special
Fiddle::TYPE_INT constant to set the expectation for that parameter.
There are a bunch of these type constants, corresponding to different standard C variable types. You can see the full list in the
After the parameter list, we need to also specify the C function's return value. The
strerror function returns a character pointer. However,
Fiddle doesn't have specialized pointer type constants. Instead, we'll just specify a generic pointer with
Now we have an object that represents the C function we want to call. In order to call it, we send it the—you guessed it!—
#call message, with an error number as an argument.
The result of this call is a
Fiddle::Pointer object. A pointer in C is just a memory address. Since
Fiddle only knows about generic pointers, it doesn't know what kind of data this memory address points to.
In the case of
strerror, the memory address points to the starting point of a C-style null-terminated string, containing a message. We can tell Ruby to interpret the pointer as a string by sending it the
And there, at last, is our system-provided error message, corresponding to error number 2.
require "fiddle" # "/lib/x86_64-linux-gnu/libc.so.6" libc = Fiddle.dlopen("libc.so.6") # => #<Fiddle::Handle:0x00562ef8840cd8> strerror = Fiddle::Function.new( libc['strerror'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP) # => #<Fiddle::Function:0x00562ef8840760 @ptr=140662671672496, @args=, @re... ptr = strerror.call(2) # => #<Fiddle::Pointer:0x00562ef8953330 ptr=0... ptr.to_s # => "No such file or directory"
We can shrink this code down, of course. We can use the dot-paren shorthand for sending the
#call message. And we can tack the
.to_s directly to the end of the send.
While we're here, let's try out some other error numbers.
require "fiddle" # "/lib/x86_64-linux-gnu/libc.so.6" libc = Fiddle.dlopen("libc.so.6") # => #<Fiddle::Handle:0x005609c9401f68> strerror = Fiddle::Function.new( libc['strerror'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP) # => #<Fiddle::Function:0x005609c9401978 @ptr=140551530828976, @args=, @re... strerror.(2).to_s # => "No such file or directory" strerror.(3).to_s # => "No such process" strerror.(4).to_s # => "Interrupted system call"
By the way, remember earlier when I told you we needed to know the exact name of the library before we could get a handle on it? That's not always true. If we pass
nil instead of a library name, that's a special flag which tells Ruby "get me a handle to all of the currently loaded libraries.
Since, as we saw, Ruby already links to
libc anyway, our code here still works when we switch to using this special flag.
require "fiddle" libc = Fiddle.dlopen(nil) # => #<Fiddle::Handle:0x005589f24c6310> strerror = Fiddle::Function.new( libc['strerror'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP) # => #<Fiddle::Function:0x005589f24c5d70 @ptr=139627743982768, @args=, @re... strerror.(2).to_s # => "No such file or directory" strerror.(3).to_s # => "No such process" strerror.(4).to_s # => "Interrupted system call"
So now you know how to call out to C libraries from Ruby code, without compiling an extension.
Setting up function handles this way is a bit tedious and verbose.
Fiddle also provides another way to do it that lets us wrap a whole library API a lot more quickly and concisely. But we can talk about that on another day.
One last thing. If you're a longtime viewer, you might be thinking: wait a second, how is this different from the FFI gem we talked about a long time ago?
The difference between FFI and Fiddle is this: FFI is a third-party gem. It is intended to work across multiple implementations of Ruby, including both MRI and JRuby.
By contrast, Fiddle ships with MRI as a standard library. So at the cost of a little portability, Fiddle enables us to call out to C libraries from Ruby without any extra dependencies.
And that's all for now. Happy hacking!