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], :intThe 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
endAs 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.