Open source programming language zig summary

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.

0. About the programming language zig

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, ʻifandswitch 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 andstruct / ʻ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 command

The 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.

emacs zig-mode

The zig language official zig-mode is registered in MELPA.

There also seems to be modes for vim, vscode, sublime:

2. Basics of the zig command

All 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

Command example 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.

3. zig program: FFT implementation

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.


3.1. Load standard library

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;

Standard library and built-in functions

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.

Variable model

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 type

zig 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.

Named const variable to type

The 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.


3.2. Definition of complex number 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 attribute

The 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.

ʻExport` attribute

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 **.

Variation of 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 astruct 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.

Member function of 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 ().

Numeric type

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 initializer

The 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 copies of the values

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.


3.3. Test code for structure 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 part

The 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.

Structure method call

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 byCN.add (a, b)`.


3.4. CN array test code

The 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);
}

array of zig

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.)


3.5. Index bit inversion for FFT

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);
}

Bitwise built-in

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.

Explicit cast

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`)


3.6. FFT body

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);
    }
}

Pointer type

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.

To make a slice from an array pointer (c3), it is necessary to specify the index of ** end point **.

zig variables cannot be hidden in block scope

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 loop type and block type

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

In 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 valuedbreak` 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 labeledbreak` **, for which you need to label the block expression something.

Inline function call

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.


3.7. FFT test code

The following code snippet is a test code for the implementation of the looped FFT.

`fft.zig(7)``


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

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

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

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

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

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

Function definition in 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.

Reference to pointer type

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.

Console output by 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.


3.8. zig test execution result

Running 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.
$

4. Zig code of the executable file using 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").


4.1. zig's main function overview

The 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 code

The ** mainfunction ** with nopub` 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 anoreturn 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.

Error set type and error union type

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 bereturn** 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.

catchOperator 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.

Environment variables and optional types

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 whennull, with ʻelse. It also has the ʻorelsebinary operator for optional types, in the same position as thecatch` 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 nullisreturn`, and it seems that zig-0.4.0 will result in a non-trappable forced termination.

Command arguments

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.


4.2. main zig code that uses 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 file

You 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 /.

Use of huge memory

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 statement

The defer statement describes in advance the code that will be executed when the block escapes. If there are multiple defers 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.


4.3. Build the executable file

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.


5. Compile fft.zig into WebAseembly module and use it in JavaScript

When 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

5.1. JavaScript code that uses fft.wasm in node.js

The 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.

Initialization of wasm module generated by zig

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_ctorsandmemmory 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.

Memory in WebAssembly

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.

Reference: When creating wasm that accepts ʻimports`

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 asWebAssembly.instantiate (buf, {env: {sin: Math.sin}})`. ..

Code ʻexport` limit for WebAssembly

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 ofstruct 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.

6. Make fft.zig an object file and use it from C language

The 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.

6.1 Object file and C header file generation

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.


6.2 C language program that calls the FFT implementation

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 **.

6.3. Build

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)

7. Read the object file written in C language from the zig program

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.

7.1. FFT implementation program by C11

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.

7.2. C header file for use with 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 double2 is used instead.

7.3. Zig program to capture C header files

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-in

By 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 **.

C pointer type

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

Reference: How to use object files without @ 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 **.

7.4. Build

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.

8. Build description by 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.

9. Benchmark results

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).

Recommended Posts

Open source programming language zig summary
Recommended programming language
Popular programming language ranking
Popular programming language ranking
Summary of Java language basics
[Programming language] User name list
About the programming language Crystal