Search code examples
arraysrubyimagepointersffi

Ruby FFI to call C function with array of arrays


I have the following C interface:

int foo(void* bar, void* baz);

What this does, basically, is take an array of RGB values, process them, and return a new array of RGB values.

I wrote the following Ruby FFI wrapper for it:

module MyLibrary
  extend FFI::Library
  ffi_lib "path/to/mylibrary.so"
  attach_function :foo, [:pointer, :pointer], :int
end

However, I have not really succeeded to pass a Ruby array-of-arrays to this FFI wrapper. In Ruby, I have something like:

pixels = [[3, 34, 123], [32, 253, 34], ..., [1, 1, 34]]
result = [[0, 0, 0], [0, 0, 0], ..., [0, 0, 0]]

# This does not work!
MyLibrary.foo(pixels, result)

I've looked in the Ruby FFI docs, however I did not get how the Ruby arrays should be passed to the FFI wrapper.


Solution

  • To pass the data in to the function you need to to use a MemoryPointer, first copying the data from the Ruby arrays into it so that it is in the correct form when the C code sees it. Copying the data for a single dimensional array is fairly straightforward using one of the write_array_of_* methods. For a multi dimensional array it is a little trickier, you need to copy each array into the correct place int the memory managed to the MemoryPointer.

    Similarly for data returned by the function through a pointer you need to provide a MemoryPointer of the right size and then copy the data out into Ruby arrays. Again this is fairly easy for a single dimension array with the read_array_of* methods and is a little more work for a multi dimension array.

    Here is a simple example. Here I assume the arguments to the C function always consists of three three-element int arrays – int[3][3].

    The C function:

    int foo(void* bar, void* baz) {
        // assume both arrays are [3][3]
        int(*bar_)[3] = (int (*)[3]) bar;
        int(*baz_)[3] = (int (*)[3]) baz;
    
        // Highly complex processing - double each entry.
        for (int i = 0; i< 3; i++) {
            for (int j = 0; j < 3; j++) {
                baz_[i][j] = 2 * bar_[i][j];
            }
        }
    
        return 0;
    }
    

    Here is the Ruby code to access it:

    require 'ffi'
    
    module MyLibrary
      extend FFI::Library
      ffi_lib "path/to/mylibrary.so"
    
      # Give function a different name. You might also want to make
      # it private.
      attach_function(:ffi_foo, :foo, [:pointer, :pointer], :int)
    
      # Wrap the C function with a friendly method that packages
      # and unpackages the data.
      def self.foo(pixels)
        # Create the MemoryPointers for input and output. They are
        # both 9 entry (3 * 3) arrays of uint32.
        input = FFI::MemoryPointer.new(:uint32, 9)
        output = FFI::MemoryPointer.new(:uint32, 9)
    
        # Copy the input data into the input MemoryPointer
        pixels.each_with_index do |ary, idx|
          # The offset here is in bytes. int32 is 4 bytes, each
          # array is three elements so total is 3 * 4 = 12.
          input.put_array_of_int32(idx * 12, ary)
        end
    
        # Call the C function.
        ffi_foo(input, output)
    
        result = []
    
        # Copy the data back into a Ruby array.
        3.times do |idx|
          result << output.get_array_of_int32(idx * 12, 3)
        end
    
        # Return the final result
        result
    
      end
    end
    

    You can then use it like this:

    pixels = [[3, 34, 123], [32, 253, 34], [1, 1, 34]]
    p MyLibrary.foo(pixels) #=>[[6, 68, 246], [64, 506, 68], [2, 2, 68]]
    

    Obviously you will need to adapt this to match the details of your own function. You should probably also add error checking, otherwise you could be liable to getting segfaults.