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
) - ビット数指定の数値型、式としての
if
やswitch
やループ構文、ブロック脱出時実行の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用のモードもあるようです:
- https://github.com/ziglang/zig.vim
- https://github.com/ziglang/vscode-zig
- https://github.com/ziglang/sublime-zig-language
2. zig
コマンドの基本
開発ツールは、すべてzig
コマンドのサブコマンドとして提供されています。
zigファイル実行系コマンド:
-
zig run xxx.zig
: ソースファイルxxx.zig
のmain
関数を(バイナリ生成せずに)実行します -
zig test xxx.zig
: ソースファイルxxx.zig
に記述されたtest
構文内のコードを順に実行し、テスト結果のサマリを表示します -
zig build
: ビルド記述build.zig
に記述されたビルド処理を実行します
zigファイルコンパイル系コマンド:
-
zig build-exe xxx.zig
: zigファイル(xxx.zig
)から、**各種ターゲットのバイナリ(xxx
、xxx.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.so
、libxxx.dylib
、xxx.dll
とxxx.pdb
等)などを生成します
ユーティリティ系コマンド:
-
zig init-exe
: このディレクトリ以下に、実行ファイル用のプロジェクトテンプレート(build.zig
とsrc/main.zig
)を生成します -
zig init-lib
: このディレクトリ以下に、スタティックライブラリ用のプロジェクトテンプレート(build.zig
とsrc/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での実行ファイル名(この場合、
fft
やfft.exe
)になります
- デフォルトのアウトプットファイル名は、メインのzigファイル名をベースにした、ターゲットOSでの実行ファイル名(この場合、
3. zigプログラム: FFT実装
言語のもつ各種機能に触れられる程度には複雑なコードの例として、高速フーリエ変換FFTのループ実装を採用し、これをzigで記述します。
main
関数のないライブラリとしてfft.zig
を記述し、この中でtest
構文でテストを記述し、zig test fft.zig
でテスト実行で動作確認します。
fft.zig
のコード全体にはテストコードを含めて200行弱になります。ここではコードを7分割し、そのコード片を上から順に切り出し、そのコード片ごとに含むzig言語の機能や特徴を説明していきます。
3.1. 標準ライブラリ読み込み
以下のコード片は、標準ライブラリから円周率値、sin/cos関数の読み込みをするコードです:
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.0
のconst
変数へは、const
型のポインタvar ptr: *const f64 = &a
でなければコンパイルエラーとなります。
型への名前付けのconst
変数
const
変数は、struct
/union
/enum
/error
などの型(type
型変数)への別名付けにも使われます。
zigの型チェックは、型名での一致ではなく、構造の一致で判断します。このため、別名の型の変数と元の名前の型の変数との間で代入可能です。
のコードでは標準ライブラリの関数や変数をconst
で変数名を割り当てて使用していますが、@import("std")
は式であり、コード内のどこでも使えます。
また、zig-0.4.0では、sin
やcos
は標準ライブラリ提供の関数となっていますが、次のバージョンではビルトインに移籍し@sin
、@cos
として使用することになるようです。
3.2. 複素数struct
の定義
以下のコード片は、FFTで扱う複素数構造体と、そのコンストラクタやメソッドの宣言です:
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
属性は、(標準ライブラリのpi
やsin
のように)他のzigファイルから**@import
されたときに利用させる**変数や関数につける属性です。
pub
がなければ@import
されても、変数や関数は直接利用できません。
export
属性
extport
属性は、Cヘッダ(.h)から利用させる関数や変数につける属性です。オブジェクトファイルやライブラリファイルから利用させる対象へつける属性です。
関数にexport
をつけた場合、Cヘッダファイルにその関数に対応するプロトタイプ宣言が埋め込まれます。
struct
のような型への変数にexport
をつける場合、Cヘッダファイルにその型情報が埋め込まれます。
Cでのextern
と同様に、ファイルをまたいでも名前が被らない必要があります。
struct
のバリエーション
zigのstruct
型は、メモリレイアウトとexport
での扱いの違いで、3タイプが存在します。
-
struct
: メンバーの順序を含むメモリレイアウトを規定しないstruct
。export
時のCヘッダではstruct
ポインタでの宣言として埋め込まれる -
packed struct
: 宣言順にメンバーを配置するメモリレイアウトのstruct
。export
時のCヘッダではstruct
ポインタ宣言として埋め込まれる -
extern struct
: 宣言順にメンバーを配置するメモリレイアウトのstruct
。export
時のCヘッダでは**struct
のメンバー定義宣言**として埋め込まれる
struct
のメンバー関数
zigでは、struct
内部に、関数fn
を宣言できます。この関数は型名.関数名(...)
のかたちで呼び出します。add
やsub
のように、第一引数が定義する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
また、**シフト演算(>>
、<<
)の右辺は、u3
、u4
、u5
、u6
, u7
**といった左辺のビット数-1を上限値とする整数型を使います(変数は、これらの型へダウンキャストします)。
zigではtrue
とfalse
を受ける**bool
型は数値型ではありません**が、ビルトインを使って0/1の数値型にキャスト可能です。
-
type
は(struct
等のカスタムタイプも含む)型の型 -
void
は値なしの型で、(break
で値を渡さない)ブロック式の型でもある -
noreturn
は、途中でプロセス終了したり、無限ループしたりなど、正常に終わらない関数のための戻り値の型
struct
イニシャライザ
構造体のイニシャライザ(リテラル)は構造体名 { .メンバー名 = 値, ...}
です。C99の構造体メンバーのイニシャライザ((型) {.メンバー名=値,...}
)に似た構文です。
関数の引数や戻り値は値のコピー
関数の引数や戻り値は値のコピーのモデルです。struct
や配列だろうとコピー扱いです。
引数や戻り値の型としてポインタ型を使うことは可能です。
どんな大きさの型だろうとスタック上でコピーするため、配列型を下手に扱うと、OSのスタックリミットにかかる可能性があります。
3.3. 構造体CN
のテストコード
以下のコード片は、テストコード構文内で、宣言したCN
のメンバー関数を利用しているコードです。
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
の配列での利用のテストコードです。
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
の定義とそのテストコードです。
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(変換型、 値)
: ビットレイアウトを保存するキャスト。ビットフィールドとしてf64
をu64
に変換する場合など。 -
@ptrCast(変換ポインタ型, ポインタ値)
: ポインタの型変換 -
@intCast(変換整数型, 整数値)
: ビット数の違う整数値の型変換 -
@floatCast(変換浮動小数型, 浮動小数値)
: ビット数の違う浮動小数値の型変換 -
@floatToInt(変換整数型, 浮動小数値)
: 整数への数値変換 -
@intToFloat(変換不動小数型, 整数値)
: 浮動小数への数値変換 -
@ptrToInt(変換整数型, ポインタ値)
: ポインタアドレスの整数への変換 -
@intToPtr(変換ポインタ型, 整数値)
: ポインタアドレス値からポインタへの変換 -
@boolToInt(真偽値)
: 真偽値の整数(u1
)への変換
3.6. FFT本体
以下のコード片は、ループ版FFTの実装コードです。
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.len
、b.len
、c.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には、for
とwhile
のループがあります。
-
for
ループ: 配列等へのイテレータループ -
while
ループ: 条件ループ
while
ループには、以下のバリエーションがあります。
-
while (条件式) {...}
: C言語でのwhile (条件式) {...}
相当 -
while (条件式) : (後処理式) {...}
: C言語でのfor (; 条件式; 後処理式) {...}
相当 -
while (実行式) |変数| {...}
: C言語でのwhile ((変数 = 実行式)) {...}
相当
zigでは、while
でのループカウンタの変数はループ外で宣言する必要があります。
zigでは、for
もwhile
もループアウト後に処理される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;
)
この例はどちらも、cond
がtrue
ならv
は1
、false
ならv
は2
になります。
最小例2でのelse
部はブロックスタイルの構文ではなく、zigの式構文の一つであるブロック式です。
ブロック式の値は、ラベル付きbreak
で渡す必要があり、このためにブロック式になにかラベルを付ける必要があります。
関数のインライン呼び出し
zigのfn
にもinline
をつけることができ、この場合すべての呼び出し箇所が強制的にインライン化されます。
しかし、inline
なfn
は利用制限が付きます。たとえば、fn
を変数や引数に渡して使うことができなくなります。
また、C言語と違い、zigでは、一つでもfn
にインライン化できない状況があるとinline
がコンパイルエラーになります。
zigでは、定義ですべてインライン化を指定するのではなく、普通の関数を呼び出し側で強制インライン化を指定することが可能です。それがビルトイン@inlineCall
です。
3.7. FFTテストコード
以下のコード片は、ループ版FFTの実装のテストコードです。
```zig: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`を実行すると、以下の結果になります:
```shell-executable
$ 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コードを扱うだけのコード例です:
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
な関数であるため、main
をnoreturn
にしています。
os.exit()
を使う場合、exitコードを引数(u8
型)に渡します。
戻り値が!void
の場合、エラーならexitコードは1
になります。
エラーセット型と、エラーユニオン型
zigの**error
構文で定義するエラーセット型**は、struct
やenum
のようなカスタムタイプの一つで、enum
と似た列挙型となっています。例: const MyError = error {Fail, Dead,};
enum
と違う部分は、エラーセット型では、以下のように、その要素であるエラーを集合的に扱う点です。
- 全カスタムエラーセット型を受ける**
anyerror
型**の存在 - 2つのエラーセット型の和集合のエラーセット型が作れる(
const ErrorsC = ErrosA || ErrorsB;
) - エラーセット型内のエラー名は、エラーセット型が違っても同名のものは同値扱いされる(例えば、
MyError.Dead
はconst 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
と違い、エラーやnull
がreturn
されるわけではなく、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を実行させたベンチマーク時間を表示させるプログラムです。
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.Allocator
のcreate
の引数で渡すのは値型ですが、戻り値はその値型へのポインタ型(のエラーユニオン型)になっているところです。
-
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
でのブロック脱出時には、errdefer
とdefer
の混ざった並びでの後ろから順に実行されます。
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
では、``fftと
ifft`関数を`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した実行時間を出すベンチマークとなっています。
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_ctors
とmemmory
が入っています。
__wasm_call_ctors
は関数で、もしzigコードに初期化処理が含まれていれば、この**__wasm_call_ctors()
を呼び出すことで初期化処理を実行**します。
memory
は、WebAssemblyのMemory
オブジェクトで、初期値では空です。
コード内でメモリを使うなら、memory.grow()
メソッドを呼び出し、メモリ領域を確保する必要があります。
引数は拡大するメモリページ数で、1ページ64KBです。
WebAssemblyでのメモリ
exports
にあるmemory.buffer
は、Typed ArrayのArrayBuffer
オブジェクトです。
JavaScript側からは、このmemory.buffer
をFloat64Array
などでラップし、値を入れたり、参照したりします。
ポインタはアドレス値であり、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
は、以下の内容になっています。
#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
です。
#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
です。
#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.zig
のCN
と同じメモリレイアウトになります。
そして、zig上でdouble complex
への操作をしなくてすむよう、double
値から複素数値を作るコンストラクタ関数rect
も用意します。
7.2. zigで使うためのCヘッダファイル
Cヘッダファイルfft-c.h
は、zigのbuild-obj
が書き出すヘッダファイルのやり方に合わせた内容にしてあります。
#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ヘッダファイルではdouble
2つを持つstruct
で代用します。
7.3. Cヘッダファイルを取り込むzigプログラム
先のfft-bench-zig.zig
と同様の、例のデータでのFFT/IFFT変換表示し、そのあとで100万要素のデータをFFT/IFFTした実行時間を出すベンチマークとなっています。
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
した標準ライブラリの内容も全部入るので大量に出てきます。
必要な部分だけ取り出すと、
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
には、pub
なbuild
関数を記述します。
この中で、ビルドで実行する内容をステップのグラフとして定義します。
各種コンパイルやコマンド実行1つが、それぞれ1つのステップとなります。
各ステップには、事前にこなす必要のあるステップの依存関係を指定できます。
また、ビルドターゲットにあたる、名前付きステップがあり、そこに各コンパイルやコマンド実行のステップを順に加えていけます。
ビルドシステムとしてデフォルトのステップがあり、ステップ名指定なしのzig build
で実行される内容を記述するためのものです。(名前付きステップのうち、いくつかはデフォルトステップに加えることになるでしょう。)
ただし、zig-0.4.0現在では、build.zig
で記述可能なことが、完全にはそろっていないようです。たとえば、Cファイルのコンパイルやファイルの削除は、実行環境にあるコマンドを引数を指定して直接実行させる必要があります。
(0.4.0のドキュメントにはaddCExecutable
というステップが記載されていますが、実際にはzig-0.4.0には存在しません)
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ファイルをメインとする実行ファイルを作成するためのステップ -
clean
とdist-clean
: ビルドで生成したファイルの削除を行うステップ
clean
とdist-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による数学関数埋め込みでsin
やcos
をimport
して呼び出す必要がないのは、v8のmath-intrinsics最適化を加えても、実行速度の面で大きいのかもしれません(zigベースのwasmでは、sinやcosの関数自体インライン化されていた)。