前回、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では、FiddleやRuby-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
GList
やGHashTable
などのコンテナタイプの場合にのみ有効です。- コンテナの所有権を移動しますが、その要素の所有権は移動しません。
GList
やGHashTable
などのコンテナを新たに作成して返しますが、その各要素は既存の値の参照である、という場合に設定します。
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
GList
やGHashTable
などのコンテナタイプの引数や返り値に対して、その要素の型を定義します。
文字列には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_table
はGHashTable
型であり、その値が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を始め、様々な自由ソフトウェアの開発・サポートを行っております。 詳しくは次をご覧いただき、こちらのお問い合わせフォームよりお気軽にお問い合わせください。
また、クリアコードではこのように業務の成果を公開することを重視しています。 業務の成果を公開する職場で働きたい人はクリアコードの採用情報をぜひご覧ください。