Search code examples
crubyffi

creating a Dynamic array class in ruby using FFI and C function


I would like to create my own dynamic array class in ruby (as a training). The idea is to have a class DynamicArray that has a capacity (the number of elements it can hold at one given moment), a size (the number of elements that were actually pushed in the array at a given moment) and a static_array which is a static array of ints of a fixed sized. Whenever this static_array is full, we will create a new static array with twice the capacity of the original static_array and copy every elements inside the new static_array. As there is no static array in ruby, my idea was to use FFI https://github.com/ffi/ffi. to create a function in c that creates a static array of int of size n then be able to use it in my ruby program. I have very little knowledge in C and am having a hard time understanding the doc of FFI Here's what I have so far, a create_array.c file that defines my c function to create an array.

#include<stdio.h>
int * createArray ( int size )
{
  int array[size];
  return 0;

}

a create_array.h file (from what I understood of FFI, you need to put your c functions in a c library.):

int * createArray ( int size )

and this is my dynamic_array.rb file that would do something along this lines :

require 'ffi'
class DynamicArray
  extend FFI::Library
  ffi_lib "./create_array.h"
  attach_function :create_array, [:int], :int
  def initialize
    @size = 0
    @capacity = 1
    @current_index = 0
    @static_array = create_array(@capacity)
  end

  def add(element)
    @size += 1
    resize_array if @size > @capacity
    @static_array[@current_index] = element
    @current_index += 1
  end

  private

  def resize_array
    @capacity = @capacity*2
    new_arr = create_array(@capacity)
    @static_array.each_with_index do |val, index|
      new_arr[index] = val
    end
    @static_array = new_arr
  end
end

Here are some tests for add and resize :

  def test_add
    dynamic_arr = DynamicArray.new
    dynamic_arr.add(1)
    dynamic_arr.add(2)
    assert_equal(1, dynamic_arr.static_array[0])
    assert_equal(2, dynamic_arr.static_array[1])
  end

  def test_resize_array
    dynamic_arr = DynamicArray.new
    dynamic_arr.add(1)
    dynamic_arr.add(2)
    assert_equal(2, dynamic_arr.capacity)
    dynamic_arr.resize_array
    assert_equal(4, dynamic_arr.capacity)
    assert_equal
  end

Can you please explain me what I should do to make this work ?


Solution

  • It seems that you are not working with the C code properly.

    In create_array C function:

    • you are not returning the array, so there's no way the ruby code will work with the newly created array, you need to return it
    • if you want to return an array you actually need to return it's pointer
    • In C, in order to create an array and the size is not known before compilation you need to allocate it's memory with malloc (or some other function in the allocfamily)

    to put it all together, this is how your create_array.c file would look like:

    #include <stdlib.h> /* in order to use malloc */
    
    int * create_array (int size){
      int *a = malloc(size * sizeof(int));
      return a; /* returning the pointer to the array a*/
    }
    
    

    and your header file create_array.h:

    int * create_array(int);
    

    and to wrap everything up you still need to compile it before ruby can touch it:

    gcc -shared -o create_array.so -fPIC create_array.c
    

    this command is using gcc to compile your C code into a shared library called create_array.so from create_array.c source file. gcc needs to be installed for this to work.

    Finally you can use the C function in ruby, with some modifications in your dynamic_array.rb:

    require 'ffi'
    class DynamicArray
      extend FFI::Library
      ffi_lib "./create_array.so" # using the shared lib
      attach_function :create_array, [:int], :pointer # receiving a pointer to the array
      # rest of your code
    

    Now, this should work! But there are still some issues with your ruby code:

    • when you do @static_array = create_array(@capacity) you are receiving a C pointer to the allocated array, not the array itself, not at least in ruby.
    • writing @static_array[@current_index] = element will not work NoMethodError: undefined method '[]=' for #<FFI::Pointer address=0x000055d50e798600>
    • If you want to add an element to the array, C code must do it. Something like:
    void add_to_array (int * array, int index, int number){
      array[index] = number;
    }
    
    attach_function :add_to_array, [:pointer, :int, :int], :void
    add_to_array(@static_array, @current_index, element)
    
    • Same goes for the @static_array.each_with_index you need to code this in C.