5

For several years I've been using the Ruby C API to add the ability to use Ruby code in my C text editor (a variant of MicroEMACS). This has been working well in Linux Mint 21 (essentially the same as Ubuntu 22.04), which uses Ruby 3.0.

Then I started running this code on Mint 22 (Ubuntu 24.04), which uses Ruby 3.2, and things failed. Specifically, certain commonly used Class methods started giving unexpected output or exceptions about missing methods. The problems I ran into (there may be others I don't know about) were that Symbol#to_s seems to do nothing, and Time.now is undefined. Then when I tried this same code on Fedora 42, which uses Ruby 3.4, Array#last was now undefined, in addition to the other problems.

These problems don't occur when I run Ruby as a separate process.

I have boiled down the problems into a simple test program:

#include <ruby.h> #include <string.h> void check_exception (int state) { if (state) { printf("Exception occurred!\n"); VALUE exception = rb_errinfo (); if (RTEST(exception)) { VALUE msg; VALUE msglines; VALUE bt; msg = rb_funcall (exception, rb_intern("to_s"), 0); printf("%s\n", StringValueCStr (msg)); } /* rb_set_errinfo (Qnil); */ /* This causes a coredump for some reason. */ } } void run (const char *rubycode) { int state; printf("====\nAbout to run '%s'\n", rubycode); rb_eval_string_protect(rubycode, &state); check_exception (state); } int main(int argc, char* argv[]) { ruby_init(); ruby_init_loadpath(); run ("puts \"Hello, world!\""); /* This should print "sym" but prints #<Symbol:0x000000000076010c> instead. * The missing Symbol#to_s causes all the subsequent error messages * to print #<Symbol:0x....> instead of the actual method name. */ run ("puts :sym.to_s"); /* Array#last is undefined. */ run ("puts [0].last"); /* Time.now is undefined. */ run ("puts Time.now"); return ruby_cleanup(0); } 

Here is the output on Fedora 42:

==== About to run 'puts "Hello, world!"' Hello, world! ==== About to run 'puts :sym.to_s' #<Symbol:0x000000000076010c> ==== About to run 'puts [0].last' Exception occurred! undefined method '#<Symbol:0x000000000040610c>' for an instance of Array ==== About to run 'puts Time.now' Exception occurred! undefined method '#<Symbol:0x000000000076110c>' for class Time eval:1:in '<main>': undefined method '#<Symbol:0x000000000076110c>' for class Time (NoMethodError) eval:1:in '<main>': undefined method '#<Symbol:0x000000000040610c>' for an instance of Array (NoMethodError) 

Clearly, the Ruby interpreter is doing something to define these methods, but I don't know how to get them to be defined using the C API. Are there some additional API calls that I'm missing in this little test program?

1 Answer 1

5

tl;dr

  • You should call ruby_init_stack() (or use the RUBY_INIT_STACK macro), because otherwise bad things will happen

  • Undefined Methods Error:

    • Builtins are initialized by the internal function rb_call_builtin_inits()
    • To call rb_call_builtin_inits() you must call one of the following functions after ruby_init():
      void* rb_load_file(const char* file); void* rb_load_file_str(VALUE file); void* ruby_process_options(int argc, char** argv); void* ruby_options(int argc, char** argv); 

That seems to happen because ruby's builtins are not initialized.

Some methods are defined in special builtin .rb files in the ruby source - e.g. Time#now and Time#initialize which are defined in timev.rb

class Time def self.now(in: nil) Primitive.time_s_now(Primitive.arg!(:in)) end # [...] end 

That also includes Symbol#to_s and Array#last in newer ruby versions.

Those ruby files are processed by tool/mk_builtin_loader.rb and eventually end up as *.rbinc files that are included in the C code.

Each one of them has an initialization function (e.g. Init_builtin_timev for timev.rb).
Those initialization functions are then called by rb_call_builtin_inits().

Unfortunately there is only one function that calls rb_call_builtin_inits(), and that is ruby_opt_init().

There are a total of 4 public functions that eventually call ruby_opt_init():

void* rb_load_file(const char* file); void* rb_load_file_str(VALUE file); void* ruby_process_options(int argc, char** argv); void* ruby_options(int argc, char** argv); 

You must call one of these four to actually initialize the builtins.


Example

The easiest option is probably to pass dummy command line options, e.g. something like this:

int main(int argc, char* argv[]) { RUBY_INIT_STACK; ruby_init(); char *dummy_args[] = {"ruby", "-e0"}; void *iseq = ruby_options(2, &dummy_args); int status; if (!ruby_executable_node(iseq, &status)) { // TODO handle error here } // ... your code here ... return ruby_cleanup(0); } 
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, this fixed the problem in my test program, and I'm hopeful it will fix the problem in MicroEMACS as well.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.