Zig is a general-purpose programming language and toolchain for maintaining robust , optimal , and reusable software.

Focus on debugging your application rather than debugging your programming language knowledge.

Zig's entire syntax is specified with a 500-line PEG grammar file.

There is no hidden control flow, no hidden memory allocations, no preprocessor, and no macros. If Zig code doesn't look like it's jumping away to call a function, then it isn't. This means you can be sure that the following code calls only foo() and then bar() , and this is guaranteed without needing to know the types of anything:

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

Examples of hidden control flow:

D has @property functions, which are methods that you call with what looks like field access, so in the above example, c.d might call a function.

functions, which are methods that you call with what looks like field access, so in the above example, might call a function. C++, D, and Rust have operator overloading, so the + operator might call a function.

operator might call a function. C++, D, and Go have throw/catch exceptions, so foo() might throw an exception, and prevent bar() from being called.

Zig promotes code maintenance and readability by making all control flow managed exclusively with language keywords and function calls.

Zig has four build modes, and they can all be mixed and matched all the way down to scope granularity.

Parameter Debug ReleaseSafe ReleaseFast ReleaseSmall Optimizations - improve speed, harm debugging, harm compile time -O3 -O3 -Os Runtime Safety Checks - harm speed, harm size, crash instead of undefined behavior On On

Here is what Integer Overflow looks like at compile time, regardless of the build mode:

test.zig

test "integer overflow at compile time" { const x: u8 = 255 ; const y = x + 1 ; }

$ zig test test.zig ./docgen_tmp/test.zig:3:17: error: operation caused overflow const y = x + 1; ^

Here is what it looks like at runtime, in safety-checked builds:

test.zig

test "integer overflow at runtime" { var x: u8 = 255 ; x += 1 ; }

$ zig test test.zig 1/1 test "integer overflow at runtime"...integer overflow /home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:7 : 0x204e00 in test "integer overflow at runtime" (test) x += 1; ^ /home/andy/Downloads/zig/lib/std/special/test_runner.zig:48:28 : 0x2308d5 in std.special.main (test) } else test_fn.func(); ^ /home/andy/Downloads/zig/lib/std/start.zig:252:37 : 0x205c4d in std.start.posixCallMainAndExit (test) const result = root.main() catch |err| { ^ /home/andy/Downloads/zig/lib/std/start.zig:123:5 : 0x20598f in std.start._start (test) @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{}); ^ Tests failed. Use the following command to reproduce the failure: /home/andy/dev/www.ziglang.org/docgen_tmp/test

Those stack traces work on all targets, including freestanding.

With Zig one can rely on a safety-enabled build mode, and selectively disable safety at the performance bottlenecks. For example the previous example could be modified like this:

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

Zig uses undefined behavior as a razor sharp tool for both bug prevention and performance enhancement.

Speaking of performance, Zig is faster than C.

The reference implementation uses LLVM as a backend for state of the art optimizations.

What other projects call "Link Time Optimization" Zig does automatically.

For native targets, advanced CPU features are enabled ( -march=native ), thanks to the fact that Cross-compiling is a first-class use case.

), thanks to the fact that Cross-compiling is a first-class use case. Carefully chosen undefined behavior. For example, in Zig both signed and unsigned integers have undefined behavior on overflow, contrasted to only signed integers in C. This facilitates optimizations that are not available in C.

Zig directly exposes a SIMD vector type, making it easy to write portable vectorized code.

Please note that Zig is not a fully safe language. For those interested in following Zig's safety story, subscribe to these issues:

The Zig Standard Library integrates with libc, but does not depend on it. Here's Hello World:

hello.zig

const std = @import ( "std" ); pub fn main () void { std.debug.print( "Hello, world!

" , .{}); }

$ zig build-exe hello.zig $ ./hello Hello, world!

When compiled with --release-small, debug symbols stripped, single-threaded mode, this produces a 9.8 KiB static executable for the x86_64-linux target:

$ zig build-exe hello.zig --release-small --strip --single-threaded $ wc -c hello 9944 hello $ ldd hello not a dynamic executable

A Windows build is even smaller, coming out to 4096 bytes:

$ zig build-exe hello.zig --release-small --strip --single-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

Top level declarations such as global variables are order-independent and lazily analyzed. The initialization value of global variables is evaluated at compile-time.

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 test "global variables"...OK All 1 tests passed.

In other programming languages, null references are the source of many runtime exceptions, and even stand accused of being the worst mistake of computer science.

Unadorned Zig pointers cannot be null:

test.zig

test "null @intToPtr" { const ptr = @intToPtr (* i32 , 0x0 ); }

$ zig test test.zig ./docgen_tmp/test.zig:2:17: error: pointer type '*i32' does not allow address zero const ptr = @intToPtr(*i32, 0x0); ^

However any type can be made into an optional type by prefixing it with ? :

optional_syntax.zig

const std = @import ( "std" ); const assert = std.debug.assert; test "null @intToPtr" { const ptr = @intToPtr (?* i32 , 0x0 ); assert(ptr == null ); }

$ zig test optional_syntax.zig 1/1 test "null @intToPtr"...OK All 1 tests passed.

To unwrap an optional value, one can use orelse to provide a default value:

extern fn malloc (size: size_t) ?* u8 ; fn doAThing () ?*Foo { const ptr = malloc( 1234 ) orelse return null ; }

Another option is to use if:

fn doAThing (optional_foo: ?*Foo) void { if (optional_foo) |foo| { doSomethingWithFoo(foo); } }

The same syntax works with while:

iterator.zig

const std = @import ( "std" ); pub fn main () void { const msg = "hello this is dog" ; var it = std.mem.tokenize(msg, " " ); while (it.next()) |item| { std.debug.print( "{}

" , .{item}); } }

$ zig build-exe iterator.zig $ ./iterator hello this is dog

A library written in Zig is eligible to be used anywhere:

In order to accomplish this, Zig programmers must manage their own memory, and must handle memory allocation failure.

This is true of the Zig Standard Library as well. Any functions that need to allocate memory accept an allocator parameter. As a result, the Zig Standard Library can be used even for the freestanding target.

In addition to A fresh take on error handling, Zig provides defer and errdefer to make all resource management - not only memory - simple and easily verifiable.

For an example of defer , see Integration with C libraries without FFI/bindings. Here is an example of using 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={})" , id); errdefer allocator.free(device.name); if (id == 0 ) return error .ReservedDeviceId; return device; } };

Errors are values, and may not be ignored:

discard.zig

const std = @import ( "std" ); pub fn main () void { _ = std.fs.cwd().openFile( "does_not_exist/foo.txt" , .{}); }

$ zig build-exe discard.zig ./docgen_tmp/discard.zig:4:30: error: error is discarded _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}); ^

Errors can be handled with 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: {}

" , .{err}); const stderr = std.io.getStdErr(); break :label stderr; }; file.writeAll( "all your codebase are belong to us

" ) catch return ; }

$ zig build-exe catch.zig $ ./catch unable to open file: error.FileNotFound all your codebase are belong to us

The keyword try is a shortcut for 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

" ); }

$ zig build-exe try.zig $ ./try error: FileNotFound /home/andy/Downloads/zig/lib/std/os.zig:1167:23 : 0x22a73b in std.os.openatZ (try) ENOENT => return error.FileNotFound, ^ /home/andy/Downloads/zig/lib/std/fs.zig:711:13 : 0x20c93a in std.fs.Dir.openFileZ (try) try os.openatZ(self.fd, sub_path, os_flags, 0); ^ /home/andy/Downloads/zig/lib/std/fs.zig:651:9 : 0x20a93f in std.fs.Dir.openFile (try) return self.openFileZ(&path_c, flags); ^ /home/andy/dev/www.ziglang.org/docgen_tmp/try.zig:4:18 : 0x22f4b8 in main (try) const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{}); ^

Note that is an Error Return Trace, not a stack trace. The code did not pay the price of unwinding the stack to come up with that trace.

The switch keyword used on an error ensures that all possible errors are handled:

test.zig

const std = @import ( "std" ); test "switch on error" { const result = 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 ./docgen_tmp/test.zig:4:51: error: error.Overflow not handled in switch const result = parseInt("hi", 10) catch |err| switch (err) {}; ^ ./docgen_tmp/test.zig:3:24: note: referenced here test "switch on error" { ^ ./docgen_tmp/test.zig:4:51: error: error.DigitExceedsRadix not handled in switch const result = parseInt("hi", 10) catch |err| switch (err) {}; ^ ./docgen_tmp/test.zig:4:51: error: error.InvalidCharacter not handled in switch const result = parseInt("hi", 10) catch |err| switch (err) {}; ^

The keyword unreachable is used to assert that no errors will occur:

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

" ) catch unreachable ; }

$ zig build-exe unreachable.zig $ ./unreachable attempt to unwrap error: FileNotFound /home/andy/Downloads/zig/lib/std/os.zig:1167:23 : 0x22a56b in std.os.openatZ (unreachable) ENOENT => return error.FileNotFound, ^ /home/andy/Downloads/zig/lib/std/fs.zig:711:13 : 0x20c76a in std.fs.Dir.openFileZ (unreachable) try os.openatZ(self.fd, sub_path, os_flags, 0); ^ /home/andy/Downloads/zig/lib/std/fs.zig:651:9 : 0x20a76f in std.fs.Dir.openFile (unreachable) return self.openFileZ(&path_c, flags); ^ ???:?:? : 0x206b6c in ??? (???) /home/andy/dev/www.ziglang.org/docgen_tmp/unreachable.zig:4:71 : 0x22f325 in main (unreachable) const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable; ^ /home/andy/Downloads/zig/lib/std/start.zig:242:22 : 0x2049ef in std.start.posixCallMainAndExit (unreachable) root.main(); ^ /home/andy/Downloads/zig/lib/std/start.zig:123:5 : 0x2047cf in std.start._start (unreachable) @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{}); ^ (process terminated by signal)

This invokes undefined behavior in the unsafe build modes, so be sure to use it only when success is guaranteed.

The stack traces and error return traces shown on this page work on all Tier 1 Support and some Tier 2 Support targets. Even freestanding!

In addition, the standard library has the ability to capture a stack trace at any point and then dump it to standard error later:

stack_traces.zig

const std = @import ( "std" ); const builtin = @import ( "builtin" ); var address_buffer: [ 8 ] usize = undefined ; var trace1 = builtin.StackTrace{ .instruction_addresses = address_buffer[ 0 .. 4 ], .index = 0 , }; var trace2 = builtin.StackTrace{ .instruction_addresses = address_buffer[ 4 ..], .index = 0 , }; pub fn main () void { foo(); bar(); std.debug.print( "first one:

" , .{}); std.debug.dumpStackTrace(trace1); std.debug.print( "



second one:

" , .{}); 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/andy/Downloads/zig/lib/std/debug.zig:183:29 : 0x236d14 in std.debug.captureStackTrace (stack_traces) addr.* = it.next() orelse { ^ /home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:27:32 : 0x23486c in foo (stack_traces) std.debug.captureStackTrace(null, &trace1); ^ /home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:17:8 : 0x22f278 in main (stack_traces) foo(); ^ /home/andy/Downloads/zig/lib/std/start.zig:242:22 : 0x2049cf in std.start.posixCallMainAndExit (stack_traces) root.main(); ^ second one: /home/andy/Downloads/zig/lib/std/debug.zig:183:29 : 0x236d14 in std.debug.captureStackTrace (stack_traces) addr.* = it.next() orelse { ^ /home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:31:32 : 0x23488c in bar (stack_traces) std.debug.captureStackTrace(null, &trace2); ^ /home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:18:8 : 0x22f27d in main (stack_traces) bar(); ^ /home/andy/Downloads/zig/lib/std/start.zig:242:22 : 0x2049cf in std.start.posixCallMainAndExit (stack_traces) root.main(); ^

You can see this technique being used in the ongoing GeneralPurposeDebugAllocator project.

Types are values that must be known at compile-time:

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 test "types are values"...OK All 1 tests passed.

A generic data structure is simply a function that returns a 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 , }; std.debug.print( "{}

" , .{list.items.len}); }

$ zig build-exe generics.zig $ ./generics 10

The @typeInfo builtin function provides reflection:

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( "{} has a field called {} with type {}

" , .{ @typeName (T), field.name, @typeName (field.field_type), }, ); } }

$ zig build-exe reflection.zig $ ./reflection Header has a field called magic with type u32 Header has a field called name with type []const u8

The Zig Standard Library uses this technique to implement formatted printing. Despite being a Small, simple language, Zig's formatted printing is implemented entirely in Zig. Meanwhile, in C, compile errors for printf are hard-coded into the compiler. Similarly, in Rust, the formatted printing macro is hard-coded into the compiler.

Zig can also evaluate functions and blocks of code at compile-time. In some contexts, such as global variable initializations, the expression is implicitly evaluated at compile-time. Otherwise, one can explicitly evaluate code at compile-time with the comptime keyword. This can be especially powerful when combined with assertions:

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 ; comptime { assert(array.len == 12345 ); } }

$ zig test test.zig /home/andy/Downloads/zig/lib/std/debug.zig:223:14: error: unable to evaluate constant expression if (!ok) unreachable; // assertion failure ^ ./docgen_tmp/test.zig:13:15: note: called from here assert(array.len == 12345); ^ ./docgen_tmp/test.zig:9:32: note: called from here test "compile-time evaluation" { ^ ./docgen_tmp/test.zig:13:15: note: referenced here assert(array.len == 12345); ^

@cImport directly imports types, variables, functions, and simple macros for use in Zig. It even translates inline functions from C into Zig.

Here is an example of emitting a sine wave using libsoundio:

sine.zig

const c = @cImport ( @cInclude ( "soundio/soundio.h" )); const std = @import ( "std" ); fn sio_err (err: c_int ) ! void { switch ( @intToEnum (c.SoundIoError, err)) { .None => {}, .NoMem => return error .NoMem, .InitAudioBackend => return error .InitAudioBackend, .SystemResources => return error .SystemResources, .OpeningDevice => return error .OpeningDevice, .NoSuchDevice => return error .NoSuchDevice, .Invalid => return error .Invalid, .BackendUnavailable => return error .BackendUnavailable, .Streaming => return error .Streaming, .IncompatibleDevice => return error .IncompatibleDevice, .NoSuchClient => return error .NoSuchClient, .IncompatibleBackend => return error .IncompatibleBackend, .BackendDisconnected => return error .BackendDisconnected, .Interrupted => return error .Interrupted, .Underflow => return error .Underflow, .EncodingString => return error .EncodingString, } } 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 { const outstream = @ptrCast (*c.SoundIoOutStream, maybe_outstream); const layout = &outstream.layout; const float_sample_rate = outstream.sample_rate; const seconds_per_frame = 1.0 / @intToFloat ( f32 , 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 ([*]?[*]c.SoundIoChannelArea, &areas), &frame_count, )) catch |err| std.debug.panic( "write failed: {}" , @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 sample = std.math.sin((seconds_offset + @intToFloat ( f32 , frame) * seconds_per_frame) * radians_per_second); { var channel: usize = 0 ; while (channel < @intCast ( usize , layout.channel_count)) : (channel += 1 ) { const channel_ptr = areas[channel].ptr; const sample_ptr = &channel_ptr[ @intCast ( usize , areas[channel].step * frame)]; @ptrCast (* f32 , @alignCast ( @alignOf ( f32 ), sample_ptr)).* = sample; } } } seconds_offset += seconds_per_frame * @intToFloat ( f32 , frame_count); sio_err(c.soundio_outstream_end_write(maybe_outstream)) catch |err| std.debug.panic( "end write failed: {}" , @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}

" , .{([*] const u8 )(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

This Zig code is significantly simpler than the equivalent C code, as well as having more safety protections, and all this is accomplished by directly importing the C header file - no API bindings.

Zig is better at using C libraries than C is at using C libraries.

Here's an example of Zig building some C code:

hello.c

#include <stdio.h> int main(int argc, char **argv) { printf("Hello world

"); return 0; }

$ zig build-exe --c-source hello.c --library c $ ./hello Hello world

You can use --verbose-cc to see what C compiler command this executed:

$ zig build-exe --c-source 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

Note that if I run the command again, there is no output, and it finishes instantly:

$ time zig build-exe --c-source hello.c --library c --verbose-cc real 0m0.027s user 0m0.018s sys 0m0.009s

This is thanks to Build Artifact Caching. Zig automatically parses the .d file uses a robust caching system to avoid duplicating work.

Not only can Zig compile C code, but there is a very good reason to use Zig as a C compiler: Zig ships with libc.

One of the primary use cases for Zig is exporting a library with the C ABI for other programming languages to call into. The export keyword in front of functions, variables, and types causes them to be part of the library API:

mathtest.zig

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

To make a static library:

$ zig build-lib mathtest.zig

To make a shared library:

$ zig build-lib mathtest.zig -dynamic

Here is an example with the Zig Build System:

test.c

#include "mathtest.h" #include <stdio.h> int main(int argc, char **argv) { int32_t result = add(42, 1337); printf("%d

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

terminal

$ zig build test 1379

Zig can build for any of the targets from the Support Table with Tier 3 Support or better. No "cross toolchain" needs to be installed or anything like that. Here's a native Hello World:

hello.zig

const std = @import ( "std" ); pub fn main () void { std.debug.print( "Hello, world!

" , .{}); }

$ zig build-exe hello.zig $ ./hello Hello, world!

Now to build it for x86_64-windows, x86_64-macosx, and aarch64v8-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-macosx $ file hello hello: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE> $ zig build-exe hello.zig -target aarch64v8-linux $ file hello hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

This works on any Tier 3+ target, for any Tier 3+ target.

You can find the available libc targets with zig targets :

... "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" ],

What this means is that --library c for these targets does not depend on any system files!

Let's look at that C hello world example again:

$ zig build-exe --c-source 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 does not support building statically, but musl does:

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

In this example, Zig built musl libc from source and then linked against it. The build of musl libc for x86_64-linux remains available thanks to the caching system, so any time this libc is needed again it will be available instantly.

This means that this functionality is available on any platform. Windows and macOS users can build Zig and C code, and link against libc, for any of the targets listed above. Similarly code can be cross compiled for other architectures:

$ zig build-exe --c-source hello.c --library c -target aarch64v8-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

In some ways, Zig is a better C compiler than C compilers!

This functionality is more than bundling a cross-compilation toolchain along with Zig. For example, the total size of libc headers that Zig ships is 22 MiB uncompressed. Meanwhile, the headers for musl libc + linux headers on x86_64 alone are 8 MiB, and for glibc are 3.1 MiB (glibc is missing the linux headers), yet Zig currently ships with 40 libcs. With a naive bundling that would be 444 MiB. However, thanks to this process_headers tool that I made, and some good old manual labor, Zig binary tarballs remain roughly 30 MiB total, despite supporting libc for all these targets, as well as compiler-rt, libunwind, and libcxx, and despite being a clang-compatible C compiler. For comparison, the Windows binary build of clang 8.0.0 itself from llvm.org is 132 MiB.

Note that only the Tier 1 Support targets have been thoroughly tested. It is planned to add more libcs (including for Windows), and to add test coverage for building against all the libcs.

It's planned to have a Zig Package Manager, but it's not done yet. One of the things that will be possible is to create a package for C libraries. This will make the Zig Build System attractive for Zig programmers and C programmers alike.

Zig comes with a build system, so you don't need make, cmake, or anything like that.

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

" ); }

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

Let's have a look at that --help menu.

$ 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

You can see that one of the available steps is run .

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

Here are some example build scripts:

Zig 0.5.0 introduced async functions. This feature has no dependency on a host operating system or even heap-allocated memory. That means async functions are available for the freestanding target.

Zig infers whether a function is async, and allows async / await on non-async functions, which means that Zig libraries are agnostic of blocking vs async I/O. Zig avoids function colors.

The Zig Standard Library implements an event loop that multiplexes async functions onto a thread pool for M:N concurrency. Multithreading safety and race detection are areas of active research.

Zig uses a "support tier" system to communicate the level of support for different targets. Note that the bar for Tier 1 Support is high - Tier 2 Support is still quite useful.

Not only can Zig generate machine code for these targets, but the standard library cross-platform abstractions have implementations for these targets. Thus it is practical to write a pure Zig application with no dependency on libc.

The CI server automatically tests these targets on every commit to master branch, and updates the download page with links to pre-built binaries.

These targets have debug info capabilities and therefore produce stack traces on failed assertions.

libc is available for this target even when cross compiling.

All the behavior tests and applicable standard library tests pass for this target. All language features are known to work correctly.

The standard library supports this target, but it's possible that some APIs will give an "Unsupported OS" compile error. One can link with libc or other libraries to fill in the gaps in the standard library.

These targets are known to work, but may not be automatically tested, so there are occasional regressions.

Some tests may be disabled for these targets as we work toward Tier 1 Support.

The standard library has little to no knowledge of the existence of this target.

Because Zig is based on LLVM, it has the capability to build for these targets, and LLVM has the target enabled by default.

These targets are not frequently tested; one will likely need to contribute to Zig in order to build for these targets.

The Zig compiler might need to be updated with a few things such as what sizes are the C integer types C ABI calling convention for this target bootstrap code and default panic handler

zig targets is guaranteed to include this target.

Support for these targets is entirely experimental.

LLVM may have the target as an experimental target, which means that you need to use Zig-provided binaries for the target to be available, or build LLVM from source with special configure flags. zig targets will display the target if it is available.

will display the target if it is available. This target may be considered deprecated by an official party, such as macosx/i386 in which case this target will remain forever stuck in Tier 4.

This target may only support --emit asm and cannot emit object files.

The reference Zig compiler is not completely self-hosted yet, but no matter what, it will remain exactly 3 steps to go from having a system C++ compiler to having a fully self-hosted Zig compiler for any target. As Maya Rashish notes, porting Zig to other platforms is fun and speedy.

Non-debug build modes are reproducible/deterministic.

There is a JSON version of the download page.

Several members of the Zig team have experience maintaining packages.

Thanks to people who sponsor Zig, the project is accountable to the open source community rather than corporate shareholders. In particular, these fine folks sponsor Zig for $50/month or more:

Karrick McDermott

Raph Levien

ryanworl

Stevie Hryciw

Josh Wolfe

SkunkWerks, GmbH

wilsonk

Ryan Saunderson

Jeff Kelley

Santiago Andaluz

Bakhazard

Jessica Hargis

Matthew Campbell

419928194516

Rickard Andersson

Yurii Rashkovskii

Andrew Schaaf

vchakrav

iohzrd

Loris Cro

Russel Simmons

Christoph Müller

drfuchs

Michael Raymond

haze

THFKA4

This section is updated at the beginning of each month.