🌰

Zig プロジェクトで C 言語の依存関係を使用する

2023/07/04に公開

この記事では、C 言語で書かれた依存関係を Zig プロジェクトで使用する方法を具体的な例を用いて説明します。例は

https://github.com/aklomp/base64

このプロジェクトは、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行を追加して、このファイルをリンクします。

build.zig
    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 言語ランタイムをリンクする必要があります。

build.zig
    lib.linkLibC();

完了しました。詳細なコードは以下を参照してください。

https://github.com/dying-will-bullet/base64-simd/tree/dynlib

方法 2・Cコードのコンパイルに Zig を使用する方法

方法1の不便な点は、手動で make を実行し、gcc を使用して libbase64.o を生成する必要があることです。パッケージを作成する際にはあまり便利ではありません。Zig 自体が Cコードを直接コンパイルできるため、Zig を使用して直接構築することができます。

まず、コンパイルに参加する C ファイルを定義します。

build.zig
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 をビルドするためのロジックを追加します。

build.zig
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 のコードに変換します。

build.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 => {},
    }

最後、わずかな修正を加えることで完了しました。すべてのコードは以下を参照してください。

https://github.com/dying-will-bullet/base64-simd

Discussion