mruby + Qt (QObject・QML) で色々やってみた

Garnet というmrubyとQt (QObject) のバインディングを開発して、Qtとmrubyの連携を色々試してみました。

QMetaObject

Qtには、C++自体にはないリフレクション機能をサポートするためのQMetaObjectというものがあります。

QMetaObjectは、QObject (Qtの様々なクラスの継承元になっている) から派生した各クラスごとにビルド時に作成されるオブジェクトで、QObjectに対する動的なメソッドの呼び出しやプロパティのアクセス、シグナル・スロットのシステムなどを可能にしています。

動的なUI記述言語であるQMLも、このQMetaObjectのシステムを基盤にしていて、C++で書かれた機能にQML・JavaScriptからアクセスできるのはこのためです。

Garnet

Garnetは、このQMetaObjectを使ってmrubyからQtのオブジェクトにアクセスするためのライブラリで、 元々はh2so5さんが開発されていたプロジェクトなのですが、ここ最近は私が開発をしていました。

このライブラリを使って、以下のことができるようになりました。

  • QtらしいAPIでmrubyのスクリプトを実行させる
  • C++・Qtの値とmrubyの値の相互変換 (StringやArrayなど)
  • QObject派生のオブジェクトのメソッドやプロパティへのmrubyからのアクセス

C++とmrubyの連携

Garnetのエンジンにオブジェクトを登録する(正確には、Kernelモジュールのメソッドに追加する)ことによって、mrubyからアクセスできるようになります。

また、クラスを登録して、mruby内でnewすることもできます。

ちなみに、メソッドをmrubyからアクセスできる(QMetaObjectの対象になる)ようにするためには、前にQ_INVOKABLEを付けるか、slotにする必要があります。

// Person.h
class Person : public QObject {
public:
    Q_INVOKABLE void greet() {
        qDebug() << "Hello!!";
    }
}
#include <Garnet/Engine>

Garnet::Engine engine;
Person person;
engine.registerObject("person", &person);
engine.evaluate("person.greet")

QMLとmrubyの連携

QML内で作られたオブジェクトに対しては、動的にQMetaObjectが生成されます。 それを使うことで、QMLのプロパティやメソッドへのアクセスが可能になります。

ある意味では、mruby - C++ - JavaScript の二重のバインディングになっています。

// C++
#include <Garnet/Engine>

qmlRegisterType<Garnet::Engine>("Garnet", 1, 0, "Engine");
// QML
import Garnet 1.0 as Garnet

Rectangle {
    width: 360
    height: 360
    Text {
        id: resultText
        anchors.fill: parent
    }
    Garnet.Engine {
        Component.onCompleted: {
            registerObject("result_text", resultText)
            engine.evaluate("result_text.text = 'Hello, world!'")
        }
    }
}

思ったこと

mrubyは、記述力が高くかつアプリケーションに組み込めるのが魅力だと感じ、今後が楽しみになりました。

QMetaObjectを使うと、特別なバインディングを書かなくても、スクリプト言語などから、Qtの機能にアクセスすることができます。

QtのC++向けAPIでは、QMetaObjectからアクセス可能になっていないメソッドが多いので、実用するのは難しいですが、QMLではすべての機能がQMetaObjectからアクセスできるので、QMetaObjectによるバインディングはmrubyに限らず有用ではないかと感じました。