ククログ

株式会社クリアコード > ククログ > Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法

Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法

最近、milter managerというフリーソフトウェアを Mesonというビルドシステムに対応させる作業を行っている福田です。

その主な目的は、RubyやPython用のバインディングを自動で生成できるようにすることです。

ライブラリーがGObjectを利用している場合、 GObject Introspectionを使えば(ほぼ)自動でバインディングを生成できます。

MesonにはGObject Introspectionサポート(Meson Integration) が組み込まれているので、Mesonを使うと簡単にGObject Introspectionを利用できます。

今回は、Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法を説明します。

Meson

GObject Introspection対応をするためには、次の2つビルドシステムのどちらかを使うと便利です。

GObject Introspectionを開発しているGNOMEの関連プロジェクトがMesonに移行しているので、 今後のことを考えるとMesonを使うべきです。

GNU AutotoolsでGObject Introspection対応する方法については、次の記事で紹介しています。

今回の記事は、このMeson版の説明になります。

実際に対応したmilter manager(GitHub上のリポジトリー)を例に説明します。

プロジェクトトップのmeson.buildファイル

Mesonでビルドを可能にするために、meson.buildファイルを作成して設定します。

今回は、milter/coremilter/clientといった特定のサブディレクトリ毎にライブラリーをビルドしたいので、 プロジェクトのトップにmeson.buildファイルを作成して共通の設定を行い、各サブディレクトリにもmeson.buildファイルを作成します。

実際に作成したプロジェクトトップのmeson.buildファイルは次になります。

以下で重要な部分を説明します。

# プロジェクトの定義:
#   https://mesonbuild.com/Reference-manual_functions.html#project
# licenseの値の形式: https://spdx.org/licenses/
project('milter-manager',
        'c',
        license: 'LGPL-3.0-or-later',
        version: '2.1.6')

# ライブラリーのバージョンなど、共通して用いる変数を定義する。
# ライブラリーのバージョンは、プロジェクトのバージョンとは別に管理する。(補足1)
api_version = '2.0'
so_version = 2
library_version = '@0@.0.0'.format(so_version)

# prefixオプションの値を取得する。(補足2)
prefix = get_option('prefix')
# ビルトインオブジェクト「meson」の関数を使い定義したプロジェクト名を参照。
# https://mesonbuild.com/Reference-manual_builtin_meson.html
# インストール先のパスについて定義している。(補足3)
project_include_sub_dir = meson.project_name()
milter_include_sub_dir = project_include_sub_dir / 'milter'
milter_include_dir = get_option('includedir') / milter_include_sub_dir
data_dir = prefix / get_option('datadir')
gir_dir = data_dir / 'gir-1.0'

# 用いるモジュールをインポート。
# 例: GNOME module: https://mesonbuild.com/Gnome-module.html
fs = import('fs')
gnome = import('gnome')
pkgconfig = import('pkgconfig')
# pcファイルの生成時に用いる。
# Mesonを使ってGObject Introspection対応しようとする別のライブラリが
# GIRファイルを探し出せるようにする。(補足4)
pkgconfig_variables = ['girdir=@0@'.format(gir_dir)]

(...)

package_platform = get_option('package_platform')
(...)

# meson-config.h.inの変数を置換してconfig.hを生成する。
# ソースコードで用いる定数などをビルド時に定義したいため。
config_h_conf = configuration_data()
(...)
config_h_conf.set_quoted('MILTER_MANAGER_PACKAGE_PLATFORM', package_platform)
config_h_conf.set_quoted('PACKAGE', meson.project_name())
config_h_conf.set_quoted('PREFIX', prefix)
config_h_conf.set_quoted('VERSION', meson.project_version())
config_h = configure_file(input: 'meson-config.h.in',
                          output: 'config.h',
                          configuration: config_h_conf)
# 後で依存関係に加えるため、config.hをdependencyにしておく。(補足5)
config = declare_dependency(compile_args: '-DHAVE_CONFIG_H',
                            include_directories: include_directories('.'),
                            sources: config_h)

# 指定したディレクトリに入り、そのディレクトリのmeson.buildを実行する。
subdir('milter/core')
(...)

補足1: ライブラリーのバージョン

# ライブラリーのバージョンなど、共通して用いる変数を定義する。
# ライブラリーのバージョンは、プロジェクトのバージョンとは別に管理する。(補足1)
api_version = '2.0'
so_version = 2
library_version = '@0@.0.0'.format(so_version)

ライブラリーのバージョンは、アプリケーションバイナリインターフェース(ABI)の互換性の観点で管理します。 アップデートによってプロジェクトのバージョンが上がっても、ABIの互換性が保たれているならばライブラリーのバージョンは変更しなくても良いです。

特に共有ライブラリーは、以下のようにシンボリックリンクとその本体で構成されます。

libmilter-core.so.2 -> libmilter-core.so.2.0.0
libmilter-core.so.2.0.0

シンボリックリンクの名前libmilter-core.so.2には、メジャーバージョンの2しか含まれていません。 つまり、メジャーバージョンを変更するということは、共有ライブラリーのシンボリックリンクを変更するということになります。 ABIの互換性を保てない更新があった場合のみメジャーバージョンを更新するべきです。

詳しくはライブラリーのバージョンとアプリケーションバイナリインターフェースをご覧下さい。

補足2: オプション

# prefixオプションの値を取得する。(補足2)
prefix = get_option('prefix')

get_option()でオプションの値を取得できます。

オプションには、デフォルトで定義されているビルトインオプションと、ユーザーが定義するオプションの2種類があります。 後者はmeson_options.txtというファイルを作成して定義します。

例えばprefixはビルトインのオプションですが、package_platformmeson_options.txtで定義したオプションです。

ビルトインのオプションはmesonコマンドの引数として指定できます。 ユーザーが定義したオプションは、-Dオプションで指定します。 例えば次のように使います。

$ meson setup --prefix=/tmp/local -Dpackage_platform=debian ../milter-manager.build .

補足3: インストール先のパス

# インストール先のパスについて定義している。(補足3)
project_include_sub_dir = meson.project_name()
milter_include_sub_dir = project_include_sub_dir / 'milter'
milter_include_dir = get_option('includedir') / milter_include_sub_dir
data_dir = prefix / get_option('datadir')
gir_dir = data_dir / 'gir-1.0'

この後にインストールに用いる各関数は、デフォルトでprefixオプションの値をインストール先として用います。

例えば、ヘッダーをインストールするinstall_headers()では {prefix}/{includedir}がデフォルトのインストール先になりますし、 ライブラリーをインストールするlibrary()では {prefix}/{libdir}がデフォルトのインストール先になります。

各関数でこのインストール先を変更できるので、あとで変更するためにここでパスを定義しています。 パスの連結は、/演算子を使って行います。 この演算子はjoin_paths()と同じ機能を提供します。

補足4: GIRファイルとTypelibファイル

# pcファイルの生成時に用いる。
# Mesonを使ってGObject Introspection対応しようとする別のライブラリが
# GIRファイルを探し出せるようにする。(補足4)
pkgconfig_variables = ['girdir=@0@'.format(gir_dir)]

GObject Introspection対応のライブラリーをビルドするときは、GIRファイルとTypelibファイルを生成します。

GIRファイルを生成する際、依存している他のGIRファイルを参照する必要があります。 参照するのでどこにGIRファイルがあるか分からないといけません。 Mesonは依存しているライブラリーの.pcファイル内にgirdir変数が定義されていれば、その変数の値であるディレクトリーをGIRファイルの検索パスに追加してくれます。 このライブラリーのGIRファイルを、Mesonを使っている他のライブラリーから使いやすくするために、このライブラリーの.pcファイルにもgirdir変数を定義しています。

補足5: declare_dependency について

# 後で依存関係に加えるため、config.hをdependencyにしておく。(補足5)
config = declare_dependency(compile_args: '-DHAVE_CONFIG_H',
                            include_directories: include_directories('.'),
                            sources: config_h)

Mesonでライブラリーをビルドするにはlibrary()関数を使います。 library()関数にマクロ定義やヘッダーの検索パスや依存関係を指定するには、library()関数の各引数に個別に指定するか Dependency objectを使って指定する方法があります。 Dependency objectを使うと複数のlibrary()で指定内容を共有できるので、何度も指定する情報はDependency objectを使うとスッキリします。

外部でインストール済みのライブラリーであればdependency()を用い、 このプロジェクト内で作成する依存関係であればdeclare_dependency()を用います。

ここでは、compile_argsを指定することで、この依存を使ったライブラリーのビルドに指定したオプションを追加します。 milter managerでは元々GNU Autotoolsを用いており、その慣習で、HAVE_CONFIG_Hが定義済みの場合に限りconfig.hを読み込むようにコーディングしている、 という事情があります。 そのためmilter managerでは、このconfig.hに依存するライブラリーのビルドに、常にHAVE_CONFIG_Hオプションに付ける必要があります。

coreライブラリーのmeson.buildファイル

milter managerのcoreライブラリー用のmeson.buildファイルを次のように作成しました。

# ビルド対象に含めるソースファイル。
sources = files(
  'milter-agent.c',
  'milter-command-decoder.c',
  (...)
)

# ビルド対象に含めるヘッダーファイル。
headers = files(
  'milter-agent.h',
  'milter-command-decoder.h',
  (...)
)

# milter-version.h.inの変数を置換してmilter-version.hを生成し、headersに含める。
version_h_conf = configuration_data()
# str型のsplitメソッド:
#   https://mesonbuild.com/Reference-manual_elementary_str.html#strsplit
version_components = meson.project_version().split('.')
version_h_conf.set('MILTER_MANAGER_VERSION', meson.project_version())
version_h_conf.set('MILTER_MANAGER_VERSION_MAJOR', version_components[0])
version_h_conf.set('MILTER_MANAGER_VERSION_MINOR', version_components[1])
version_h_conf.set('MILTER_MANAGER_VERSION_MICRO', version_components[2])
version_h = configure_file(input: 'milter-version.h.in',
                           output: 'milter-version.h',
                           configuration: version_h_conf)
headers += version_h

# enumのGObject用ファイルを生成する。(補足1)
enums = gnome.mkenums_simple('milter-enum-types',
                             body_prefix: '#include <config.h>',
                             identifier_prefix: 'Milter',
                             install_dir: milter_include_dir / 'core',
                             install_header: true,
                             sources: headers,
                             symbol_prefix: 'milter')
enums_h = enums[1]

# ヘッダーファイルをインストール。
install_headers(headers, subdir: milter_include_sub_dir / 'core')
install_headers(['../core.h'], subdir: milter_include_sub_dir)
headers += ['../core.h']

# 依存関係を定義する。
(...)
dependencies = [
  config,
  (...)
]
# ライブラリーを作成してインストール。
libmilter_core = library('milter-core',
                         c_args: '-DMILTER_LOG_DOMAIN="milter-core"',
                         sources: sources + headers + enums,
                         install: true,
                         dependencies: dependencies,
                         soversion: so_version,
                         version: library_version)
# 他のライブラリーがこのライブラリーを依存関係として使う。
# 生成したenumのGObject用ファイルのヘッダーを依存関係に含める。(補足2)
milter_core = declare_dependency(dependencies: dependencies,
                                 link_with: libmilter_core,
                                 sources: [enums_h])

# pcファイルを作成してインストール。
pkgconfig.generate(libmilter_core,
                   description: 'common milter features',
                   filebase: 'milter-core',
                   name: 'milter core library',
                   requires: ['gobject-2.0'],
                   subdirs: project_include_sub_dir,
                   variables: pkgconfig_variables)

# GIRファイルとTypelibファイルを生成してインストール。(補足3)
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')

補足1: enumのGObject用ファイルの生成

# enumのGObject用ファイルを生成する。(補足1)
enums = gnome.mkenums_simple('milter-enum-types',
                             body_prefix: '#include <config.h>',
                             identifier_prefix: 'Milter',
                             install_dir: milter_include_dir / 'core',
                             install_header: true,
                             sources: headers,
                             symbol_prefix: 'milter')
enums_h = enums[1]

gnome.mkenums_simple() を使い、enumのGObject用のファイルを生成します。

sourcesで指定したファイルで定義されているenumGEnumにしてGObject対応します。

install_header: trueを指定することで、生成したヘッダーファイルをインストールしています。 このため、後のinstall_headers()で指定しているインストールするヘッダーファイルのリストに、ここで生成したヘッダーファイルを含めていません。

ヘッダーファイルのインストール先をinstall_dirで指定しています。 prefixで指定したパスに対する相対パスを指定します。 一方でinstall_headers(){prefix}/{includedir}に対する相対パスをsubdirで指定しています。

identifier_prefixには、コード上のenumの定義において、名前空間としてprefixに付与している部分を指定します。 下の例では、MilterAgentErrorMilterを指定します。

symbol_prefixには、ヘッダーファイルで宣言される関数名や#defineされるenum名のprefixを指定します。 通常はidentifier_prefixで指定した部分に対応する文字列を_区切りの小文字で指定します。

実際に生成されるコードの例は、次のようになります。

元々のenum定義

typedef enum
{
    MILTER_AGENT_ERROR_IO_ERROR,
    MILTER_AGENT_ERROR_DECODE_ERROR,
    MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR
} MilterAgentError;

生成されるmilter-enum-types.h

  • ここのprefixがidentifier_prefixsymbol_prefixによって決まります。
GType milter_agent_error_get_type (void);
#define MILTER_TYPE_AGENT_ERROR (milter_agent_error_get_type())

生成されるmilter-enum-types.c

GType
milter_agent_error_get_type (void)
{
  static volatile gsize gtype_id = 0;
  static const GEnumValue values[] = {
    { C_ENUM(MILTER_AGENT_ERROR_IO_ERROR), "MILTER_AGENT_ERROR_IO_ERROR", "io-error" },
    { C_ENUM(MILTER_AGENT_ERROR_DECODE_ERROR), "MILTER_AGENT_ERROR_DECODE_ERROR", "decode-error" },
    { C_ENUM(MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR), "MILTER_AGENT_ERROR_NO_EVENT_LOOP_ERROR", "no-event-loop-error" },
    { 0, NULL, NULL }
  };
  if (g_once_init_enter (&gtype_id)) {
    GType new_type = g_enum_register_static (g_intern_static_string ("MilterAgentError"), values);
    g_once_init_leave (&gtype_id, new_type);
  }
  return (GType) gtype_id;
}

補足2: 生成したenumのGObject用ファイルのヘッダーを依存関係に含める

# 他のライブラリーがこのライブラリーを依存関係として使う。
# 生成したenumのGObject用ファイルのヘッダーを依存関係に含める。(補足2)
milter_core = declare_dependency(dependencies: dependencies,
                                 link_with: libmilter_core,
                                 sources: [enums_h])

他のライブラリーがこのライブラリーを依存関係として用いるため、dependencyを定義しておきます。

gnome.mkenums_simple()等を使いヘッダーファイルを自動生成している場合は、sourcesに自動生成したヘッダーファイルを含める必要があります。

これに依存するライブラリーのコンパイル時に、このライブラリーの各ヘッダーファイルが必要となります。 そのため、その前にヘッダーファイルの自動生成が完了している必要があります。

Cではコンパイルとリンクはフェーズが別れているため、link_withでリンクの設定をするだけでは、 これに依存するライブラリーのコンパイル時にヘッダーファイルの生成が完了していない可能性があります。

sourcesに自動生成するヘッダーファイルを指定することで、そのファイルのビルドが完了してから、これに依存するライブラリーをコンパイルできます。

補足3: GIRファイルとTypelibファイルの生成

# GIRファイルとTypelibファイルを生成してインストール。(補足2)
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')

gnome.generate_gir() を使って、バインディングに用いるファイルであるGIRファイルとTypelibファイルを生成します。

fatal_warnings: trueを指定することで、警告をエラーとすることができます。 バインディングを生成するにはGObject Introspection Annotations というアノテーションを適切に設定する必要があり、アノテーションに問題があればビルド時に警告が発生します。 この設定をすることでそのような警告をエラーとすることができます。

header: 'milter/core.h'は、ただのドキュメント用の情報です。

namespaceで、生成するバインディングの名前空間を設定します。

identifier_prefixsymbol_prefixは、gnome.mkenums_simple()と同様に設定します。 例えば、前者をMilter、後者をmilterと設定することで、MilterAgentMilter名前空間内のAgentクラスとして認識されたり、 milter_xxx()関数がxxx()関数として認識されたりするようになります。

ビルド

例えば、以下のようにビルドできます。

$ meson setup --prefix=/tmp/local ../milter-manager.build .
$ ninja -C ../milter-manager.build
  • 再起動時に自動でクリーンされる/tmp以下のパスをprefixに利用することで、以前にインストールした成果物の影響を受けにくくしています。
  • ビルド用のディレクトリとして、../milter-manager.buildを利用しています。

インストールは次のように行うことができます。

$ meson install -C ../milter-manager.build

上インストールコマンドはビルドも兼ねるので、ビルドだけしたいという場合でなければ、次のようにビルド&インストールできます。

$ meson setup --prefix=/tmp/local ../milter-manager.build .
$ meson install -C ../milter-manager.build

まとめ

本記事では、Mesonを使ってGObject Introspection対応のビルドシステムを構築する方法について紹介しました。

GObject Introspectionを利用してバインディングを生成するには、 GObject Introspection Annotations というアノテーションを適切に管理する必要があります。

それについては今後の記事でご紹介します。

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