123456789_123456789_123456789_123456789_123456789_

Consider the following Ruby code:

string1 = "Hello world"
string2 = File.read("data.txt", "r")
string3 = File.read("image.jpg", "rb")

All three string variable are of class String, yet the data stored in string3 is binary image data rather than text data.

Check it for yourself using the String#encoding method. string1 uses the __ENCODING__ of the source file, which is most likely Encoding::UTF_8. string2 uses the Encoding::default_external encoding, which is most likely Encoding::UTF_8 as well. But string3 uses Encoding::ASCII_8BIT (a.k.a. Encoding::BINARY).

At the Ruby level we not not care about this because Ruby abstracts that all away. However, when using FFI, how to pass this data to and from C libraries suddenly becomes very important.

Passing binary data

If a Ruby string contains binary data, then you cannot use the following:

attach_function :example_function, [:string], :int

The reason being that FFI converts a Ruby string into a C style (null-terminated) string. This will cause trouble if your binary data contains a null byte (and chances are that it will).

When dealing with binary data, most C code will use memory buffers. The following example shows how a file would be read into memory for use in a convert() function in a C program:

  char                *inputName = "cat.jpg";
  FILE                *inputFile = 0;
  unsigned char       *picBuf = 0;
  unsigned int        picBufSize = 0;
  int                 result = 0;

  // Open the file handle for reading binary
  inputFile = fopen(inputName, "rb")

  // Get the filesize
  fseek(inputFile, 0, SEEK_END);
  picBufSize = ftell(inputFile);

  // Return to the beginning of the file and set the buffer size
  fseek(inputFile, 0, SEEK_SET);
  picBuf = malloc(picBufSize * sizeof(unsigned char));

  // Read the data and close the file
  fread(picBuf, 1, picBufSize, inputFile)
  fclose(inputFile);

  // Now do something with the picBuf
  // The signature for this function is int convert(void *picBuf, unsigned int picBufSize);
  result = convert(picBuf, picBufSize)

  // ...

  free(picBuf);

If we want to use the convert() function from FFI, then we will need to pass in the binary data directly. To do this, we need to do the following:

module ExampleLib
  extend FFI::Library
  ffi_lib "example_lib.so" 

  attach_function :convert, [:pointer, :uint], :int
end

As you can see, we've attached the function convert() with a :pointer argument.

Now we need to pass in a pointer to the beginning of the memory and the size of the allocation. We can do this as follows:

module ExampleLib
  extend FFI::Library
  ffi_lib "example_lib.so" 

  attach_function :convert, [:pointer, :uint], :int

  # Convert an image
  #
  # data contains the binary data to be converted.
  def self.convert_image(data)
    memBuf = FFI::MemoryPointer.new(:char, data.bytesize) # Allocate memory sized to the data
    memBuf.put_bytes(0, data)                             # Insert the data
    convert(memBuf, data.size)                            # Call the C function
  end
end

data = File.read("cat.jpg", "rb")
ExampleLib.convert_image(data)

Note that, because this is binary data (possibly containing null bytes), MemoryPointer#put_bytes must be used instead of MemoryPointer#put_string.