GObject Introspectionによるバインディングの(ほぼ)自動生成 - 2023-01-19 - ククログ

ククログ

株式会社クリアコード > ククログ > GObject Introspectionによるバインディングの(ほぼ)自動生成

GObject Introspectionによるバインディングの(ほぼ)自動生成

前回、Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法の記事で、Mesonを使ってGObject Introspection対応のビルドシステムを構築する基本的な方法を、milter managerというメールフィルタを管理するための自由ソフトウェアを事例に説明しました。

GObject Introspectionに対応することで、RubyやPythonなどのバインディングを(ほぼ)自動で生成できます。 milter managerは、これによって生成したPythonバインディングを利用することで、Pythonでmilterを作るためのライブラリーを提供できるようになりました。

今回は、GObject Introspectionによるバインディングの生成と利用について紹介します。

バインディングとは

そもそもバインディングとはなんでしょう。バインディングとは、Cなど他の言語で実装された機能を、RubyやPythonなどの他の言語から使うためのライブラリーです。

バインディングを作る1つの方法が、拡張ライブラリーを作ることです。 拡張ライブラリーとは、Cで実装したRuby・Python用のライブラリーです(最近はRustなど他の言語でも実装できるようになっています)。 例えば、Ruby用の拡張ライブラリーとは、Cで実装したRuby用のライブラリーのことです。 Ruby用のライブラリーをCで書きたい場合や、Cで書いたプログラムをRubyから使えるようにしたい場合に、拡張ライブラリーとしてバインディングを作ることが選択肢の1つになります。

一方で、拡張ライブラリーを作らずにバインディングを生成する方法もあります。 例えばRubyでは、FiddleRuby-FFIが有名です。 Rubyのプログラムを書いている途中でCのライブラリーを使いたくなった時に、これらの機能をRuby側で使うことで、バインディングを作ってCのライブラリーの機能を利用することができます。

今回利用するGObject Introspectionも、拡張ライブラリーを作らずにバインディングを生成する方法の1つです。

バインディングの様々な作り方については、次の記事で詳しく説明しています。こちらもぜひご覧ください。

GObject Introspectionとは

GObject Introspectionは、拡張ライブラリーを作らずにバインディングを生成する方法の1つです。

ライブラリーがGObjectを利用している場合は、これを使えば(ほぼ)自動で各言語のバインディングを生成できるので、とても強力です。 Ruby-FFI等の場合は関数のシグネチャなどをRuby側で指定する必要があるのですが、この方法ではGObject Introspection AnnotationsというアノテーションをC側で適切に付与することで、RubyやPythonといった複数言語にまとめて対応できます。

前回の記事を振り返ると、milter managerのcoreライブラリーのmeson.buildファイルの中で、次のような設定をしていました。

# バインディングに必要なファイル(GIRファイルとTypelibファイル)
# を生成してインストール
milter_core_gir = gnome.generate_gir(libmilter_core,
                                     export_packages: 'milter-core',
                                     extra_args: [
                                       '--warn-all',
                                     ],
                                     fatal_warnings: true,
                                     header: 'milter/core.h',
                                     identifier_prefix: 'Milter',
                                     includes: [
                                       'GObject-2.0',
                                     ],
                                     install: true,
                                     namespace: 'MilterCore',
                                     nsversion: api_version,
                                     sources: sources + headers + enums,
                                     symbol_prefix: 'milter')

これが、Mesonに組み込まれているGObject Introspectionサポート(Meson Integration)を利用している部分です。 Mesonを使えば、このように簡単にGObject Introspectionに対応できます。 これによって生成されたTypelibファイルを実行時に使って、RubyやPythonなどの言語からCで書かれているmilter managerのcoreライブラリーの機能を呼び出すことができます。

ただし、GObject Introspection AnnotationsというアノテーションをC側で適切に付与する必要があります。 このアノテーションの書き方について説明します。

GObject Introspection Annotations

GObject Introspectionによりバインディングを生成するには、GObject Introspection AnnotationsというアノテーションをC側で適切に付与する必要があります。

例えば、transferという所有権について設定するアノテーションがあります。主に関数の返り値に対して使われます。 バインディング側はこの設定を見て、その値を解放するタイミングを判断します。 受け取った側が開放するべき返り値であるのに、transfer none(所有権の移動を行わない)を設定した場合、バインディング側でメモリリークが発生することになります。 実装に応じて正しくアノテーションを設定しましょう。

今回実際にmilter managerの対応を行ったのですが、Mesonのビルドシステムを構築した時点でこのアノテーションが不十分であったため、ビルドを実行すると次のような警告が複数発生しました。

Warning: Milter: milter_agent_get_decoder: return value: Missing (transfer) annotation

このような警告に全て対応すれば、アノテーションを十分に付与できたことになります。

アノテーションの書き方

アノテーションは、関数の実装に付けます。(関数の宣言に付けることもできますが、実装の方に付けることが一般的です)。

/**で開始して*/で終わる関数のドキュメント内に、決まった書式でアノテーションを書きます。

書き方

/**
 * {関数名}: (関数に対するアノテーション)
 * @{引数名}: (引数に対するアノテーション): {引数の説明}
 * @{引数名}: (引数に対するアノテーション): {引数の説明}
 * ...
 * 
 * {関数の説明}
 *
 * Returns: (返り値に対するアノテーション): {返り値の説明}
 * 
 * {その他(tag)}
 */

/**
 * milter_agent_get_decoder:
 * @agent: A agent from which to get the decoder.
 *
 * Returns: (transfer none): The decoder of the agent.
 */
MilterDecoder *
milter_agent_get_decoder (MilterAgent *agent)
{
    return MILTER_AGENT_GET_PRIVATE(agent)->decoder;
}

ポイント

  • 各アノテーションは、必要がなければ省略します。
    • そもそも、全ての関数にアノテーションを付ける必要はありません。
    • 適切なデフォルト値がない場合に、アノテーションが必要となります。
    • 例えば、gintを返すものはtransferを指定する必要がありません。C言語ではgint(GLibがtypedef int gintしており、intと同じである型)は動的にメモリーを確保しなくていいので、所有権の情報が必要ない(デフォルトでtransfer noneである)からです。
  • 返り値が無い関数の場合は、Returnsの行は書きません。
  • 複数のアノテーションを付ける場合は、(アノテーション1) (アノテーション2):のように括弧を並べて書きます。

以下で、よく使うアノテーションについて主な使い方を説明します。

より詳しい説明は公式ドキュメントをご覧ください。

transfer

主に返り値に対して用い、返り値を受け取る側にその値の所有権を移動するかどうかを定義します。

設定可能な値:

  • none
    • 所有権を移動しません。
    • 関数の呼び出し側は、その値を解放する必要はありません。
    • シングルトンや、既存の値を参照する場合に設定します。
  • full
    • 所有権を移動します。
    • 受け取った値を使い終わったら、関数の呼び出し側がそれを開放する責任を持ちます。
    • 新たにコンストラクトしたり、g_object_refで参照を増やして返す場合に設定します。
  • container
    • GListGHashTableなどのコンテナタイプの場合にのみ有効です。
    • コンテナの所有権を移動しますが、その要素の所有権は移動しません。
    • GListGHashTableなどのコンテナを新たに作成して返しますが、その各要素は既存の値の参照である、という場合に設定します。

noneの例

シングルトンを返す場合:

/**
 * milter_logger:
 *
 * Returns: (transfer none): The singleton logger in this process.
 */
MilterLogger *
milter_logger (void)
{
    return singleton_milter_logger;
}

既存の値を返す場合:

/**
 * milter_agent_get_encoder:
 * @agent: A agent from which to get the encoder.
 *
 * Returns: (transfer none): The encoder of the agent.
 */
MilterEncoder *
milter_agent_get_encoder (MilterAgent *agent)
{
    return MILTER_AGENT_GET_PRIVATE(agent)->encoder;
}

fullの例

新たにコンストラクトした値を返す場合:

/**
 * milter_option_copy:
 * @option: A option to be copied.
 *
 * Returns: (transfer full): The copied option.
 */
MilterOption *
milter_option_copy (MilterOption *option)
{
    return g_object_new(MILTER_TYPE_OPTION,
                        "version", milter_option_get_version(option),
                        "action", milter_option_get_action(option),
                        "step", milter_option_get_step(option),
                        NULL);
}

g_object_refで参照を増やして返す場合:

/**
 * milter_libev_event_loop_default:
 *
 * Returns: (transfer full): The default event loop.
 */
MilterEventLoop *
milter_libev_event_loop_default (void)
{
    if (!default_event_loop) {
        default_event_loop =
            g_object_new(MILTER_TYPE_LIBEV_EVENT_LOOP,
                         "ev-loop", ev_default_loop(EVFLAG_FORKCHECK),
                         NULL);
    } else {
        g_object_ref(default_event_loop);
    }

    return default_event_loop;
}

containerの例

関数内でGListを作成して返すが、その各要素は既存の値である場合:

/**
 * milter_manager_module_collect_names:
 * @modules: (element-type MilterManagerModule):
 *   A list of #MilterManagerModule.
 *
 * Returns: (transfer container) (element-type utf8):
 *    Names of @modules.
 */
GList *
milter_manager_module_collect_names (GList *modules)
{
    GList *results = NULL;
    GList *node;

    for (node = modules; node; node = g_list_next(node)) {
        MilterManagerModule *module;

        module = node->data;
        results = g_list_prepend(results, G_TYPE_MODULE(module)->name);
    }

    return results;
}

element-type

GListGHashTableなどのコンテナタイプの引数や返り値に対して、その要素の型を定義します。

文字列にはutf8を指定し、定義している型はそれをそのまま指定します。

その他の基本型は次を参照して下さい。

utf8の例

/**
 * milter_macros_requests_get_symbols:
 * @requests: A #MilterMacrosRequests.
 * @command: A #MilterCommand.
 *
 * Returns: (transfer none) (element-type utf8): The symbols of the requests.
 */
GList *
milter_macros_requests_get_symbols (MilterMacrosRequests *requests,
                                    MilterCommand command)
{
    GHashTable *symbols_table;

    symbols_table = MILTER_MACROS_REQUESTS_GET_PRIVATE(requests)->symbols_table;
    return g_hash_table_lookup(symbols_table, GINT_TO_POINTER(command));
}

次を確認すると効率的に型はなにかを判定できます。

  • 初期化処理
  • 解放処理
  • 設定処理

それでは、実際に型がなにかを調べてみましょう。

前述の関数の返り値はGList *であり、その各要素の型を調べたいです。 この関数の実装を見ると、MilterMacrosRequestsのプライベートメンバーであるsymbols_tableGHashTable型であり、その値がGList *型であることが分かります。

初期化処理と解放処理は次のようになっています。

static void
symbols_free (gpointer data)
{
    GList *symbols = data;

    g_list_foreach(symbols, (GFunc)g_free, NULL);
    g_list_free(symbols);
}

static void
milter_macros_requests_init (MilterMacrosRequests *requests)
{
    MilterMacrosRequestsPrivate *priv;

    priv = MILTER_MACROS_REQUESTS_GET_PRIVATE(requests);

    priv->symbols_table = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                                NULL, symbols_free);
}

symbols_tableの値の解放処理としてsymbols_free()関数を登録しています。 symbols_free()関数の実装を見ると、GList *型の各要素の解放をg_free()関数で行っています。 何かの構造体であれば専用の解放処理を行うはずなので、g_free()関数で各要素を開放していることから、各要素は単なる文字列である可能性が高いと推測できます。

そこで、このsymbols_tableの設定処理を探すと、次のような関数が見つかります。

void
milter_macros_requests_set_symbols_string_array (MilterMacrosRequests *requests,
                                                 MilterCommand command,
                                                 const gchar **strings)
{
    MilterMacrosRequestsPrivate *priv;
    GList *symbols = NULL;
    gint i;

    for (i = 0; strings[i]; i++) {
        symbols = g_list_append(symbols, g_strdup(strings[i]));
    }

    priv = MILTER_MACROS_REQUESTS_GET_PRIVATE(requests);
    g_hash_table_insert(priv->symbols_table, GINT_TO_POINTER(command), symbols);
}

この実装を見ると、gchar **型の値からGList *を生成して、symbols_tableの値として挿入していることが分かります。 よって、想定通りGList *の各要素は文字列であることを確認できました。

以上から、milter_macros_requests_get_symbols()関数の返り値にはelement-type utf8を付与すれば良い、ということになります。

skip

主に関数全体に対して使い、その関数をGObject Introspectionの対象から除外します。

バインディングを作る必要のない関数に使います。

以下の例では、可変長引数...を使う関数はバインディングを生成できないので、skipを設定しています。

/**
 * milter_manager_configuration_instantiate: (skip)
 * @first_property: The value of the first property.
 * 
 * This function takes the property values as variable length arguments.
 *
 * Returns: (transfer full): A newly created #MilterManagerConfiguration.
 */
MilterManagerConfiguration *
milter_manager_configuration_instantiate (const gchar *first_property,
                                          ...)
{
    MilterManagerConfiguration *configuration;
    va_list var_args;

    va_start(var_args, first_property);
    configuration =
        milter_manager_configuration_instantiate_va_list(first_property,
                                                         var_args);
    va_end(var_args);

    return configuration;
}

その他のアノテーション

  • nullable: nullの可能性がある場合に付与します。
  • out: 出力用の引数として使う場合に付与します。
    • 関数内で該当変数に指定したアドレスに値を設定します。

例:

/**
 * milter_decoder_decode_negotiate:
 * @buffer: A buffer that has the target data.
 * @length: The number of bytes of @buffer.
 * @processed_length: (out): The number of bytes that are processed.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Returns: (transfer full) (nullable): The decoded #MilterOption on success,
 *   %NULL on error.
 */
MilterOption *
milter_decoder_decode_negotiate (const gchar *buffer,
                                 gint length,
                                 gint *processed_length,
                                 GError **error)
{
    gsize i;
    guint32 version, action, step;

    *processed_length = 0;

    i = 1;
    if (!milter_decoder_check_command_length(
            buffer + i, length - i, sizeof(version),
            MILTER_DECODER_COMPARE_AT_LEAST, error,
            "version on option negotiation command")) {
        return NULL;
    }
    memcpy(&version, buffer + i, sizeof(version));
    i += sizeof(version);

    if (!milter_decoder_check_command_length(
            buffer + i, length - i, sizeof(action),
            MILTER_DECODER_COMPARE_AT_LEAST, error,
            "action flags on option negotiation command")) {
        return NULL;
    }
    memcpy(&action, buffer + i, sizeof(action));
    i += sizeof(action);

    if (!milter_decoder_check_command_length(
            buffer + i, length - i, sizeof(step),
            MILTER_DECODER_COMPARE_AT_LEAST, error,
            "step flags on option negotiation command")) {
        return NULL;
    }
    memcpy(&step, buffer + i, sizeof(step));
    i += sizeof(step);

    *processed_length = i;

    return milter_option_new(g_ntohl(version), g_ntohl(action), g_ntohl(step));
}

生成したバインディングの利用

前回の記事と合わせて、milter managerに以下の対応を行いました。

  • Mesonを使ってGObject Introspection対応のビルドシステムを構築しました。
  • GObject Introspection Annotationsを適切に設定しました。

以上で、いよいよバインディングを使うことができます。 試しに使ってみましょう。

Mesonを使ってmilter managerをビルド、インストールします。

// milter-managerをcloneします(本記事執筆時点でv2.2.5です)。
$ git clone git@github.com:milter-manager/milter-manager.git -b 2.2.5
$ cd milter-manager

// milter-manager.build というディレクトリーを作りビルドします。
// setup は省略可能です(次のコマンドは"meson setup ..."とするのと同じです)。
// 前回の記事と同様に、とりあえず"/tmp/local"にインストールします。
// 今回は開発用のインストールなので、"--libdir=lib"を指定することで、
// "/tmp/local/lib"直下にライブラリーをインストールします。
// (環境によっては"lib/x86_64-linux-gnu"など、アーキテクチャーに応じた
// サブディレクトリーの下にインストールされます)。
$ meson ../milter-manager.build --prefix=/tmp/local --libdir=lib

// インストールを実行します。
$ meson install -C ../milter-manager.build

/tmp/local/lib配下に、libmilter-core.soなどの各ライブラリーファイルと、girepository-1.0というディレクトリーが生成されます。 このgirepository-1.0配下に、バインディングが使うTypelibファイルがインストールされています。

これを使って、試しにcoreライブラリーのMilterLoggerクラスをコンストラクトしてみます。

coreライブラリーのmeson.buildの設定内容を復習します。

milter_core_gir = gnome.generate_gir(libmilter_core,
                                     export_packages: 'milter-core',
                                     extra_args: [
                                       '--warn-all',
                                     ],
                                     fatal_warnings: true,
                                     header: 'milter/core.h',
                                     identifier_prefix: 'Milter',
                                     includes: [
                                       'GObject-2.0',
                                     ],
                                     install: true,
                                     namespace: 'MilterCore',
                                     nsversion: api_version,
                                     sources: sources + headers + enums,
                                     symbol_prefix: 'milter')
  • namespace: 'MilterCore'
  • identifier_prefix: 'Milter'
  • symbol_prefix: 'milter'

これらの設定により、MilterLoggerクラスはMilterCore名前空間配下のLoggerクラス(MilterCore.Logger)となります。 また、例えばmilter_logger_get_interesting_level()メソッドは、prefix部分を除いてget_interesting_level()メソッドとなります。

例えば、次のようなPythonスクリプトでバインディングを使うことができます。

test.py

import gi
from gi.repository import MilterCore

logger = MilterCore.Logger()

print(logger)
print(logger.get_interesting_level())

今回はインストール先にパスが通っていないので、以下の環境変数を指定した上でスクリプトを実行します。

  • GI_TYPELIB_PATHに、Typelibファイル(今回はMilterCore-2.0.typelib)のディレクトリーのパスを指定します。
  • LD_LIBRARY_PATHに、ライブラリーファイル(今回はlibmilter-core.so)のディレクトリーのパスを指定します。
$ GI_TYPELIB_PATH=/tmp/local/lib/girepository-1.0 \
    LD_LIBRARY_PATH=/tmp/local/lib \
    python3 test.py

すると、次のようにLoggerクラスをコンストラクトできたことや、get_interesting_level()メソッドを呼べていることが分かります。

<MilterCore.Logger object at 0x7f02593b1780 (MilterLogger at 0x22bfd70)>
<flags MILTER_LOG_LEVEL_CRITICAL | MILTER_LOG_LEVEL_ERROR | MILTER_LOG_LEVEL_WARNING | MILTER_LOG_LEVEL_MESSAGE | MILTER_LOG_LEVEL_STATISTICS of type MilterCore.LogLevelFlags>

milter managerにおけるバインディング

milter managerでは、このように生成したバインディングを利用して、Pythonでmilterを実装するためのライブラリーを提供しました。

元々、Rubyでmilterを実装するためのライブラリーを、拡張ライブラリーを利用して提供していたのですが、今回Pythonにも対応した形になります。 GObject Introspectionに対応したことで、今回生成したバインディングをPythonとRubyの双方で利用することができるようになっています(今回は時間が足りず、まだRubyの方は旧来の方式のままですが)。 それどころか、必要があればその他の言語も対応できるようになったわけです。 GObject Introspectionの強力さがよく分かりますね。

Pythonでのmilter作りについては、また今後の記事で紹介する予定です。

まとめ

本記事では、前回のMesonを使ってGObject Introspection対応のビルドシステムを構築する方法に続いて、GObject Introspection Annotationsを作って、実際にバインディングを生成するところまでを紹介しました。

これによって、milter managerはPythonでmilterを実装するためのライブラリーを提供することができました。 Pythonでのmilter作りについては、また今後の記事で紹介する予定です。

クリアコードではmilter managerを始め、様々な自由ソフトウェアの開発・サポートを行っております。 詳しくは次をご覧いただき、こちらのお問い合わせフォームよりお気軽にお問い合わせください。

また、クリアコードではこのように業務の成果を公開することを重視しています。 業務の成果を公開する職場で働きたい人はクリアコードの採用情報をぜひご覧ください。