GoでMySQL/MariaDB/PostgreSQL向けUser Defined Functions(UDF)を作成しました

システム開発事業部

はじめに

MySQL/MariaDB/PostgreSQLといったデータベースではユーザ独自の処理を追加できる仕組みが提供されており、User Defined Functions(UDF)やLoadable Functionsなどと呼ばれています。
(以降の記載は、UDFに統一します。)
DBにおいてユーザ独自の処理というと、例えばSQLやDB専用言語を用いるストアドファンクションがありますが、UDFはC/C++といった汎用的なプログラミング言語で実装することができます。

今回は主にGoでMySQL/MariaDB/PostgreSQL向けUDFの実装を行いました。
成果物は以下のリポジトリです。
https://github.com/ArmadaSuit/udf-go

作成するにあたり躓いた点が多くあったため、可能な限り詳細に記載していきます。

UDFについて

前述の通り、UDFは汎用的なプログラミング言語で実装できることがストアドファンクションと異なる点です。
汎用的なプログラミング言語で実装できる利点は、条件によって置換する内容が変わるような文字列処理はDBで用意されている関数だけで頑張るには少々複雑になってしまうような処理を作成したい場合、実装やテストがシンプルになったり、外部ライブラリを利用できたり、処理によってはパフォーマンスが上がるといったことが挙げられます。
一方で欠点は、共有ライブラリ(soファイルやdllファイルなど)を配置する必要があり、特にクラウドのマネージドサービスではファイル配置がほぼできないと思われるため、マネージドサービスで十分なことが多くなった昨今の状況では直接導入できる場面が少ないことです。
(ストアドファンクションの場合、処理内容を記載した作成クエリを実行するだけのはずなのでマネージドサービスでも作成が許可されていれば導入可能です。)

ここではマネージドサービスでの欠点については置いておいて、MySQL/MariaDB/PostgreSQL向けUDFの作成方法について記載していきます。

それぞれのUDFの作成方法に関する公式のドキュメントは以下の通りです。

MySQL
https://dev.mysql.com/doc/extending-mysql/8.1/en/adding-loadable-function.html

MariaDB
https://mariadb.com/kb/en/creating-user-defined-functions/

PostgreSQL
https://www.postgresql.org/docs/current/xfunc-c.html
(日本語訳版(バージョン15系): https://www.postgresql.jp/document/15/html/xfunc-c.html)

これらはC/C++での実装方法が記載されていますが、コードフォーマットやテストなどの準備の手間を考慮するとC/C++よりもGoで実装する方が手軽なため、今回はGoを採用しました。GoでC/C++の共有ライブラリを作成する方法は後述します。
(今回作成した範囲では外部ライブラリを利用していませんが、今後より複雑な処理が必要になった場合はライブラリ管理の容易さを含めGoのエコシステムは心強い味方と考えています。)

大まかな作業の流れはいずれも、実装→コンパイル→ファイル配置→UDFを登録、です。
ただし、MySQLとMariaDBの実装方法はほぼ同じですが、PostgreSQLの実装方法は異なるため分けて記載します。

GoでC/C++の共有ライブラリを作成する方法について

GoではC/C++と連携するための仕組みでcgoがあります。
cgoで検索すると多くの有用な記事がありますが、アップデートが重ねられ記事作成当時から大きく変化している可能性があるため、実装する際は以下の公式ドキュメントを読むことをおすすめします。
https://pkg.go.dev/cmd/cgo
https://github.com/golang/go/wiki/cgo

cgoを利用する方法ですが、ここではLinuxを前提に記載していきます。

まず、C/C++のコンパイラ環境が必要です。デフォルトではgccが利用されます。
必要なものについてより具体的には、公式のDockerfileが参考になります。
https://github.com/docker-library/golang/blob/079c1fa6a2b011b1a81f902da6498d527f311dd5/1.21/bookworm/Dockerfile#L9-L19

今回は共有ライブラリ、すなわちLinux環境ではsoファイルの生成のためのビルドは、以下のようなコマンドを実行します。
ビルドする対象ファイルの構成により少々コマンドは変わりますが、ここではudf.goをudf.soとして生成する例を記載します。

CGO_ENABLED=1 go build -buildmode=c-shared -o udf.so udf.go

デフォルトではcgoは有効ではないので、CGO_ENABLED=1が必要です。
なお、cgoが関係するbuildmodeは他にもありますが、ここでは割愛します。

ドキュメントにあるような簡単な例でビルドに成功すればcgoを利用する準備は整っています。

他の実装例については、Go本体のテストコードが参考になります。
例えば以下の実装は、Goで実装した関数をCで実装した関数から呼び出して、その関数をGoで実装した関数から呼び出す、ということをしています。
https://github.com/golang/go/blob/go1.21.3/src/cmd/cgo/internal/test/issue20910.c
https://github.com/golang/go/blob/go1.21.3/src/cmd/cgo/internal/test/testx.go#L533-L542

このように呼び出し側と呼び出される側が少々離れていることがあると思いますが、C/Goに記載されている関数名で検索をしていけば参考になる実装が見つかるはずです。

MySQL/MariaDB向けのUDFの実装方法について

ここでは単純な関数の作成について記載します。
sum関数のような集計関数が必要な場合は別途公式ドキュメントを参照ください。

2023/10/20時点では、MySQLとMariaDBに差異はないはずなので、いずれかの記載のみでも他でも同じこととご理解ください。

命名規則について

まず関数の命名にルールがあります。
SQLから実行する際の関数名をmy_functionにしたい場合、最低限必要な関数はmy_function_initmy_functionです。(xxxに対して、xxx_init関数、xxx関数というルール)
イメージとしては、xxx_init関数で引数のバリデーションを行い、xxx関数で実処理を行います。
他には、後処理(主な用途はメモリ解放)向けにxxx_deinit関数がありますが必須ではないためここでは割愛します。

型と実装ルールについて

次にこれらの関数の引数と戻り値の型のルールがあります。また、引数と戻り値の使い方もルールがあり実装する上で必要なので記載します。

xxx_init関数

xxx_init関数は、C++で実装する想定の型では以下の通りです。
なお、UDF_INITUDF_ARGSはMySQLで独自定義されている型で、定義されているヘッダファイル(mysql.h)のincludeが必要です。

#include <mysql.h>

bool xxx_init(UDF_INIT *initid, UDF_ARGS *args, char *message);

cgoの場合では以下の通りです。
(あまり良くないですが、bool型の扱いは結構雑でも大丈夫なようにできているようでGoのboolでもC.intでも問題なさそうです。コンパイルでき、実行も可能でした。)

// #include <mysql.h>
import "C"

//export xxx_init
func xxx_init(initid *C.UDF_INIT, args *C.UDF_ARGS, message *C.char) C.bool

xxx_init関数の主な役割は、関数名の通り初期化処理を行います。
ここでは最小限のことを記載しますので、詳細は公式ドキュメントを参照ください。

最も使われると思われる内容が、引数のバリデーションです。
trueを返すとSQL実行時にエラーが発生し、messageに設定した文字列が表示されます。
falseを返すと、xxx関数が実行され結果が返却されます。

SQLから受け取る引数は、第2引数(UDF_ARGS型)に設定されます。
arg_count: 全引数の数
arg_type: 各引数の型の配列(文字列はSTRING_RESULT、整数はINT_RESULT、といったenumの配列)
args: 各引数のポインタの配列(値の取得にはキャストが必要)
maybe_null: 各引数がNULLかどうかの配列(0: NULLではない、1: NULL)
cgoの場合のバリデーションは以下のように実装します。

if args.arg_count != 2 {
	m := C.CString("2 arguments expected")
	defer C.free(unsafe.Pointer(m))
	C.strcpy(message, m)
	return C.bool(true)
}

argsTypes := unsafe.Slice(args.arg_type, args.arg_count)

if argsTypes[0] != C.STRING_RESULT || argsTypes[1] != C.STRING_RESULT {
	m := C.CString("2 arguments must be string")
	defer C.free(unsafe.Pointer(m))
	C.strcpy(message, m)
	return C.bool(true)
}

cgo全般のことですが、C.CStringで作成した文字列はメモリ解放が必要なので忘れずに行います。

もう一つ役割として、xxx関数でも使えるメモリを確保することがあります。
確保先は、第1引数(UDF_INIT型)のptr変数です。
確保したメモリの解放は、xxx_deinit関数で行う想定のようです。(xxx関数で開放してはいけないということではありません)
集計関数の場合、関数間で共有できるメモリ領域が必要になってくることが多いと思いますが、Goで確保したメモリをそのまま格納すると、後でC.freeでは解放できなくなる場合があるので確保しないで済むなら触らない方が無難です。
(この辺りについては筆者の知見が不足しておりこれ以上の説明はできないためここまでとします。)

xxx関数

xxx関数は戻り値の型によって引数の型も変わります。
ここでは文字列型を戻り値とする場合のみ記載します。
C++で実装する想定の型では以下の通りです。(includeは省略)

char *xxx(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error);

cgoの場合では以下の通りです。(includeは省略)

func xxx(initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *C.ulong, isNull *C.char, err *C.char) *C.char

xxx関数は処理本体です。
戻り値が文字列の場合、第3引数に結果文字列のコピーと第4引数にバイト数を代入しつつ、そのままポインターを戻り値にする必要があります。
cgoでは以下のような実装です。

cstr := C.CString("result string")
defer C.free(unsafe.Pointer(cstr))
C.strcpy(result, cstr);
*length = C.ulong(13);
return result;

結果がNULLの場合、第5引数に1を代入します。
cgoでは以下のような実装です。

*isNull = C.char(1)

エラーが発生した場合、第6引数に1を代入します。
cgoでは以下のような実装です。

*err = C.char(1)

エラーの場合(第6引数に1を代入)は結果がNULLになり、後続の行ではxxx関数の実行はされず、実行結果はすべてNULLになります。
結果が本当にNULLとの違いは、次の行でxxx関数が実行されるかどうかです。

以上のルールで実装することでUDFの作成ができます。

【補足】ヘッダファイルについて

mysql.hはクライアント開発用パッケージ(Debian系: libmysqlclient-dev、Red Hat系: mysql-devel、など)をインストールすると付属され、/usr/include/mysql/mysql.hに配置されることが多いと思います。
これ以外でも何かしらのパッケージをインストールした結果mysql.hが利用できる場合がありますが、もしかするとビルドに失敗するかもしれません。
そういった場合は、リポジトリから直接ダウンロード(依存関係があるのでinclude以下は全部あった方が無難です)するか、
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/include
最小限の型で十分な場合はudf_registration_types.hだけをincludeするように修正することでビルドが成功するはずです。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/include/mysql/udf_registration_types.h

ビルド

次にビルドですが、前述の通り少なくとも型定義用のヘッダファイル(mysql.h)が必要になります。
多くの場合、mysql.hの格納ディレクトリは/usr/include/mysqlのように決め打ちすることができると思うので、cgo用のコメントとして

/*
#cgo CFLAGS: -I/usr/include/mysql

#include <mysql.h>
*/
import "C"

のように記載することで指定できます。

もしも複数のビルド環境に適用する必要がある場合、cgoでは環境変数を解釈し展開してくれるので

// #cgo CFLAGS: -I${INCLUDE_DIR}/mysql

と記載したり、プログラムではなくビルド時に利用されるCFLAGS用の環境変数へ以下のように含めることで柔軟に指定することができます。

CGO_CFLAGS="... -I/usr/include/mysql"

共有ライブラリの格納ディレクトリ

次はビルドした共有ライブラリをプラグイン用ディレクトリに格納します。
前述のクライアント開発用パッケージがインストールできれば、mysql_config --plugindirというコマンドが実行できるはずで、これで格納先ディレクトリが表示されます。
※コマンドが実行できず、プラグイン用ディレクトリが直ぐにわからない場合のスマートな対処方法はわかっていません。(インストール時に特別な指定をしていない限り、ディレクトリパスにmysql(または mariadb)、plugin、といったものが含まれているはずなので、該当するディレクトリをしらみつぶしに探していくことになってしまうでしょうか。)
デフォルトでいくつかプラグイン用ファイルが含まれているはずなので、同様にディレクトリに格納します。(ビルドするとヘッダファイルも生成されますが、soファイルのみで十分です。)

UDFの登録

最後に関数を登録します。
処理結果が文字列の場合、DBへ接続し以下のようにSQLクエリを実行します。
(処理本体の実装時の型情報と同様に、型によって異なるので適切なものにする必要があります。)

CREATE FUNCTION xxx RETURNS STRING
  SONAME 'xxx.so';

共有ライブラリのファイル名は任意で問題ありませんが、関数名は前述の通り実装と一致しなければ登録できないので注意が必要です。

以上で、SELECT xxx(.....);といったSQLを実行することができるようになります。

PostgreSQL向けのUDFの実装方法について

こちらも単純な関数の作成について記載します。
集計関数が必要な場合は別途公式ドキュメントを参照ください。

大まかな内容は、MySQL/MariaDBと同様ですがマクロを利用する前提から、Goだけで実装するのは手間がかかるため、Cも利用して実装する方法を記載します。
主要な処理はGoで実装しますが、SQLから呼び出されるのはC側で実装した関数で、その関数からGoで実装した関数を呼び出すという構成です。

命名規約と型について

まず、MySQL/MariaDBと異なり、関数は1つ用意すれば十分です。
関数名をxxxとした場合のCでの実装は以下の通りです。(SQLから実行する際の関数名はUDF登録時に決められるます。)
なお、PostgreSQLで独自定義されている型やマクロが定義されているヘッダファイル(postgres.h)のincludeが必要です。

#include <postgres.h>

PG_MODULE_MAGIC; // 必須ではない

PG_FUNCTION_INFO_V1(xxx);

Datum xxx(PG_FUNCTION_ARGS)
{
// 処理
}

早速ですが、PG_MODULE_MAGICは必須ではありません。PostgreSQLのメジャーバージョンでのみ動作を担保するための仕組みで、例えばビルド環境のPostgreSQLのメジャーバージョンが10で、実行環境のバージョンが10以外の場合、UDF登録に失敗します。(恐らくビルド時に参照するヘッダファイルの問題なので、PostgreSQLのバージョンというよりもヘッダファイルのバージョンという方が正確だと思います。)登録するためには同じバージョン環境で再ビルドが必要です。

PG_FUNCTION_INFO_V1(xxx)Datum xxx(PG_FUNCTION_ARGS)は必須です。2023/10/20時点でサポート中のバージョンでは、V1という規格の元実装すれば十分なので、関数名を一致するようにそれぞれ定義すれば問題ありません。

SQLとやり取りする型は、MySQL/MariaDBよりも細かく定義されています。
例えばバージョン14ではSQLでの整数型はsmallint、integet、bigintがあり、対応するCの型がint16、int32、int64と定義されています。
その他の型の対応は公式ドキュメントを参照ください。バージョンによって対応するCの型が異なる場合があるので注意が必要です。

なお、各型定義に関連してPostgreSQLの内部でのデータの取り扱いに関してはもう少し踏み込んだ内容を把握しておいた方が良いですが、ここではこの後の実装ベースでの説明に留めます。

最小限の定義の時点でマクロ(アッパースネークケースなもの)が複数含まれている一方で、cgoではヘッダファイルのincludeだけでは利用できず、マクロを利用するためには展開後の型を再定義する必要がありムダに煩雑になってしまうため断念しました。

実装ルール

実際の処理以外の部分については、マクロや関数を呼び出すことでほぼ完結するようになっています。マクロや関数について1つ1つ見ていくよりも、先に実装全体を見た方が必要な処理を把握しやすいと思うので、ドキュメントにある文字列結合のサンプルにコメントを加えたものを記載します。

#include "postgres.h"
#include <string.h>
#include "fmgr.h"

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    // SQLからの引数を取得
    text  *arg1 = PG_GETARG_TEXT_PP(0); // 第1引数
    text  *arg2 = PG_GETARG_TEXT_PP(1); // 第2引数
    // データ長を取得
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    // 結合後のデータ長 + PostgreSQLで必要なデータ長(VARHDRSZ)
    int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
    // 戻り値用の変数のメモリ確保
    // PostgreSQLが用意しているpallocを利用することで、
    // トランザクションを抜けると解放してくれる(自分でfreeする必要がない)
    text *new_text = (text *) palloc(new_text_size);

    // 変数に必要なデータ長を書き込み
    SET_VARSIZE(new_text, new_text_size);
    // 戻り値用の変数へ書き込み
    // VARDATA、VARDATA_ANYを使って実際の値を取得する
    memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
    memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
    // returnの実行
    PG_RETURN_TEXT_P(new_text);
}

まず、SQLからの引数の取得は基本的にPG_GETARG_型名PG_GETARG_型名_PPG_GETARG_型名_PPというマクロで行います。
~_P、~_PPはポインタ用で、マクロの戻り値が値(真偽値、整数など)かポインタ(インターバルや文字列など)かによって命名されています。
型については公式ドキュメントの表にある通りで、型定義のヘッダファイルは記載場所にあります。一方で、対応するマクロは別に定義されていたりするので、場合によっては探す手間がかかるもしれません。
参考までに戻り値用のマクロ(PG_RETURN_~)も合わせていくつか定義箇所を挙げておきます。
※公式ドキュメントにも書ききれていないと思われる有用なマクロ定義も含まれています。
https://github.com/postgres/postgres/blob/REL_16_0/src/include/fmgr.h#L266-L323
https://github.com/postgres/postgres/blob/REL_16_0/src/include/fmgr.h#L351-L375
https://github.com/postgres/postgres/blob/REL_16_0/src/include/utils/timestamp.h#L63-L69

SQLからの引数が取得できたので、実際に処理に移っていきますが、戻り値を作成していくにあたり、型によって取り扱いが異なるので注意が必要です。
具体的には、型定義がデータ長が固定か可変かによって異なります。
固定長の場合(真偽値や整数のような値とPoint*のようにすべてのメンバが固定長な構造体)、特別な操作は不要です。
一方で、文字列(text*VarChar*)のようにデータ長が可変な場合、PostgreSQL用のメモリ領域も考慮して操作する必要があります。これについても、マクロや関数を利用することで煩雑な操作を最小限にすることができます。
※固定長の場合については、公式ドキュメントのサンプルを見れば十分と思うのでここではサンプル実装は記載しません。

実装の説明に戻ります。
可変長の場合、変数宣言で確保が必要なメモリサイズは、実データ長 + VARHDRSZです。
メモリ確保は、palloc関数で行います。これを使うことでメモリ解放はPostgreSQLに任せることができるので、解放忘れを防ぐことができます。
メモリ確保だけでは変数宣言は不足しており、SET_VARSIZEマクロでサイズを設定する必要があります。
データ操作については、構造体のメンバを直接利用してはいけません。
実データを取得する際は、VARDATA_ANYマクロで行います。
同様に、実データのバイト数を取得する際は、VARSIZE_ANY_EXHDRマクロで行います。
データの書き込みには、VARDATAマクロで取得した変数へ行います。
なお、文字列(text*VarChar*)の場合、NULL文字は含まないため文字列処理をする際は注意が必要です。

最後に各型用の戻り値マクロを呼び出して終了です。

エラー処理について

引数不正のようにエラー終了したい場合の実装も同様にマクロや関数を利用します。
Cでの呼び出しでは、以下のように実装します。

ereport(ERROR,
        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
         errmsg("invalid argument %s: %s", "foo", "bar")));

ereportの第1引数(エラーレベル)がERROR以上の場合、そこで終了します。
他のエラーレベルが必要な場合、対応するヘッダファイルを参照ください。
https://github.com/postgres/postgres/blob/REL_16_0/src/include/utils/elog.h
また、errcodeで渡すコード値はドキュメントに記載のあるものです。
https://www.postgresql.org/docs/16/errcodes-appendix.html
このコード値表に対応するソースコードは自動生成されているようで、生成元のファイルは以下のようです。
https://github.com/postgres/postgres/blob/REL_16_0/src/backend/utils/errcodes.txt

【補足】ヘッダファイルについて

ヘッダファイルは、MySQL/MariaDBと同様に開発用パッケージをインストールすると付属されるはずです。
ただし、前述の通りPG_MODULE_MAGICマクロを使って実装するとメジャーバージョンが一致していないと利用できないので実行環境とビルド環境のメジャーバージョンには注意が必要です。
そういう配慮のためか、公式のリポジトリでは、Debian系ではpostgresql-server-dev-${PG_MAJOR}、Red Hat系ではpostgresql${PG_MAJOR}-develといったメジャーバージョンを含めたパッケージ名になっているようなので、状況に応じて必要なバージョンを指定してインストールします。
ヘッダファイルの配置も同様にメジャーバージョン付きのパスになっていると思います。

Goとの連携

メインの処理はGoで実装したいため、CからGoで作成した関数を呼び出す必要があります。
また、Cで作成した関数を(PostgreSQLから)呼び出すようにする必要があります。
cgoでこれを実現する仕組みを提供しています。
例えばCのadd関数をエントリーポイントとして、内部でGoのgo_add関数を呼び出すことを考えます。
Goは以下のようにします。(一部省略)

// extern int add(int a, int b);
import "C"

//export go_add
func go_add(a C.int, b C.int) C.int {
	return a + b
}

Cは以下のようにします。

#include "_cgo_export.h"

int add(int a, int b) {
	return go_add(a, b);
}

このように実装したCとGoのファイルがあるディレクトリで共有ライブラリ生成用のビルドをすると、1つの共有ライブラリが生成されます。

ビルド

CとGoをビルドすることになりますが、PostgreSQL用のヘッダファイルを指定すること以外は、MySQL/MariaDBと同様のコマンドです。
ビルド時の注意というよりも実装時の注意ですが、必要なヘッダファイルは、postgres.hだけでは不足することが多く、特にSQLからの引数を取り扱うマクロを利用するためにはfmgr.hが実質的に必須です。また、利用する型によってはutils/geo_decls.hdatatype/timestamp.hなどと必要なものが変わります。前述の通り型によっては必要なマクロが別のヘッダファイルにあったりもするので、include漏れにはご注意ください。

共有ライブラリの格納ディレクトリ

次はビルドした共有ライブラリをプラグイン用ディレクトリに格納します。
MySQL/MariaDBと同様で、前述の開発用パッケージがインストールできれば、pg_config --pkglibdirというコマンドが実行できるはずで、これで格納先ディレクトリが表示されます。

UDFの登録

最後に関数を登録します。
処理結果が文字列の場合、DBへ接続し以下のようにSQLクエリを実行します。

CREATE FUNCTION udf_xxx(text, text) RETURNS text
  AS 'so_file_name', 'xxx'
  LANGUAGE C STRICT;

SQLで呼び出す関数名(udf_xxx)や共有ライブラリのファイル名(so_fine_name)は任意で問題ありませんが、(当たり前ですが)共有ライブラリの関数名(xxx)だけは一致する必要があります。
最後のSTRICTは、いずれかの引数がNULLを許容する場合は不要です。(STRICTの場合、いずれかの引数がNULLの場合、関数が呼び出されることなくNULLを返します。)

以上で、SELECT udf_xxx(.....);といったSQLを実行することができるようになります。

まとめ

Goを利用して、MySQL/MariaDBとPostgreSQLそれぞれについてUDFの作成方法をまとめました。
公式ドキュメントではいずれもC/C++での作成方法のみの記載ですが、cgoを使うことでGoだけでも十分作成できることがわかりました。また、特にPostgreSQLのように既存のC/C++資産を利用する方が良い場合でも、部分的にGoを利用してUDFもとい共有ライブラリが作成できることもわかりました。

おまけ

元々はZig(https://ziglang.org/)の勉強がてら簡単な共有ライブラリを作ってみよう思い、題材をUDFに決めました。
作成中にmysql.hをincludeしていると内蔵のコンパイラであるClangではコンパイルができず(原因は細かく調べていません)、最小限な定義であるudf_registration_types.hを見つけ、コンパイルができるようになりました。
コンパイルできたのでDockerで起動したDBで動作確認をしようとしていましたが、手元でコンパイルしては動的リンクするglibcのバージョンが合わず実行できませんでした。そんな時、Zigではglibcのバージョンを指定してコンパイル(例えばzig -target x86_64-linux-gnu.2.10)できることを知り、lddコマンドで確認すると確かにしていしたバージョンになっており、実行できるようになりました。
※バージョン指定について公式ドキュメントで見つけられていませんが、公式に最も近いものとしてZigの生みの親であるAndrew Kelley氏のブログ(https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html)に記述がありました。
このように少々トラブルはあったもののZigで共有ライブラリを作成する下地はできました。しかし、処理を実装し始めてみたところ、Cとの型変換が思いのほか煩雑になってしまう場面があり、学びたてレベルでは簡単に解決方法を見つけられそうになかったので、最終的にGoを使ってUDFを作成するに至りました。
なお、Goではcgoを利用するとデフォルトのコンパイラはgccのため、前述のようにglibcのバージョン差異は発生する可能性があります。そんな時はコンパイラをzig ccにしてバージョン指定すると実行環境に合わせてビルドすることができます。(CC="zig cc -target x86_64-linux-gnu.2.10" go build ...)
当初のZigを学ぶということからは遠のいてしまいましたが、C/C++と他言語の連携について多くを学ぶよい機会となりました。

関連記事

カテゴリー

アーカイブ