Android Emulatorでmrubyを動かす

Webで使えるmrubyシステムプログラミング入門

この本を読み終わり、何かやってみたいな、ということでAndroidで動かしてみることにしました。

iOSは結構やっている人がいるようだったし、僕自身iOSにはそこそこ慣れてることもあり学びが少なそうだったので、 あえてAndroidで挑戦です。

プロジェクト作成

  • Phone and TabletNative C++を選択
  • LanguageKotlin
  • Minumu SDKAPI 29
  • C++ StandardToolchain Default

後は適当。

プロジェクト準備ができるまでしばらく待ってから実行すると、Hello from C++と表示される。
この文字列はnative-lib.cppというファイルで実装されたプログラムから返された文字列をkotlinで表示している。

このnative-lib.cppでmrubyを実行して結果を返してあげれば目標は達成。

f:id:tnantoka:20201203223033p:plain

mrubyのbuild

native-lib.cppからmrubyを呼び出すため、Androidエミュレーターで動くlibmruby.aを作ります。

$ cd ~/Documents/AndroidStudio/Hellomruby
$ git clone --depth 1 https://github.com/mruby/mruby.git -b 2.1.2 mruby-2.1.2

https://github.com/mruby/mruby/issues/4563#issuecomment-510281362 を参考にcustom_build_config.rbを用意。

MRuby::Build.new do |conf|
  conf.toolchain :clang
  conf.gem core: 'mruby-bin-mrbc'
end

MRuby::CrossBuild.new('android_x86') do |conf|
  api = 29
  conf.toolchain :android, arch: 'x86', platform: "android-#{api}"

  Dir.glob("#{root}/mrbgems/mruby-*/mrbgem.rake") do |x|
    g = File.basename File.dirname x
    conf.gem :core => g unless g =~ /^mruby-(test|bin-.*)$/
  end

  conf.cc do |cc|
    cc.defines << "__ANDROID_API__=#{api}"
    cc.flags << "--sysroot=#{ENV['ANDROID_NDK_HOME']}/sysroot"
  end
end

custom_build_config.rbを使ってビルドします。

$ cp -r ~/Library/Android/sdk/ndk/21.1.6352462/sysroot/usr/include/android ~/Library/Android/sdk/ndk/21.1.6352462/platforms/android-29/arch-x86/usr/include

$ cd mruby-2.1.2
$ rake MRUBY_CONFIG=../custom_build_config.rb ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/21.1.6352462/

ビルドしたファイルを含め、mruby関連のファイルをAndroidのProjectにコピー。

$ cd ..

$ mkdir -p app/src/main/cpp/imported-lib/x86/
$ cp mruby-2.1.2/build/android_x86/lib/libmruby.a app/src/main/cpp/imported-lib/x86/

$ cp -r mruby-2.1.2/include/mruby app/src/main/cpp/mruby
$ cp mruby-2.1.2/include/mruby.h mruby-2.1.2/include/mrbconf.h app/src/main/cpp/

https://github.com/PierceLBrooks/MobiBot-Android/blob/50cbcdba1380f460418215427d0ceba7fd1f72d0/app/CMakeLists.txt を参考にCMakeLists.txtに追記します。

add_library(
        mruby
        STATIC
        IMPORTED )

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

set_target_properties(
        mruby
        PROPERTIES IMPORTED_LOCATION
        ${CMAKE_CURRENT_SOURCE_DIR}/imported-lib/${ANDROID_ABI}/libmruby.a )

target_link_libraries(
        native-lib
        mruby)

以下を参考に native-lib.cpp で、引数を受け取って、それをmrubyで実行した結果を返すように変更します。

#include <jni.h>
#include <string>

#include <mruby.h>
#include <mruby/compile.h>
#include <mruby/string.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_tnantoka_hellomruby_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */,
        jstring arg) {
    const char *source = env->GetStringUTFChars(arg, 0);

    mrb_state *mrb = mrb_open();
    mrb_value v;

    v = mrb_load_string(mrb, source);
    env->ReleaseStringUTFChars(arg, source);

    if (mrb->exc) {
        std::string error = std::string(mrb_string_cstr(mrb, mrb_inspect(mrb, mrb_obj_value(mrb->exc))));
        return env->NewStringUTF(std::string("error: " + error).c_str());
    }

    const char *ret;
    if (mrb_string_p(v)) {
        ret = mrb_string_cstr(mrb, v);
    } else {
        ret = mrb_string_cstr(mrb, mrb_inspect(mrb, v));
    }

    mrb_close(mrb);
    return env->NewStringUTF(std::string(ret).c_str());
}

最後にMainActivity.ktからRubyっぽいコードを渡して結果を受け取ります。

-        findViewById<TextView>(R.id.sample_text).text = stringFromJNI()
+        findViewById<TextView>(R.id.sample_text).text = stringFromJNI("'hello! ' * 3")

-    external fun stringFromJNI(): String
+    external fun stringFromJNI(arg: String): String

実行すると…

f:id:tnantoka:20201203233448p:plain

動きました!

次は実機で動かしたい。