Skip to content

Building your Ruby environment and accessing it.

Alex Fonseka edited this page Dec 19, 2019 · 4 revisions

Building your Ruby environment

You can create classes both from C and Ruby together. Define the class in Ruby code just as a regular Ruby class, and then add C-code the manipulate the class definition. Or define everything in C. Your choice.

Access Ruby from C

Lets first define some class in Ruby code, and try to access this from C:

Create a file wiki-example.rb with the following content. This class is already prepared for the next section (manipulating Ruby classes from C). We now only need the 'get_version' method.

module WikiExample
	class WikiManager
		attr_accessor :active
		def connect
			self.active = _we_connected
		end

		def get_version
			return 2
		end

		# _we_connected() is defined in C
	end
end

Then we write a C stub and small program to access this code. We first initialize mruby, then load the code file and the third step is to access the module, class and instance method.

Name the file wiki-example.c.

#include "mruby.h"
#include "mruby/irep.h"

int
main(void)
{
	mrb_state *mrb = mrb_open();
	if (!mrb) { /* handle error */ }
	FILE *fp = fopen("wiki-example.rb","r");

	// Load the data from the .rb file into the Ruby environment
	mrb_value obj = mrb_load_file(mrb,fp);

	// close the file
	fclose(fp); 

	// First access the module
    struct RClass *module = mrb_module_get(mrb, "WikiExample");
    
    // Get the class that is defined in the WikiExample module
    struct RClass *class = mrb_class_get_under(mrb, module, "WikiManager");

    // Create a new instance of WikiManager, no arguments are needed (0, NULL)
    mrb_value c = mrb_obj_new(mrb, class, 0, NULL);

    // Call the get_version method on the instance.
	mrb_value res = mrb_funcall(mrb, c, "get_version", 0);

	// Convert the result (a fixed number wrapped in a mrb_value)
	printf("result: %i\n", mrb_fixnum(res));
	
	// If crashed, provide exception info
	if (mrb->exc) 
  	{
    	mrb_print_error(mrb);
  	}
  	// Close the Ruby environment
	mrb_close(mrb);
}

Compile this code with gcc -std=c99 -Iinclude wiki-example.c build/host/lib/libmruby.a -o wiki-example and then run wiki-example. The output is result: 2.

Calling C methods from Ruby

The strategy is first to update the existing class. Here we add a C-method to the class, but we can even define the module and class from C.

To add a method to a class, we first need a reference to the class. We can copy the lines from the first example for getting a reference to the class WikiManager, of just insert the next lines just before the If crashed... comment line.

	// add the method to the WikiManager class
	mrb_define_method(mrb, class, "_we_connected", we_connected, MRB_ARGS_NONE());

        // call the connect method on WikiManager
	mrb_value res2 = mrb_funcall(mrb, c, "connect", 0);

Then we add the following method between the #include and int main lines:

mrb_value 
we_connected(mrb_state* mrb, mrb_value self){
	// we need an int from the args
	mrb_int i = 0;

        // retrieve one arg of type int (see mruby.h)
	mrb_get_args(mrb, "i", &i);

        // we always need to return a mrb_value object, and mrb_bool_value converts a C bool to a mrb_value.
	return mrb_bool_value(i >= 2);
}

Compile (see above) and run. You will get the output result: 2. Now edit the Ruby file and change the get_version to return 1. Do not compile, just run the executable again, and see the output result: 1 Connection failed.

This will give you unlimited results, as you don't even need to recompile to update your Ruby code. Image writing unit-tests in Ruby calling your C-methods in classes. You will be able to mock (semi)classes the Ruby-way.

Passing the arguments is quite straightforward (both ways). But maybe someone can update this page with some code on how to pass a C struct to Ruby and use the result.