Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating C structs using the RubyInline gem

Tags:

c

ruby

I've got a Rails app that needs to show the results of a Monte Carlo sim, and for the first time, Ruby just isn't fast enough for my needs. So, I started poking around to see if I could rewrite my simulation in C and use those results in Ruby, and some googling turned up the RubyInline gem for easily writing faster C code directly in Ruby. Doing simple stuff works great, for instance, some basic functions written in both Ruby and C:

class FasterFunctions
  inline do |builder|
    builder.c '
    double rand_sum(int trials)
    {
      double sum = 0.0;
      for (int i = 0; i<trials; i++)
      {
        sum += (double)rand()/(double)RAND_MAX;
      }
      return sum;
    }'

    builder.c '
    long loop_sum(long trials)
    {
      long sum = 0;
      for (long i = 0; i<trials; i++)
      {
        sum+=i;
      }
      return sum;
    }'
  end
end

#the C version is 4 orders of magnitude faster
trials = 1_000_000
ruby_sum = 0
trials.times {|i| ruby_sum += i}
c_sum = FasterRand.new.loop_sum(trials)

# the C version is 1 order of magnitude faster
ruby_sum = 0.0
trials.times {ruby_sum += rand}
c_sum = FasterRand.new.rand_sum(trials)

So, great, it should definitely speed up my sim since it's pretty much just generating random numbers and adding stuff up based on those results. Unfortunately, beyond basic stuff like this, I can't figure out how to actually write my program. I need to pass some structs around to act as state variables, so the first order of business is figuring out how to create a C struct. Reading the documentation, it seems like it should be fairly simple.

accessor(method, type, member = method)

Adds a reader and writer for a C struct member wrapped via Data_Wrap_Struct. method is the ruby name to give the accessor, type is the C type. Unless the C member name is overridden with member, the method name is used as the struct member.

builder.struct_name = 'MyStruct' 
builder.accessor :title,        'char *' 
builder.accessor :stream_index, 'int',   :index

The documentation isn't entirely clear to me, but I assume that I'd put that in a class much like before and do something like the following to access it:

class MyStructClass
  inline do |builder|
    builder.struct_name = 'MyStruct'
    builder.accessor :title, 'char *'
    builder.accessor :stream_index, 'int', :index
  end
end

#possible this
struct = MyStructClass.new.MyStruct
struct.title = 'A Title'

#or maybe
struct = MyStructClass.new
struct.title = 'A Title'

Unfortunately, I can't even get that far

*/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:456:3: error: use of undeclared identifier 'MyStruct' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:456:13: error: use of undeclared identifier 'pointer' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:458:35: error: use of undeclared identifier 'pointer' Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:6: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:458:25: error: use of undeclared identifier 'MyStruct' Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:15: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:458:3: error: expected expression Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:20: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:460:23: error: use of undeclared identifier 'pointer' return (rb_str_new2(pointer->title)); ^ */.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/intern.h:786:27: note: expanded from macro 'rb_str_new_cstr' (__builtin_constant_p(str)) ? \ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:460:23: error: use of undeclared identifier 'pointer' */.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/intern.h:787:14: note: expanded from macro 'rb_str_new_cstr' rb_str_new((str), (long)strlen(str)) : \ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:460:23: error: use of undeclared identifier 'pointer' */.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/intern.h:787:33: note: expanded from macro 'rb_str_new_cstr' rb_str_new((str), (long)strlen(str)) : \ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:460:23: error: use of undeclared identifier 'pointer' */.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/intern.h:788:18: note: expanded from macro 'rb_str_new_cstr' rb_str_new_cstr(str); \ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:460:10: error: returning 'void' from a function with incompatible result type 'VALUE' (aka 'unsigned long') return (rb_str_new2(pointer->title)); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:478:3: error: use of undeclared identifier 'MyStruct' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:478:13: error: use of undeclared identifier 'pointer' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:480:35: error: use of undeclared identifier 'pointer' Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:6: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:480:25: error: use of undeclared identifier 'MyStruct' Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:15: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:480:3: error: expected expression Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:20: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:482:3: error: use of undeclared identifier 'pointer' pointer->title = StringValuePtr(value); ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:456:3: error: use of undeclared identifier 'MyStruct' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:456:13: error: use of undeclared identifier 'pointer' MyStruct *pointer; ^ */.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/RubyInline-3.12.4/lib/inline.rb:458:35: error: use of undeclared identifier 'pointer' Data_Get_Struct(self, MyStruct, pointer); ^ /.rbenv/versions/2.1.2/include/ruby-2.1.0/ruby/ruby.h:1038:6: note: expanded from macro 'Data_Get_Struct' (sval) = (type)DATA_PTR(obj);\ ^ fatal error: too many errors emitted, stopping now [-ferror-limit=]

I've done a bunch of googling, but pretty much every example I've found deals only with the simple use case I outlined at first. The first error it spit out was that MyStruct was undeclared, so I tried adding a struct definition before the accessor methods, like so:

builder.c '
  typedef struct
  {
    char *title;
    int index;
  } MyStruct;
'

That doesn't do anything, and the documentation seems to be clear that the c method is only for declaring functions, not structs. I'm not really sure what to try next, does anyone else have some experience with this?

like image 325
Devin Avatar asked Sep 28 '22 06:09

Devin


1 Answers

RubyInline seems to assume that you know your way around writing Ruby extensions if you are doing anything more complex than the basics.

You do need to define the struct yourself as you tried, but you should use the prefix method rather than the c method (which is for defining methods). You also need to define the allocation method for the class. You can do this with the c_singleton method by adding a function named allocate which RubyInline will recognise and use as the allocation function (this doesn’t seem to be documented, I found it by looking at the source).

Putting it all together looks something like this.

class Foo
  inline do |builder|

    # First define the struct that will be wrapped in the
    # class.
    builder.prefix <<-DEFINE_STRUCT
      typedef struct {
        char* bar;
      } MyStruct;
    DEFINE_STRUCT

    # Next define the allocation function that will
    # allocate and wrap a MyStruct struct when creating a
    # new Foo object. You might want to initialize the values
    # to avoid possible segfaults.
    builder.c_singleton <<-ALLOCATE
      VALUE allocate() {
        MyStruct* pointer = ALLOC(MyStruct);
        return Data_Wrap_Struct(self, NULL, free, pointer);
      }
    ALLOCATE

    # Finally we can use the struct_name and accessor methods.
    builder.struct_name = 'MyStruct'
    builder.accessor 'bar', 'char *' 
  end

end

Note you can see the generated C code if you look under the directory .ruby_inline in your home directory.

like image 93
matt Avatar answered Oct 03 '22 01:10

matt