A Simple Example of Calling a C Library from Zig

Michael Lynch

ZigからCライブラリを呼び出すシンプルな例

原文は Michael Lynch により に公開されました。 このブログを購読する

Zigは、新しく独立して開発された低水準プログラミング言語です。Cを現代的に再解釈したもので、Cが持つパフォーマンス上の利点をすべて保ちつつ、過去30年間のツールや言語設計の進歩も取り入れようとしています。

ZigはCを置き換えることを目的に設計されているため、ZigアプリケーションからCライブラリを呼び出せることは第一級の機能の一つです。ZigのC相互運用機能を示すシンプルな例が見つからなかったので、自分で書くことにしました。

ZigからCを呼び出すことに関する既存のリソース

ZigからCコードを呼び出す方法を解説した記事がいくつか見つかりました。どれも有益な情報は含んでいましたが、内容が抽象的すぎるか、私がやりたいことよりもはるかに複雑なシナリオを扱っていました:

  • “C/C++/Zig” by Loris Cro
    • 素晴らしいチュートリアルですが、内容は複雑です。単にCライブラリを呼び出すだけでなく、巨大なCアプリケーションをZigでビルドする方法を考え、さらに元のCコードを呼び出しつつCコードからも呼び出される新しい関数を書くという内容です。
    • このチュートリアルからは多くを学べましたが、よりシンプルなシナリオでZigからCを呼び出す方法をこの連載から理解するのは難しかったです。
    • このチュートリアルはZig 0.8.1向けに書かれたもので、コードはZig 0.14.0ではもうコンパイルできません。
  • “Extending a C Project with Zig” (2023)
    • 比較的新しい記事なので、現在のバージョンのZigでもコンパイルできます。
    • 上記のチュートリアルと同様、この記事も大規模で複雑なCアプリケーションをコンパイルする方法を扱っているため、そこで得た知見をよりシンプルなシナリオにどう応用すればよいのか理解するのが困難でした。
  • ziglearn Chapter 4 - Working with C
    • この記事ではZigとCの相互運用のための低水準な仕組みが解説されていますが、完全なサンプルは示されていません。

上記の2つの「Cプロジェクトを拡張する」系チュートリアルの大きな制約の一つは、複雑なMakefileをZigのビルドシステムに移植する方法を知っていることが前提になっている点です。どちらも「ほら、このややこしい100行のMakefileを見てください。はい、今度はややこしい100行のbuild.zigファイルになりました!」といった調子で、肝心の方法についてはあまり説明してくれません(90分の動画を見ない限りは)。

完全なZig初心者として、大きなMakefileをZigのビルドシステムに変換する方法を学びたいとは思いませんでした。その代わりに、アプリケーション全体をZigネイティブのビルドシステムに移植するのではなく、Cアプリケーションの一部だけをZigでビルドするシンプルな例を試してみたかったのです。

シンプルなCアプリケーションを作る

他のZig + Cの例で私がつまずいたのは、Cのコードが複雑すぎて、ZigからCコードを呼び出す基本的な仕組みが見えにくくなっていたことでした。

ZigのC相互運用機能をより分かりやすくするために、シンプルなCアプリケーションとライブラリを作ることにしました。

最初のCヘッダーファイルは次のとおりです:

// arithmetic.h

int add(int x, int y);

実装は次のとおりです:

// arithmetic.c

#include "arithmetic.h"

int add(int x, int y) {
  return x + y;
}

特に凝ったことはしていません。できるだけシンプルに保つことが目的です。

最後に、add関数を試すためのテスト用アプリケーションを作ります:

// main.c

#include <stdio.h>

#include "arithmetic.h"

int main(void)
{
  int x = 5;
  int y = 16;
  int z = add(x, y);
  printf("%d + %d = %d\n", x, y, z);
  return 0;
}

すべてが正しく動作していれば、このアプリケーションを標準的なCコンパイラであるgccでコンパイルできるはずです:

$ gcc arithmetic.c main.c -o ./bin/example
$ ./bin/example
5 + 16 = 21

うまくいきました!

この段階の完全なサンプルはGitHubにあります

コンパイラをZigに切り替える

ここまでは純粋なCプロジェクトで、Zigはまったく使っていません。

次に、Zigをインストールします。Zigのインストール方法はいくつかありますが、私はNixを使います。最近お気に入りのパッケージマネージャーだからです。Nixはインストールのためだけに使っているので、まだNix信者でない方は、別の方法でZig 0.14.0をインストールしても構いません。

プロジェクトに以下のflake.nixファイルを追加しました。これで環境にZig 0.14.0が取り込まれます:

{
  description = "Dev environment for zig-c-simple";

  inputs = {
    flake-utils.url = "github:numtide/flake-utils";

    # 0.14.0
    zig-nixpkgs.url = "github:NixOS/nixpkgs/f6db44a8daa59c40ae41ba6e5823ec77fe0d2124";
  };

  outputs = { self, flake-utils, zig-nixpkgs }@inputs :
    flake-utils.lib.eachDefaultSystem (system:
    let
      zig-nixpkgs = inputs.zig-nixpkgs.legacyPackages.${system};
    in
    {
      devShells.default = zig-nixpkgs.mkShell {
        packages = [
          zig-nixpkgs.zig
        ];

        shellHook = ''
          echo "zig" "$(zig version)";
        '';
      };
    });
}

ここからnix developを実行すると、プロジェクト環境でZig 0.14.0が利用可能になっていることが確認できます:

$ nix develop
zig 0.14.0

Zigには組み込みのCコンパイラがあり、gccのドロップイン置換として機能します。先ほどのコンパイルを再試行しますが、gccの代わりにzig ccを呼び出します:

$ zig cc arithmetic.c main.c -o ./bin/example
$ ./bin/example
5 + 16 = 21

いい感じです。まだ問題なく動作しており、これでコンパイルにZigを使っていることになります。まだZigのコードは一切使っていないので、次はそこに取り組みます。

この段階の完全なサンプルはGitHubにあります

同等のZigアプリを作る

Zigアプリケーションを作るために、ボイラープレートのZig実行ファイルを生成するzig init-exeを使います:

$ zig init-exe
info: Created build.zig
info: Created src/main.zig

src/main.zigを以下の内容に置き換えます。これで上記のmain.cと同等のZigアプリケーションが作成されます。

// src/main.zig

const std = @import("std");

fn add(x: i32, y: i32) i32 {
    // TODO: Instead of reimplementing this in Zig, call the C version.
    return x + y;
}

pub fn main() !void {
    const x: i32 = 5;
    const y: i32 = 16;
    var z: i32 = add(x, y);

    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try stdout.print("{d} + {d} = {d}\n", .{ x, y, z });
    try bw.flush();
}

test "test add" {
    try std.testing.expectEqual(@as(i32, 21), add(5, 16));
}

実行すると、C版と同じ出力が得られます:

$ zig build run
5 + 16 = 21

いい感じですが、私の目標はすべてをZigで書き直すことではなく、ZigからCコードを呼び出すことです。次は、addのZig実装をネイティブなC実装に置き換える方法を考えます。

この段階の完全なサンプルはGitHubにあります

ZigアプリケーションをネイティブCライブラリにリンクする

ここまでは基本的な「hello, world!」のような内容でした。ここからが、これまで私がつまずいてきた部分、すなわちZigからネイティブなCコードを呼び出すところです。

まず、ZigのコードとCのコードを分けるためにファイルを再構成します。新しいフォルダ構成は次のとおりです:

c-src/
  arithmetic.c
  arithmetic.h
  main.c
src/
  main.zig
build.zig

次に、ZigアプリケーションがCソースファイルにアクセスできるようにbuild.zigを調整します:

    const exe = b.addExecutable(.{
        .name = "zig-c-simple",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.addIncludePath(b.path("c-src"));   // Look for C source files

Zigのユニットテストのビルドターゲットについても同様のことを行います:

    const unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    unit_tests.addIncludePath(b.path("c-src"));   // Look for C source files

これでZigのビルドがCのarithmeticライブラリにアクセスできるようになりましたが、まだライブラリを呼び出していません。このサンプルを完成させるために、src/main.zigファイルに次の変更を加える必要があります:

// src/main.zig

const arithmetic = @cImport({
    @cInclude("arithmetic.c");
});

fn add(x: i32, y: i32) i32 {
    return arithmetic.add(x, y);
}

上記の変更により、add関数のZigネイティブな実装が、arithmetic.cファイル内のネイティブなCのadd関数を呼び出すラッパーに置き換わります。

いよいよ真実の時です。すべて期待どおりにコンパイルされ、実行されるでしょうか?

$ zig build run
5 + 16 = 21

うまくいきました!

ユニットテストも試してみます:

$ zig build test --summary all
Build Summary: 3/3 steps succeeded; 1/1 tests passed
test success
└─ run test 1 passed 1ms MaxRSS:1M
   └─ zig test Debug native success 2s MaxRSS:201M

ユニットテストも通っています。すべて順調です!

Zigは本当にCを呼び出しているのか?

他のプログラミング言語からCコードを呼び出したことがありますが、こんなに簡単だったことはありません。自分自身を騙しているのではないか、Zigが実際には私のCコードを呼び出していないのではないかと心配になったので、わざとCコードにバグを仕込みました:

// arithemtic.c

int add(int x, int y) {
  return x + y - 1; // Intentionally return incorrect results.
}

もしZigアプリケーションが本当にCを呼び出しているなら、基盤となるCコードが不正確になったため、Zigのユニットテストは失敗するはずです。

どうなるかユニットテストを実行してみました:

$ zig build test --summary all
test
└─ run test 0/1 passed, 1 failed
error: 'main.test.test add' failed: expected 21, found 20
/nix/store/dzdlr4lms4wgjvi02r1pcqh54iiq9pn5-zig-0.14.0/lib/zig/std/testing.zig:103:17: 0x1048bad in expectEqualInner__anon_421 (test)
                return error.TestExpectedEqual;
                ^
/tmp/tmp.LCH7Soiq5V/src/main.zig:25:5: 0x1048c7f in test.test add (test)
    try std.testing.expectEqual(@as(i32, 21), add(5, 16));
    ^
error: while executing test 'main.test.test add', the following test command failed:
/tmp/tmp.LCH7Soiq5V/.zig-cache/o/ab02faa31a7c5067027f9f3a2e4ce1f9/test --seed=0x4b485ae6 --cache-dir=/tmp/tmp.LCH7Soiq5V/.zig-cache --listen=-
Build Summary: 1/3 steps succeeded; 1 failed; 0/1 tests passed; 1 failed
test transitive failure
└─ run test 0/1 passed, 1 failed
   └─ zig test Debug native success 1s MaxRSS:257M
error: the following build command failed with exit code 1:
/tmp/tmp.LCH7Soiq5V/.zig-cache/o/159f82a7dcc12c245254f0919e2ecdf2/build /nix/store/dzdlr4lms4wgjvi02r1pcqh54iiq9pn5-zig-0.14.0/bin/zig /nix/store/dzdlr4lms4wgjvi02r1pcqh54iiq9pn5-zig-0.14.0/lib/zig /tmp/tmp.LCH7Soiq5V /tmp/tmp.LCH7Soiq5V/.zig-cache /home/mike/.cache/zig --seed 0x4b485ae6 -Zabdc51211068b123 test --summary all

素晴らしい!テストは予想どおりexpected 21, found 20というエラーで失敗しました。ユニットテストは、私がCのadd関数に仕込んだバグを正しく検出しました。

Zigはヘッダーの参照を追跡しているのか?

この解決策で驚くほどよく機能しているもう一つの点は、.hファイルを通じて関数を参照できることです。しばらくC/C++プログラミングをしていませんが、.cファイルでインポートすることはできないという記憶があるので、Zigでこれほど簡単にできるのは驚きです。

Zigが何らかのごまかしをしているかどうかをテストするため、arithmetic.hヘッダーに新しい関数とプリプロセッサマクロを追加しました:

// arithmetic.h

#define INCREMENT_AMOUNT 1
int increment(int x);

そして、この新しい関数の定義をarithmetic.cに追加します:

// arithemtic.c

int increment(int x) {
  return x + INCREMENT_AMOUNT;
}

最後に、この新しい関数の簡単なユニットテストをsrc/main.zigファイルに追加します:

test "test increment" {
    try std.testing.expectEqual(@as(i32, 6), arithmetic.increment(5));
}

もしZigがCヘッダーの#includeディレクティブを無視しているなら、コンパイルエラーになるか、テストが通らなくなるはずです。新しいテストを実行してみます:

$ zig build test --summary all
Build Summary: 3/3 steps succeeded; 2/2 tests passed
test success
└─ run test 2 passed 830us MaxRSS:1M
   └─ zig test Debug native success 2s MaxRSS:201M

通りました!これは、ZigがCソース内の#include参照を追跡する便利な機能を持っており、これまで使ったどの言語よりもCコードの呼び出しを簡単にしていることを示しています。

まとめ

この記事では、ZigからCコードを呼び出す方法を示すために私が考えつく限り最もシンプルな例を紹介しました。

この手法を使えば、Cライブラリの一部をZigのビルドシステムに移植し、そこからZigを使ってそのライブラリを呼び出すことが可能です。

ソースコード

完全なソースコードはGitHubで公開しています。プロジェクトの各段階ごとに分けています:


Stéphane Bortzmeyer氏とIntegratedQuantum氏に、この解決策をシンプルにするための提案をいただいたことに感謝します。Daniel Bartley氏には、この解決策をZig 0.14.0に対応させてくれたことに感謝します。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント