Compiling

Embedding Ruby requires one header ruby.h, which includes a platform-specific header ruby/config.h. You will probably need to tell your compiler about the include paths for these headers. You will also need to link with the Ruby lib. On my machine, my minimal compiler options are:

$ gcc foo.c -I/usr/include/ruby-3.0.0 -I/usr/include/ruby-3.0.0/x86_64-linux -lruby

If available, you can use pkg-config to get the appropriate options for your OS:

$ pkg-config --cflags --libs ruby-3.0

Those approaches might not work if Ruby is installed in a nonstandard location on your machine or your OS does not provide standard header/library directories. A more robust approach is to ask Ruby itself where things are and to set up the run-time library path:

#!/usr/bin/env ruby
require 'shellwords'

# location of ruby.h
hdrdir = Shellwords.escape RbConfig::CONFIG["rubyhdrdir"]
# location of ruby/config.h
archhdrdir = Shellwords.escape RbConfig::CONFIG["rubyarchhdrdir"]
# location of libruby
libdir = Shellwords.escape RbConfig::CONFIG["libdir"]

# args for GCC
puts "-I#{hdrdir} -I#{archhdrdir} -L#{libdir} -Wl,-rpath,#{libdir}"

Windows

On Windows, I highly recommend using the RubyInstaller with Devkit. This gives you access to GCC on the Windows command line as well as pkgconf for getting the build flags.

$ ridk enable # enable devkit
$ pkgconf.exe --cflags --libs C:\Ruby33-x64\lib\pkgconfig\ruby-3.3.pc # show GCC args
$ gcc foo.c ...

Note that the GCC args might not be parsed correctly by PowerShell, so use cmd instead. Also the Ruby 3.3 installer is missing the lib location from its pkgconf output for some reason, so you’ll need to add the -L option manually. You can locate that with RbConfig::CONFIG["libdir"] as above; the installer put it in C:\Ruby33-x64\lib for me.

Finally, Windows has no rpath so you will need to copy any linked DLLs alongside the built executable for it to run. That includes any DLLs needed by Ruby itself. The installer put those in C:\Ruby33-x64\bin\ruby_builtin_dlls for me.

Startup, Teardown

Including the Ruby interpreter in your C/C++ program is pretty simple. Just include the header, call a startup function in main before you use the API, and a cleanup function after you’re done:

#include <ruby.h>

int main(int argc, char* argv[])
{
	/* construct the VM */
	ruby_init();

	/* Ruby goes here */

	/* destruct the VM */
	return ruby_cleanup(0);
}

If the VM fails to start during ruby_init() it will print an error and exit your program! If you would rather have a softer error, you can instead call ruby_setup() which returns a nonzero value if a failure occurred (unfortunately it is not clear how to get a message for the error1).

If an error occurs during rb_cleanup(), it returns a nonzero value—otherwise it returns the argument you passed it. This allows a little shortcut for returning an error status if the cleanup fails (as demonstrated in the previous example).

Technically you don’t have to call ruby_init/ruby_setup in main, but the Ruby VM assumes that all future Ruby code will be run from the same stack frame or a lower one (for garbage collection purposes). The easiest way to ensure this is to do set up at the top-level of your program, though other approaches could work. But it would be a bad idea, for example, to init Ruby in some deeply-nested function, pop a bunch of stack frames, and then run a bunch of Ruby code.

During cleanup, the VM might evaluate more Ruby code (if you passed a block to at_exit, for example) which could raise an exception. ruby_cleanup() handles these by returning a nonzero value and printing an error message. If you instead call ruby_finalize() they will be raised normally (see the section on Exceptions for how to handle them).

Here’s an alternative example:

#include <ruby.h>

int main(int argc, char* argv[])
{
	if (ruby_setup())
	{
		/* run code without Ruby */
	}
	else
	{
		/* Ruby goes here */

		ruby_finalize(); /* XXX rescue exceptions here!!! */
	}

	return 0;
}

Limitations

Other than the stack frame warning above, there is another limitation: you only get one Ruby VM per process. The startup/teardown might make it look like you can keep on destroying and rebuilding the VM over and over again, but ruby_cleanup only makes sure that your Ruby code is all cleaned up and done. It doesn’t fully clean up the VM state such that it is ready to be re-initialized: if you call ruby_init again, it will fail.

If for some reason you need multiple Ruby VMs in your program, you will need to spin them off in multiple processes to bypass this limitation.

Tweaking the VM

You now have a bare-bones Ruby VM running, but you may want to set up a little more stuff before you start running Ruby code. To set the name of the Ruby script (e.g. $0) for error messages and such, use

ruby_script("new name")

To set up the load path so that gems can be loaded with require, use

ruby_init_loadpath()

You can also pass options to the VM just like you would to ruby on the command line. This is handy for stuff like setting the warning level or verbose mode2.

#include <ruby.h>

int main(int argc, char* argv[])
{
	ruby_init();

	char* options[] = { "-v", "-eputs 'Hello, world!'" };
	void* node = ruby_options(2, options);

	int state;
	if (ruby_executable_node(node, &state))
	{
		state = ruby_exec_node(node);
	}

	if (state)
	{
		/* handle exception, perhaps */
	}

	return ruby_cleanup(state);
}

The arguments to ruby_options are argc and argv just like a main function. And just like the main of the ruby program, the VM expects to get some Ruby code when you call it. If you don’t give it the filename of a script to load or code to run with -e, it will try to read from stdin. If you want to set options but not run any Ruby code, you can pass it an empty line: "-e ".

ruby_options() returns a “node” that represents the compiled Ruby code. In some cases (such as a syntax error) the node will be invalid and you shouldn’t run it. ruby_executable_node() checks for this. If the node is valid, you can run it with ruby_exec_node(). The state returned by ruby_executable_node() (through the pointer) and by ruby_exec_node() will be nonzero if an exception was raised while compiling or running the code. You can read the exception yourself, or just pass state to ruby_cleanup() and it will print an appropriate error message.

Ruby currently doesn’t support any other way of compiling and running code separately3.

Success

Now you’re ready to interact with Ruby! Go back to the C API.

Footnotes

  1. ruby_init() uses error_print() to get an error message, but this function isn’t exposed to the API. Is this a normal exception? 

  2. In my tests I couldn’t get flags like -w and -v to do anything. This could be related to ruby_prog_init(). And really it should be possible to do this without parsing command line options. 

  3. It looks like the function rb_load_file() should do this, but I haven’t had any luck getting it to work. 

Comments