In Progress
Unit 1, Lesson 1
In Progress

Fiddle

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 message attribute.

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 Fiddle.dlopen.

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 ldd utility.

This utility spits out a list of libraries linked into the ruby executable. As we can see, one of them is libc.

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 strerror.

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 fiddle documentation.

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 Fiddle::TYPE_VOIDP.

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 #to_s message.

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=[4], @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=[4], @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=[4], @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!

Responses