Zig プロジェクトで C 言語の依存関係を使用する
この記事では、C 言語で書かれた依存関係を Zig プロジェクトで使用する方法を具体的な例を用いて説明します。例は
このプロジェクトは、SIMD 操作を可能な限り利用して、Base64 のエンコードとデコードのパフォーマンスを向上させます。Nodejs はこのプロジェクトを Base64 の基盤実装として使用しています。
以下は、C 言語 のプロジェクトを使用する方法を説明するために。まず、新しいプロジェクトを作成し、base64 をサブモジュールとして追加します。
$ zig init-lib
$ mkdir deps
$ cd deps && git submodule add https://github.com/aklomp/base64.git
方法 1・Zig でオブジェクトファイルをリンクする
ビルドプロセスで生成された .o
ファイルを使用することができます。具体的な方法は、依存するプロジェクトのビルド方法によって異なります。たとえば
$ make
cc -std=c99 -O3 -Wall -Wextra -pedantic -o bin/base64.o -c bin/base64.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/avx512/codec.o -c lib/arch/avx512/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/avx2/codec.o -c lib/arch/avx2/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/generic/codec.o -c lib/arch/generic/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/neon32/codec.o -c lib/arch/neon32/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/neon64/codec.o -c lib/arch/neon64/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/ssse3/codec.o -c lib/arch/ssse3/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/sse41/codec.o -c lib/arch/sse41/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/sse42/codec.o -c lib/arch/sse42/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/arch/avx/codec.o -c lib/arch/avx/codec.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/lib.o -c lib/lib.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/codec_choose.o -c lib/codec_choose.c
cc -std=c99 -O3 -Wall -Wextra -pedantic -Ilib -o lib/tables/tables.o -c lib/tables/tables.c
ld -r -o lib/libbase64.o lib/arch/avx512/codec.o lib/arch/avx2/codec.o lib/arch/generic/codec.o lib/arch/neon32/codec.o lib/arch/neon64/codec.o lib/arch/ssse3/codec.o lib/arch/sse41/codec.o lib/arch/sse42/codec.o lib/arch/avx/codec.o lib/lib.o lib/codec_choose.o lib/tables/tables.o
objcopy --keep-global-symbols=lib/exports.txt lib/libbase64.o
cc -std=c99 -O3 -Wall -Wextra -pedantic -o bin/base64 bin/base64.o lib/libbase64.o
最終的に lib/libbase64.o
ファイルが生成されました。.o
ファイルは、ソースコードをコンパイルした後に生成されるオブジェクトファイルです。これには、コンパイラによってソースコードが機械語に変換される中間結果が含まれていますが、まだ最終的なリンク処理は行われていません。一般的に、.o
ファイルは特定のプラットフォームとコンパイルオプションに関連付けられており、リンカによって使用され、最終的な実行ファイルや共有ライブラリが生成されます。リンクプロセスでは、複数の .o
ファイルが結合され、最終的な実行ファイルやライブラリファイルが作成されます。
build.zig
ファイルに以下の2行を追加して、このファイルをリンクします。
lib.addIncludePath("deps/base64/include");
lib.addObjectFile("deps/base64/lib/libbase64.o");
コンパイル後、次のエラーが発生します。
error: ld.lld: undefined symbol: __stack_chk_fail
note: referenced by tables.c
note: /home/kumiko/zig/base64-simd/deps/base64/lib/libbase64.o:(base64_encode)
note: referenced by tables.c
note: /home/kumiko/zig/base64-simd/deps/base64/lib/libbase64.o:(base64_decode)
__stack_chk_fail
は、コンパイラで定義された関数であり、プログラムのスタックオーバーフローが発生した場合にスタックチェックの失敗をトリガーするために使用されます。これは、バッファオーバーフローやスタックの破壊など、一般的なセキュリティ上の問題を検出するための保護機構です。
gcc で生成されたオブジェクトファイルを使用しているため、Zig でリンクする際に__stack_chk_fail
シンボルが見つからないというエラーが発生しています。そのため、C 言語ランタイムをリンクする必要があります。
lib.linkLibC();
完了しました。詳細なコードは以下を参照してください。
方法 2・Cコードのコンパイルに Zig を使用する方法
方法1の不便な点は、手動で make
を実行し、gcc を使用して libbase64.o
を生成する必要があることです。パッケージを作成する際にはあまり便利ではありません。Zig 自体が Cコードを直接コンパイルできるため、Zig を使用して直接構築することができます。
まず、コンパイルに参加する C ファイルを定義します。
const source_files = &.{
"deps/base64/lib/arch/avx512/codec.c",
"deps/base64/lib/arch/avx2/codec.c",
"deps/base64/lib/arch/generic/codec.c",
"deps/base64/lib/arch/neon32/codec.c",
"deps/base64/lib/arch/neon64/codec.c",
"deps/base64/lib/arch/ssse3/codec.c",
"deps/base64/lib/arch/sse41/codec.c",
"deps/base64/lib/arch/sse42/codec.c",
"deps/base64/lib/arch/avx/codec.c",
"deps/base64/lib/lib.c",
"deps/base64/lib/codec_choose.c",
"deps/base64/lib/tables/tables.c",
};
次に、libbase64
をビルドするためのロジックを追加します。
fn buildLibBase64(b: *Build, step: *CompileStep) !*CompileStep {
const lib64 = b.addStaticLibrary(.{
.name = "libbase64",
.target = step.target,
.optimize = step.optimize,
});
// For `__stack_chk_fail`
lib64.linkLibC();
// C Source
inline for (source_files) |file| {
lib64.addCSourceFile(
file,
&.{
"-std=c99",
"-O3",
"-Wall",
"-Wextra",
"-pedantic",
},
);
}
// header
step.addIncludePath("deps/base64/include");
return lib64;
}
しかし、このようにするとエラーが発生します 'config.h' file not found
。その原因は、config.h
は base64 で make
を使用して生成されたファイルであるため、私たちもビルド中に生成する必要があるからです。
ifdef AVX512_CFLAGS
HAVE_AVX512 = 1
endif
lib/config.h:
@echo "#define HAVE_AVX512 $(HAVE_AVX512)" > $@
上記の Makefile のロジックを Zig のコードに変換します。
switch (builtin.cpu.arch) {
.x86_64 => {
if (std.Target.x86.featureSetHas(builtin.cpu.features, .avx512vbmi)) {
_ = try stream.write("#define HAVE_AVX512 1\n");
} else {
_ = try stream.write("#define HAVE_AVX512 0\n");
}
},
else => {},
}
最後、わずかな修正を加えることで完了しました。すべてのコードは以下を参照してください。
Discussion