GObjectについて
この記事では C++ とか Java とかを触ってる人が、 それと同等のコードを GObject 上に構築するにはどうしたらいいのかという視点で解説していきたいと思います。 ただし、一部にそれらの内部実装に関する知識を要する部分があるので、 使ったことがあるというだけでは理解が難しい部分があるかもしれません。
GObject 使おうとしても意外と Web 上に情報がなく…… (これは単に調べるのが下手という説もある)。
GObject とは
GObject は C で書かれたユーティリティライブラリである GLib の一部で、 主に GTK の構築に使われているオブジェクトシステム (オブジェクト指向ぽくコード書くフレームワーク) です。
オブジェクト関連の言語機能がない C で簡単に OOP できます。
……ということになってる (のか知らないけど) けど、C++ から便利機能とか オブジェクト関連の構文を取り除いた感じで、割と自分で管理しないといけない部分が多い。 実際のところ感覚的には他言語のバインディングを触ってる感覚に近いです。
というわけでこのライブラリが満たせる需要は、
- GTK をハックしたい
- どうしても C で書かなければならず、C で OOP するための仕組みを自分で作るのは避けたい
という感じだと思います。 逆に、手っ取り早く C 的な、オブジェクトシステムのない辛い世界から抜け出したいという 需要なら普通に C++ で書く方が良いと思います。
実行環境
この記事では以下の GLib のバージョンを使用して検証しています。
- GLib 2.68.3
おそらく GLib 2 系であれば大きくは変わらないと思いますが。
GObject を使用するには pkg-config で glib-2.0
と gobject-2.0
を指定します。このへんのパッケージ名はディストロによって微妙に異なるかもしれません。
Meson であれば以下のようになります。
project('foo', 'c')
glib_deps = [dependency('glib-2.0'), dependency('gobject-2.0')]
sources = ['main.c', 'foo.c', 'bar.c']
executable(meson.project_name(), sources, dependency : glib_deps)
おおまかな流れ
だいたい決まったパターンとして、ヘッダファイルで G_DECLARE_*
系のマクロを使って
クラスやインターフェースを宣言し、C ファイルで G_DEFINE_*
系のマクロと
必要な関数を実装してクラスを定義していくという流れになります。
final class
の宣言と定義
おそらく最も簡単なパターンがこれです。 クラスが継承されるという前提が必要ないということは virtual なメソッドが必要ない、 つまり vtable を考える必要がないということになります。
この部分に相当するコードを Java で書くとするとこのようになります。
public final class Counter {
private int count;
public Counter() {
count = 0;
}
public int getCount() {
return count;
}
public void setCount(int newCount) {
count = newCount;
}
public final void increase() {
count += 1;
}
}
まず、ヘッダファイルでクラスの宣言をします。 これは GLib のマクロがあるので 1 行でできます。
#ifndef COUNTER_H
#define COUNTER_H
#include <glib-object.h>
G_DECLARE_FINAL_TYPE(SampleCounter, sample_counter, SAMPLE, COUNTER, GObject)
#define SAMPLE_TYPE_COUNTER sample_counter_get_type()
SampleCounter *sample_counter_new(void);
#endif
G_DECLARE_FINAL_TYPE
の第 5 引数は親クラスのオブジェクトクラスです。
SAMPLE_TYPE_COUNTER
を宣言していますが、これは利便性のためで、必須ではありません
(が、慣習的に宣言するようです)。
また、利便性のために sample_counter_new
という関数を宣言していますが、
なくても何ら問題ありません。
それから C ファイルで実際に定義をしていきます。
#include "counter.h"
struct _SampleCounter {
GObject parent_instance;
};
G_DEFINE_TYPE(SampleCounter, sample_counter, G_TYPE_OBJECT)
void sample_counter_init(SampleCounter *self) {}
void sample_counter_class_init(SampleCounterClass *klass) {}
SampleCounter *sample_counter_new(void) {
return g_object_new(sample_counter_get_type(), NULL);
}
_SampleCounter
構造体の最初のメンバは親クラスの型のメンバを入れることになっています。
また、G_DEFINE_TYPE
の第 3 引数には親クラスを表す GType を渡します
(*_get_type()
系の関数から取ることができ、多くの場合
MODULE_TYPE_CLASS
という命名規則のマクロが定義されています)。
sample_counter_init
という関数はインスタンスが生成されるたびに呼ばれ、
メンバの初期化を行います。
sample_counter_class_init
はそのクラス (もしくはサブクラス) が最初にインスタンス化された
ときに呼ばれ、クラス変数の初期化を行います。
ちなみに、self
、klass
といった変数名を使用しているのは、
C++ で予約されていない名前を使うことが推奨されているからで、
this
や class
と意味的な違いはありません。
マクロ: G_DECLARE_FINAL_TYPE
継承できないクラスの宣言をするマクロです。
なぜ継承できないかというと、GObject で継承をするにはオブジェクトのインスタンスを
子クラスの構造体の先頭に埋め込むのですが、このマクロを使用する場合にはこれが定義されず、
不完全型になるからです。
逆に言うとこの構造体をヘッダファイルの中に書くと継承できるようになりますが、
その場合は素直に後述の G_DECLARE_DERIVABLE_TYPE
を使えば良いでしょう。
final class
にプロパティを足す
final でないクラスでも共通の方法ですが、隠蔽したい (private) のメンバを格納する 構造体を作る必要があります (final のクラスの場合、クラスのメンバの構造体を公開する必要がないため 全体が private になっており、ここで宣言することも可能ですが)。
クラスに private なデータを追加する方法を解説します。
先ほどの C ファイルに少し手を加えます。
#include "counter.h"
struct _SampleCounter {
GObject parent_instance;
};
typedef struct {
gint count;
} SampleCounterPrivate;
G_DEFINE_TYPE_WITH_PRIVATE(SampleCounter, sample_counter, G_TYPE_OBJECT)
void sample_counter_init(SampleCounter *self) {}
void sample_counter_class_init(SampleCounterClass *klass) {}
SampleCounter *sample_counter_new(void) {
return g_object_new(sample_counter_get_type(), NULL);
}
G_DEFINE_TYPE
が G_DEFINE_TYPE_WITH_PRIVATE
に変わり、
SampleCounterPrivate
が追加されたのが分かると思います。
SampleCounterPrivate
が実際にプライベートなメンバを入れる構造体です。
sample_counter_init
でこれを初期化してみましょう。
void sample_counter_init(SampleCounter *self) {
SampleCounterPrivate *priv = sample_counter_get_instance_private(self);
priv->count = 0;
}
G_DEFINE_TYPE_WITH_PRIVATE
を使うと、プライベート構造体へのポインタを取得する
*_get_instance_private
関数が自動的に定義されます。
そのポインタを使って中のメンバにアクセスすることができます。
次に GObject の特徴とも言えるプロパティを作る方法です。 以下の内容を C ファイルに追加します。
enum { PROP_0, PROP_COUNT, N_PROP };
static GParamSpec *PROPS[N_PROP] = {NULL};
static void sample_counter_set_property(GObject *self, guint prop_id,
const GValue *val, GParamSpec *spec) {
SampleCounterPrivate *priv =
sample_counter_get_instance_private(SAMPLE_COUNTER(self));
switch (prop_id) {
case PROP_COUNT:
priv->count = g_value_get_int(val);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(self, prop_id, spec);
break;
}
}
static void sample_counter_get_property(GObject *self, guint prop_id,
GValue *val, GParamSpec *spec) {
SampleCounterPrivate *priv =
sample_counter_get_instance_private(SAMPLE_COUNTER(self));
switch (prop_id) {
case PROP_COUNT:
g_value_set_int(val, priv->count);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(self, prop_id, spec);
break;
}
}
void sample_counter_class_init(SampleCounterClass *klass) {
GObjectClass *object_klass = G_OBJECT_CLASS(klass);
object_klass->set_property = sample_counter_set_property;
object_klass->get_property = sample_counter_get_property;
PROPS[PROP_COUNT] = g_param_spec_int(
"count", "Count", "Value of the counter", 0, 100, 0, G_PARAM_READWRITE);
g_object_class_install_properties(object_klass, N_PROP, PROPS);
}
少し量が多いですが、やっていることは単純です。
まず、sample_counter_class_init
で GParamSpec *
の配列に
プロパティの名前や型に関する情報を入れ (g_param_spec_int
)、
それをクラスに追加 (g_object_class_install_properties
) しています。
g_param_spec_*
のバリエーションでいろいろな型のプロパティを追加することができます。
object_klass->set_property
と object_klass->get_property
は
プロパティに値をセットするときやプロパティから値を取得するときに、
実際に値を保存したり読み出したりするための関数をセットします。
g_param_spec_int
の第 7 引数に G_PARAM_READWRITE
を
渡していますが、この READ や WRITE に対応する get/set 関数を
先にセットしておかないとプロパティを追加できないので注意が必要です。
メソッド
final なクラスなので virtual なメソッドを作ることはないと思います。 (オーバーライドを含む仮想関数の取り扱いについては後述)。
単純に第一引数に自身を取る関数を作ります。
void sample_counter_increase(SampleCounter *self) {
SampleCounterPrivate *priv =
sample_counter_get_instance_private(SAMPLE_COUNTER(self));
priv->count++;
}
継承できるクラス
継承できないクラスを作る場合とほとんど変わりません。 ただし、ヘッダファイルでクラス構造体を定義し、 C ファイルの方ではオブジェクトの構造体を定義しません。
Java だとこんな感じ。
public class Counter {
private int count;
// [...]
public void print() {
System.out.printf("Counter#print: %d\n", count);
}
// [...]
}
public class DoubleCounter extends Counter {
// [...]
@Override
public void print() {
System.out.printf("DoubleCounter#print: %d\n", getCount() * 2);
}
// [...]
}
以下が具体的な例です。
まずヘッダファイル
#ifndef COUNTER_H
#define COUNTER_H
#include <glib-object.h>
G_DECLARE_DERIVABLE_TYPE(SampleCounter, sample_counter, SAMPLE, COUNTER, GObject)
struct _SampleCounterClass {
GObjectClass parent_instance;
};
SampleCounter *sample_counter_new(void);
#endif
次に C ファイルです。
#include "counter.h"
G_DEFINE_TYPE(SampleCounter, sample_counter, G_TYPE_OBJECT)
static void sample_counter_init(SampleCounter *self) {}
static void sample_counter_class_init(SampleCounterClass *klass) {}
SampleCounter *sample_counter_new(void) {
return g_object_new(sample_counter_get_type(), NULL);
}
ヘッダファイルの _SampleCounterClass
構造体の最初のメンバは親クラスのクラス構造体を
埋め込むことになっています。
プライベートメンバを追加したい場合も、final class の場合と同様です。
別のプロパティを追加
親クラスと名前が被らない限り、意図した通りに動作します (あたかも同じオブジェクトで宣言されているように動く)。
仮想関数
仮想関数はクラス構造体に関数ポインタを追加することで実現します。 つまり、クラス構造体を vtable のように使うということです。
例として、count
として保持されている値を出力するメソッドを作ってみます。
ヘッダファイルの struct _SampleCounterClass
の定義を以下のように変更します。
struct _SampleCounterClass {
GObjectClass parent_instance;
void (*print)(SampleCounter *self);
};
そして、ここに sample_counter_class_init
の中で適切な関数ポインタをセットします。
static void sample_counter_print_internal(SampleCounter *self) {
SampleCounterPrivate *priv = sample_counter_get_instance_private(self);
g_print("SampleCounter#print: %d\n", priv->count);
}
static void sample_counter_class_init(SampleCounterClass *klass) {
klass->print = sample_counter_print_internal;
}
サブクラスでこのメソッドをオーバーライドする場合は次のように書きます。 count を 倍にして表示する DoubleCounter クラスを作る例です。
static void sample_double_counter_print(SampleCounter *self) {
int count;
g_object_get(self, "count", &count, NULL);
g_print("SampleDoubleCounter#print: %d\n", count * 2);
}
static void sample_double_counter_class_init(SampleDoubleCounterClass *klass) {
SampleCounterClass *parent_klass = SAMPLE_COUNTER_CLASS(klass);
parent_klass->print = sample_double_counter_print;
}
なお、慣習的に構造体のメンバへのアクセスをよしとしないようなので (単純に分かりにくくもある)、 この関数ポインタを使用して値を出力するラッパーを作っておいた方が良いかもしれません。 (こういうラッパーがない仮想関数をプライベートと呼んでいたりする)。
void sample_counter_print(SampleCounter *self) {
SampleCounterClass *klass = SAMPLE_COUNTER_GET_CLASS(self);
klass->print(self);
}
以下のコードで、動的なポリモーフィズムが実現できていることが確認できると思います。
#include <glib.h>
#include "counter.h"
#include "doublecounter.h"
int main(void) {
SampleCounter *counter = sample_counter_new();
g_object_set(counter, "count", 10, NULL);
sample_counter_print(counter);
g_object_unref(counter);
SampleDoubleCounter *double_counter = sample_double_counter_new();
g_object_set(double_counter, "count", 10, NULL);
sample_counter_print(SAMPLE_COUNTER(double_counter));
g_object_unref(double_counter);
}
// 出力
// SampleCounter#print: 10
// SampleDoubleCounter#print: 20
継承可能なクラスを作る場合の ABI 互換性
構造体のサイズが変わるとそのサブクラス全部のオフセットが変わることになるので ABI 互換性が崩れます (つまり構造体末尾にメンバを追加することが原理的にできない)。 そこで、クラス構造体には以下のようにパディングを入れ、仮想関数を増やせるようにする ことが推奨されています。
struct _SampleCounterClass {
GObjectClass parent_instance;
void (*print)(SampleCounter *self);
gpointer padding[12];
};
おしまい
本当はインターフェースについても書こうと思ったんですが、疲れたのでとりあえず終わりにします。 インターフェースについて書いたらここにリンクを貼ろうと思います。 (数ヶ月経って貼られていなかったら察してください……)
参考
- https://developer.gnome.org/gobject/stable/howto-gobject.html
- ここの説明は割と適当です。
- よく使うようであれば、glib2-docs とかで入るオフラインのドキュメントがおすすめです。
- https://gitlab.gnome.org/GNOME/glib
- 結局ソースコード見るのが早いとか。
- https://gitlab.gnome.org/GNOME/gtk
- 一番ヘビーに使ってるはず (マクロは使ってないのでそこは参考にならん)。当然強い。