96
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

オープンソースプログラミング言語zigまとめ

WebAssemblyのモジュール記述のためのプログラミング言語を調べていると、比較的新しいオープンソースのプログラミング言語zigがWebAssemblyモジュールを書くのに適している感じだったので、ソースコードを書いて動かしながら調べたことについて、コードに出ている順で並べています。

この文章中で用いているソースコードはmacOSやlinux上でビルド可能な状態で以下のURLにおいてあります:

Qiitaは現時点でzigコードのシンタックスハイライトに未対応であり視認性がよくないですが、githubではzigコードでもシンタックスハイライトされます。

0. プログラミング言語zigについて

オープンソースのプログラミング言語zigは、以下のURLで公開されています。

zigはLLVMで実装しているプログラミング言語ですが、C++やGoやRustと違い、C言語のようにヒープメモリの利用がオプションであり、newのようなものが言語構文に組み込まれていません。

zig言語の特徴は:

  • 各OS環境の主要パッケージシステムでインストールできる
  • 開発ツールがzigコマンド1つに統合されている
  • スクリプト言語のように、ソースファイル(.zigファイル)を直接実行できる(zig run xxx.zig)
  • ユニットテスト用の構文が組み込まれており、テストコードを実装と同一のソースファイル中に記述でき、zigコマンドでテストを実行できる(zig test xxx.zig)
  • 実行ファイルなどを生成する、ビルド記述build.zigもzig言語で記述し、zigコマンドでビルド実行できるビルドシステムを備える(zig build)
  • シンタックスフォーマッタを組み込んでいる(zig fmt xxx.zig)
  • ビット数指定の数値型、式としてのifswitchやループ構文、ブロック脱出時実行のdefer文、配列のスライス、コルーチン、コンパイル時実行式、等のモダンな言語仕様
  • C言語とABIレベルの相互運用ができる言語モデル
  • Cライブラリのヘッダファイル(.h)にある関数やstruct/union/enumを、zigの関数やstruct/union/enumとして直接使用するためのCプログラムを組み込む機構(@cImport)を言語として組み込んでいる
  • C言語より厳しい型制約、そして、型推論を備える
  • musl-libc実装を含み、libcコードを埋め込んだスタンドアロンのバイナリを生成できる
  • WebAssemblyのwasmファイルやUEFI用バイナリ、各OS用のスタティック/ダイナミックライブラリを含む多彩なビルドターゲットを持つ

などです。

ただし、構文や標準ライブラリの仕様がまだ安定していない点は、注意です。
現在のバージョン0.4.0と、その次のバージョンでは、文字列の配列型の構文のレベルで非互換が起きるような段階です。
そして以下、この記事では、バージョン0.4.0の仕様に基づいてすべて記述しています。

最初に、zig zenコマンドを実行すると、zig言語のポリシーを垣間見ることができます:

$ zig zen

 * Communicate intent precisely.
 * Edge cases matter.
 * Favor reading code over writing code.
 * Only one obvious way to do things.
 * Runtime crashes are better than bugs.
 * Compile errors are better than runtime crashes.
 * Incremental improvements.
 * Avoid local maximums.
 * Reduce the amount one must remember.
 * Minimize energy spent on coding style.
 * Together we serve end users.

$

zig言語では、twitterなどのハッシュタグではziglangが使われています。

1. zigコマンドのインストール

zig言語は、メジャーなパッケージマネージャのhomebrew, snap, nixos, scoop, chocolatey, packmanで、zig(scoopではziglang)パッケージを提供しています。これらのパッケージマネージャからzigコマンドをインストールしするのが手軽です。

emacsのzig-mode

MELPAに、zig言語オフィシャルのzig-modeが登録されています。

vim, vscode, sublime用のモードもあるようです:

2. zigコマンドの基本

開発ツールは、すべてzigコマンドのサブコマンドとして提供されています。

zigファイル実行系コマンド:

  • zig run xxx.zig: ソースファイルxxx.zigmain関数を(バイナリ生成せずに)実行します
  • zig test xxx.zig: ソースファイルxxx.zigに記述されたtest構文内のコードを順に実行し、テスト結果のサマリを表示します
  • zig build: ビルド記述build.zigに記述されたビルド処理を実行します

zigファイルコンパイル系コマンド:

  • zig build-exe xxx.zig: zigファイル(xxx.zig)から、各種ターゲットのバイナリ(xxxxxx.exe等)を生成します
  • zig build-obj xxx.zig: zigファイル(xxx.zig)から、Cヘッダファイル(xxx.h)とCオブジェクトファイル(xxx.o, xxx.obj等)を生成します
  • zig build-lib xxx.zig: zigファイル(xxx.zig)から、Cヘッダファイル(xxx.h)とCスタティックライブラリファイル(libxxx.a, xxx.lib等)を生成します
  • zig build-lib -dynamic xxx.zig: zigファイルから、Cヘッダファイル(xxx.h)とDLLファイル(libxxx.solibxxx.dylibxxx.dllxxx.pdb等)などを生成します

ユーティリティ系コマンド:

  • zig init-exe: このディレクトリ以下に、実行ファイル用のプロジェクトテンプレート(build.zigsrc/main.zig)を生成します
  • zig init-lib: このディレクトリ以下に、スタティックライブラリ用のプロジェクトテンプレート(build.zigsrc/main.zig)を生成します
  • zig fmt xxx.zig: xxx.zigに、シンタックスフォーマッタをかけて上書きします
  • zig translate-c xxx.c: Cヘッダファイルや簡単なC実装ファイルを、zigコード化して標準出力します

情報表示系コマンド:

  • zig --help: サブコマンドの一覧およびコマンド引数の一覧ヘルプを表示します
  • zig builtin: zigコマンドのランタイム環境情報のモジュール@import("builtin")を、zigソースコード形式で表示します(ビルドターゲット等の情報があり、build.zig内のコードで主に使う)
  • zig targets: zig build-exe等で扱えるビルドターゲットの一覧を表示します
  • zig version: zigコマンドの仕様バージョン(0.4.0など)を表示します
  • zig libc: zigコマンド実行環境のlibcのディレクトリパス情報を表示します
  • zig zen: zig言語のデザインポリシーを表示します

コマンド例 zig build-exe --release-fast -target wasm32-freestanding --name fft.wasm fft.zig

このコマンドラインは、zigライブラリコードからWebAssemblyでの.wasmモジュールファイルを生成するものです。
コマンドラインオプションは以下の意味となります。

  • --release-fast: 速度重視のビルドを指定しています
    • --relase-safe: 速度重視だがトラップ処理は埋め込んだままのビルドを指定します
    • --release-small: (トラップなしで)サイズ重視のビルドを指定します
  • -target wasm32-freestandingは、WebAssemblyの32bit wasm仕様で、importする依存なし(freestanding)のターゲットを指定しています
    • -target x84_64-windows: Windowsの64bitコンソールコマンド(exe)をターゲットに指定します
  • --name fft.wasm: アウトプットファイル名をfft.wasmに設定します
    • デフォルトのアウトプットファイル名は、メインのzigファイル名をベースにした、ターゲットOSでの実行ファイル名(この場合、fftfft.exe)になります

3. zigプログラム: FFT実装

言語のもつ各種機能に触れられる程度には複雑なコードの例として、高速フーリエ変換FFTのループ実装を採用し、これをzigで記述します。

main関数のないライブラリとしてfft.zigを記述し、この中でtest構文でテストを記述し、zig test fft.zigでテスト実行で動作確認します。

fft.zigのコード全体にはテストコードを含めて200行弱になります。ここではコードを7分割し、そのコード片を上から順に切り出し、そのコード片ごとに含むzig言語の機能や特徴を説明していきます。


3.1. 標準ライブラリ読み込み

以下のコード片は、標準ライブラリから円周率値、sin/cos関数の読み込みをするコードです:

fft.zig(1)
const pi = @import("std").math.pi;
const sin = @import("std").math.sin;
const cos = @import("std").math.cos;

標準ライブラリとビルトイン関数

zigでは、@import("std")によって標準ライブラリ の機能を利用できます。
@importのような@で始まる識別子はzigではビルトイン関数 と呼び、型のキャストもこのビルトイン関数による機能として提供されます。

変数のモデル

zigでは、(C言語と同じく)変数は連続メモリ領域のモデルです。

このためconst変数は、メモリ領域の内容全体が変更不可能となり、構造体の場合での各メンバーや配列の場合での各要素も変更不可能となります。
また、zigでは、関数の引数もconst変数扱いです。内容が変更可能な変数はvarで宣言します。

const

zigにはconstをつけた型があります。const型は、(ポインタ利用で)その参照経由の変更操作は不許可、という意味であり、const変数とは別の作用になります。

たとえば、var a: f64 = 0.0の変数は値を変更でき、var ptr: *const f64 = &aで参照できますが、この参照からの変更ptr.* = 1.0はコンパイルエラーとなります(ptr.*はメンバー全部の意味でポインタの先のメモリ領域を指す。Cでの*ptrのこと)。
逆に、var a: f64 = 0.0の変数への、const変数でのポインタconst ptr: *f64 = &aに対しては、ptr.* = 1.0が可能です。

また、const a: f64 = 0.0const変数へは、const型のポインタvar ptr: *const f64 = &aでなければコンパイルエラーとなります。

型への名前付けのconst変数

const変数は、struct/union/enum/errorなどの型(type型変数)への別名付けにも使われます。
zigの型チェックは、型名での一致ではなく、構造の一致で判断します。このため、別名の型の変数と元の名前の型の変数との間で代入可能です。

のコードでは標準ライブラリの関数や変数をconstで変数名を割り当てて使用していますが、@import("std")は式であり、コード内のどこでも使えます。

また、zig-0.4.0では、sincosは標準ライブラリ提供の関数となっていますが、次のバージョンではビルトインに移籍し@sin@cosとして使用することになるようです。


3.2. 複素数structの定義

以下のコード片は、FFTで扱う複素数構造体と、そのコンストラクタやメソッドの宣言です:

fft.zig(2)
pub export const CN = extern struct {
    pub re: f64,
    pub im: f64,
    pub fn rect(re: f64, im: f64) CN {
        return CN{.re = re, .im = im,};
    }
    pub fn expi(t: f64) CN {
        return CN{.re = cos(t), .im = sin(t),};
    }
    pub fn add(self: CN, other: CN) CN {
        return CN{.re = self.re + other.re, .im = self.im + other.im,};
    }
    pub fn sub(self: CN, other: CN) CN {
        return CN{.re = self.re - other.re, .im = self.im - other.im,};
    }
    pub fn mul(self: CN, other: CN) CN {
        return CN{
            .re = self.re * other.re - self.im * other.im,
            .im = self.re * other.im + self.im * other.re,
        };
    }
    pub fn rdiv(self: CN, re: f64) CN {
        return CN{.re = self.re / re, .im = self.im / re,};
    }
};

pub属性

pub属性は、(標準ライブラリのpisinのように)他のzigファイルから@importされたときに利用させる変数や関数につける属性です。
pubがなければ@importされても、変数や関数は直接利用できません。

export属性

extport属性は、Cヘッダ(.h)から利用させる関数や変数につける属性です。オブジェクトファイルやライブラリファイルから利用させる対象へつける属性です。

関数にexportをつけた場合、Cヘッダファイルにその関数に対応するプロトタイプ宣言が埋め込まれます。
structのような型への変数にexportをつける場合、Cヘッダファイルにその型情報が埋め込まれます。

Cでのexternと同様に、ファイルをまたいでも名前が被らない必要があります。

structのバリエーション

zigのstruct型は、メモリレイアウトとexportでの扱いの違いで、3タイプが存在します。

  • struct: メンバーの順序を含むメモリレイアウトを規定しないstructexport時のCヘッダではstructポインタでの宣言として埋め込まれる
  • packed struct: 宣言順にメンバーを配置するメモリレイアウトのstructexport時のCヘッダではstructポインタ宣言として埋め込まれる
  • extern struct: 宣言順にメンバーを配置するメモリレイアウトのstructexport時のCヘッダではstructのメンバー定義宣言として埋め込まれる

structのメンバー関数

zigでは、struct内部に、関数fnを宣言できます。この関数は型名.関数名(...)のかたちで呼び出します。addsubのように、第一引数が定義するstructである場合は、メソッド呼び出しスタイルでも記述できます(CN.add(a, b)を、a.add(b)としても記述ができる)。

zig-0.4.0では、struct内の関数でも、export時のCヘッダでは、構造体名のプレフィックスなどなにもない、関数名そのままの関数名でのプロトタイプ宣言となります(CN.rect()exportすると、CN_rect()のようになるのではなく、ただのrect()になる)。

数値型

zigの数値型は、ビット数を明示します。C言語インタフェース用のターゲット依存の数値型もあります:

  • 非数値型: type, noreturn, void, bool
  • 符号付き整数型: i8, i16, i32, i64, i128
  • 符号なし整数型: u8, u16, u32, u64, u128
  • 浮動小数型: f16, f32, f64, f128
  • ターゲット依存数値型: isize, usize, c_short, c_ushort, c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong, c_longdouble

また、シフト演算(>><<)の右辺は、u3u4u5u6, u7といった左辺のビット数-1を上限値とする整数型を使います(変数は、これらの型へダウンキャストします)。

zigではtruefalseを受けるbool型は数値型ではありませんが、ビルトインを使って0/1の数値型にキャスト可能です。

  • typeは(struct等のカスタムタイプも含む)型の型
  • voidは値なしの型で、(breakで値を渡さない)ブロック式の型でもある
  • noreturnは、途中でプロセス終了したり、無限ループしたりなど、正常に終わらない関数のための戻り値の型

structイニシャライザ

構造体のイニシャライザ(リテラル)は構造体名 { .メンバー名 = 値, ...}です。C99の構造体メンバーのイニシャライザ((型) {.メンバー名=値,...})に似た構文です。

関数の引数や戻り値は値のコピー

関数の引数や戻り値は値のコピーのモデルです。structや配列だろうとコピー扱いです。
引数や戻り値の型としてポインタ型を使うことは可能です。

どんな大きさの型だろうとスタック上でコピーするため、配列型を下手に扱うと、OSのスタックリミットにかかる可能性があります。


3.3. 構造体CNのテストコード

以下のコード片は、テストコード構文内で、宣言したCNのメンバー関数を利用しているコードです。

fft.zig(3)
test "CN" {
    const assert = @import("std").debug.assert;
    const eps = @import("std").math.f64_epsilon;
    const abs = @import("std").math.fabs;

    const a = CN.rect(1.0, 0.0);
    const b = CN.expi(pi / 2.0);
    assert(a.re == 1.0 and a.im == 0.0);
    assert(abs(b.re) < eps and abs(b.im - 1.0) < eps);

    const apb = a.add(b);
    const asb = a.sub(b);
    assert(abs(apb.re - 1.0) < eps and abs(apb.im - 1.0) < eps);
    assert(abs(asb.re - 1.0) < eps and abs(asb.im + 1.0) < eps);

    const bmb = b.mul(b);
    assert(abs(bmb.re + 1.0) < eps and abs(bmb.im) < eps);

    const apb2 = apb.rdiv(2.0);
    assert(abs(apb2.re - 0.5) < eps and abs(apb2.im - 0.5) < eps);
}

test

test "名前" {...}は、zig testコマンド時に実行されるコードを記述する部分です。

zig-0.4.0現在、test内で直接的には関数を定義できません。
しかし、test内でもstruct内での関数宣言は可能で、メソッド呼び出しによって関数を使うことは可能です。

構造体のメソッド呼び出し

struct内のfnの関数は、構造体名.関数名()で呼び出せ、とくに第一引数がそのstructの場合のみ、変数名.関数名()でも呼び出せる、という構文になっています。

このコードでのa.add(b)は、CN.add(a, b)でも呼び出し可能となります。


3.4. CN配列のテストコード

以下のコード片は、CNの配列での利用のテストコードです。

fft.zig(4)
test "CN array" {
    const assert = @import("std").debug.assert;
    const eps = @import("std").math.f64_epsilon;
    const abs = @import("std").math.fabs;

    var cns: [2]CN = undefined;
    cns[0] = CN.rect(1.0, 0.0);
    cns[1] = CN.rect(0.0, 1.0);
    cns[0] = cns[0].add(cns[1]);
    assert(abs(cns[0].re - 1.0) < eps and abs(cns[0].im - 1.0) < eps);
}

zigの配列

zigの配列型は[要素数]要素型で、要素数が明示された連続領域を意味します。
多次元配列型は[外側要素数][内側要素数]要素型です。メモリレイアウトは、内側要素数個が、外側要素数個並びます。

要素型がstructならメンバーが順に並んで、それが要素数個ある、メモリレイアウトになります。
配列の要素への代入は値の上書きになります。

var変数による配列の未初期化宣言はvar 変数名: [要素数]要素型 = undefinedです。
要素数は必須です。

配列型のイニシャライザ(リテラル)は、[要素数]要素型{初期値0, 初期値1, ....}です。
イニシャライザでの総素数は省略できます。
同一値でのイニシャライザは、[]要素型{初期値0, 初期値1, ...} ** 繰り返し数も可能です。この場合、初期値数✕繰り返し数の要素数の配列型になります。

var変数による配列の初期値あり宣言は配列型のイニシャライザをつかって、var 変数名 = [要素数]要素型 {初期値0, 初期値1, ...}となります。
const変数では初期値あり宣言const 変数名 = [要素数]要素型 {初期値0, 初期値1, ...}を使うのが普通です(例: const v = [3]u8{1,2,3})。

注意点は、関数内で配列変数を宣言するとスタックメモリに領域が割り当てられることです。
このため、配列を使うとOSのスタックリミット(8MBなど)に到達しやすいので注意が必要です。
(標準ライブラリでヒープ等を使う方法は後述します。)


3.5. FFT用インデックスビット反転

以下のコード片は、FFTでの配列インデックスのビット反転の関数revbitの定義とそのテストコードです。

fft.zig(5)
fn revbit(k: u32, n0: u32) u32 {
    return @bitreverse(u32, n0) >> @truncate(u5, 32 - k);
}

test "revbit" {
    const assert = @import("std").debug.assert;
    const n: u32 = 8;
    const k: u32 = @ctz(n);
    assert(revbit(k, 0) == 0b000);
    assert(revbit(k, 1) == 0b100);
    assert(revbit(k, 2) == 0b010);
    assert(revbit(k, 3) == 0b110);
    assert(revbit(k, 4) == 0b001);
    assert(revbit(k, 5) == 0b101);
    assert(revbit(k, 6) == 0b011);
    assert(revbit(k, 7) == 0b111);
}

ビット演算のビルトイン

zigでは、CPU命令で提供される(CPU intrinsics)ビット演算は、ビルトインで用意されています。

  • @bitreverse(数値型, 数値): ビット上下反転 (注: 次期バージョンでは@bitReverseに名前が変わる)
  • @ctz(数値): 最下位ビットから0が続く数(count trailing zeros)。2のべき乗値なら、log2(数値)と同じ値。

他には、@clz(最上位ビットから0が続く数)、@popCount(1の数)、@bswap(バイト単位で上下反転)、など。

キャストの明示

zigでは同一数値型種でビット数が増える場合のみ、暗黙的に拡大できます。
それ以外は、変換の仕方に応じたそれぞれのキャスト用ビルトイン関数を選んで用いる必要があります。

  • @truncate(変換型, 値): 変換型のビット数までの下位ビットだけを切りとるキャスト

シフト演算ではシフトする変数の型のビット数にフィットした数値型に切り詰める必要があります。
32は2の5乗なので、u32のシフト演算の右辺値では、型をu5に切り詰めることが必須となります。

キャストビルトイン関数は大量にありますが、以下は代表的なものです

  • @bitCast(変換型、 値): ビットレイアウトを保存するキャスト。ビットフィールドとしてf64u64に変換する場合など。
  • @ptrCast(変換ポインタ型, ポインタ値): ポインタの型変換
  • @intCast(変換整数型, 整数値): ビット数の違う整数値の型変換
  • @floatCast(変換浮動小数型, 浮動小数値): ビット数の違う浮動小数値の型変換

  • @floatToInt(変換整数型, 浮動小数値): 整数への数値変換

  • @intToFloat(変換不動小数型, 整数値): 浮動小数への数値変換

  • @ptrToInt(変換整数型, ポインタ値): ポインタアドレスの整数への変換

  • @intToPtr(変換ポインタ型, 整数値): ポインタアドレス値からポインタへの変換

  • @boolToInt(真偽値): 真偽値の整数(u1)への変換


3.6. FFT本体

以下のコード片は、ループ版FFTの実装コードです。

fft.zig(6)
fn fftc(t0: f64, n: u32, c: [*]CN, r: [*]CN) void {
    {
        const k: u32 = @ctz(n);
        var i: u32 = 0;
        while (i < n) : (i += 1) {
            r[i] = c[@inlineCall(revbit, k, i)];
        }
    }
    var t = t0;
    var nh: u32 = 1;
    while (nh < n) : (nh <<= 1) {
        t /= 2.0;
        const nh2 = nh << 1;
        var s: u32 = 0;
        while (s < n) : (s += nh2) {
            var i: u32 = 0;
            while (i < nh) : (i += 1) {
                const li = s + i;
                const ri = li + nh;
                const re = @inlineCall(CN.mul, r[ri], @inlineCall(CN.expi, t * @intToFloat(f64, i)));
                const l = r[li];
                r[li] = @inlineCall(CN.add, l, re);
                r[ri] = @inlineCall(CN.sub, l, re);
            }
        }
    }
}
pub export fn fft(n: u32, f: [*]CN, F: [*]CN) void {
    fftc(-2.0 * pi, n, f, F);
}
pub export fn ifft(n: u32, F: [*]CN, f: [*]CN) void {
    fftc(2.0 * pi, n, F, f);
    const nf64 = @intToFloat(f64, n);
    var i: u32 = 0;
    while (i < n) : (i += 1) {
        f[i] = f[i].rdiv(nf64);
    }
}

ポインタ型

zigの変数はメモリ領域で、その先頭アドレスを指すポインタ型があります。
値型へのポインタ型は、*u8など、型の頭に*をつけた型になります。

zigでは、ポインタ型の変数と、その元になった型の変数は、同じ記述でアクセスできるようになっています。

zigでは、配列も連続メモリ領域を表す値型です。C言語と違い、配列型と、配列へのポインタ型は明確に区別されます。

zig-0.4.0では、配列ポインタ型は、[*]要素型で表現します。
配列ポインタ型は、配列上の途中の要素も指すことができ、Cと同様にオフセットインデックス数を足し引きできます。

配列のポインタ型変数も、配列型変数と同じ記述で配列の要素へアクセスできます。

配列ポインタに関係するものとしては他に以下の型があります。

  • *[要素数]要素型: 配列全体へのポインタ。コンパイル時に要素数(.len)が確定している
  • []要素型: 実行時に要素数(.len)が得られるスライス

zigのスライスは、配列領域上の連続領域を指すzig組み込みのデータ構造で、先頭アドレスの配列ポインタ(.ptr)と要素数(.len)を持ちます。

まとめると、

  • const a: [12]u8 = []u8 {1} ** 12;: 配列変数。要素数はコンパイル時に確定
  • const b: *const [12]u8 = &a;: 配列全体へのポインタ変数。要素数はコンパイル時に確定
  • const c: []const u8 = &a;: スライス型。要素数は実行時に確定
  • const d: [*]const u8 = &a;: 配列へのポインタ変数。要素数を持たない

上3つの変数は、要素数をa.lenb.lenc.lenで取り出せるけど、dにはその手段がありません。

以下は、その他スライスや配列ポインタへ代入するための構文です。

  • const c0: []const u8 = a[0..];
  • const c1: []const u8 = b;
  • const c2: []const u8 = c[1..];
  • const c3: []const u8 = d[0..12];
  • const d1: [*]const u8 = b;
  • const d2: [*]const u8 = c.ptr;
  • const d3: [*]const u8 = d + 1;

配列のポインタからスライスを作る(c3)には、終点のインデックスの指定が必須です。

zigの変数は隠蔽不可能なブロックスコープ

zigの変数はブロックスコープですが、多くの言語と違い、ブロックの外で定義済みの変数と同名の変数をブロック内部で定義することができません

関数fftcの最初の、ビット反転したインデックスへコピーする部分では、変数iを使っています。
この部分を囲う{}を消すと、下のループ内でもiを使っているため、コンパイルエラーとなります。

zigのループ式とブロック式

zigには、forwhileのループがあります。

  • forループ: 配列等へのイテレータループ
  • whileループ: 条件ループ

whileループには、以下のバリエーションがあります。

  • while (条件式) {...} : C言語でのwhile (条件式) {...}相当
  • while (条件式) : (後処理式) {...}: C言語でのfor (; 条件式; 後処理式) {...} 相当
  • while (実行式) |変数| {...} : C言語でのwhile ((変数 = 実行式)) {...}相当

zigでは、whileでのループカウンタの変数はループ外で宣言する必要があります。

zigでは、forwhileもループアウト後に処理されるelse部で式をつけることが可能です。

ループは、式として結果値を持てます。ループ式ではelse部が必須で、値付きbreakにわたす値が式の値になります。

  • ループ式の最小例1: const v = while (cond) {break 1;} else 2;
  • ループ式の最小例2: const v = while (cond) {break 1;} else ret: {break :ret 2;};
  • (zigのif式: const v = if (cond) 1 else 2;)

この例はどちらも、condtrueならv1falseならv2になります。

最小例2でのelse部はブロックスタイルの構文ではなく、zigの式構文の一つであるブロック式です。
ブロック式の値は、ラベル付きbreakで渡す必要があり、このためにブロック式になにかラベルを付ける必要があります。

関数のインライン呼び出し

zigのfnにもinlineをつけることができ、この場合すべての呼び出し箇所が強制的にインライン化されます。
しかし、inlinefnは利用制限が付きます。たとえば、fnを変数や引数に渡して使うことができなくなります。
また、C言語と違い、zigでは、一つでもfnにインライン化できない状況があるとinlineがコンパイルエラーになります。

zigでは、定義ですべてインライン化を指定するのではなく、普通の関数を呼び出し側で強制インライン化を指定することが可能です。それがビルトイン@inlineCallです。


3.7. FFTテストコード

以下のコード片は、ループ版FFTの実装のテストコードです。

fft.zig(7)`
test "fft/ifft" {
    const warn = @import("std").debug.warn;
    const assert = @import("std").debug.assert;
    const abs = @import("std").math.fabs;
    const eps = 1e-15;

    const util = struct {
        fn warnCNs(n: u32, cns: [*]CN) void {
            var i: u32 = 0;
            while (i < n) : (i += 1) {
                warn("{} + {}i\n", cns[i].re, cns[i].im);
            }
        }
    };

    const n: u32 = 16;
    const v = [n]i32{ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 };
    var f: [n]CN = undefined;
    {
        var i: u32 = 0;
        while (i < n) : (i += 1) {
            f[i] = CN.rect(@intToFloat(f64, v[i]), 0.0);
        }
    }
    warn("\n[f]\n");
    util.warnCNs(n, &f);

    var F: [n]CN = undefined;
    var r: [n]CN = undefined;
    fft(n, &f, &F);
    ifft(n, &F, &r);

    warn("\n[F]\n");
    util.warnCNs(n, &F);
    warn("\n[r]\n");
    util.warnCNs(n, &r);

    {
        var i: u32 = 0;
        while (i < n) : (i += 1) {
            assert(abs(r[i].re - @intToFloat(f64, v[i])) < eps);
        }
    }
}

test内での関数定義は、struct内で行える

zig-0.4.0では、test部のトップレベルでfnは記述できませんが、struct内では関数が記述可能で、スタティックメソッド的に呼び出すことは可能でした。

ポインタ型への参照

zigで、値型からポインタを得る構文は&変数です。
C言語と違い配列変数は配列ポインタとして暗黙的には扱えず、明示的に&をつける必要があります。

ポインタ型から要素を参照する方法は、値型と同じです(a[i]a.m)。

ポインタ固有の式として、値全体のデリファレンスがあり、変数の後に.*を付与します(v = a.*a.* = v)。これはC言語でのポインタデリファレンス*ptr相当の意味です。

warnによるコンソール出力

標準ライブラリのwarnを使うと、フォーマット文字列を使って、コンソールに文字を(stderrへ)出力させることが可能です。
zig test実行でも出力されます。


3.8. zig test実行結果

このfft.zigに対して、zig test fft.zigを実行すると、以下の結果になります:

$ zig test fft.zig
Test 1/4 CN...OK
Test 2/4 CN array...OK
Test 3/4 revbit...OK
Test 4/4 fft/ifft...
[f]
1.0e+00 + 0.0e+00i
3.0e+00 + 0.0e+00i
4.0e+00 + 0.0e+00i
2.0e+00 + 0.0e+00i
5.0e+00 + 0.0e+00i
6.0e+00 + 0.0e+00i
2.0e+00 + 0.0e+00i
4.0e+00 + 0.0e+00i
0.0e+00 + 0.0e+00i
1.0e+00 + 0.0e+00i
3.0e+00 + 0.0e+00i
4.0e+00 + 0.0e+00i
5.0e+00 + 0.0e+00i
6.2e+01 + 0.0e+00i
2.0e+00 + 0.0e+00i
3.0e+00 + 0.0e+00i

[F]
1.07e+02 + 0.0e+00i
2.329589166141268e+01 + 5.1729855807372815e+01i
-5.35477272147525e+01 + 4.2961940777125584e+01i
-4.921391810443094e+01 + -2.567438445589562e+01i
3.612708057484687e-15 + -5.9e+01i
4.9799704542057846e+01 + -2.4260170893522517e+01i
3.554772721475249e+01 + 4.89619407771256e+01i
-1.9881678099039597e+01 + 5.314406936974591e+01i
-6.3e+01 + 0.0e+00i
-1.9881678099039586e+01 + -5.314406936974591e+01i
3.55477272147525e+01 + -4.8961940777125584e+01i
4.9799704542057846e+01 + 2.4260170893522528e+01i
-3.612708057484687e-15 + 5.9e+01i
-4.921391810443094e+01 + 2.567438445589561e+01i
-5.354772721475249e+01 + -4.29619407771256e+01i
2.329589166141269e+01 + -5.1729855807372815e+01i

[r]
1.0e+00 + 0.0e+00i
3.0e+00 + -3.497202527569243e-15i
3.999999999999999e+00 + -1.0143540619928351e-16i
1.9999999999999996e+00 + 8.465450562766819e-16i
5.0e+00 + 0.0e+00i
6.0e+00 + 0.0e+00i
2.0e+00 + 4.592425496802568e-17i
4.0e+00 + -7.077671781985373e-16i
0.0e+00 + 0.0e+00i
1.0e+00 + -5.551115123125783e-17i
3.000000000000001e+00 + 9.586896263232146e-18i
4.0e+00 + 5.134781488891349e-16i
5.0e+00 + 0.0e+00i
6.2e+01 + 3.552713678800501e-15i
2.0e+00 + 4.592425496802568e-17i
2.9999999999999996e+00 + -6.522560269672795e-16i
OK
All tests passed.
$

4. fft.zigを使った実行ファイルのzigコード

注: zigでは、コマンドやライブラリの起動処理は標準ライブラリ内で実装されています。
ここの記述はzig-0.4.0での標準ライブラリ実装をもとに行っています。この辺の仕様は、将来的に大きく変わる可能性があるでしょう。

まずコマンド実行時にzig標準ライブラリから呼び出されるmain関数の使い方だけを説明し、その後で@import("fft.zig")して、前述のFFT実装を呼び出すzigプログラムを説明します。


4.1. zigのmain関数オーバービュー

以下は、コマンドライン実行でのエントリーポイントmain関数で、環境変数、コマンド引数、exitコードを扱うだけのコード例です:

example-main.zig
pub fn main() noreturn {
    // environment variable
    const pathOptional: ?[]const u8 = @import("std").os.getEnvPosix("SHELL");
    const path: []const u8 = pathOptional orelse "";
    @import("std").debug.warn("SHELL: {}\n", path);

    // command args
    var count: u8 = 0;
    var args = @import("std").os.args();
    const total = while (args.nextPosix()) |arg| {
        @import("std").debug.warn("{}: {}\n", count, arg);
        count += 1;
    } else count;

    // exit with code: `echo $?`
    @import("std").os.exit(total);
}

main関数の型とexitコード

pubな引数なしmain関数がユーザーコードのエントリーポイントです。

mainの戻り値の型は、noreturn(最後名で実行されずに終了する)のほかに、u8(return値がexit codeになる)、void(exit codeは0)、!void(エラーかvoid)のどれかです。

このコードの場合では、中で使っているexitする標準ライブラリのos.exit()noreturnな関数であるため、mainnoreturnにしています。
os.exit()を使う場合、exitコードを引数(u8型)に渡します。

戻り値が!voidの場合、エラーならexitコードは1になります。

エラーセット型と、エラーユニオン型

zigのerror構文で定義するエラーセット型は、structenumのようなカスタムタイプの一つで、enumと似た列挙型となっています。例: const MyError = error {Fail, Dead,};

enumと違う部分は、エラーセット型では、以下のように、その要素であるエラーを集合的に扱う点です。

  • 全カスタムエラーセット型を受けるanyerrorの存在
  • 2つのエラーセット型の和集合のエラーセット型が作れる(const ErrorsC = ErrosA || ErrorsB;)
  • エラーセット型内のエラー名は、エラーセット型が違っても同名のものは同値扱いされる(例えば、MyError.Deadconst e: MyError = error.Dead;でもイニシャライズできる)
  • エラーユニオン型(!つき型)と、エラーユニオン専用構文(catch演算子、try演算子)が存在する

戻り値型での!u8や変数型でのMyError!u8のような、!がついた型は、zigのエラーユニオン型というものです。
エラーユニオン型は、値の型とerror型の双方を受けられる変数や戻り値の型です。
このエラーユニオン型を戻り値の型にすることで、zigでは、エラーもreturnさせる値になります。
戻り値型の!u8は左側のエラーセット型が関数実装コードから推論されるもので、実際には、MyError!u8等のエラーユニオン型になります。

エラーユニオン型から値の型の式にするには、zigではcatch二項演算子とtry単項演算子が使えます。

  • catch二項演算子: エラー時のフォールバック値を渡すことができます。例: const v: u8 = errorOrInt() catch |err| 10;
  • try単項演算子: エラーのときはそのエラーをreturnする、という式です。例: const v: u8 = try errorOrInt();

catch演算子のキャプチャ部|err|は、エラー値を使わない場合は省略できます。
try演算子を中で使う関数の戻り値は、エラーユニオン型である必要があります。

また、if式やswitch式の条件式にすることでも、エラーユニオン型からエラー時をelseとして振り分けることが可能です。

  • if例: const v: u8 = if (errorOrInt()) |n| n else |err| 0;
  • switch例: const v: u8 = switch (errorOrInt()) { n => n, else => 0, };

環境変数とオプショナル型

環境変数は標準ライブラリのos.getEnvPosix()で取り出せます。この関数の戻り値は、?[]const u8です。

この?が頭についた型は、zigのオプショナル型です。オプショナル型は、zigのnullか値、という型です。(このため、ポインタ型はオプショナル型でなければnullにできないことになります)

エラーユニオン型と同様、if式やswitch式で、nullのときはelseでフォールバック値を与えることで、値の型にすることができます。
また、エラーユニオンでのcatch二項演算子と同じ位置づけで、オプショナル型用にorelse二項演算子が備わっています。

  • 例: const v: u8 = nullOrInt() orelse 0;

const v: u8 = nullOrInt().?.?をつけることでも、(error型でのtry単項演算子のように)強制的に値型にできます。
しかしtryと違い、エラーやnullreturnされるわけではなく、zig-0.4.0ではトラップ不可能な強制終了となるようです。

コマンド引数

標準ライブラリのos.args()でコマンドライン引数を取り出せます。
これはイテレータであり、nextPosix()メソッドを使って、?[]const u8型のオプショナル型文字列として次々と取り出すことができます。

イテレータからnullになるまで逐次取り出しをするには、条件値をキャプチャするwhileループを使います。nullになればelse部に入ります。

バイナリにした場合コマンド引数の0番目はコマンド名になり、引数はその次になります。

zig runでの場合、zig run example-main.zig -- a b c--の後にコマンド引数を渡せます。zig-0.4.0では、この場合では、引数は0番目から始まります。


4.2. fft.zig@importして使うmainのzigコード

以下は、テストで用いたデータでFFT/IFFTの結果を表示させ、続いて100万(2の20乗)要素のFFTとIFFTを実行させたベンチマーク時間を表示させるプログラムです。

fft-bench-zig.zig
const heap = @import("std").heap;
const warn = @import("std").debug.warn;
const sin = @import("std").math.sin;
const milliTimestamp = @import("std").os.time.milliTimestamp;

const CN = @import("./fft.zig").CN;
const fft = @import("./fft.zig").fft;
const ifft = @import("./fft.zig").ifft;

fn warnCNs(n: u32, cns: [*]CN) void {
    var i: u32 = 0;
    while (i < n) : (i += 1) {
        warn("{} + {}i\n", cns[i].re, cns[i].im);
    }
}

pub fn main() !void {
    { //example
        const n: u32 = 16;
        const v = [n]i32{ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 };
        var f: [n]CN = undefined;
        {
            var i: u32 = 0;
            while (i < n) : (i += 1) {
                f[i] = CN.rect(@intToFloat(f64, v[i]), 0.0);
            }
        }
        warn("\n[f]\n");
        warnCNs(n, &f);

        var F: [n]CN = undefined;
        var r: [n]CN = undefined;
        fft(n, &f, &F);
        ifft(n, &F, &r);

        warn("\n[F]\n");
        warnCNs(n, &F);
        warn("\n[r]\n");
        warnCNs(n, &r);
    }

    { //benchmark
        //NOTE: allocators in std will be changed on 0.5.0
        var direct_allocator = heap.DirectAllocator.init();
        var arena = heap.ArenaAllocator.init(&direct_allocator.allocator);
        const allocator = &arena.allocator;
        defer direct_allocator.deinit();
        defer arena.deinit();

        const n: u32 = 1024 * 1024;
        var f = try allocator.create([n]CN);
        {
            var i: u32 = 0;
            while (i < n) : (i += 1) {
                const if64 = @intToFloat(f64, i);
                f[i] = CN.rect(sin(if64) * if64, 0.0);
            }
        }

        var F = try allocator.create([n]CN);
        var r = try allocator.create([n]CN);

        const start = milliTimestamp();
        fft(n, f, F);
        ifft(n, F, r);
        const stop = milliTimestamp();
        warn("fft-ifft: {}ms\n", stop  -start);
    }
}

zigファイルからの@import

自分で作ったzigソースファイルも、標準ライブラリと同様に@importしてpub属性の変数や関数を利用できます。

実際、標準ライブラリも、zigソースファイルとして存在します。
インストールディレクトリのlib/zig/std/以下に展開されていて、実装コードを確認できます。

巨大メモリの利用

zig言語では、基本、変数はスタックメモリ(スタティックメモリ)の領域を使います。
大きい配列を使う場合は、実行時にOSのスタックリミットを超えてしまうので、プログラムではヒープなどのスタック外のメモリ領域を使用する必要があります。

標準ライブラリでは、ヒープメモリを扱うメモリアロケータが用意されています。以下は、zig-0.4.0でのアロケータ実装です:

  • heap.DirectAllocator: ヒープメモリを確保するアロケータ(init, deinit, alloc, shrink, realloc)
  • heap.ArenaAllocator: 他のアロケータを使ってメモリプールを構成し、これまで確保したメモリをdeinitで一括開放するアロケータ
  • mem.Allocator: 型指定でメモリを割り当てるインタフェース(create, destroy)

zig-0.4.0では、heapのアロケータは、mem.Allocatorインタフェースをメンバー.allocatorとして持っています(このコードではarena.allocator)。

注意点は、mem.Allocatorcreateの引数で渡すのは値型ですが、戻り値はその値型へのポインタ型(のエラーユニオン型)になっているところです。

  • allocator.create([1048576]CN)の戻り値の型は、!*[1048576]CN
  • var f = try allocator.create([1048576]CN);aの型は*[1048576]CN
  • f: *[1048576]CNは、[*]CNの変数(コードではfft()の第二引数)へ明示キャストせずに代入できる

defer

ブロック脱出時に実行されるコードを、前もって記述するのがdefer文です。
同一ブロックに複数のdeferがある場合は、後ろ側から順に実行されます。
deferを使って近い場所に書くことで、初期化処理と、その初期化に対応する終了処理を、ペアとして記述することが可能になります。

似たようなものにerrdeferがあります。エラーがreturnされるときのブロック脱出時に実行されるdeferです。
エラーreturnでのブロック脱出時には、errdeferdeferの混ざった並びでの後ろから順に実行されます。


4.3. 実行ファイルのビルド

このfft-bench-zig.zigから、以下のコマンドラインで、実行ファイルfft-bench-zigが生成できます。

$ zig build-exe --release-fast fft-bench-zig.zig

ビルドしなくても、zig run fft-bench-zig.zigでコードを実行させることも可能です。


5. fft.zigをWebAseemblyモジュールへコンパイルしてJavaScriptで利用する

Rustなど普通のプログラミング言語で書いたコードからWebAssemblyモジュールを生成する場合、そのwasmモジュールを使うためには、そのプログラミング言語のランタイムライブラリ実装、とりわけヒープメモリ関係の機能に配慮する必要があります。そのwasmモジュールのimportsやexportsで、その言語でのメモリ操作等のためのメソッドを持ち、その使い方に合わせて実装、利用する必要があります。

しかしzigプログラムからwasmモジュールを作ると、言語ランタイム関係のメソッドが一切外部に出ないいwasmモジュールを生成します。この一番大きな違いは、zigがヒープメモリを基本的には使用しないことに由来します(C言語も基本はそなってますが、ヒープ管理を持つstdlib.hを一切使用しないで書くことはより困難)。

先に実装したfft.zigでは、`fftifft関数をexportしており、そのままwasmファイルへコンパイルすることが可能となっています。

以下のコマンドラインでfft.wasmを生成できます。

 $ zig build-exe --release-fast -target wasm32-freestanding --name fft.wasm fft.zig

5.1. node.jsでfft.wasmを使用するJavaScriptコード

以下のJavaScriptコードは、このzigコードから生成したwasmファイルをロードして使うプログラムfft-bench-nodejs.jsです。
先のfft-bench-zig.zigと同様の、例のデータでのFFT/IFFT変換表示し、そのあとで100万要素のデータをFFT/IFFTした実行時間を出すベンチマークとなっています。

fft-bench-nodejs.js
const fs = require("fs").promises;

(async function () {
  const buf = await fs.readFile("./fft.wasm");
  const {instance} = await WebAssembly.instantiate(buf, {});
  const {__wasm_call_ctors, memory, fft, ifft} = instance.exports;
  __wasm_call_ctors();
  memory.grow(1);

  {// example
    const N = 16, fofs = 0, Fofs = N * 2 * 8, rofs = N * 4 * 8;
    const f = new Float64Array(memory.buffer, fofs, N * 2);
    const F = new Float64Array(memory.buffer, Fofs, N * 2);
    const r = new Float64Array(memory.buffer, rofs, N * 2);

    const fr0 = [1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2,3];
    fr0.forEach((v, i) => {
      [f[i * 2], f[i * 2 + 1]] = [v, 0.0];
    });

    fft(N, fofs, Fofs);
    ifft(N, Fofs, rofs);

    console.log(`[fft]`);
    for (let i = 0; i < N; i++) {
      console.log([F[i * 2], F[i * 2 + 1]]);
    }

    console.log(`[ifft]`);
    for (let i = 0; i < N; i++) {
      console.log([r[i * 2], r[i * 2 + 1]]);
    }
  }

  { // benchmark
    const N = 1024 * 1024;
    const fr0 = [...Array(N).keys()].map(i => Math.sin(i) * i);
    const f0 = fr0.map(n => [n, 0]);

    const BN = N * 2 * 8 * 3, fofs = 0, Fofs = N * 2 * 8, rofs = N * 4 * 8;
    while (memory.buffer.byteLength < BN) memory.grow(1);
    const f = new Float64Array(memory.buffer, fofs, N * 2);
    const F = new Float64Array(memory.buffer, Fofs, N * 2);
    const r = new Float64Array(memory.buffer, rofs, N * 2);
    fr0.forEach((v, i) => {
      [f[i * 2], f[i * 2 + 1]] = [v, 0.0];
    });

    console.time(`fft-ifft`);
    fft(N, fofs, Fofs);
    ifft(N, Fofs, rofs);
    console.timeEnd(`fft-ifft`);
  }
})().catch(console.error);

生成したfft.wasmとこのfft-bench-nodejs.jsを同一ディレクトリに置いて、node fft-bench-node.jsで実行できます。

zigで生成したwasmモジュールの初期化

zigランタイムには外部からimportさせなくてはいけない機能は存在しないので、await WebAssembly.instantiate(buf, {})で作ります。

exportsには、コードでexportした関数以外に、__wasm_call_ctorsmemmoryが入っています。

__wasm_call_ctorsは関数で、もしzigコードに初期化処理が含まれていれば、この__wasm_call_ctors()を呼び出すことで初期化処理を実行します。

memoryは、WebAssemblyのMemoryオブジェクトで、初期値では空です。
コード内でメモリを使うなら、memory.grow()メソッドを呼び出し、メモリ領域を確保する必要があります。
引数は拡大するメモリページ数で、1ページ64KBです。

WebAssemblyでのメモリ

exportsにあるmemory.bufferは、Typed ArrayのArrayBufferオブジェクトです。
JavaScript側からは、このmemory.bufferFloat64Arrayなどでラップし、値を入れたり、参照したりします。

ポインタはアドレス値であり、WebAssemblyのメモリのアドレス値はArrayBufferとしてのmemory.bufferのバイトオフセット値を使います。
zigには数値型のバイト数ごとにアラインメントがあるので注意です。
f64なら、アドレス値は8の倍数である必要があり、Float64Arrayコンストラクタの第二引数はこの8の倍数値を使うようにします。

参考: importsを受け付けるwasmを作る場合

zigは言語ランタイムを外部モジュールで与える必要のないwasmを作るのであって、自分で用意した外部のモジュールをimportsさせてwasm内で使うことが不可能なわけではありません。

外部で実装した関数をimportsに与えて使いたい場合、extern fn sin(x: f64) f64;のようなextern関数プロトタイプを宣言し、コード内で使うことで可能です。
zig-0.4.0ではwasmファイル内での外部モジュール名は"env"固定で、WebAssembly.instantiate(buf, {env: {sin: Math.sin}})として与えることで利用できます。

WebAssembly用コードのexport制限

WebAssemblyとしてJavaScript側へexportできる関数にはより多くの制限がかかります。
たとえば、zig-0.4.0では、CN.rect()のような戻り値がstruct型の関数はこの制限にかかります。
この制限にかかるexportが1つでもあるとwasmに変換できなくなるので、zigコードでexportをつけるのは注意が必要です。

対策としては、pubのみする実装コードのzigファイルと、実装のzigファイルを@importしてターゲットごとに適切なexportをつけるだけのzigファイルとに分ける、という手段が考えられます。

6. fft.zigをオブジェクトファイルにしてC言語から利用する

zigコードは、zig build-objコマンドでC言語から利用できるオブジェクトファイルに変換できます。オブジェクトファイルに対応したCヘッダファイルも同時に生成されます。

ここでは、fft.zigを使うC言語プログラムを作って、コンパイルして実行ファイルを生成します。

6.1 オブジェクトファイルおよびCヘッダファイル生成

以下のコマンドラインで、fft.zigから、オブジェクトファイルfft.oとヘッダファイルfft.hを生成します:

$ zig build-obj --release-fast fft.zig

生成されたヘッダファイルfft.hは、以下の内容になっています。

fft.h
#ifndef FFT_H
#define FFT_H

#include <stdint.h>

#ifdef __cplusplus
#define FFT_EXTERN_C extern "C"
#else
#define FFT_EXTERN_C
#endif

#if defined(_WIN32)
#define FFT_EXPORT FFT_EXTERN_C __declspec(dllimport)
#else
#define FFT_EXPORT FFT_EXTERN_C __attribute__((visibility ("default")))
#endif

struct CN {
    double re;
    double im;
};

FFT_EXPORT void fft(uint32_t n, struct CN * f, struct CN * F);
FFT_EXPORT void ifft(uint32_t n, struct CN * F, struct CN * f);

#endif

このヘッダファイルfft.hに、zigコードでexportした関数とstructが入っていることが確認できます。


6.2 FFT実装を呼び出すC言語プログラム

以下は、テストで使ったものと同じデータ例でFFT/IFFTを実行して表示し、続いて1000万要素のデータをFFT/IFFT実行させるmain関数のC11コードfft-bench-c.cです。

fft-bench-c.c
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <time.h>
#include "./fft.h"

int main() {
  {// example
    const int n = 16;
    const int v[16] = {1,3,4,2, 5,6,2,4, 0,1,3,4, 5,62,2,3};
    struct CN f[n], F[n], r[n];
    for (size_t i = 0; i < n; i++) f[i] = (struct CN) {.re = v[i], .im = 0.0};

    puts("[f]");
    for (size_t i = 0; i < n; i++) printf("%f + %fi\n", f[i].re, f[i].im);

    fft(n, f, F);
    ifft(n, F, r);

    puts("[F]");
    for (size_t i = 0; i < n; i++) printf("%f + %fi\n", F[i].re, F[i].im);
    puts("[r]");
    for (size_t i = 0; i < n; i++) printf("%f + %fi\n", r[i].re, r[i].im);
  }

  {// benchmark
    const int n = 1024 * 1024;
    struct CN * f = calloc(n, sizeof (struct CN));
    for (size_t i = 0; i < n; i++) {
      f[i] = (struct CN) {.re = sin(i) * i, .im = 0.0};
    }

    struct CN * F = calloc(n, sizeof (struct CN));
    struct CN * r = calloc(n, sizeof (struct CN));
    fft(n, f, F);
    ifft(n, F, r);
    free(f);
    free(F);
    free(r);
  }
  return 0;
}

zigで実装されているからと言って構造体や関数を使う場合、C側で特別なことを事を行う必要はありません

6.3. ビルド

実行ファイルfft-bench-cを生成するコマンドラインは以下のとおりです(Cコンパイラ実装はclangかgccを想定しています):

$ cc -Wall -Wextra -std=c11 -pedantic -O3 -o fft-bench-c fft-bench-c.c fft.o

(このオプションは、コードを拡張なしのC11標準そのもので解釈させることを指定するものです)

7. C言語で記述したオブジェクトファイルをzigプログラムから読み込む

fft-bench-c.cのときとは逆に、C言語で書かれたオブジェクトファイルを、そのCヘッダファイルをつかってzigプログラムで取り込むことも可能です。

ここでは、C11コードfft-c.cでFFTを実装し、その実装をzigから使うためのCヘッダファイルfft-c.hを記述します。
そして、fft-bench-zig.zigと同内容の事を行うfft-bench-cimport.zigで、fft-c.hを読み込んで実装します。

7.1. C11によるFFT実装プログラム

以下は、C11仕様での複素数double complexを使用したFFT実装fft-c.cです。

fft-c.c
#include <complex.h> // complex, I, cexp
#include <math.h> // M_PI
#include <stdbool.h> // bool
#include <stddef.h> // size_t
#include <stdint.h> // uint64_t

typedef double complex CN;

static inline uint32_t reverse_bit(uint32_t s0) {
  uint32_t s1 = ((s0 & 0xaaaaaaaa) >> 1) | ((s0 & 0x55555555) << 1);
  uint32_t s2 = ((s1 & 0xcccccccc) >> 2) | ((s1 & 0x33333333) << 2);
  uint32_t s3 = ((s2 & 0xf0f0f0f0) >> 4) | ((s2 & 0x0f0f0f0f) << 4);
  uint32_t s4 = ((s3 & 0xff00ff00) >> 8) | ((s3 & 0x00ff00ff) << 8);
  return ((s4 & 0xffff0000) >> 16) | ((s4 & 0x0000ffff) << 16);
}

static inline uint32_t rev_bit(uint32_t k, uint32_t n) {
  return reverse_bit(n) >> (8 * sizeof (uint32_t) - k);
}

static inline uint32_t trailing_zeros(uint32_t n) {
  uint32_t k = 0, s = n;
  if (!(s & 0x0000ffff)) {k += 16; s >>= 16;}
  if (!(s & 0x000000ff)) {k += 8; s >>= 8;}
  if (!(s & 0x0000000f)) {k += 4; s >>= 4;}
  if (!(s & 0x00000003)) {k += 2; s >>= 2;}
  return (s & 1) ? k : k + 1;
}

static void fftc(double t, uint32_t N, const CN c[N], CN ret[N]) {
  uint32_t k = trailing_zeros(N);
  for (uint32_t i = 0; i < N; i++) ret[i] = c[rev_bit(k, i)];
  for (uint32_t Nh = 1; Nh < N; Nh <<= 1) {
    t *= 0.5;
    for (uint32_t s = 0; s < N; s += Nh << 1) {
      for (uint32_t i = 0; i < Nh; i++) { //NOTE: s-outside/i-inside is faster
        uint32_t li = s + i;
        uint32_t ri = li + Nh;
        CN re = ret[ri] * cexp(t * i * I);
        CN l = ret[li];
        ret[li] = l + re;
        ret[ri] = l - re;
      }
    }
  }
}

extern CN rect(double re, double im) {
  return re + im * I;
}
extern void fft(uint32_t N, const CN f[N], CN F[N]) {
  fftc(-2.0 * M_PI, N, f, F);
}
extern void ifft(uint32_t N, const CN F[N], CN f[N]) {
  fftc(2.0 * M_PI, N, F, f);
  for (size_t i = 0; i < N; i++) f[i] /= N;
}

アルゴリズムはfft.zigと一緒です。

またdouble complexは実数部と虚数部のdouble値が連続して配置される値なので、fft.zigCNと同じメモリレイアウトになります。
そして、zig上でdouble complexへの操作をしなくてすむよう、double値から複素数値を作るコンストラクタ関数rectも用意します。

7.2. zigで使うためのCヘッダファイル

Cヘッダファイルfft-c.hは、zigのbuild-objが書き出すヘッダファイルのやり方に合わせた内容にしてあります。

fft-c.h
#ifndef FFT_C_H
#define FFT_C_H
#include <stdint.h>

#ifdef __cplusplus
#define FFT_C_EXTERN_C extern "C"
#else
#define FFT_C_EXTERN_C
#endif

#if defined(_WIN32)
#define FFT_C_EXPORT FFT_C_EXTERN_C __declspec(dllimport)
#else
#define FFT_C_EXPORT FFT_C_EXTERN_C __attribute__((visibility ("default")))
#endif

struct CN {
    double re;
    double im;
};

FFT_C_EXPORT struct CN rect(double re, double im);
FFT_C_EXPORT void fft(uint32_t n, struct CN f[], struct CN F[]);
FFT_C_EXPORT void ifft(uint32_t n, struct CN F[], struct CN f[]);

#endif

zigには複素数型に対応する型がないので、zigプログラムで読み込ませるCヘッダファイルではdouble2つを持つstructで代用します。

7.3. Cヘッダファイルを取り込むzigプログラム

先のfft-bench-zig.zigと同様の、例のデータでのFFT/IFFT変換表示し、そのあとで100万要素のデータをFFT/IFFTした実行時間を出すベンチマークとなっています。

fft-bench-cimport.zig
const heap = @import("std").heap;
const warn = @import("std").debug.warn;
const sin = @import("std").math.sin;
const milliTimestamp = @import("std").os.time.milliTimestamp;

const libfft = @cImport({
    @cInclude("fft-c.h");
});
const CN = libfft.CN;
const rect = libfft.rect;
const fft = libfft.fft;
const ifft = libfft.ifft;

fn warnCNs(n: u32, cns: [*]CN) void {
    var i: u32 = 0;
    while (i < n) : (i += 1) {
        warn("{} + {}i\n", cns[i].re, cns[i].im);
    }
}

pub fn main() !void {
    { //example
        const n: u32 = 16;
        const v = [n]i32{ 1, 3, 4, 2, 5, 6, 2, 4, 0, 1, 3, 4, 5, 62, 2, 3 };
        var f: [n]CN = undefined;
        {
            var i: u32 = 0;
            while (i < n) : (i += 1) {
                f[i] = rect(@intToFloat(f64, v[i]), 0.0);
            }
        }
        warn("\n[f]\n");
        warnCNs(n, &f);

        var F: [n]CN = undefined;
        var r: [n]CN = undefined;
        fft(n, @ptrCast([*c]CN, &f), @ptrCast([*c]CN, &F));
        ifft(n, @ptrCast([*c]CN, &F), @ptrCast([*c]CN, &r));

        warn("\n[F]\n");
        warnCNs(n, &F);
        warn("\n[r]\n");
        warnCNs(n, &r);
    }

    { //benchmark
        //NOTE: allocators in std will be changed on 0.5.0
        var direct_allocator = heap.DirectAllocator.init();
        var arena = heap.ArenaAllocator.init(&direct_allocator.allocator);
        const allocator = &arena.allocator;
        defer direct_allocator.deinit();
        defer arena.deinit();

        const n: u32 = 1024 * 1024;
        var f: [*]CN = try allocator.create([n]CN);
        {
            var i: u32 = 0;
            while (i < n) : (i += 1) {
                const if64 = @intToFloat(f64, i);
                f[i] = rect(sin(if64) * if64, 0.0);
            }
        }

        var F: [*]CN = try allocator.create([n]CN);
        var r: [*]CN = try allocator.create([n]CN);

        const start = milliTimestamp();
        fft(n, f, F);
        ifft(n, F, r);
        const stop = milliTimestamp();
        warn("fft-ifft: {}ms\n", stop - start);
    }
}

@cImportビルトイン

@cImportビルトインを使うことで、Cヘッダからzigのstructや関数として取り込むことができます。
@cImportの中で、@cIncludeビルトインによって、解釈させるヘッダファイル名を指定します。

Cポインタ型

zigのポインタ型は値型のポインタ型と配列へのポインタ型が別れていますが、Cのポインタ型ではその区別がありません。
このため@cImportした関数の引数や戻り値でポインタ型を使っている場合、zig上ではどちらの型にも対応可能な[*c]値型で記述するCポインタ型として解釈されます。

ここでは、Cヘッダからの関数fftは、fft(n: i32, f: [*c]CN, F: [*c]CN)という型になります。

このCポインタ型[*c]CNは、*CN[*]CNからは、キャストなしで割り当てられます(コードの後半)。
しかしzig-0.4.0では、[*]CNへキャストなしで割り当てられる&f: *[16]CNからは、@ptrCastによる明示キャストが必要になります(コードの前半)。

  • *[16]CN =キャスト不要=> [*]CN =キャスト不要=> [*c]CN
  • *[16]CN =要@ptrCast=> [*c]CN

参考: @cImportせずにオブジェクトファイルを使う方法extern fn

@cImportで何をやっているかは、zig translate-c fft-c.hコマンドを実行して、そのアウトプットのzigコードをみるとわかります。ただし、#includeした標準ライブラリの内容も全部入るので大量に出てきます。

必要な部分だけ取り出すと、

fft-c.h.zig
pub const CN = extern struct {
    re: f64,
    im: f64,
};
pub extern "./fft-c.o" fn rect(re; f64, im: f64) CN;
pub extern "./fft-c.o" fn fft(n: u32, f: [*c]CN, F: [*c]CN) void;
pub extern "./fft-c.o" fn ifft(n: u32, F: [*c]CN, f: [*c]CN) void;

と同じ内容になります。この内容を埋め込めば、fft-c.hやインクルードパスを加えるzig buildオプションの-isystem .は不要です。
また、externの直後にオブジェクトファイルパス(./は必須)を埋め込むと--object fft-c.oも不要になります。
(./がないとライブラリとして解決しようとします("fft-c"-lfft-cとして扱われる)。)

zigでは、externな関数プロトタイプがあると、ライブラリやオブジェクトファイルにある同名の関数を使用する、という宣言になります。

7.4. ビルド

以下のコマンドラインでビルドできます。

$ cc -Wall -Wextra -std=c11 -pedantic -O3 -c fft-c.c
$ zig build-exe --release-fast --object fft-c.o -isystem . fft-bench-cimport.zig

オプション--object fft-c.oでオブジェクトファイルを指定し、-isystem .@cInclude でヘッダファイルを読み込むディレクトリを指定しています。

この場合でも、zig run --object fft.o -isystem . fft-bench-cimport.zigで実行ファイルを作らずに実行させることができます。

8. build.zigによるビルド記述

zigではzig buildコマンドで、makeなどの外部ビルドシステムを使わずに、クロスプラットフォームビルドできるようにする方向を目指しているようです。

zig build実行でのビルド内容を記述するbuild.zigには、pubbuild関数を記述します。

この中で、ビルドで実行する内容をステップのグラフとして定義します。
各種コンパイルやコマンド実行1つが、それぞれ1つのステップとなります。
各ステップには、事前にこなす必要のあるステップの依存関係を指定できます。

また、ビルドターゲットにあたる、名前付きステップがあり、そこに各コンパイルやコマンド実行のステップを順に加えていけます。
ビルドシステムとしてデフォルトのステップがあり、ステップ名指定なしのzig buildで実行される内容を記述するためのものです。(名前付きステップのうち、いくつかはデフォルトステップに加えることになるでしょう。)

ただし、zig-0.4.0現在では、build.zigで記述可能なことが、完全にはそろっていないようです。たとえば、Cファイルのコンパイルやファイルの削除は、実行環境にあるコマンドを引数を指定して直接実行させる必要があります。
(0.4.0のドキュメントにはaddCExecutableというステップが記載されていますが、実際にはzig-0.4.0には存在しません)

build.zig
const builtin = @import("builtin");
const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    { //[case: WebAssembly wasm] build fft.wasm
        const wasmStep = b.step("fft.wasm", "build fft.wasm");
        const wasm = b.addExecutable("fft.wasm", "fft.zig");
        wasm.setTarget(builtin.Arch.wasm32, builtin.Os.freestanding, builtin.Abi.musl);
        wasm.setBuildMode(builtin.Mode.ReleaseFast);
        wasm.setOutputDir(".");
        wasmStep.dependOn(&wasm.step);
        b.default_step.dependOn(wasmStep);
    }
    { //[case: zig code only] build fft-bench-zig command
        const exeStep = b.step("fft-bench-zig", "build fft-bench-zig command");
        const zigExe = b.addExecutable("fft-bench-zig", "fft-bench-zig.zig");
        zigExe.setBuildMode(builtin.Mode.ReleaseFast);
        zigExe.setOutputDir(".");
        exeStep.dependOn(&zigExe.step);
        b.default_step.dependOn(exeStep);
    }
    { //[case: c exe with zig lib] build fft-bench-c command
        // build fft.h and fft.o
        const objStep = b.step("fft.o", "build fft.h and fft.o");
        const fftObj = b.addObject("fft", "fft.zig");
        fftObj.setBuildMode(builtin.Mode.ReleaseFast);
        fftObj.setOutputDir(".");
        objStep.dependOn(&fftObj.step);

        // build fft-bench-c command (cc: expected clang or gcc)
        const cExeStep = b.step("fft-bench-c", "build fft-bench-c command");
        cExeStep.dependOn(objStep);
        const cExe = b.addSystemCommand([][]const u8{
            "cc", "-Wall", "-Wextra", "-O3", "-o", "fft-bench-c", "fft-bench-c.c", "fft.o",
        });
        cExeStep.dependOn(&cExe.step);
        b.default_step.dependOn(cExeStep);
    }
    { //[case: zig exe with c lib] build fft-bench-cimport command
        const exeStep = b.step("fft-bench-cimport", "build fft-bench-cimport command");

        const cLibStep = b.step("fft-c", "build fft-c.o");
        const cLibObj = b.addSystemCommand([][]const u8{
            "cc", "-Wall", "-Wextra", "-std=c11", "-pedantic", "-O3", "-c", "fft-c.c",
        });
        cLibStep.dependOn(&cLibObj.step);
        exeStep.dependOn(cLibStep);

        const mainObj = b.addExecutable("fft-bench-cimport", "fft-bench-cimport.zig");
        mainObj.addIncludeDir("."); //NOTE: same as `-isystem .`
        mainObj.setBuildMode(builtin.Mode.ReleaseFast);
        mainObj.addObjectFile("fft-c.o");
        mainObj.setOutputDir(".");
        exeStep.dependOn(&mainObj.step);

        b.default_step.dependOn(exeStep);
    }

    { // clean and dist-clean
        const cleanStep = b.step("clean", "clean-up intermediate iles");
        const rm1 = b.addSystemCommand([][]const u8{
            "rm", "-f", "fft.wasm.o", "fft-bench-zig.o", "fft-bench-c.o", "fft-c.o", "fft-bench-cimport.o",
        });
        cleanStep.dependOn(&rm1.step);
        const rmDir = b.addRemoveDirTree("zig-cache");
        cleanStep.dependOn(&rmDir.step);

        const distCleanStep = b.step("dist-clean", "clean-up build generated files");
        distCleanStep.dependOn(cleanStep);
        const rm2 = b.addSystemCommand([][]const u8{
            "rm", "-f", "fft.wasm", "fft-bench-zig", "fft-bench-c", "fft.o", "fft.h", "fft-bench-cimport",
        });
        distCleanStep.dependOn(&rm2.step);
    }
}

この中では、6つの名前付きステップを作っています。ブロックごとに上から:

  • fft.wasm: wasmファイル生成のためのステップ
  • fft-bench-zig: zigファイルのみから実行ファイル生成するためのステップ
  • fft-bench-c: zigファイルをオブジェクトファイルにし、それを使うcファイルをメインとする実行ファイルを作成するためのステップ
  • fft-bench-cimport: cファイルをオブジェクトファイルにし、それを使うzigファイルをメインとする実行ファイルを作成するためのステップ
  • cleandist-clean: ビルドで生成したファイルの削除を行うステップ

cleandist-clean以外は、デフォルトステップに追加してあります。

どの名前付きステップが使えるかは、zig build --helpで一覧が表示されます。

今後のバージョンアップで大きく変わる部分だろうとは思いますが、ビルドする4つそれぞれのケースでビルド記述としてどのように構成すればよいかの参考になるかもしれません。

9. ベンチマーク結果

最後に生成した実行ファイルのベンチマークの結果を載せておきます。後半の100万要素のFFT/IFFTにかかった時間です。

command time (ms) runtime
node fft-bench-nodejs.js 945.458ms node-12.6.0
./fft-bench-zig 856ms zig-0.4.0
./fft-bench-cimport 802ms Apple LLVM version 10.0.1 (clang-1001.0.46.4)

また、wasmでの参考まで、「2019年のWebAssembly事情」で行った同内容でのFFT/IFFTベンチマークの、同一環境での実行結果も並べておきます。

command time (ms) runtime
node call-js-fft.js 1074.170ms node-12.6.0
node --experimental-modules call-fft-dynamic.js (fast) 1117.932ms node-12.6.0
node --experimental-modules call-fft-dynamic.js (slow) 2177.661ms node-12.6.0

zigによる数学関数埋め込みでsincosimportして呼び出す必要がないのは、v8のmath-intrinsics最適化を加えても、実行速度の面で大きいのかもしれません(zigベースのwasmでは、sinやcosの関数自体インライン化されていた)。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
96
Help us understand the problem. What are the problem?