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
.