심층 개요

주요 기능

작고 단순한 언어

프로그래밍 언어 지식을 디버깅할게 아니라 여러분의 애플리케이션을 디버깅하는데 집중하십시오.

Zig의 전체 문법은 500줄 PEG 문법 파일로 표현됩니다.

숨겨진 제어 흐름이 없으며, 숨겨진 메모리 할당도 없고, 전처리기와 매크로도 없습니다. 만약 Zig 코드가 멀리 점프해서 함수를 호출하려는걸로 보이지 않다면, 실제로 그런겁니다. 이는 다음의 코드가 그저 foo()를 호출한 뒤 bar()를 호출하는 것을 확신해도 된다는 것이며, 어떤 것의 타입도 알 필요가 없음을 보장합니다:

var a = b + c.d;
foo();
bar();

숨겨진 제어 흐름의 예:

Zig는 모든 제어 흐름을 언어의 키워드와 함수 호출로 명시적으로 관리함으로써 유지보수를 용이하게 하고 가독성을 향상시켜 줍니다.

퍼포먼스와 안정성: 둘 다 고르세요

Zig에는 네 가지의 빌드 모드가 있으며, 세분화된 범위에 따라 조합하고 섞어서 사용할 수 있습니다.

파라미터DebugReleaseSafeReleaseFastReleaseSmall
최적화 - 속도 향상, 디버깅 어려움, 컴파일 시간 김-O3-O3-Os
런타임 안전성 확인 - 속도 느림, 용량 큼, 정의되지 않은 동작 대신 크래시 발생OnOn

빌드 모드에 상관 없이 컴파일 타임의 정수 오버플로우는 다음과 같습니다:

test.zig

test "integer overflow at compile time" {
    const x: u8 = 255;
    _ = x + 1;
}
$ zig test test.zig
doctest-f8f203d0/test.zig:3:11: error: overflow of integer type 'u8' with value '256'
    _ = x + 1;
        ~~^~~

안전성 확인 빌드의 런타임에서 발생하는 결과는 다음과 같습니다:

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
1/1 test.test.integer overflow at runtime... thread 2743 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-afa91a02/test.zig:3:7: 0x1038e9e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:158:25: 0x10442a2 in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:35:28: 0x103a28b in main (test)
        return mainTerminal();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10393c9 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1038f31 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/2f16506eee97800cfa3f4405c2b8ffc7/test

스택트레이스는 프리스탠딩을 포함한 모든 타겟에서 작동합니다.

Zig에서는 안전성 확인이 켜진 빌드모드에 의존해도 되고, 성능 병목이 있는 곳에서만 선택적으로 안전성 확인을 꺼도 됩니다. 예를 들어 이전 예제는 다음과 같이 수정될 수 있습니다:

test "actually undefined behavior" {
    @setRuntimeSafety(false);
    var x: u8 = 255;
    x += 1; // XXX undefined behavior!
}

Zig는 정의되지 않은 동작을 통해 날카롭게 버그 방지와 성능 향상을 꾀합니다.

성능에 대해 얘기하자면, Zig는 C보다 빠릅니다.

Zig가 완전히 안전한 언어는 아니니 주의하십시오. Zig의 안전성과 관련된 이야기에 관심있으신 분은 다음 이슈를 구독하세요:

ZIG는 C에 의존하는게 아니라 경쟁한다

Zig 표준 라이브러리는 libc와 연동하지만, 의존하지는 않습니다. 여기 Hello World가 있습니다:

hello.zig

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe hello.zig
$ ./hello
Hello, world!

x86_64-linux 타겟으로 -O ReleaseSmall에 디버그 심볼을 제거하고 단일 쓰레드 모드로 컴파일 하면, 정적으로 컴파일된 9.9 KiB의 실행파일이 만들어집니다:

$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
  not a dynamic executable

Windows용 빌드는 더 작아서, 4096 byte입니다:

$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded -target x86_64-windows
$ wc -c hello.exe
4096 hello.exe
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows

순서와 무관한 최상위 선언

전역 변수 같은 최상위 선언은 순서와 무관하며 게으르게 분석됩니다. 초기화 값과 전역 변수는 컴파일 타임에 평가됩니다.

global_variables.zig

var y: i32 = add(10, x);
const x: i32 = add(12, 34);

test "global variables" {
    assert(x == 46);
    assert(y == 56);
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}

const std = @import("std");
const assert = std.debug.assert;
$ zig test global_variables.zig
1/1 global_variables.test.global variables... OK
All 1 tests passed.

널 포인터 대신 선택적 타입 사용

다른 프로그래밍 언어에서 null 참조는 많은 런타임 예외의 원인이며 컴퓨터 과학에서의 최악의 실수라고까지 일컬어집니다.

평범한 Zig 포인터는 null이 될 수 없습니다:

test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
$ zig test test.zig
doctest-881dc42c/test.zig:2:35: error: pointer type '*i32' does not allow address zero
    const foo: *i32 = @ptrFromInt(0x0);
                                  ^~~

하지만 어떤 타입이든 앞에 ?를 붙여 선택적 타입으로 만들 수 있습니다:

optional_syntax.zig

const std = @import("std");
const assert = std.debug.assert;

test "null @intToPtr" {
    const ptr: ?*i32 = @ptrFromInt(0x0);
    assert(ptr == null);
}
$ zig test optional_syntax.zig
1/1 optional_syntax.test.null @intToPtr... OK
All 1 tests passed.

선택적 값을 풀어서 쓰고 싶은 경우, orelse를 사용하여 기본값을 가져올 수 있습니다:

// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;

fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    // ...
}

다른 방법은 if를 사용하는겁니다:

fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }

    // do some stuff
}

while도 같은 문법이 적용됩니다:

iterator.zig

const std = @import("std");

pub fn main() void {
    const msg = "hello this is dog";
    var it = std.mem.tokenize(u8, msg, " ");
    while (it.next()) |item| {
        std.debug.print("{s}\n", .{item});
    }
}
$ zig build-exe iterator.zig
$ ./iterator
hello
this
is
dog

수동 메모리 관리

Zig로 작성한 라이브러리는 어디에나 쓸 수 있습니다:

이를 달성하기 위해 Zig 개발자는 반드시 메모리를 직접 관리해야 하며, 메모리 할당 실패를 처리해야 합니다.

이는 Zig 표준 라이브러리에서도 마찬가지입니다. 메모리 할당이 필요한 모든 함수는 할당자 파라미터를 필요로 합니다. 결과적으로 Zig 표준 라이브러리는 프리스탠딩 타겟에서도 사용할 수 있습니다.

오류 처리에 대한 새로운 접근 방식에 더해, Zig는 모든 리소스 관리(메모리 뿐 아니라)를 간단하고 쉽게 검증할 수 있도록 해주는 defererrdefer를 제공합니다.

defer의 예제는 FFI/바인딩 없이 C 라이브러리와 연동을 확인하세요. errdefer의 예제는 다음과 같습니다:

const Device = struct {
    name: []u8,

    fn create(allocator: *Allocator, id: u32) !Device {
        const device = try allocator.create(Device);
        errdefer allocator.destroy(device);

        device.name = try std.fmt.allocPrint(allocator, "Device(id={d})", id);
        errdefer allocator.free(device.name);

        if (id == 0) return error.ReservedDeviceId;

        return device;
    }
};

오류 처리에 대한 새로운 접근

오류도 값이며, 무시되면 안됩니다:

discard.zig

const std = @import("std");

pub fn main() void {
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
$ zig build-exe discard.zig
doctest-efc51471/discard.zig:4:30: error: error is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-efc51471/discard.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: zig/lib/std/start.zig:501:17
    callMainWithArgs: zig/lib/std/start.zig:469:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

오류는 catch로 처리할 수 있습니다:

catch.zig

const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| label: {
        std.debug.print("unable to open file: {}\n", .{err});
        const stderr = std.io.getStdErr();
        break :label stderr;
    };
    file.writeAll("all your codebase are belong to us\n") catch return;
}
$ zig build-exe catch.zig
$ ./catch
unable to open file: error.FileNotFound
all your codebase are belong to us

try 키워드는 catch |err| return err를 줄인 것입니다:

try.zig

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
    defer file.close();
    try file.writeAll("all your codebase are belong to us\n");
}
$ zig build-exe try.zig
$ ./try
error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1768:23: 0x1066558 in openatZ (try)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:880:16: 0x1036f37 in openFileZ (try)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:827:5: 0x1033a7e in openFile (try)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-1b8f5a7c/try.zig:4:18: 0x10338c8 in main (try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

스택트레이스가 아니라 오류 리턴 트레이스임을 주의하세요. 저 코드는 스택을 풀어 트레이스를 만드는 비용이 없습니다.

오류에 switch 키워드를 사용하면 발생 가능한 모든 오류가 처리되도록 할 수 있습니다:

test.zig

const std = @import("std");

test "switch on error" {
    _ = parseInt("hi", 10) catch |err| switch (err) {};
}

fn parseInt(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = try charToDigit(c);

        if (digit >= radix) {
            return error.DigitExceedsRadix;
        }

        x = try std.math.mul(u64, x, radix);
        x = try std.math.add(u64, x, digit);
    }

    return x;
}

fn charToDigit(c: u8) !u8 {
    const value = switch (c) {
        '0'...'9' => c - '0',
        'A'...'Z' => c - 'A' + 10,
        'a'...'z' => c - 'a' + 10,
        else => return error.InvalidCharacter,
    };

    return value;
}
$ zig test test.zig
doctest-eaef1263/test.zig:4:40: error: switch must handle all possibilities
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^~~~~~~~~~~~~~~
doctest-eaef1263/test.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
doctest-eaef1263/test.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'
doctest-eaef1263/test.zig:4:40: note: unhandled error value: 'error.Overflow'

unreachable 키워드는 오류가 발생하지 않을 것을 assert 하는데 사용합니다:

unreachable.zig

const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
    file.writeAll("all your codebase are belong to us\n") catch unreachable;
}
$ zig build-exe unreachable.zig
$ ./unreachable
thread 2966 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1768:23: 0x1069498 in openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:880:16: 0x1038717 in openFileZ (unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:827:5: 0x1035eee in openFile (unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-29d25546/unreachable.zig:4:77: 0x1033c9f in main (unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10334a9 in posixCallMainAndExit (unreachable)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1033011 in _start (unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

이는 안전하지 않은 빌드 모드에서 undefined behavior를 발생시키므로, 반드시 성공이 보장되는 때에만 사용하십시오.

모든 타겟에서 스택트레이스 지원

이 페이지에 있는 스택트레이스와 오류 리턴 트레이스는 모든 티어 1 Support와 일부 티어 2 지원 타겟에서 작동합니다. 프리스탠딩에서도요!

더하여, 표준 라이브러리는 아무 지점에서나 스택트레이스를 저장했다가 나중에 표준 오류로 출력할 수 있는 기능이 있습니다.

stack_traces.zig

const std = @import("std");

var address_buffer: [8]usize = undefined;

var trace1 = std.builtin.StackTrace{
    .instruction_addresses = address_buffer[0..4],
    .index = 0,
};

var trace2 = std.builtin.StackTrace{
    .instruction_addresses = address_buffer[4..],
    .index = 0,
};

pub fn main() void {
    foo();
    bar();

    std.debug.print("first one:\n", .{});
    std.debug.dumpStackTrace(trace1);
    std.debug.print("\n\nsecond one:\n", .{});
    std.debug.dumpStackTrace(trace2);
}

fn foo() void {
    std.debug.captureStackTrace(null, &trace1);
}

fn bar() void {
    std.debug.captureStackTrace(null, &trace2);
}
$ zig build-exe stack_traces.zig
$ ./stack_traces
first one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x10383b7 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-c76f3067/stack_traces.zig:26:32: 0x1035e1c in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-c76f3067/stack_traces.zig:16:8: 0x1033d88 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033639 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^


second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x10383b7 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-c76f3067/stack_traces.zig:30:32: 0x1035e3c in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-c76f3067/stack_traces.zig:17:8: 0x1033d8d in main (stack_traces)
    bar();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033639 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^

이 기술은 현재 진행 중인 GeneralPurposeDebugAllocator 프로젝트에서 사용되고 있는 것을 볼 수 있습니다.

Generic한 데이터구조와 함수

타입은 컴파일 타임에 반드시 알고 있어야 하는 값입니다:

types.zig

const std = @import("std");
const assert = std.debug.assert;

test "types are values" {
    const T1 = u8;
    const T2 = bool;
    assert(T1 != T2);

    const x: T2 = true;
    assert(x);
}
$ zig test types.zig
1/1 types.test.types are values... OK
All 1 tests passed.

Generic 데이터 구조는 단순히 type을 리턴하는 함수입니다:

generics.zig

const std = @import("std");

fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

pub fn main() void {
    var buffer: [10]i32 = undefined;
    var list = List(i32){
        .items = &buffer,
        .len = 0,
    };
    list.items[0] = 1234;
    list.len += 1;

    std.debug.print("{d}\n", .{list.items.len});
}
$ zig build-exe generics.zig
$ ./generics
10

컴파일 타임 리플렉션과 컴파일 타입 코드 실행

빌트인 함수인 @typeInfo는 리플렉션을 제공합니다:

reflection.zig

const std = @import("std");

const Header = struct {
    magic: u32,
    name: []const u8,
};

pub fn main() void {
    printInfoAboutStruct(Header);
}

fn printInfoAboutStruct(comptime T: type) void {
    const info = @typeInfo(T);
    inline for (info.Struct.fields) |field| {
        std.debug.print(
            "{s} has a field called {s} with type {s}\n",
            .{
                @typeName(T),
                field.name,
                @typeName(field.type),
            },
        );
    }
}
$ zig build-exe reflection.zig
$ ./reflection
reflection.Header has a field called magic with type u32
reflection.Header has a field called name with type []const u8

Zig 표준 라이브러리는 이 기술을 이용하여 포맷 출력을 구현합니다. 작고 단순한 언어이지만, Zig의 포맷 출력은 모두 Zig 안에 구현되어 있습니다. 반면 C에서는 printf를 위한 컴파일 오류가 컴파일러에 하드코딩 되어 있습니다. 비슷하게 Rust에서는 포맷 출력 매크로는 컴파일러에 하드코딩 되어 있습니다.

Zig는 또한 함수와 코드 블록을 컴파일 타임에 평가할 수 있습니다. 어떤 경우 전역 변수 초기화 같은 표현은 암시적으로 컴파일 타임에 평가됩니다. 다른 방법으로는 comptime 키워드를 이용하여 명시적으로 컴파일 타임에 코드를 평가할 수 있습니다. 이는 특히 assertion과 함께 사용하면 강력합니다:

test.zig

const std = @import("std");
const assert = std.debug.assert;

fn fibonacci(x: u32) u32 {
    if (x <= 1) return x;
    return fibonacci(x - 1) + fibonacci(x - 2);
}

test "compile-time evaluation" {
    var array: [fibonacci(6)]i32 = undefined;

    @memset(&array, 42);

    comptime {
        assert(array.len == 12345);
    }
}
$ zig test test.zig
zig/lib/std/debug.zig:403:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
doctest-d20f5426/test.zig:15:15: note: called from here
        assert(array.len == 12345);
        ~~~~~~^~~~~~~~~~~~~~~~~~~~

FFI나 바인딩 없이 C 라이브러리와 연동

@cImport는 Zig에서 타입, 변수, 함수, 그리고 단순한 매크로를 직접적으로 가져와 사용하는 데 쓰입니다. 심지어 C의 인라인 함수도 변환하여 Zig로 가져옵니다.

libsoundio를 사용하여 사인파를 만들어내는 예제입니다:

sine.zig

const c = @cImport(@cInclude("soundio/soundio.h"));
const std = @import("std");

fn sio_err(err: c_int) !void {
    switch (err) {
        c.SoundIoErrorNone => {},
        c.SoundIoErrorNoMem => return error.NoMem,
        c.SoundIoErrorInitAudioBackend => return error.InitAudioBackend,
        c.SoundIoErrorSystemResources => return error.SystemResources,
        c.SoundIoErrorOpeningDevice => return error.OpeningDevice,
        c.SoundIoErrorNoSuchDevice => return error.NoSuchDevice,
        c.SoundIoErrorInvalid => return error.Invalid,
        c.SoundIoErrorBackendUnavailable => return error.BackendUnavailable,
        c.SoundIoErrorStreaming => return error.Streaming,
        c.SoundIoErrorIncompatibleDevice => return error.IncompatibleDevice,
        c.SoundIoErrorNoSuchClient => return error.NoSuchClient,
        c.SoundIoErrorIncompatibleBackend => return error.IncompatibleBackend,
        c.SoundIoErrorBackendDisconnected => return error.BackendDisconnected,
        c.SoundIoErrorInterrupted => return error.Interrupted,
        c.SoundIoErrorUnderflow => return error.Underflow,
        c.SoundIoErrorEncodingString => return error.EncodingString,
        else => return error.Unknown,
    }
}

var seconds_offset: f32 = 0;

fn write_callback(
    maybe_outstream: ?[*]c.SoundIoOutStream,
    frame_count_min: c_int,
    frame_count_max: c_int,
) callconv(.C) void {
    _ = frame_count_min;
    const outstream: *c.SoundIoOutStream = &maybe_outstream.?[0];
    const layout = &outstream.layout;
    const float_sample_rate: f32 = @floatFromInt(outstream.sample_rate);
    const seconds_per_frame = 1.0 / float_sample_rate;
    var frames_left = frame_count_max;

    while (frames_left > 0) {
        var frame_count = frames_left;

        var areas: [*]c.SoundIoChannelArea = undefined;
        sio_err(c.soundio_outstream_begin_write(
            maybe_outstream,
            @ptrCast(&areas),
            &frame_count,
        )) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});

        if (frame_count == 0) break;

        const pitch = 440.0;
        const radians_per_second = pitch * 2.0 * std.math.pi;
        var frame: c_int = 0;
        while (frame < frame_count) : (frame += 1) {
            const float_frame: f32 = @floatFromInt(frame);
            const sample = std.math.sin((seconds_offset + float_frame *
                seconds_per_frame) * radians_per_second);
            {
                var channel: usize = 0;
                while (channel < @as(usize, @intCast(layout.channel_count))) : (channel += 1) {
                    const channel_ptr = areas[channel].ptr;
                    const sample_ptr: *f32 = @alignCast(@ptrCast(&channel_ptr[@intCast(areas[channel].step * frame)]));
                    sample_ptr.* = sample;
                }
            }
        }
        const float_frame_count: f32 = @floatFromInt(frame_count);
        seconds_offset += seconds_per_frame * float_frame_count;

        sio_err(c.soundio_outstream_end_write(maybe_outstream)) catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)});

        frames_left -= frame_count;
    }
}

pub fn main() !void {
    const soundio = c.soundio_create();
    defer c.soundio_destroy(soundio);

    try sio_err(c.soundio_connect(soundio));

    c.soundio_flush_events(soundio);

    const default_output_index = c.soundio_default_output_device_index(soundio);
    if (default_output_index < 0) return error.NoOutputDeviceFound;

    const device = c.soundio_get_output_device(soundio, default_output_index) orelse return error.OutOfMemory;
    defer c.soundio_device_unref(device);

    std.debug.print("Output device: {s}\n", .{device.*.name});

    const outstream = c.soundio_outstream_create(device) orelse return error.OutOfMemory;
    defer c.soundio_outstream_destroy(outstream);

    outstream.*.format = c.SoundIoFormatFloat32NE;
    outstream.*.write_callback = write_callback;

    try sio_err(c.soundio_outstream_open(outstream));

    try sio_err(c.soundio_outstream_start(outstream));

    while (true) c.soundio_wait_events(soundio);
}

$ zig build-exe sine.zig -lsoundio -lc
$ ./sine
Output device: Built-in Audio Analog Stereo
^C

이 Zig 코드는 동일한 C 코드에 비해 월등히 단순할 뿐 아니라, 더 많은 안전성 보호를 해주며, API 바인딩도 없이 C 헤더 파일을 직접 가져와 이 모든 일을 해냅니다.

Zig는 C가 C 라이브러리를 사용하는 것보다도 더 C 라이브러리를 사용하는 데에 좋습니다.

Zig도 하나의 C 컴파일러

Zig로 C 코드를 빌드하는 예입니다:

hello.c

#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello world\n");
    return 0;
}
$ zig build-exe hello.c --library c
$ ./hello
Hello world

--verbose-cc를 사용하면 무슨 C 컴파일러 명령어를 사용했는지 확인할 수 있습니다:

$ zig build-exe hello.c --library c --verbose-cc
zig cc -MD -MV -MF zig-cache/tmp/42zL6fBH8fSo-hello.o.d -nostdinc -fno-spell-checking -isystem /home/andy/dev/zig/build/lib/zig/include -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-gnu -isystem /home/andy/dev/zig/build/lib/zig/libc/include/generic-glibc -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-any -isystem /home/andy/dev/zig/build/lib/zig/libc/include/any-linux-any -march=native -g -fstack-protector-strong --param ssp-buffer-size=4 -fno-omit-frame-pointer -o zig-cache/tmp/42zL6fBH8fSo-hello.o -c hello.c -fPIC

명령어를 다시 실행하면 출력되는 것 없이 바로 종료됩니다:

$ time zig build-exe hello.c --library c --verbose-cc

real	0m0.027s
user	0m0.018s
sys	0m0.009s

이는 빌드 결과 캐싱 덕분입니다. Zig는 .d 파일을 자동으로 파싱하여 중복된 작업을 하지 않도록 해주는 강력한 캐싱 시스템을 사용합니다.

Zig는 C 코드를 컴파일할 수 있을 뿐 아니라 Zig를 C 컴파일러로 쓸 좋은 이유가 있습니다: Zig에는 libc가 포함됩니다.

C 코드가 의존할 수 있도록 함수, 변수, 타입을 export

Zig의 주된 용도 중 하나는 다른 프로그래밍 언어에서 쓸 수 있도록 C ABI로 라이브러리를 export하는 것입니다. export 키워드를 함수, 변수, 타입 앞에 쓰면 라이브러리 API의 일부가 됩니다.

mathtest.zig

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

정적인 라이브러리를 만들려면:

$ zig build-lib mathtest.zig

동적 라이브러리를 만들려면:

$ zig build-lib mathtest.zig -dynamic

Zig 빌드 시스템의 예제입니다:

test.c

#include "mathtest.h"
#include <stdio.h>

int main(int argc, char **argv) {
    int32_t result = add(42, 1337);
    printf("%d\n", result);
    return 0;
}

build.zig

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

pub fn build(b: *Builder) void {
    const lib = b.addSharedLibrary("mathtest", "mathtest.zig", b.version(1, 0, 0));

    const exe = b.addExecutable("test", null);
    exe.addCSourceFile("test.c", &[_][]const u8{"-std=c99"});
    exe.linkLibrary(lib);
    exe.linkSystemLibrary("c");

    b.default_step.dependOn(&exe.step);

    const run_cmd = exe.run();

    const test_step = b.step("test", "Test the program");
    test_step.dependOn(&run_cmd.step);
}

$ zig build test
1379

크로스 컴파일 기본 제공

Zig는 지원 목록에 있는 타겟 중 티어 3 지원 이상이면 모두 빌드할 수 있습니다. “크로스 툴체인” 같은 것은 설치할 필요도 없습니다. 네이티브 Hello World입니다:

hello.zig

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe hello.zig
$ ./hello
Hello, world!

이제 x86_64-windows, x86_64-macos, aarch64-linux로 빌드하려면:

$ zig build-exe hello.zig -target x86_64-windows
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
$ zig build-exe hello.zig -target x86_64-macos
$ file hello
hello: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
$ zig build-exe hello.zig -target aarch64-linux
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

티어 3 이상의 모든 타겟에서 동작하며, 티어 3 이상의 모든 타겟을 대상으로 사용 가능합니다.

Zig에는 libc가 포함됩니다

zig targets로 사용 가능한 libc 타겟을 확인할 수 있습니다:

...
 "libc": [
  "aarch64_be-linux-gnu",
  "aarch64_be-linux-musl",
  "aarch64_be-windows-gnu",
  "aarch64-linux-gnu",
  "aarch64-linux-musl",
  "aarch64-windows-gnu",
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "armeb-windows-gnu",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-windows-gnu",
  "i386-linux-gnu",
  "i386-linux-musl",
  "i386-windows-gnu",
  "mips64el-linux-gnuabi64",
  "mips64el-linux-gnuabin32",
  "mips64el-linux-musl",
  "mips64-linux-gnuabi64",
  "mips64-linux-gnuabin32",
  "mips64-linux-musl",
  "mipsel-linux-gnu",
  "mipsel-linux-musl",
  "mips-linux-gnu",
  "mips-linux-musl",
  "powerpc64le-linux-gnu",
  "powerpc64le-linux-musl",
  "powerpc64-linux-gnu",
  "powerpc64-linux-musl",
  "powerpc-linux-gnu",
  "powerpc-linux-musl",
  "riscv64-linux-gnu",
  "riscv64-linux-musl",
  "s390x-linux-gnu",
  "s390x-linux-musl",
  "sparc-linux-gnu",
  "sparcv9-linux-gnu",
  "wasm32-freestanding-musl",
  "x86_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-windows-gnu"
 ],

이 타겟들에 대해 --library c를 사용하면 어떠한 시스템 파일에도 의존하지 않는다는 것입니다!

C hello world 예제를 다시 보겠습니다:

$ zig build-exe hello.c --library c
$ ./hello
Hello world
$ ldd ./hello
	linux-vdso.so.1 (0x00007ffd03dc9000)
	libc.so.6 => /lib/libc.so.6 (0x00007fc4b62be000)
	libm.so.6 => /lib/libm.so.6 (0x00007fc4b5f29000)
	libpthread.so.0 => /lib/libpthread.so.0 (0x00007fc4b5d0a000)
	libdl.so.2 => /lib/libdl.so.2 (0x00007fc4b5b06000)
	librt.so.1 => /lib/librt.so.1 (0x00007fc4b58fe000)
	/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fc4b6672000)

glibc는 정적인 빌드를 지원하지 않지만, musl은 지원합니다:

$ zig build-exe hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
  not a dynamic executable

이 예제에서는 Zig가 musl libc를 소스코드로 빌드한 뒤 링크했습니다. 캐싱 시스템 덕분에 x86_64-linux용 musl libc의 빌드가 계속 사용 가능하여, 다시 libc가 필요하게 되더라도 즉각 사용할 수 있습니다.

이 기능은 어떤 플랫폼에서도 사용 가능합니다. Windows와 macOS 사용자는 위에 나열된 어떤 타겟으로도 C 코드를 빌드하고 libc에 링크할 수 있습니다. 비슷하게 다른 아키텍쳐로의 크로스 컴파일도 가능합니다:

$ zig build-exe hello.c --library c -target aarch64-linux-gnu
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped

어떤 면에서 Zig는 C 컴파일러보다도 나은 C 컴파일러입니다!

이 기능은 Zig에 크로스 컴파일 툴체인을 포함하는 것 이상입니다. 예를 들어 Zig에 포함되는 libc 헤더의 전체 크기는 압축 없이 22 MiB입니다. 한편, musl libc + Linux는 x86_64 헤더만 8 MiB이며, glibc 용은 3.1 MiB 인데(glibc에는 Linux 헤더 미포함), Zig는 현재 40종의 libc를 포함합니다. 네이티브 번들링을 포함하면 444 MiB가 될겁니다. 하지만, 제가 만든 process_headers 툴수작업 덕분에 이 모든 타겟에 libc를 비롯 compiler-rt, libunwind, libcx를 지원하며 clang 호환 C 컴파일러임에도 불구하고 Zig 바이너리 압축파일들은 대략 30 MiB를 유지할 수 있었습니다. 비교하자면, llvm.org에서 받은 Windows용 clang 8.0.0 바이너리 빌드는 그 자체만으로 132 MiB 입니다.

티어 1 지원 타겟만 완전히 테스트 되었음을 주의해 주세요. 더 많은 libc들을 추가할 계획이며 (Windows 포함), 모든 libc의 빌드에 대한 테스트 커버리지도 추가할 계획입니다.

Zig 패키지 매니저도 계획되어 있으나, 아직 완성되지 않았습니다. 가능해질 일 중 하나는 C 라이브러리용 패키지를 만드는 것입니다. Zig 빌드 시스템은 Zig 개발자와 C 개발자 모두에게 매력있게 다가올 것입니다.

Zig 빌드 시스템

Zig는 빌드 시스템이 포함 되어 있으므로 make, cmake 같은 것은 사용하실 필요 없습니다.

$ zig init-exe
Created build.zig
Created src/main.zig

Next, try `zig build --help` or `zig build run`

src/main.zig

const std = @import("std");

pub fn main() anyerror!void {
    std.debug.print("All your base are belong to us.\n");
}

build.zig

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

pub fn build(b: *Builder) void {
    const mode = b.standardReleaseOptions();
    const exe = b.addExecutable("example", "src/main.zig");
    exe.setBuildMode(mode);

    const run_cmd = exe.run();

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    b.default_step.dependOn(&exe.step);
    b.installArtifact(exe);
}

--help 메뉴를 살펴보겠습니다.

$ zig build --help
Usage: zig build [steps] [options]

Steps:
  install (default)      Copy build artifacts to prefix path
  uninstall              Remove build artifacts from prefix path
  run                    Run the app

General Options:
  --help                 Print this help and exit
  --verbose              Print commands before executing them
  --prefix [path]        Override default install prefix
  --search-prefix [path] Add a path to look for binaries, libraries, headers

Project-Specific Options:
  -Dtarget=[string]      The CPU architecture, OS, and ABI to build for.
  -Drelease-safe=[bool]  optimizations on and safety on
  -Drelease-fast=[bool]  optimizations on and safety off
  -Drelease-small=[bool] size optimizations on and safety off

Advanced Options:
  --build-file [file]         Override path to build.zig
  --cache-dir [path]          Override path to zig cache directory
  --override-lib-dir [arg]    Override path to Zig lib directory
  --verbose-tokenize          Enable compiler debug output for tokenization
  --verbose-ast               Enable compiler debug output for parsing into an AST
  --verbose-link              Enable compiler debug output for linking
  --verbose-ir                Enable compiler debug output for Zig IR
  --verbose-llvm-ir           Enable compiler debug output for LLVM IR
  --verbose-cimport           Enable compiler debug output for C imports
  --verbose-cc                Enable compiler debug output for C compilation
  --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features

사용 가능한 스텝 중 하나가 실행되는걸 보실 수 있습니다.

$ zig build run
All your base are belong to us.

빌드 스크립트 예제입니다:

비동기 함수를 통한 동시성

Zig 0.5.0 버전에서 비동기 함수가 도입 되었습니다. 이 기능은 운영체제나 힙에 할당된 메모리에도 의존하지 않습니다. 이는 프리스탠딩 타겟에도 비동기 함수를 쓸 수 있다는걸 의미합니다.

Zig는 함수가 비동기인지 유추하며, async/await을 비동기가 아닌 함수에도 허용하는데, 이는 Zig 라이브러리가 I/O의 블로킹이나 비동기 여부와 상관 없다는 것을 의미합니다. Zig는 함수의 색깔을 지양합니다.

Zig 표준 라이브러리는 M:N 동시성을 위해 비동기 함수를 쓰레드 풀에 다중화 하는 이벤트 루프를 구현합니다. 멀티쓰레드 안전성과 경쟁상태 감지는 현재 활발히 진행 중인 연구 분야입니다.

다양한 타겟 지원

Zig는 “지원 티어” 체계를 이용해 다른 타겟에 대한 지원 수준을 소통합니다. 1 티어 지원은 기준이 높지만, 2 티어 지원도 꽤 괜찮으니 참고하세요.

지원 목록

free standingLinux 3.16+macOS 10.13+Windows 8.1+FreeBSD 12.0+NetBSD 8.0+DragonFly​BSD 5.8+UEFI
x86_64티어 1티어 1티어 1티어 2티어 2티어 2티어 2티어 2
arm64티어 1티어 2티어 2티어 3티어 3티어 3N/A티어 3
arm32티어 1티어 2N/A티어 3티어 3티어 3N/A티어 3
mips32 LE티어 1티어 2N/AN/A티어 3티어 3N/AN/A
i386티어 1티어 2티어 4티어 2티어 3티어 3N/A티어 2
riscv64티어 1티어 2N/AN/A티어 3티어 3N/A티어 3
bpf티어 3티어 3N/AN/A티어 3티어 3N/AN/A
hexagon티어 3티어 3N/AN/A티어 3티어 3N/AN/A
mips32 BE티어 3티어 3N/AN/A티어 3티어 3N/AN/A
mips64티어 3티어 3N/AN/A티어 3티어 3N/AN/A
amdgcn티어 3티어 3N/AN/A티어 3티어 3N/AN/A
sparc티어 3티어 3N/AN/A티어 3티어 3N/AN/A
s390x티어 3티어 3N/AN/A티어 3티어 3N/AN/A
lanai티어 3티어 3N/AN/A티어 3티어 3N/AN/A
powerpc32티어 3티어 3티어 4N/A티어 3티어 3N/AN/A
powerpc64티어 3티어 3티어 4N/A티어 3티어 3N/AN/A
avr티어 4티어 4N/AN/A티어 4티어 4N/AN/A
riscv32티어 4티어 4N/AN/A티어 4티어 4N/A티어 4
xcore티어 4티어 4N/AN/A티어 4티어 4N/AN/A
nvptx티어 4티어 4N/AN/A티어 4티어 4N/AN/A
msp430티어 4티어 4N/AN/A티어 4티어 4N/AN/A
r600티어 4티어 4N/AN/A티어 4티어 4N/AN/A
arc티어 4티어 4N/AN/A티어 4티어 4N/AN/A
tce티어 4티어 4N/AN/A티어 4티어 4N/AN/A
le티어 4티어 4N/AN/A티어 4티어 4N/AN/A
amdil티어 4티어 4N/AN/A티어 4티어 4N/AN/A
hsail티어 4티어 4N/AN/A티어 4티어 4N/AN/A
spir티어 4티어 4N/AN/A티어 4티어 4N/AN/A
kalimba티어 4티어 4N/AN/A티어 4티어 4N/AN/A
shave티어 4티어 4N/AN/A티어 4티어 4N/AN/A
renderscript티어 4티어 4N/AN/A티어 4티어 4N/AN/A

WebAssembly 지원 목록

free standingemscriptenWASI
wasm32티어 1티어 3티어 1
wasm64티어 4티어 4티어 4

티어 시스템

티어 1 지원

티어 2 지원

티어 3 지원

티어 4 지원

패키지 관리자에게 친숙

표준 Zig 컴파일러는 아직 완전히 자체 호스팅 되지 않지만, 어쨌든 정확히 3 단계만 거치면 시스템 C++ 컴파일러로부터 어떤 타겟에도 자체 호스팅 되는 완전한 Zig 컴파일러를 얻을 수 있습니다. Maya Rashish가 얘기한 것처럼, Zig를 다른 플랫폼으로 포팅하는 것은 재밌고 빠릅니다.

디버그 외의 빌드 모드들은 재현 가능하며 결정적입니다.

다운로드 페이지의 JSON 버전도 있습니다.

Zig 팀의 몇몇 멤버는 패키지 관리 경험이 있습니다.