In pursuing a gamedev project, I needed to implement a small scripting language for some logic. Lua is often the go-to choice for this, especially because of the ease of getting started. I opted for Ruby, or more specifically, MRuby. I weighed two factors when making this choice. First, I’m deeply familiar with Ruby because of my professional career, and second, MRuby has always intrigued me. That said, documentation and guides found across the internet are lacking. After much trawling the internet, I created a successful implementation compiled on Windows and macOS. I want to dive into my exploration of this project and ultimately leave you, the reader, with the same ability to compile and use MRuby yourself. This first article will cover macOS

As you read this guide, you may notice that I am using command-line tools for the respective operating systems. CMake and Makefiles are popular among C developers. It will be better to avoid abstractions to eliminate confusion. Experienced developers familiar with CMake and Makefiles will be able to make necessary changes to their setup. However, new developers starting with C or C++ can directly dive into development and simply use a .bat or a bash file to kickstart their MRuby project.

The macOS implementation

Prerequisites

  • Ruby Install, hopefully, the latest version
  • Git installed, as well as some basic knowledge to use it
  • Clang is installed, as well as having access to clang via terminal.1

How to Compile your program on macOS

We aim to simply get the below file to compile and generate an executable. Then, we should be able to run it and have it execute the ruby code in test.rb. Go ahead and create a file called main.c in our project directory and insert the following code below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// main.c
#include "mruby.h"
#include "mruby/compile.h"
#include "stdio.h"

int main(void) {
  mrb_state *state = mrb_open();

  FILE *handle = fopen("test.rb", "r");
  if (!handle) {
    return 1;
  }
  mrb_load_file(state, handle);
  fclose(handle);

  return 0;
}

Let’s try a naive compile command on main.c and run clang main.c -o example

1
2
3
4
5
main.c:1:10: fatal error: 'mruby.h' file not found

#include "mruby.h"
         ^~~~~~~~~
1 error generated.

We are currently mimruby.h e mruby.h header file. One quick solution would be to copy the entire mruby/include/* directory to the exact location as our main.c file. This would resolve the issue, but it’s not optimal. Ideally, we should use the header files generated during the MRuby compiling process. MRuby can be configured to compile with different features, so using the generated header files will prevent potential issues in the future if we decide to exclude certain MRuby features.

Downloading and Compiling MRuby

First clone MRuby into a different directory with git clone https://github.com/mruby/mruby.git. Let’s make sure we build on a stable version, so checkout the latest stable using git checkout 3.3.0.

MRuby uses Rakefile as the build system2. We want to ensure that the compiler we use to build MRuby is the same as the one being used in our project. Let’s do a quick edit to build_config/default.rb and make the follow change

1
2
3
4
MRuby::Build.new do |conf|
  # load specific toolchain settings
++  conf.toolchain :clang
--  conf.toolchain

Now that we’ve edited this to include clang we can simply run bundle and then rake. This will take a few seconds. When the rake command finishes we will have generated both the header files and static library artifacts.

Navigate to mruby/build/host/ and explore this directory.

1
2
ls
LEGAL   include lib     mrbc    mrbgems mrblib  presym  src

The three things we care about the most are LEGAL, include and lib.

With legal we want to keep a copy of it in our project when we decide to publish for credits. I would suggest renaming it to MRUBY_LEGAL and keep it in a licenses directory.

If you look at the include directory you’ll notice that we have similar header files as to mruby/include. However there are now some extra header files for the compiled gems. There are extra features compiled into our MRuby static library.

If we had just gone with the mruby/include header files we would not have had any access to the time and io gems from our C code. We reduced the opportunities to interact with Ruby from our C code. Go ahead and do a recursive copy of all the files and directories in mruby/build/host/include/* to the same directory as our main.c

The Static Library

Let’s retry clang main.c -o example. And notice the generated error from this command

1
2
3
4
5
6
7
Undefined symbols for architecture arm64:
  "_mrb_load_file", referenced from:
      _main in main-e30622.o
  "_mrb_open", referenced from:
      _main in main-e30622.o
ld: symbol(s) not found for architecture arm64
clang-14: error: linker command failed with exit code 1 (use -v to see invocation)

The linker is unable to find the code related to the header file, which is causing the issue. To fix this, we must include the static library archive that was generated in our mruby directory. If we navigate to mruby/build/host/lib/, we will find the generated files.

1
2
3
|____libmruby_core.a
|____libmruby.a
|____libmruby.flags.mak

The *.a files are the static library files that contain the compiled code we need to link against. The .flags.mak contains information on how the library was generated. I am weak in this area, but I think it’s an artifact for makefiles.

We want those *.a. Technically, we only need libmruby.a but let’s go ahead and grab both of these files and place them in the same directory as main.c

Now we need to tell clang to link those files for clang

Our command becomes clang main.c libmruby.a -o example . Finally, we will generate an executable file in the same directory called example.

1
2
# test.rb
puts "This is Ruby code ran from a C generated binary"

Run example if we will see

1
2
./example
This is Ruby code ran from a C generated binary

One temptation you may face is to place your libmruby.a in a global library archive directory, such as /usr/local/lib. However, I discourage this practice because you may have multiple mruby libraries across different systems.

The next article will explain how to perform the same steps on the MSVC environment in Windows. If you plan to use Clang or MinGW, you already have all the knowledge you need from this article.

Notes


  1. Xcode Command Line tools is a helpful search term for this as well as xcode-select –-install ↩︎

  2. Some C developers will find themselves using Makefile or CMake. I however find Rake to actually be a powerful option. I use it for my C++ projects that use MRuby. ↩︎