mruby is a lightweight, embeddable implementation of the Ruby programming language.

Ruby vs Everything-else

The most obvious scripting language to use - for pretty much anything - would probably be Lua, a scripting language that's seen widespread success in all sorts of places.

Lua is quite easy to get running for most platforms and you can get a "Hello, world!" example relatively quickly.

After Lua, Python is probably the next obvious scripting language choice.

No programming language is perfect; both Lua and Python have some oddities that some people may disagree with. For Lua, the syntax can be quite the departure from other programming languages.


-- Lua not equal condition
if a ~= b then
  print( "a does not equal b" )
end

-- Lua for-loop 1 to 10 (will include 10)
for ii = 1, 10 do
  print(ii)
end

-- Lua array index starts at 1
local arr = { "hello", "world" }
print( arr[0] ) -- nil
print( arr[1] ) -- hello
print( arr[2] ) -- world
    

Another strange aspect to Lua is that everything is a table. There is no keyword for classes or inheritance, some additional fluff is required to implement inheritance.

For Python, the entire structure of the language is based on white-space, which can be an infuriating exercise with some text-editors that automatically insert whitespace.


# prints foo bar if a equals b
if a == b:
    print( "foo" )
    print( "bar" )

# prints foo if a equals b, always prints bar
if a == b:
    print( "foo" )
print( "bar" )

# IndentationError: expected an indented block
if a == b:
print( "foo" )
    

Ruby is very flexible compared to both of these languages, with mutable classes and a million ways to do a single task. This is, of course, a double-edged sword.

These if statements are all equivalent:


unless a == b
  puts "a does not equal b"
end

if a != b
  puts "a does not equal b"
end

if !( a == b )
  puts "a does not equal b"
end

puts "a does not equal b" if a != b

puts "a does not equal b" unless a == b

# ; can be used instead of new-line
if a != b; puts "a does not equal b"; end

# ternary expression
a != b ? (puts "a does not equal b") : nil
    

I very likely have missed a thousand other ways to do this particular if statement.

For a scripting language, this kind of flexibility may be desirable. One usecase could be sending single-line script commands to a server.


Position.set x: 100, y: 110, z: 40 if Player.access == :admin
Time.set [12, 45, :pm]
Cheat.no_clip # toggle
Cheat.no_clip = true # set to true
Cheat.no_clip = false # set to false
    

mruby Android Build

The first thing is to actually get mruby. The latest commit as of this writing (8488425) has the updated Android build scripts, so cloning the latest version on github is a good idea.

Follow the compile guide prerequisites, the build steps for mruby is to first compile a host build and then cross-compile for other targets.

If you are using Visual Studio as a host on Windows, then open up the Visual Studio Cross Tools Command Prompt (the important thing is that the VS tool environment variables are set). Otherwise, open up a compatible shell of your choice (cygwin, xterm, macOS Terminal, etc). In the terminal, navigate to where the mruby git was cloned earlier.

We need to create a build script for our Android targets. In the mruby folder, create a next file "build_config_android.rb" and copy in the following:


# Host build config
# Does not have to be anything special
MRuby::Build.new do |conf|
  if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
    toolchain :visualcpp
  else
    toolchain :gcc
  end

  enable_debug
  conf.gembox 'default'
end

# Android build config
# Defaults to armeabi output using Clang
MRuby::CrossBuild.new( 'android-armeabi' ) do |conf|
  toolchain :android
end
    

Now in your terminal call the following:


./minirake MRUBY_CONFIG="build_config_android.rb"
    

If a message is printed complaining about being unable to locate the NDK home, then append the terminal call with: ANDROID_NDK_HOME="path/to/your/ndk/installation"

You may need to explicitly call ruby before the ./minirake if Ruby is not on your path variables.

If everything worked fine, the mruby build folder should contain directories "host" and "android-armeabi". Inside "android-armeabi/lib" is "libmruby.a", that's your Android mruby static library compiled.

Additional Android Targets

There's more to Android native than just regular ARM. The mruby Android build task also has support for armeabi-v7a, arm64-v8a, x86, x86_64, mips and mips64. These can be set by adding ":arch => /target_arch/" in the build script.


MRuby::CrossBuild.new( 'android-armeabi' ) do |conf|
  toolchain :android, :arch => /armeabi/
end

MRuby::CrossBuild.new( 'android-armeabi-v7a' ) do |conf|
  toolchain :android, :arch => /armeabi-v7a/
end

MRuby::CrossBuild.new( 'android-arm64-v8a' ) do |conf|
  toolchain :android, :arch => /arm64-v8a/
end

MRuby::CrossBuild.new( 'android-x86' ) do |conf|
  toolchain :android, :arch => /x86/
end

MRuby::CrossBuild.new( 'android-x86_64' ) do |conf|
  toolchain :android, :arch => /x86_64/
end

MRuby::CrossBuild.new( 'android-mips' ) do |conf|
  toolchain :android, :arch => /mips/
end

MRuby::CrossBuild.new( 'android-mips64' ) do |conf|
  toolchain :android, :arch => /mips64/
end
    

You can go further with this. By default, Android uses Clang for native projects, we can swap to using GCC while it's still around, as well as add some build arguments enabling hardware floating point support.


# armeabi-v7a hardware FPU
MRuby::CrossBuild.new( 'android-armeabi-v7a-hard' ) do |conf|
  # Set toolchain to gcc
  toolchain :android, :toolchain => :gcc, :arch => /armeabi-v7a/

  # Flags for hardware floating point
  conf.cc.flags << %W(-fpic -fstack-protector-strong -mhard-float -D_NDK_MATH_NO_SOFTFP=1 -mfpu=vfpv3-d16)

  # Compile with default gems
  conf.gembox 'default'
end
    

There's a good list of ARM compile options in the GCC online docs.

Using mruby in Android

You should be linking the libmruby.a library to your native code, so it is compiled into your Android shared object.

At the moment, mruby prints its output to stdout, so if you want to get any output on Android you'll need to forward stdout to Log. Hopefully mruby will allow output hooks in the future, but right now that's not an option.

Binding Android Log Print

Here's some code for adding the Android Log (__android_log_print) into mruby:


#include <mruby.h>
#include <mruby/string.h>
#include <android/log.h>

static mrb_value android_log_print( mrb_state * _mrb, mrb_value _self ) {
    mrb_int mode;
    mrb_value tagType;
    mrb_value strTag;
    mrb_value strMessage;

    int retVal;
    switch ( mrb_get_argc( _mrb ) ) {
        case 1:
            // Just message
            mrb_get_args( _mrb, "S", &strMessage );
            retVal = __android_log_print( ANDROID_LOG_DEBUG, "mruby", RSTRING_PTR( strMessage ) );
            break;
        case 2:
            // Tag/Message Type/Message
            mrb_get_args( _mrb, "oS", &tagType, &strMessage );
            switch ( mrb_type( tagType ) ) {
            case MRB_TT_STRING:
                retVal = __android_log_print( ANDROID_LOG_DEBUG, RSTRING_PTR( tagType ), RSTRING_PTR( strMessage ) );
                break;
            case MRB_TT_FIXNUM:
                retVal = __android_log_print( mrb_fixnum( tagType ), "mruby", RSTRING_PTR( strMessage ) );
                break;
            default:
                retVal = 0;
                break;
            }
            break;
        case 3:
            // Type/Tag/Message
            mrb_get_args( _mrb, "iSS", &mode, &strTag, &strMessage );
            retVal = __android_log_print( mode, RSTRING_PTR( strTag ), RSTRING_PTR( strMessage ) );
            break;
        default:
            retVal = 0;
            break;
    }

    return mrb_fixnum_value( retVal );
}

void BindAndroidLog( mrb_state * const _mrbState ) {
    struct RClass * const logModule = mrb_define_module( _mrbState, "Log" );

    mrb_define_module_function( _mrbState, logModule, "print", log_print, MRB_ARGS_ARG( 1, 2 ) );
    mrb_define_const( _mrbState, logModule, "UNKNOWN", mrb_fixnum_value( ANDROID_LOG_UNKNOWN ) );
    mrb_define_const( _mrbState, logModule, "DEFAULT", mrb_fixnum_value( ANDROID_LOG_DEFAULT ) );
    mrb_define_const( _mrbState, logModule, "VERBOSE", mrb_fixnum_value( ANDROID_LOG_VERBOSE ) );
    mrb_define_const( _mrbState, logModule, "DEBUG", mrb_fixnum_value( ANDROID_LOG_DEBUG ) );
    mrb_define_const( _mrbState, logModule, "INFO", mrb_fixnum_value( ANDROID_LOG_INFO ) );
    mrb_define_const( _mrbState, logModule, "WARN", mrb_fixnum_value( ANDROID_LOG_WARN ) );
    mrb_define_const( _mrbState, logModule, "ERROR", mrb_fixnum_value( ANDROID_LOG_ERROR ) );
    mrb_define_const( _mrbState, logModule, "FATAL", mrb_fixnum_value( ANDROID_LOG_FATAL ) );
    mrb_define_const( _mrbState, logModule, "SILENT", mrb_fixnum_value( ANDROID_LOG_SILENT ) );
}
    

After calling BindAndroidLog with our mruby state, the Android Log functions are available in mruby:


Log.print "Debug log with tag: mruby"
Log.print "my_tag" "Debug log with tag: my_tag"
Log.print Log::INFO, "Info log with tag: mruby"
Log.print Log::ERROR, "my_tag", "Error log with tag: my_tag"
    

Unlike with __android_log_print, the formatting is done via ruby:


x = 123
Log.print "The value of x is #{x}" # Prints "The value of x is 123"
    

There's a lot more that can be done than just the Android Log function. It is possible to write JNI functions that convert mruby types into JNI types, allowing ruby script calls from Java to be sent to JNI, to be interpreted by mruby, then sent back to JNI for translation, then finally sent back up to Java.