⚙️

GObject を Rust で使う (入門)

2022/12/09に公開

はじめに

この記事は Rust Advent Calendar 2022 の 9 日目の記事です。

GStreamer (ものすごく雑に言うと Windows の DirectShow や MediaFoundation に相当するメディアフレームワーク) を触り始めたのですが、せっかくなので Rust でやることにしました。

でプラグインを作ろうとした段階で、 GStreamer の API の基盤となっている GLib 、というか GObject と真面目に向き合わないとダメだなあ、ということで GStreamer は一旦横に置いておいて、 GObject 自体と Rust での扱い方を勉強することにしました。

この記事では Rust で GObject ベースのクラスの実装の仕方の (触りの部分) をまとめていきたいと思います。

本記事では扱いませんが、 GStreamer の Rust bindings は GStreamer のプロジェクト下で管理されているので GStreamer を Rust で扱うのは本流と見てよさそうに思います。

https://gitlab.freedesktop.org/gstreamer/gstreamer-rs

この記事のサンプルプロジェクト

https://github.com/aosoft/gobject-rust

作業環境:

  • Windows 11 (22H2)
  • WSL + Ubuntu 22.04
  • Rust 1.65.0
  • CLion 2022.2.4 (Linux 版 (WSLg で実行))

参考資料

https://github.com/sdroege/gobject-example-rs

Rust で GObject を扱うサンプルで、 API リファレンスを除いて実質これしか資料がなかったのですが、現在の glib crate と互換性がなく、ビルドが通らないので動かしての確認はできませんでした。

またそもそも GObject 自体が初見だったので基本となる C API でのチュートリアルも。

https://www.freedesktop.org/software/gstreamer-sdk/data/docs/2012.5/gobject/howto-gobject.html

GObject とは

https://docs.gtk.org/gobject/

GObject は言語非依存のオブジェクト指向コンポーネントを実装するためのライブラリで、 GLib の一部になります。

GLib は元々は有名な GUI フレームワークである GTK の一部だったのですが、そこから GUI に非依存な汎用的なライブラリを分離したものになります。

GObject の特徴としては

  • 継承可能なクラス、インターフェース、プロパティ、シグナルなどオブジェクト指向言語、コンポーネントの機能を提供している
  • 参照カウンターによるリソース管理機構を備えている
  • C 言語をベースとしており、様々な言語での利用が考慮されている (ABI 互換性が高い)

などが挙げられます。

位置付け的には Objective C の NSObject や COM (COM はインターフェースのみで NSObject のような共通となるベースクラスは存在しませんが) と同様なものかなと思います。 GObject のベースクラス実装は GObject 本体のライブラリが提供します。

導入

まず GLib のインストールをします。私は先に GStreamer をインストールした関係でそちらで入ったと思うのですが、単体で入れる場合は "libglib2.0-dev" をインストールすればよいと思います。

必要な crate は "glib" ですので、 cargo.toml に定義しておきます。

https://github.com/gtk-rs/gtk-rs-core

https://crates.io/crates/glib

GObject のクラスを Rust で実装する

glib crate には GObject のクラスを定義するための方法として次の二通りが用意されています。

  • a) C API で定義された GObject クラスの binding コードを記述する
  • b) Rust で GObject の仕様に従ったクラスを実装する

a) は基本的には他言語で書かれた GObject を Rust で使えるようにするためのものです。 GTK や GStreamer ではこちらを使って Rust から GTK や GStreamer を使えるようにしています。ただ定義する側としては C API と同じ構造体や関数テーブルなどを Rust で個々に記述する必要があるため、通常は自動生成ツール (GIR) で C のヘッダーから生成するものと思います。

https://github.com/gtk-rs/gir

ここでは b) の手順について解説していきます。

モジュール構成

gtk-rs や gstreamer-rs などを見る感じ、 GObject 実装クラスでは

  • 最終的に外部に公開する部分をメインモジュールに定義する
  • 内部実装を imp サブモジュールに定義する

という構成をとるのが一般的のようです。それに加えて gobject-example-rs では

  • 外部言語 (C API) 相当の定義を ffi サブモジュールに定義する

としています。例えば foo モジュールに実装するとして

+ src
  + foo
    + ffi.rs
    + imp.rs
    + mod.rs 

といったファイル構成になります。

ここでは gobject-example-rs の構成を参考に自分なりにアレンジした形にしています。

クラスの概要図

雰囲気理解用に mermaidjs でクラス図を書いてみました。mermaidjs の制約 (?) でモジュール名との区切りは _ にしました (imp_Foo → imp::Foo) 。 trait もクラス扱いで書きました。 trait はマクロなどによりもっと実装がされているはずです。
ObjectSubclass trait など Assoicated Types があるものはメンバで書いてしまっています (本来はその特定の型を持っているわけではないです) 。
ffi::Foo (ffi_Foo) 、 ffi::Bar (ffi_Bar) は C で書く場合に相当するものを書いています (詳細は後述) 。

Bar (Foo の継承) も基本的に Foo と同じなので一部省略しています。

基本構成の定義、実装

まずクラス、プライベートフィールドなどの基本要素の定義を行います。
(メソッドは次項)

imp サブモジュールの実装

https://github.com/aosoft/gobject-rust/blob/master/src/foo/imp.rs

imp サブモジュールではクラスに必要な要素のうち下記のものを定義していきます。

  • インスタンスが保持する private フィールド
  • クラスのメタ情報
  • vtable 定義
private フィールド

一般的なオブジェクト指向言語における private フィールドに保持するものを構造体に定義します。

https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/gobject/howto-gobject.html

で言うところの "private structure" に相当します。

foo/imp.rs
#[derive(Default)]
pub struct Foo {
    pub a: RefCell<i32>,
    pub b: RefCell<i32>,
}

private structure の名前は "そのクラスの論理的な名前" を付けます。

GObject の中では "常に更新可能である" 事を前提としているようで、 Rust 的な借用チェックは事実上機能しません。 Immutable な参照を使うしかない事もあるので更新が発生するメンバーは RefCell 等を指定しておく必要があります (よって安全性は自身で担保するしかありません) 。

クラスのメタ情報

GObject ではクラスの継承関係などの情報を定義しておく必要があります。

https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/gobject/gtype-conventions.html

で言うところの get_type の実装に相当します。

foo/imp.rs
#[glib::object_subclass]
impl ObjectSubclass for Foo {
    const NAME: &'static str = "Foo";
    const ABSTRACT: bool = false;
    type Type = super::Foo;

    fn instance_init(_obj: &InitializingObject<Self>) {
        unsafe {
            let r = _obj.as_ref().imp();
            *(r.a.borrow_mut()) = 1;
            *(r.b.borrow_mut()) = 2;
        }
    }
}

object_subclass 属性の付与が必要です。

Associated Constants, Assoicated Types として最低限 NAME と Type は定義が必須ですが、このうち Type については前項で定義した private structure ではなく GObject の型になります。この定義は後で出てくるのでとりあえずこういうものが必要だというくらいの軽い認識をしておいてください。

instance_init は initializer の実装をするところです。 private structure の初期値をここで設定します。

Foo は GObject を継承しているので、それを表す ObjectImpl trait を実装します。

foo/imp.rs
impl ObjectImpl for Foo {}

ffi サブモジュールの実装

https://github.com/aosoft/gobject-rust/blob/master/src/foo/ffi.rs

ffi サブモジュールには GObject の C API として必要な

  • GObject の instance structure
  • GObject の class (必要なら ※後述)

を定義します。

instance structure

ObjectSubClass を使う場合、 ObjectSubClass::Instance が該当するようなので

foo/ffi.rs
pub type Foo = <super::imp::Foo as ObjectSubclass>::Instance;

としています。
ここでつけている alias も private structure と同じ "そのクラスの論理的な名前" としています。

この instance structure は実際はこうなっていると思われます。

#[repr(C)]
pub struct Foo {
    pub parent: glib::gobject_ffi::GObject,
    pub priv: *mut super::imp::Foo
}

メインモジュールの実装

https://github.com/aosoft/gobject-rust/blob/master/src/foo/mod.rs

メインモジュールで最も重要なのはこれです。

foo/mod.rs
glib::wrapper! {
    pub struct Foo(ObjectSubclass<imp::Foo>);
}

glib::wrapper マクロが必要な trait 実装を行ってくれます。

メソッドの実装

メソッドを実装しないと何もできないので実装していきます。

とりあえず private structure 自体は純粋に Rust の構造体なので、一旦ここに実体メソッドを実装しておきます。

foo/imp.rs
impl Foo {
    pub fn a(&self) -> i32 {
        *(self.a.borrow())
    }
    // 略
}

メインモジュールの構造体にメソッドを実装する

メソッドの実装には private structure にアクセスする必要があります。

メインモジュールで定義した構造体には glib::wrapper マクロにより様々な trait が実装されていて、ここでは ObjectSubclassIsExt trait の imp メソッドを使うと private structure にアクセスできます。

foo/mod.rs
use glib::subclass::types::ObjectSubclassIsExt;

impl Foo {
    pub fn a(&self) -> i32 { self.imp().a() }
    // 略
}

こうすると Rust 内で閉じている状況では直接実装にアクセスできるようになります。

let foo = glib::object::Object::new::<foo::Foo>(&[]);

println!("{}", foo.a());

glib::object::Object::new は glib::wrapper マクロを適用した Rust の GObject クラスのインスタンスを生成する関数です。

C API 用の関数を実装する

C API では instance structure のポインタが this ポインタとして渡されることになっています。 instance structure は ObjectSubclass::Instance なので InstanceStructExt trait の imp メソッドを使うとこれもまた private structure にアクセスできます。

foo/ffi.rs
use glib::subclass::types::InstanceStructExt;

#[no_mangle]
pub unsafe extern "C" fn foo_get_a(this: *mut Foo) -> i32 {
    (*this).imp().a()
}

C API 用のインスタンス生成、 get_type 関数を実装する

get_type 関数は GObject のルール的に必要 (g_type_create_instance 関数でインスタンスを生成するのに使う) で、インスタンス生成の関数は必須ではないですがあると便利だと思うので一緒に定義します。

インスタンス生成は前項でも使った Object::new を用いますが、これで生成されるインスタンスは Rust のメモリ管理の仕組みで維持、破棄がされるためスコープが抜けると (参照カウンターが 0 になるため) 削除されてしまいます。

ObjectType trait に実装されている as_object_ref メソッドを使うと参照を管理している ObjectRef が取得でき、この中の to_glib_full メソッドを使うと強参照を取得 (参照カウンターを +1 する) したポインターを取得できるのでこれを返します。

foo/ffi.rs
use glib::ObjectType;

#[no_mangle]
pub unsafe extern "C" fn foo_new() -> *mut Foo {
    let obj = glib::object::Object::new::<super::Foo>(&[]);
    obj.as_object_ref().to_glib_full() as *mut Foo
}

get_type は StaticType trait の static_type メソッドを使って実装。これは定型実装になるかなと思います。

foo/ffi.rs
#[no_mangle]
pub extern "C" fn foo_get_type() -> glib::ffi::GType {
    <super::Foo as glib::StaticType>::static_type().into_glib()
}

継承クラスを実装する

親クラス (Foo) 側を継承可能にする

前項までの実装だと foo を継承するクラスを実装できないので、 foo を継承できるように変更します。

imp サブモジュールの実装

Impl 系 trait (どう言えばいいのかわからないのでこう呼びます) を定義します。 Impl 系 trait は static ライフタイム境界を持ち、ObjectImpl (またはその継承) trait を継承するものです。 Impl 系 trait は private structure に実装するので名前も private structure 名に Impl を加える形にします。

foo/imp.rs
pub trait FooImpl: ObjectImpl + 'static {}

基本的には継承関係を表現するための trait なので中の定義は必要ありませんが、 private structure に実装させたいものを定義してもよいです。

定義した trait は継承クラスである Bar の private structure に実装します (元になるクラス (Foo) は実装しなくてよいようです) 。

メインモジュールの実装

Foo のメインモジュールで IsSubclassable trait の実装をします。

foo/mod.rs
unsafe impl<T: imp::FooImpl> IsSubclassable<T> for Foo {}

継承クラス (Bar) を実装する

基本的には foo の時と同じです。

imp サブモジュールの実装

ObjectSubclass を定義しますが、今回は ParentType で親クラスを指定します。 Foo の時は未指定でしたが、この場合は暗黙で GObject が親になります。 ParentType クラスで指定できるのは IsSubclassable trait が実装されている事が条件です。

bar/imp.rs
#[glib::object_subclass]
impl ObjectSubclass for Bar {
    const NAME: &'static str = "Bar";
    const ABSTRACT: bool = false;
    type ParentType = crate::foo::Foo;
    type Type = super::Bar;
    type Class = super::ffi::BarClass;

    // 略
}

Bar は Foo を継承しているので ObjectImpl に加えて FooImpl も実装します。

bar/imp.rs
impl ObjectImpl for Bar {}

impl FooImpl for Bar {}

メインモジュールの実装

こちらも glib::wrapper マクロで継承元の親クラスを @extends ~ で指定します。

bar/mod.rs
glib::wrapper! {
    pub struct Bar(ObjectSubclass<imp::Bar>) @extends foo::Foo;
}

継承先で継承元のメソッドを使う

前項までの対応は GObject のメタ情報としての継承関係の定義で Rust の言語としての継承関係ではないので、このままだと継承クラスのインスタンスに対して継承元のメソッドを使うといった事ができません。

Rust で継承を実装する場合、 trait を用いるので、 GObject でも trait を用いて対応します。

メインモジュールの実装

Foo で公開したいメソッドを extension trait として定義します。

foo/mod.rs
pub trait FooExt {
    fn a(&self) -> i32;
    // 略
}

FooExt の実装を定義しますが、ここで IsA trait を組み合わせて指定します。 IsA trait は継承関係があるかどうかのチェックをしてくれる trait です。

foo/mod.rs
impl<O: IsA<Foo>> FooExt for O {
    fn a(&self) -> i32 {
        self.as_ref().imp().b()
    }
    // 略
}

IsA trait を通すことにより Foo 自身、及び継承しているクラス (メインモジュールの構造体) のみアクセスできる trait 実装として定義できるようになります。

O はメインモジュールの構造体であるはずなので、前項のように imp メソッドで private structure にアクセスできます。

以上により Foo, Bar 両クラスのどちらからでも Foo のメソッドにアクセスができるようになりました。

仮想メソッド対応 (vtable 定義)

継承クラス側で特定のメソッドを上書き実装をする場合、上書き対象のメソッドは仮想メソッドでなければなりません。

仮想メソッドはあらかじめ継承元クラスで vtable を定義し、その vtable に対応するメソッドへの関数ポインターを登録しておきます。上書きしたいメソッドは継承クラス側で vtable を上書き更新すればよいわけです。

一般的なオブジェクト指向言語では上記のことは言語側の機能でサポートされるわけですが、 GObject はそうではないので手作業で対応する必要があります。

親クラス (Foo) に Class クラスを定義する

ffi サブモジュールの実装

vtable は Class クラスに定義します。 Class クラスは論理的な名前の接尾に "Class" をつけたものにするのが一般的なルールのようです。

Class クラスは GObject 的には必要 (instance structure とペアで定義する) なのですが、 ObjectSubclass trait を使う場合、 Class クラスの定義は必須ではない (既定実装がうまいこと対処してくれる) ようです。例えば既存フレームワークに組み込むプラグインクラスを実装する場合などが該当します。実際、 GStreamer のプラグインは必要ないようです。

foo/ffi.rs
#[repr(C)]
pub struct FooClass {
    pub parent_class: glib::gobject_ffi::GObjectClass,
    pub get_a:Option<unsafe extern "C" fn(*mut Foo) -> i32>,
    pub set_a:Option<unsafe extern "C" fn(*mut Foo, value: i32)>,
}

unsafe impl ClassStruct for FooClass {
    type Type = super::imp::Foo;
}

ここの *mut Foo は ffi::Foo です。
Class クラスは ClassStruct trait の実装が必須です。 Type には private structure の型を指定しておきます。

vtable に登録する関数の関数ポインタとしての型は C API として公開できる形にします。

imp サブモジュールの実装

Class クラスを定義したので、前項で定義した imp サブモジュールの ObjectSubclass に Class を追加定義します。

foo/imp.rs
impl ObjectSubclass for Foo {
    // 略
    type Class = super::ffi::FooClass;
    // 略

    fn class_init(klass: &mut Self::Class) {
        // 略
        klass.get_a = Some(foo_get_a);
        // 略
    }
}

unsafe extern "C" fn foo_get_a(this: *mut super::ffi::Foo) -> i32 {
    (*this).imp().a()
}

// 以下略
  • Associated Type の Class に定義した Class クラスを指定
  • class_init メソッドを実装し、定義しておいた関数の関数ポインタを指定

vtable に登録する関数は ffi サブモジュールに定義していた C API 用関数のものにします。

vtable 経由で実装にアクセスする

前項、メインモジュールに実装した extension trait で関数ポインタを利用した形で実装をします。

foo/mod.rs
impl<O: IsA<Foo>> FooExt for O {
    fn a(&self) -> i32 {
        unsafe {
            let klass = self.as_ref().class();
            (klass.as_ref().get_a.unwrap())(
                self.as_ref().imp().instance().as_ptr() as *mut ffi::Foo
            )
        }
    }

    // 略
}

Class クラスは ObjectExt trait (ObjectSubclass が実装) している class メソッドで取得できるので、ここから定義した関数ポインタにアクセスします。

引数として渡す this ポインタは imp().instance() で取得される instance structure 、のポインタになります。

C API 用の関数を vtable 対応にする

これまでの C API 用の関数の実装は extension trait を通していなかったので vtable を通さずに直接 private structure にアクセスする形でした。これを extension trait を通す形に修正します。

foo/ffi.rs
use glib::subclass::types::ObjectSubclassExt;
use super::FooExt;

pub unsafe extern "C" fn foo_get_a(this: *mut Foo) -> i32 {
    (*this).imp().instance().a()
}

継承クラス (Bar) でオーバーライドする

継承クラスの ObjectSubclass::class_init でオーバーライドする関数を設定することでオーバーライドします。

bar/imp.rs
impl ObjectSubclass for Bar {
    // 略

    fn class_init(klass: &mut Self::Class) {
        klass.parent_class.get_a = Some(bar_get_a);
    }
}

unsafe extern "C" fn bar_get_a(this: *mut super::foo::ffi::Foo) -> i32 {
    todo!()
}

使ってみる

https://github.com/aosoft/gobject-rust/blob/master/src/main.rs

1
1, 2
10, 20
1
4, 2, 3, 4
40, 20, 30, 40
4, 2, 3, 4
40, 20, 30, 40
dropped Bar
dropped Foo
end
dropped Bar
dropped Foo
dropped Foo

一応狙った通りの動作はしてくれているようです。

おわりに

とりあえずクラスの継承までになりましたが、基本的なところは抑えられたかなあと思います。 GObject はインターフェース、プロパティ、シグナル等もあるのでその辺りもやっておきたいところです。

GTK や GStreamer を使うだけだったら GObject にあまり踏み込まなくても問題ないように思うのですが、 GStreamer のプラグインを作る場合などは GObject のクラスを作ることになるので前提知識としてこの程度は抑えておく必要があるかなとは思いました。

情報が少なくてもかなり手探りになってしまっているので間違い等ありましたらご指摘をお願いします。

Discussion