ZigからCライブラリを呼び出すシンプルな例
Zigは、独立して開発されている新しい低レベルプログラミング言語です。Cのパフォーマンス上の利点をすべて維持しながら、過去30年間に進歩したツールや言語設計も取り入れようとする、Cの現代的な再構築版です。
ZigはCに取って代わることを目指して設計されているため、ZigアプリケーションからCライブラリを呼び出せることは、主要な機能の一つです。ZigのC相互運用機能を説明するシンプルな例が見つからなかったので、自分で書いてみることにしました。
ZigからCを呼び出す既存の資料
ZigからCコードを呼び出す方法を説明した記事をいくつか見つけました。どれも役立つ情報を含んでいましたが、抽象的すぎるか、私がやろうとしていたことより複雑なケースを扱っていました。
- 「C/C++/Zig」(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 第4章 - Cとの連携
- この記事はZigとCの相互運用に関する低レベルの仕組みを説明していますが、完全な例は示していません。
上記の「Cプロジェクトを拡張する」2つのチュートリアルには、大きな制約があります。複雑な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を実行すると、プロジェクト環境でNix 0.14.0が利用できることが分かります。
$ nix develop
zig 0.14.0Zigには、gccのドロップイン置き換えとして使えるCコンパイラーが組み込まれています。先ほどのコンパイルをもう一度試しますが、今度は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.zigsrc/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 filesZigのユニットテスト用ビルドターゲットについても、同じようにします。
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の算術ライブラリへアクセスできるようになりましたが、まだライブラリを呼び出してはいません。この例を完成させるには、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で公開しています。プロジェクトの各段階に分けてあります。
- ステージ1:純粋なCによる実装
- ステージ2:ZigでCをコンパイルする
- ステージ3:同等のZig実装を作る
- ステージ4:ZigアプリケーションからCライブラリを呼び出す
- ステージ5:Zigが
#include参照をたどっていることを確認する
この解決策をシンプルにするための提案をくださったStéphane BortzmeyerさんとIntegratedQuantumさんに感謝します。また、解決策をZig 0.14.0向けに更新してくださったDaniel Bartleyさんにも感謝します。
記事をランダムに読む