When I was researching the programming language for writing WebAssembly modules, I felt that the relatively new open source programming language zig was suitable for writing WebAssembly modules, so I searched while writing and running the source code ** They are listed in the order they appear in the code **.
The source code used in this article is available at the following URL in a buildable state on macOS and linux:
Qiita does not support syntax highlighting for zig code at this time and it is not easy to see, but on github, syntax highlighting is also done for zig code.
The open source programming language zig is available at the following URL.
zig is a programming language implemented in LLVM, but unlike C ++, Go, and Rust, ** heap memory usage is an option ** like C language, and something like new
is in the language syntax. Not built in.
The features of the zig language are:
--Can be installed on the main package system of each OS environment
--Development tools are integrated into one ** zig
command **
--Like a scripting language, you can ** run the source file (.zig file) directly ** (zig run xxx.zig
)
-** Built-in syntax for unit tests ** allows you to write test code in the same source file as the implementation and run tests with the zig
command (zig test xxx.zig
)
--The build description build.zig
that generates the executable file etc. is also written in the zig language, and it has a ** build system ** that can execute the build with the zig
command (zig build
).
-** Syntax formatter ** is incorporated (zig fmt xxx.zig
)
--Numeric types with a specified number of bits, ʻifand
switch as expressions, loop syntax,
deferstatements for block escape execution, array slices, coroutines, compile-time execution expressions, etc. ** Modern languages specification** -** C language and ABI level interoperability ** language model --** to use the functions and
struct / ʻunion
/ ʻenum in the header file (.h) of the C library directly as the zig functions and
struct / ʻunion
/ ʻenum ** A mechanism for embedding C programs (
@ cImport`) is incorporated as a language **
--Equipped with stricter type constraints and type inference than C language
-Contains the musl-libc implementation and can generate a standalone binary with embedded libc code **
--Has a variety of build targets ** including WebAssembly wasm files, UEFI binaries, and static / dynamic libraries for each OS
Etc.
However, note that the syntax and ** specifications of the standard library are not yet stable **. The current version 0.4.0 and the next version are at a stage where incompatibilities occur at the level of string array type syntax. And below, in this article, everything is described based on the ** version 0.4.0 specification **.
First, run the zig zen
command to get a glimpse of the zig language policy:
$ 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.
$
In the zig language, hashtags such as twitter use ziglang.
1. Install the zig
commandThe zig language is the major package managers homebrew, snap, nixos, scoop, chocolatey, packman and provides the zig
( ziglang
in scoop) package. It's easy to install the zig
command from these package managers.
The zig language official zig-mode
is registered in MELPA.
There also seems to be modes for vim, vscode, sublime:
zig
commandAll development tools are provided as subcommands of the zig
command.
zig file execution command:
--zig run xxx.zig
: Runs the main
function of the source file xxx.zig
(without binary generation)
--zig test xxx.zig
: Executes the code in the test
syntax written in the source file xxx.zig
in order and displays a summary of the test results.
--zig build
: Executes the build process described in the build description build.zig
zig file compilation commands:
--zig build-exe xxx.zig
: Generates ** various target binaries (xxx
, xxx.exe
, etc.) ** from the zig file (xxx.zig
).
--zig build-obj xxx.zig
: From the zig file (xxx.zig
), ** C header file (xxx.h
) and C object file (xxx.o
, xxx.obj
) Etc.) ** Generate
--zig build-lib xxx.zig
: From the zig file (xxx.zig
), C header file (xxx.h
) and C static library file (libxxx.a
, xxx.lib
, etc. ) Is generated
--zig build-lib -dynamic xxx.zig
: From the zig file, C header file (xxx.h
) and DLL file (libxxx.so
, libxxx.dylib
, xxx.dll
and xxx.pdb
etc.) etc.)
Utility commands:
--zig init-exe
: Generate project templates (build.zig
and src / main.zig
) for executable files under this directory.
--zig init-lib
: Generate project templates (build.zig
and src / main.zig
) for static libraries under this directory
--zig fmt xxx.zig
: xxx.zig
is ** overwritten ** with syntax formatter
--zig translate-c xxx.c
: C header file and simple C implementation file are zig coded and output as standard.
Information display command:
--zig --help
: List of subcommands and list of command arguments Display help
-- zig builtin
: Displays the zig command runtime environment information module@ import ("builtin")
in zig source code format (there is information such as build target, and the code in build.zig
Mainly used in)
--zig targets
: Displays a list of ** build targets ** that can be handled by zig build-exe
etc.
--zig version
: Shows the spec version of the zig command (eg 0.4.0)
--zig libc
: Displays the libc directory path information of the zig command execution environment.
--zig zen
: Shows the design policy for the zig language
zig build-exe --release-fast -target wasm32-freestanding --name fft.wasm fft.zig
This command line generates a .wasm
module file in WebAssembly from the zig library code.
The command line options have the following meanings:
---- release-fast
: Specifies a speed-focused build
----relase-safe
: Speed-oriented but trap processing specifies a built with embedded
----release-small
: Specifies a size-sensitive build (without traps)
---target wasm32-freestanding
is WebAssembly's 32-bit wasm specification, which specifies a freestanding target to import. --
-target x84_64-windows: Specify the Windows 64-bit console command (exe) as the target. --
-- name fft.wasm: Set the output file name to
fft.wasm --The default output file name will be the executable file name on the target OS (in this case,
fft or
fft.exe`) based on the main zig file name.
As an example of code that is complicated enough to touch the various functions of the language, we adopt a loop implementation of the Fast Fourier Transform FFT and write it in zig.
--Reference: "Understanding the Fast Fourier Transform FFT"
Write fft.zig
as a library without the main
function, write the test in the test
syntax, and check the operation by executing the test with zig test fft.zig
.
The entire code of fft.zig
is less than 200 lines including the test code. Here, we will divide the code into 7 parts, cut out the code pieces in order from the top, and explain the functions and features of the zig language included in each code piece.
The following code snippet is the code to read the pi and sin / cos functions from the standard library:
fft.zig(1)
const pi = @import("std").math.pi;
const sin = @import("std").math.sin;
const cos = @import("std").math.cos;
In zig, you can use the features of the ** standard library ** by @ import ("std")
.
Identifiers that start with @
, such as @ import
, are called ** built-in functions ** in zig, and type casting is also provided as a function of this built-in function.
In zig, ** variables (similar to C) are models of continuous memory areas **.
Therefore, the ** const
variable cannot change the entire contents of the memory area **, and each member in the case of a structure and each element in the case of an array cannot be changed.
Also, in zig, function arguments are also treated as const
variables. Variables whose contents can be changed are declared with var
.
const
typezig has a type with const
. The const
type means that (using pointers) change operations via that ** reference are not allowed **, and has a different effect than the const
variable.
For example, a variable with var a: f64 = 0.0
can change its value and can be referenced with var ptr: * const f64 = & a
, but a change from this reference ptr. * = 1.0
will result in a compilation error. (Ptr. *
Refers to the memory area at the point of the pointer in the meaning of all members. * Ptr
in C).
Conversely, for a pointer to a variable with var a: f64 = 0.0
and a pointer to a const
variable with const ptr: * f64 = & a
, ptr. * = 1.0
is possible.
Also, for a const
variable with const a: f64 = 0.0
, a compile error will occur unless a pointer of type const`` var ptr: * const f64 = & a
.
const
variable to typeThe const
variable is also used for aliasing to types (type
type variables) such as struct
/ ʻunion / ʻenum
/ ʻerror`.
The type check of zig is judged by ** structure match **, not by type name match. Therefore, it is possible to assign between a variable of the alias type and a variable of the original name type.
In the code of, the functions and variables of the standard library are used by assigning variable names with const
, but@ import ("std")
is an expression and can be used anywhere in the code.
Also, in zig-0.4.0, sin
and cos
are functions provided by the standard library, but in the next version, they will be transferred to the built-in and used as @ sin
and @ cos
. It seems to be.
struct
The following code snippet is a declaration of the complex structure handled by the FFT and its constructors and methods:
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
attributeThe pub
attribute is an attribute attached to a ** variable or function that is used when ** @ import
is made from another zig file (like pi
or sin
in the standard library).
Without pub
, variables and functions cannot be used directly even if @ import
is done.
The ʻextport` attribute is an attribute attached to ** functions and variables that are used from the ** C header (.h). This is an attribute attached to the target ** to be used from the object file or library file.
If you add ʻexport to a function, the prototype declaration corresponding to that function is embedded in the C header file. If you add ʻexport
to a variable to a type like struct
, the type information will be embedded in the C header file.
As with ʻextern` in C, ** must be unnamed across files **.
struct
There are three types of zig's struct
type, depending on the difference in memory layout and handling in ʻexport`.
--struct
: ** Includes member order ** Does not specify memory layout ** struct
. In the C header at the time of ʻexport, it is embedded as a declaration with the
struct pointer. --
packed struct: ** A memory layout
structthat places members ** in declarative order. Embedded as a
struct pointer declaration in the C header at ʻexport
--ʻExtern struct: Memory layout
structthat arranges members in declarative order. It is embedded as a member definition declaration ** of **
struct in the C header at the time of ʻexport
.
struct
In zig, you can declare a function fn
inside struct
. This function is called in the form type name.function name (...)
. If the first argument is a struct
, such as ʻadd or
sub, you can also write it in the ** method call ** style (
CN.add (a, b),
It can also be described as a.add (b) `).
In zig-0.4.0, even if the function is in struct
, in the C header at the time of ʻexport, there is no prefix of the structure name, and the prototype is declared with the function name as it is (
CN). When .rect () is ʻexport
, it becomes justrect ()
instead ofCN_rect ()
.
The zig numeric type specifies ** the number of bits **. There are also target-dependent numeric types for the C interface:
--Non-numeric types: type
, noreturn
, void
, bool
--Signed integer types: ʻi8, ʻi16
, ʻi32, ʻi64
, ʻi128 --Unsigned integer type: ʻu8
, ʻu16, ʻu32
, ʻu64, ʻu128
--Floating point type: f16
, f32
, f64
, f128
--Target-dependent numeric types: ʻisize, ʻusize
, c_short
, c_ushort
, c_int
, c_uint
, c_long
, c_ulong
, c_longlong
, c_ulonglong
, c_longdouble
Also, the right side of the ** shift operation (>>
, <<
) has the number of bits-1 on the left side such as ʻu3, ʻu4
, ʻu5, ʻu6
, ʻu7` **. Use integer types as the upper bound (variables are downcast to these types).
In zig, ** bool
types that receive true
and false
are not numeric types **, but they can be cast to 0/1 numeric types using the built-in.
--type
is the type of type (including custom types such as struct
)
--void
is a valueless type, also a block expression type (without passing a value with break
)
--noreturn
is the return type for functions that do not end normally, such as a process terminating in the middle or an infinite loop.
struct
initializerThe structure initializer (literal) is structure name {.member name = value, ...}
. Syntax similar to the C99 structure member initializer ((type) {. member name = value, ...}
).
Function arguments and return values are ** copy values ** models. Even if it is a struct
or an array, it is treated as a copy.
It is possible to use pointer types as arguments and return types.
Any size type will be copied on the stack, so if you handle the array type poorly, you may hit the stack limit of the OS.
CN
The following code snippet is code that uses the declared CN
member function in the test code syntax.
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
partThe test" name "{...}
is the part that describes the code ** that will be executed during the ** zig test
command.
As of zig-0.4.0, you can't define a function directly in test
.
However, you can declare a function in struct
even in test
, and you can use the function by calling a method.
The function of fn
in struct
can be called by structure name.function name ()
, especially when the first argument is that struct
, it can also be called byvariable name.function name ()
, It has the syntax.
ʻA.add (b)in this code can also be called by
CN.add (a, b)`.
CN
array test codeThe following code snippet is a test code for use with an array of 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);
}
The array type of zig is [number of elements] element type
, which means ** continuous area with the number of elements specified.
The multidimensional array type is [number of outer elements] [number of inner elements] element type
. In the memory layout, several inner elements are lined up with several outer elements.
If the element type is struct
, the members are arranged in order, and it becomes a memory layout with several elements.
Assigning an array to an element overwrites the value.
The declaration of array uninitialization by the var
variable is var variable name: [number of elements] element type = undefined
.
The number of elements is mandatory.
The array type initializer (literal) is [number of elements] element type {initial value 0, initial value 1, ....}
.
The total prime number in the initializer can be omitted.
Initializers with the same value can also have a [] element type {initial value 0, initial value 1, ...} ** number of iterations
. In this case, it is an array type of the number of elements of the initial value number ✕ the number of repetitions.
Declaration with initial value of array by var
variable uses array type initializer,var variable name = [number of elements] element type {initial value 0, initial value 1, ...}
.
Declaration with initial value for const
variableconst variable name = [number of elements] Element type {initial value 0, initial value 1, ...}
is usually used (example:const v = [3] u8 {1,2,3}
).
Note that declaring an array variable in a function ** allocates space in stack memory **. For this reason, it is easy to reach the stack limit of the OS (8MB, etc.) when using an array, so be careful. (The method of using the heap etc. in the standard library will be described later.)
The following code snippet is the definition of the array index bit inversion function revbit
in FFT and its test code.
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);
}
In zig, the (CPU intrinsics) bit operations provided by ** CPU instructions are provided as built-in **.
--@bitreverse (numeric type, numeric)
: Bit upside down (Note: The name will change to @bitReverse
in the next version)
--@ctz (number)
: Count trailing zeros with 0s following the least significant bit. If it is a power of 2, it is the same value as log2 (number)
.
Others include @ clz
(number of most significant bits followed by 0), @ popCount
(number of 1s), @ bswap
(upside down in bytes), and so on.
With zig, you can implicitly expand ** only if the number of bits increases with the same numeric type. Other than that, you need to select and use each ** built-in function for casting ** according to the conversion method.
--@truncate (conversion type, value)
: Cast that cuts only the low-order bits up to the number of bits of the conversion type
In the shift operation, it is necessary to truncate to a numeric type that fits the number of bits of the variable type to be shifted.
Since 32 is 2 to the 5th power, it is mandatory to truncate the type to ʻu5 in the rvalue of the shift operation of ʻu32
.
There are a lot of cast built-in functions, but the following are typical:
--@bitCast (converted type, value)
: Cast that saves the bit layout. For example, when converting f64
to ʻu64 as a bit field. --
@ptrCast (conversion pointer type, pointer value): Pointer type conversion --
@intCast (conversion integer type, integer value): Type conversion of integer values with different number of bits --
@floatCast (conversion floating point type, floating point number)`: Type conversion of floating point number with different number of bits
--@floatToInt (conversion integer type, floating number)
: Numeric conversion to integer
--@intToFloat (conversion immovable decimal type, integer value)
: Numeric conversion to floating point
--@ptrToInt (conversion integer type, pointer value)
: Conversion of pointer address to integer
--@intToPtr (conversion pointer type, integer value)
: Pointer address value to pointer conversion
--@boolToInt (validity value)
: Conversion of boolean value to integer (ʻu1`)
The following code fragment is the implementation code of the loop version 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);
}
}
A variable of zig is a memory area, and has a pointer type ** that points to its start address.
Pointer types to value types are types with *
at the beginning of the type, such as * u8
.
In zig, pointer type variables and variables of the type from which they are based can be accessed with the same description **.
In zig, arrays are also value types that represent continuous memory areas. Unlike C, there is a clear distinction between ** array types and pointer types to arrays **.
In zig-0.4.0, array pointer types are represented by [*] element types
.
The array pointer type can also point to elements in the middle of the array, and like C, you can add or subtract the number of offset indexes.
Array pointer variables can access the elements of an array with the same description as array variables.
Other types related to array pointers are:
--* [Number of elements] Element type
: Pointer to the entire array. The number of elements (.len
) is fixed at compile time
--[] Element type
: The number of elements (.len
) is obtained at runtime ** Slice **
A zig slice is a zig built-in data structure that points to a contiguous area on an array area, with an array pointer (.ptr
) for the start address and the number of elements (.len
).
Summary,
--const a: [12] u8 = [] u8 {1} ** 12;
: Array variable. The number of elements is fixed at compile time
--const b: * const [12] u8 = & a;
: Pointer variable to the entire array. The number of elements is fixed at compile time
--const c: [] const u8 = & a;
: Slice type. The number of elements is fixed at runtime
--const d: [*] const u8 = & a;
: Pointer variable to the array. Does not have the number of elements
The above three variables can retrieve the number of elements with ʻa.len,
b.len, and
c.len, but
d` does not have that means.
The following is the syntax for assigning to other slices and array pointers.
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;
To make a slice from an array pointer (c3
), it is necessary to specify the index of ** end point **.
Variables in zig are block scoped, but unlike many languages, variables with the same name as variables defined outside the block cannot be defined inside the block **.
The variable ʻi is used in the first part of the function
fftcto copy to the bit-inverted index. If you delete the
{``} that surrounds this part, you will get a compile error because ʻi
is also used in the loop below.
zig has a loop of for
and while
.
--for
loop: Iterator loop to array etc.
--while
loop: Conditional loop
The while
loop has the following variations:
--while (conditional expression) {...}
: Equivalent to while (conditional expression) {...}
in C language
--while (conditional expression): (post-processing expression) {...}
: for (; conditional expression; post-processing expression) {...}
equivalent in C language
while (Execution formula) |variable| {...}
:In C languagewhile ((variable = Execution formula)) {...}
EquivalentIn zig, ** loop counter variables in while
must be declared outside the loop **.
In zig, both for
and while
can have expressions in the ʻelse` part that is processed after the loopout.
The loop can have a result value as an expression. In the loop expression, the ʻelsepart is required, and the value passed to the valued
break` is the value of the expression.
--Minimal loop expression 1: const v = while (cond) {break 1;} else 2;
--Minimum example of loop expression 2: const v = while (cond) {break 1;} else ret: {break: ret 2;};
-(zig's ʻif expression:
const v = if (cond) 1 else 2;`)
In both of these examples, v
is 1
if cond
is true
, and v
is 2
if false
.
The ʻelsepart in minimum example 2 is not a block-style syntax, but a ** block expression **, which is one of zig's expression syntax. ** The value of the block expression must be passed in a labeled
break` **, for which you need to label the block expression something.
You can also add ʻinline to zig's
fn, which forces all calls to be inlined. However, ʻinline
and fn
have usage restrictions. For example, you will not be able to pass fn
as a variable or argument.
Also, unlike C language, in zig, ʻinline will cause a compile error if there is a situation where even one cannot be inlined in
fn`.
In zig, it is possible to specify ** force inline ** on a regular function on the caller instead of specifying all inlining in the definition. That is the built-in @inlineCall
.
The following code snippet is a test code for the implementation of the looped FFT.
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
can be done in struct
In zig-0.4.0, fn
cannot be described at the top level of the test
part, but a function can be described in ** struct
**, and it was possible to call it as a static method.
In zig, the syntax for getting a pointer from a value type is & variable
.
Unlike C language, array variables cannot be treated implicitly as array pointers and must be ** explicitly added with &
**.
The method of referencing an element from a pointer type is the same as that of a value type (ʻa [i] and ʻa.m
).
A pointer-specific expression has a ** whole value dereference *, which is followed by a . *
( V = a. *
Or ʻa. * = V). This is equivalent to the pointer dereference
ptr` in C.
warn
The standard library warn
allows you to use a format string to output characters (to stderr
) to the console.
It is also output by executing zig test
.
zig test
execution resultRunning the zig test fft.zig
against this fft.zig
gives the following results:
$ 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.
$
fft.zig
Note: In zig, the ** startup process of commands and libraries is implemented ** in the standard library. The description here is based on the standard library implementation in zig-0.4.0. The specifications around here are likely to change significantly in the future.
First, I will explain only how to use the main
function that is called from the zig standard library when executing a command, and then I will explain the zig program that calls the above FFT implementation by@ import ("fft.zig")
.
main
function overviewThe following is a code example of the entry point main
function in command line execution, which only handles environment variables, command arguments, and exit code:
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
function type and exit codeThe ** mainfunction ** with no
pub` arguments is the entry point for your user code.
The return type of main
is noreturn
(exit without being executed by the last name), ʻu8(return value becomes exit code),
void(exit code is 0). ,
! Void (error or
void`).
In the case of this code, main
is set to noreturn
because ʻos.exit ()of the standard library to exit is a
noreturn function. When using ʻos.exit ()
, pass the exit code as an argument (type ʻu8`).
If the return value is ! Void
, the exit code will be 1
if there is an error.
The error set type ** defined in zig's ** ʻerror syntax is one of the custom types like
struct and ʻenum
, and is an enumeration similar to ʻenum. Example:
const MyError = error {Fail, Dead,};`
The difference from ʻenum` is that the error set type handles the error that is its element collectively as follows.
--Existence of ** ʻany error type ** that receives all custom error set types -Two error set types**Union error set type**Can be made(
const ErrorsC = ErrosA || ErrorsB;) --Error names in error set types are treated as equivalent ** even if the error set types are different ** (for example,
MyError.Dead is
const e: MyError = error.Dead; Can be initialized) --There are error union types (types with
!) And error union-specific syntax (
catch operators,
try` operators).
Types with !
, Such as! U8
for return types and MyError! U8
for variable types, are zig's ** error union types **.
Error union types are ** variable and return types that can accept both ** value types and ʻerrortypes. By making this error union type a return type, in zig, ** errors will also be
return** values. In the return type
! U8, the error set type on the left side is inferred from the function implementation code, and it is actually an error union type such as
MyError! U8`.
To change from an error union type to a value type expression, zig can use the catch
binary operator and the try
unary operator.
catch
Binary operator:You can pass the fallback value on error. Example: const v: u8 = errorOrInt() catch |err| 10;
--try
unary operator: In case of an error, the expression returns
the error. Example: const v: u8 = try errorOrInt ();
catch
Operator capture section|err|
Can be omitted if the error value is not used.
The return value of a function that uses the try
operator inside must be of type error union.
It is also possible to sort the error time as ʻelse from the error union type by making it a conditional expression of ʻif
expression or switch
expression.
if
Example: const v: u8 = if (errorOrInt()) |n| n else |err| 0;
--switch
Example:const v: u8 = switch (errorOrInt ()) {n => n, else => 0,};
Environment variables can be retrieved with the standard library ʻos.getEnvPosix (). The return value of this function is
? [] Const u8`.
The type with this ?
in the head is zig's ** optional type **. The optional type is zig's ** null
value ** or value. (Therefore, the pointer type cannot be null
unless it is an optional type.)
Similar to the error union type, it can be a value type by giving a fallback value with ʻif expression or
switchexpression, and when
null, with ʻelse
.
It also has the ʻorelsebinary operator for optional types, in the same position as the
catch` binary operator in the error union.
--Example: const v: u8 = nullOrInt () orelse 0;
You can also force a value type (like the try
unary operator of type ʻerror) by adding
const v: u8 = nullOrInt ().? And
.? . However, unlike
try, no error or
nullis
return`, and it seems that zig-0.4.0 will result in a non-trappable forced termination.
You can retrieve command line arguments with the standard library ʻos.args () . This is an iterator, which can be retrieved one after another as an optional string of type
? [] Const u8 using the
nextPosix () `method.
To retrieve sequentially from the iterator until it becomes null
, use a while
loop ** that captures the ** condition value. If it becomes null
, it will enter the ʻelse` part.
When converted to binary, the ** 0th command argument is the command name **, and the argument is next.
For zig run
, you can pass command arguments after --
in zig run example-main.zig --a b c
. In zig-0.4.0, in this case the arguments start at 0th.
fft.zig
as @ import
The following is a program that displays the FFT / IFFT results with the data used in the test, and then displays the benchmark time when the FFT and IFFT of 1 million (2 to the 20th power) elements were executed.
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);
}
}
@Import
from the zig fileYou can also ** @ import
** @ import
and use variables and functions of the pub
attribute ** for your own zig source file as well as the standard library.
In fact, the standard library also exists as a zig source file.
You can see the implementation code in the installation directory under lib / zig / std /
.
In the zig language, basically, variables use the area of stack memory (static memory). If you use a large array, ** you will exceed the OS stack limit at run **, so your program must use an off-stack memory area such as the heap.
The standard library provides a ** memory allocator ** that handles heap memory. Below is the allocator implementation in zig-0.4.0:
--heap.DirectAllocator
: Allocator to allocate heap memory (ʻinit,
deinit, ʻalloc
, shrink
, realloc
)
--heap.ArenaAllocator
: An allocator that configures a memory pool using other allocators and releases all the memory allocated so far with deinit
.
--mem.Allocator
: Interface to allocate memory by typing (create
, destroy
)
In zig-0.4.0, the heap
allocator has the mem.Allocator
interface as a member .allocator
(in this code, ʻarena.allocator`).
Note that the value type is passed in the create
argument of mem.Allocator
, but the return value is a pointer type (error union type) to that value type.
--ʻAllocator.create ([1048576] CN) The return type of is
! * [1048576] CN --
var f = try allocator.create ([1048576] CN); ʻa
type is * [1048576] CN
--f: * [1048576] CN
can be assigned to the variable of [*] CN
(the second argument of fft ()
in the code) without explicit casting.
defer
statementThe defer
statement describes in advance the code that will be executed when the block escapes.
If there are multiple defer
s in the same block, they will be executed ** in order from the back side **.
By writing in a close location using defer
, it is possible to describe the initialization process and the end process corresponding to the initialization as a pair.
A similar one is ʻerrdefer. The
defer that is executed when the block escapes ** when the error is
returned. When escaping a block with the error
return, it is executed in order from the back in a mixed sequence of ʻerrdefer
and defer
.
From this fft-bench-zig.zig
, you can generate the executable file fft-bench-zig
with the following command line.
$ zig build-exe --release-fast fft-bench-zig.zig
You can also run your code with zig run fft-bench-zig.zig
without having to build it.
fft.zig
into WebAseembly module and use it in JavaScriptWhen you generate a WebAssembly module from code written in a normal programming language such as Rust, you need to consider the runtime library implementation of that programming language, especially the functions related to heap memory, in order to use the wasm module. In imports and exports of the wasm module, it is necessary to have a method for memory operation etc. in that language, and implement and use it according to its usage.
However, if you create a wasm module from a zig program, you will generate a wasm module ** that does not expose any language runtime related methods. The biggest difference is that zig basically doesn't use heap memory (C language is basically the same, but I write it without using stdlib.h
which has heap management at all. That is more difficult).
In the previously implemented fft.zig
, the
fft and ʻifft
functions are ʻexport`, and it is possible to compile to a wasm file as it is.
You can generate fft.wasm
with the following command line.
$ zig build-exe --release-fast -target wasm32-freestanding --name fft.wasm fft.zig
fft.wasm
in node.jsThe JavaScript code below is the program fft-bench-nodejs.js
that loads and uses the wasm file generated from this zig code.
Similar to the previous fft-bench-zig.zig
, it is a benchmark that displays the FFT / IFFT transform with the example data, and then FFT / IFFT the execution time of 1 million elements of data.
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);
You can put the generated fft.wasm
and this fft-bench-nodejs.js
in the same directory and execute it with node fft-bench-node.js
.
The zig runtime ** doesn't have a feature that needs to be ʻimport externally **, so create it with ʻawait WebAssembly.instantiate (buf, {})
.
ʻExports contains
__wasm_call_ctorsand
memmory in addition to the functions ʻexport
in the code.
__wasm_call_ctors
is a function, and if the zig code contains an initialization process, it will execute the initialization process by calling this **__wasm_call_ctors ()
.
memory
is a WebAssembly Memory
object that is initially empty.
If you want to use memory in your code, you need to call the memory.grow ()
method to allocate memory space.
The argument is the number of memory pages to expand, 64KB per page.
The memory.buffer
in ʻexports is a Typed Array ʻArrayBuffer
object.
From the JavaScript side, wrap this memory.buffer
with Float64Array
etc., enter a value, and refer to it.
** Pointers are address values **, and WebAssembly memory address values use ** memory.buffer
byte offset value ** as ʻArrayBuffer. Note that zig has an alignment for each number of bytes in the numeric type. For
f64, the address value must be a multiple of 8, and the second argument of the
Float64Array` constructor should use this multiple of 8.
zig creates wasm that doesn't need to give the language runtime in an external module, so it's not impossible to ʻimports` an external module you prepared and use it in wasm.
If you want to give an externally implemented function to ʻimports and use it, you can declare a ** ʻextern
function prototype such as ʻextern fn sin (x: f64) f64;and use it in your code **. Is possible. In zig-0.4.0, the external module name in the wasm file is fixed to
" env "and can be used by giving it as
WebAssembly.instantiate (buf, {env: {sin: Math.sin}})`. ..
Functions that can be ʻexportto the JavaScript side as WebAssembly have more restrictions. For example, in zig-0.4.0, ** functions ** with a return value of
struct type, such as
CN.rect () , are subject to this limitation. Be careful with ʻexport
in your zig code, as you will not be able to convert to wasm if there is at least one ʻexport` for this limitation.
As a countermeasure, it is possible to separate the zig file of the implementation code that only pub
and the zig file of the implementation just @ import
and add an appropriate ʻexport` for each target. I will.
fft.zig
an object file and use it from C languageThe zig code can be converted to an object file available from C with the zig build-obj
command. A C header file corresponding to the object file is also generated at the same time.
Here, create a C language program that uses fft.zig
, compile it, and generate an executable file.
Generate the object file fft.o
and the header file fft.h
from fft.zig
on the following command line:
$ zig build-obj --release-fast fft.zig
The generated header file fft.h
has the following contents.
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
You can see that this header file fft.h
contains the ʻexport function and
struct` in the zig code.
The following is the C11 code fft-bench-cc
of the main
function that executes and displays the FFT / IFFT with the same data example used in the test, and then executes the FFT / IFFT for 10 million elements of data. is.
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;
}
If you use a struct or function just because it is implemented in zig, you don't have to do anything special on the ** C side **.
The command line to generate the executable fft-bench-c
is as follows (C compiler implementation assumes clang or gcc):
$ cc -Wall -Wextra -std=c11 -pedantic -O3 -o fft-bench-c fft-bench-c.c fft.o
(This option specifies that the code should be interpreted by the unextended C11 standard itself)
Contrary to the case of fft-bench-c.c
, it is also possible to import an object file written in C language with the zig program using the C header file.
Here, implement the FFT with the C11 code fft-c.c
, and describe the C header file fft-c.h
to use the implementation from zig.
Then, with fft-bench-cimport.zig
, which does the same thing as fft-bench-zig.zig
, read and implement fft-c.h
.
The following is the FFT implementation fft-c.c
using the complex number double complex
in the C11 specification.
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;
}
The algorithm is the same as fft.zig
.
Also, since double complex
is a value in which the double
value of the real part and the imaginary part are arranged consecutively, it has the same memory layout ** as the CN
of ** fft.zig
.
Then, a constructor function rect
that creates a complex number from a double
value is also prepared so that you do not have to operate on the double complex
on zig.
The C header file fft-c.h
has contents that match the method of the header file written by build-obj
of zig.
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
Since zig does not have a type corresponding to the complex number type, in the C header file read by the zig program, struct
with double
2 is used instead.
Similar to the previous fft-bench-zig.zig
, it is a benchmark that displays the FFT / IFFT transform with the example data, and then FFT / IFFT the execution time of 1 million elements of data.
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
built-inBy using ** @ cImport
built-in **, it can be imported as a zig struct
or function from the C header.
In @ cImport
, specify the header file name to be interpreted by ** @ cInclude
built-in **.
The pointer type of zig is divided into the pointer type of value type and the pointer type to array, but the pointer type of C does not distinguish between them.
Therefore, if the pointer type is used in the argument or return value of the function @cImport
, it is described by [* c] value type
which can correspond to both types on zig ** C pointer type * Interpreted as *.
Here, the function fft
from the C header is of typefft (n: i32, f: [* c] CN, F: [* c] CN)
.
This C pointer type [* c] CN
is assigned from * CN
and [*] CN
without casting (late code).
However, in zig-0.4.0, & f: * [16] CN
, which is assigned to [*] CN
without casting, requires an explicit cast by @ ptrCast
(first half of the code).
--* [16] CN
= No cast required => [*] CN
= No cast required => [* c] CN
--* [16] CN
= Required @ptrCast
=> [* c] CN
@ cImport
ʻextern fn`You can see what you are doing with @ cImport
by running the zig translate-c fft-c.h
command and looking at the zig code for that output. However, since all the contents of the standard library #include
are included, a large amount will appear.
If you take out only the necessary part,
zig: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;
It will be the same content as. If you embed this content, you don't need the fft-c.h
or the zig build
option -isystem .
to add the include path.
Also, embedding the object file path (./
is required) immediately after ʻextern eliminates the need for
--object fft-c.o. (If
./ is missing, it will try to resolve as a library (
"fft-c" is treated as
-lfft-c`).)
In zig, if there is a ʻextern` function prototype, it will be declared to ** use the function of the same name in the library or object file **.
You can build with the following command line.
$ 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
The option --object fft-c.o
specifies the object file, and -isystem .
specifies the directory to read the header file with @ cInclude
.
Even in this case, you can run it without creating an executable file with zig run --object fft.o -isystem .fft-bench-cimport.zig
.
build.zig
zig seems to be aiming to enable cross-platform builds with the zig build
command without using an external build system such as make
.
In build.zig
, which describes the build contents by executing zig build
, describe ** pub`` build
function **.
In this, define what you want to do in the build as a ** step graph **. Each compilation and command execution is one step. For each step, you can specify the step dependencies that need to be done in advance.
There is also a ** named step **, which is the build target, to which you can add each compilation and command execution step in turn.
There is a ** default step ** as the build system, and it is for describing what is executed by zig build
without specifying the step name. (Some of the named steps will be added to the default steps.)
However, as of zig-0.4.0, it seems that everything that can be described in build.zig
is not completely complete. For example, compiling a C file or deleting a file requires the command in the execution environment to be executed directly with arguments.
(The 0.4.0 documentation has a step called ʻaddCExecutable`, but it doesn't actually exist in 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);
}
}
In this, we are making 6 named steps. From the top for each block:
-- fft.wasm
: wasm Steps for file generation
-- fft-bench-zig
: Steps to generate an executable from only the zig file
--fft-bench-c
: Steps to make a zig file an object file and create an executable file mainly with a c file that uses it
--fft-bench-cimport
: Steps to make a c file an object file and create an executable file mainly with a zig file that uses it
--clean
and dist-clean
: Steps to delete the files generated by the build
Except for clean
and dist-clean
, they have been added to the default steps.
You can see which named steps are available with zig build --help
.
I think that it will change significantly in future version upgrades, but it may be a reference for how to configure it as a build description in each of the four build cases.
The benchmark result of the last generated executable file is posted. It is the time taken for FFT / IFFT of 1 million elements in the latter half.
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) |
Also, for reference in wasm, the FFT / IFFT benchmark with the same content performed in "WebAssembly circumstances in 2019" was executed in the same environment. The results are also listed.
--Source: https://gist.github.com/bellbind/36b7e71b3203e868c10405a61cdd2d09
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 |
The fact that it is not necessary to call sin
or cos
by ʻimport` when embedding mathematical functions with zig may be large in terms of execution speed even with v8 math-intrinsics optimization (. In zig-based wasm, the sin and cos functions themselves were inlined).