Zig Cheatsheet
Language
useful built-in functions
The built-in functions begin with @.
import
const std = @import("std");
bitReverse(interge: anytype) T
mathematical operation
// @sqrt @sin @cos @exp @log @floor @addWithOverflow
type casting operation
// @as @errorFromInt @floatFromInt @ptrFromInt @intFromPtr @intFromEnum @intCast
function call
// @call(modifier: std.lang.CallModifier, function: anytype, args: anytype) anytype // const CallModifier = enum { // auto, // never_tail, // never_inline, // no_suspend, // always_tail, // always_inline, // compile_time, // }
string literal
more details refer: https://ziglang.org/documentation/master/#String-Literals-and-Unicode-Code-Point-Literals
// @embedFile(comptime path: []const u8) *const [N:0]u8
debug tools
// @compileError(comptime msg: []const u8) noreturn // @compileLog(...) void // This function prints the arguments passed to it at compile-time. // @src() std.lang.SourceLocation
arrays
The variables is mutable default, unless declare with const.
var foo: [3]u32 = [3]u32{32, 150, 468}; // {} is initialized list const bar = foo[2]; // get values of an array using array[index] notation foo[0] = 58; // in-place changes const length = foo.len; // .len property
concatenate
use ++ to concatenate two arrays.
const a = [_]u8{1}; const b = [_]u8{2}; const c = a ++ b; // {1, 2}
repeat
use ** to repeat an array.
const d = [_]u8{1, 2, 3} ** 2; // {1, 2, 3, 1, 2, 3}
strings
single quotes for individual characters and strings use double quotes and not interchangeable.
const ziggy = "hello"; const lyrics = \\Ziggy played guitar \\and Spidders from Mars ; // use \\ for multi-line strings
NOTE: string literals are immutable(const) array of u8 characters at all.
control flow
if
NOTE: condition only accepts boolean values. it won't coerce numbers or other types of data to true and false!
if (condition) { } else { } const discount = true; const price: u8 = if (discount) 17 else 20; // if statements are also valid expressions!
Use if error clause to handle "error union":
const MyNumberError = error { TooBig, TooSmall, }; fn canFail() MyNumberError!u8 {} pub fn main() void { const n = canFail(); if (n) |value| { // normal thing } else |err| switch (err) { // handle error } }
while
// while (condition): (continue expression) {}
for
TIPs: could loop multiple different items at once.
// for (items) |item| {} // for (items, 0..) |item, index| {} // for (items1, items2, items3) |item1, item2, item3| {}
switch
TIPs: switch statement could be used as an expression.
NOTE: switch statement must be exhaustive, or add an else clause to handle defaults.
const players = 2; switch (players) { 1 => hello(), 2 => work(), else => { alert(); }, }
functions
fn foo(n: u8) u8 {} // default is private pub fn bar(n: u8) u8 {}
defer execute
we can defer execute a block of code with defer keyword. Commonly used for resources cleanup or release.
// { // defer runLater(); // runNow(); // }
errors
const MyNumberError = error { TooBig, TooSmall, };
error union
Zig lets us make "error union" which is a value which could either be a regular value OR an error from a set.
var text: MyError!Text = getText("foo.txt");
ignore error with catch
// catch error and replace it with a default value const foo = canFail() catch 5; // if fails, foo will be assigned 5
catch then handle
// calFail() catch |err| { // if (err == MyError.TooBig) { // } // }
NOTE: we could use errdefer for special handle before the block of code exit when error occurs.
// { // errdefer cleanup(); // try canFail(); // }
bubble up
// try canFail(); => canFail() catch |err| return err;
primative type
enum(internal is number)
const Ops = enum {inc, pow, dec}; // Since enum is really a set of numbers, we can assign them explicitly const Stuff = enum(u8) {foo = 15}; // default value const my_stuff: u8 = @intFromEnum(Stuff.foo); // get number from enum
struct
const Character = struct { health: u8, gold: u32, experience: u32, };
pointer
var foo: u8 = 5; var bar: *u8 = &foo;
| type | comment |
|---|---|
| u8 | u8 value |
| foo | the value of 5 |
| *u8 | the type of a pointer to a u8 value |
| &foo | a reference to foo |
| bar | a pointer to the value at foo |
| bar.* | the value of 5 (dereference) |
different pointer
const foo: u8 = 5; var bar: u8 = 10; const p1: *const u8 = &foo; // const pointer to const value, so p1 is a const readonly pointer // error: cannot assign to constant // p1 = &bar; // p1.* = 10; var p2: *const u8 = &foo; // pointer to const value, so p2 is a mutable but pointer to a readonly value // p2 = &bar; // Ok // error: cannot assign to constant // p2.* = 10; const p3: *u8 = &foo; // though p3 declare as a pointer to u8, but foo declare as const, so it still can't be changed // error: cannot assign to constant // p3.* = 10;
optional
var foo: ?u32 = 10; // it could be u32 or null var bar = foo orelse 2; // if foo == null; then bar = 2; const bzz = bar.?; // bar orelse unreachable;
no value
undefined
undefined tells the compiler that we are not assigning a value yet.
null
truely no value, typically used with optional type.
error
special value indicate that the value we were looking for does not exist.
void
type not a value. which is a Zero bit types.
slices
slices is a pointer with a length property.
var card = [4]u8{1, 2, 3, 4}; const hand: []u8 = card[0..3]; // slice to first 3 card
union
the compiler sets enough memory for the largest thing that needs.
const Foo = union { small: u8, large: u64, };
tagged union
a union which used like a enum;
const Foo = union(enum) { small: u8, large: u64, };
coercion rules
types can always be made more restrictive
var foo: u8 = 5; var p1: *u8 = &foo; var p2: *const u8 = p1; // mutable to immutable
numeric types can coerce to larger types
var n1: u8 = 5; var n2: u16 = n1; var n3: f16 = 45.2; var n4: f32 = n3;
single-item pointers to arrays coerce to slices and many-item pointers
const arr: [3]u8 = [3]u8{1, 2, 3}; const s: []const u8 = &arr; // pointer to slice const p: [*]const u8 = &arr;
single-item mutable pointers can coerce to single-item pointers pointing to an array of length 1
var five: u8 = 5; var a_five: *[1]u8 = &five;
payload types and null coerce to optionals
var num: u8 = 6; var maybe_num: ?u8 = num; maybe_num = null;
labels
we could custom label for a block of code, which like loop body.
// my_label: {} // outer_block: while(true) { // break :outer_block; // } // const bar: u8 = two_loop: while (true) { // while (true) { // break :two_loop 5; // } // }
comptime
All numeric literals in Zig are of type comptime_int or comptime_float. For that declare with var
which could be changed at runtime, the compiler must know exactly how much memory to reserve for
these value, so we need to specify numeric types with specific sizes.
var var_int: u32 = 123456; // u32 must be declare or something like this comptime var var_int2 = 123456; // or declare as comptime
comptime type (generic programming)
// fn foo(comptime MyType: type) void {} fn makeSequence(comptime T: type, comptime size: usize) [size]T { var sequence: [size]T = undefined; var i: usize = 0; while (i < size): (i += 1) { sequence[i] = @as(T, @intCast(i)) + 1; } return sequence; }
comptime value (static dispatch)
Above is a example generic for Type(T), but what if we want to have generic for value?
The answer is anytype placeholder, which tells Zig to infer the actual type of a parameter at
compile time.
// fn foo(thing: anytype) void {}
Then we can use builtins such as @TypeOf(), @typeInfo(), @typeName(), @hasDecl(), and @hasField() to
determine more about the type. All of this logic will be performed entirely at compile time.
const Duck = struct { eggs: u8, loudness: u8, location_x: i32 = 0, location_y: i32 = 0, fn waddle(self: *Duck, x: i16, y: i16) void { self.location_x += x; self.location_y += y; } fn quack(self: Duck) void { if (self.loudness < 4) { print("\"Quack.\" ", .{}); } else { print("\"QUACK!\" ", .{}); } } }; const RubberDuck = struct { in_bath: bool = false, location_x: i32 = 0, location_y: i32 = 0, fn waddle(self: *RubberDuck, x: i16, y: i16) void { self.location_x += x; self.location_y += y; } fn quack(self: RubberDuck) void { // Assigning an expression to '_' allows us to safely // "use" the value while also ignoring it. _ = self; print("\"Squeek!\" ", .{}); } fn listen(self: RubberDuck, dev_talk: []const u8) void { // Listen to developer talk about programming problem. // Silently contemplate problem. Emit helpful sound. _ = dev_talk; self.quack(); } }; const Duct = struct { diameter: u32, length: u32, galvanized: bool, connection: ?*Duct = null, fn connect(self: *Duct, other: *Duct) !void { if (self.diameter == other.diameter) { self.connection = other; } else { return DuctError.UnmatchedDiameters; } } }; const DuctError = error{UnmatchedDiameters}; pub fn main() void { // This is a real duck! const ducky1 = Duck{ .eggs = 0, .loudness = 3, }; // This is not a real duck, but it has quack() and waddle() // abilities, so it's still a "duck". const ducky2 = RubberDuck{ .in_bath = false, }; // This is not even remotely a duck. const ducky3 = Duct{ .diameter = 17, .length = 165, .galvanized = true, }; print("ducky1: {}, ", .{isADuck(ducky1)}); print("ducky2: {}, ", .{isADuck(ducky2)}); print("ducky3: {}\n", .{isADuck(ducky3)}); } // This function has a single parameter which is inferred at // compile time. It uses builtins @TypeOf() and @hasDecl() to // perform duck typing ("if it walks like a duck and it quacks // like a duck, then it must be a duck") to determine if the type // is a "duck". fn isADuck(possible_duck: anytype) bool { // We'll use @hasDecl() to determine if the type has // everything needed to be a "duck". // // In this example, 'has_increment' will be true if type Foo // has an increment() method: // // const has_increment = @hasDecl(Foo, "increment"); // // Please make sure MyType has both waddle() and quack() // methods: const MyType = @TypeOf(possible_duck); const walks_like_duck = @hasDecl(MyType, "waddle"); const quacks_like_duck = @hasDecl(MyType, "quack"); const is_duck = walks_like_duck and quacks_like_duck; if (is_duck) { // We also call the quack() method here to prove that Zig // allows us to perform duck actions on anything // sufficiently duck-like. // // Because all of the checking and inference is performed // at compile time, we still have complete type safety: // attempting to call the quack() method on a struct that // doesn't have it (like Duct) would result in a compile // error, not a runtime panic or crash! possible_duck.quack(); } return is_duck; }
inline loop (for comptime type)
inline for (.{u8, u16, u32, u64}) |T| { // these type only available at compile time print("{} ", .{@typeInfo(T).int.bits}); }
const foo = [3]*const [5]u8{"~{s}~", "<{s}>", "d{s}b"}; comptime var i = 0; inline while (i < foo.len) : (i += 1) { print(foo[i] ++ "\n", .{foo[i]}); }
comptime expression
// function // comptime llama(); // expression // bar = comptime baz(); // block // comptime { // ... // } // get a value from a block // var llama = comptime bar: { // ... // }
sentinel
Zig supports sentinel-terminated arrays, slices, and pointers.
const a: [4:0]u32 = [_]u32{1, 2, 3, 4}; const b: [:0]u32 = &[_]u32{1, 2, 3, 4}; const c: [*:0]u32 = &[_]u32{1, 2, 3, 4};
@ptrCast() to go back to sentinel-terminated pointer.
pub fn main() void { // Again, we've coerced the sentinel-terminated string to a // many-item pointer, which has no length or sentinel. const data: [*]const u8 = "Weird Data!"; // Please cast 'data' to 'printable': const printable: [*:0]const u8 = @ptrCast(data); print("{s}\n", .{printable}); }
quoted-identifiers
pub fn main() void { const @"55_cows": i32 = 55; const @"isn't true": bool = false; print("Sweet freedom: {}, {}.\n", .{ @"55_cows", @"isn't true", }); }
interface
inline else for interface method.
const Insect = union(enum) { ant: Ant, bee: Bee, grasshopper: Grasshopper, // Thanks to 'inline else', we can think of this print() as // being an interface method. Any member of this union with // a print() method can be treated uniformly by outside // code without needing to know any other details. Cool! pub fn print(self: Insect) void { switch (self) { inline else => |case| return case.print(), } } };
async
Concurrency backends:
- Threaded - thread-pool based I/O
- Evented - chooses the best event-loop backend according to OS:
- Uring on Linux (iouring)
- Kqueue on BSD/MacOS
- Dispatch on MacOS (Grand Central Dispatch)
Io struct:
const Io = struct { userdata: ?*anyopaque, // opqaue state of the backend vtable: *const VTable, // table of function pointers }; // In Zig 0.16, main() receives a std.process.Init struct to opt into I/O and concurrency support pub fn main(init: std.process.Init) !void {}
async/await/cancel
var future = io.async(someFunction, .{arg1, arg2}); // NOTE: the future may run immediately or on another thread defer _ = future.cancel(io); // safety const result = future.await(io); // NOTE: every future MUST be either .await or .cancel, leaks resources otherwise. // both .await and .cancel BLOCK until the task finished and return the result. // calling either one more than once is safe - subsquent calls just return a copy of the result!
group tasks
When we have many tasks that don't return individual values, use a Group! A
Group is an unordered set of tasks that can only be awaited or canceled as a
whole:
var group: std.Io.Group = .init; group.async(io, myTask, .{arg1}); group.async(io, myTask, .{arg2}); try group.await(io); // blocks until all tasks finish
Important rules:
- The return type of functions spawned in a group must be
coercibletoCancelble!void(i.e. void, or errorCanceled!void). - Once call group.async(), MUST eventually call group.await() or group.cancel() to release resources.
- group.cancel() requests cancellation on ALL members, then blocks until they all finish.
structured cancellation
A cancellation point is any Io function that can return error.Canceled - most
commonly io.sleep(). The different from killing a thread is that the task gets a
chance to "clean up" and return a value.
fn myTask(io: std.Io) u32 { io.sleep(...) catch |err| switch (err) { error.Canceled => return 0, }; return 5; }
race multiple tasks
Select is like a Group, but lets we receive individual results as tasks
complete - one at a time:
const Race = std.Io.Select(union(enum) { fast: u32, slow: u32, }); var buffer: [2]Race.Union = undefined; var sel = Race.init(io, &buffer); sel.async(.fast, fastFn, .{io}); sel.async(.slow, slowFn, .{io}); const winner = try sel.await(); // returns first completed switch (winner) { .fast => |var| ..., .slow => |var| ..., } sel.cancelDiscard(); // cancel remaining, discard results
synchronization
When multiple async tasks access shared data, we need synchronization.
Mutex (exclusive)
var mutex: std.Io.Mutex = .init; try mutex.lock(io); // blocks until lock is acquired, it's a cancellation point which can return error.Canceled. defer mutex.unlock(io); // ... critical section for safely modify shared data ...
RwLock
Semaphore
Futex
Batch
Condition
communication
Queue (bounded, thread-safe channel)
var backing: [16]u32 = undefined; var queue: std.Io.Queue(u32) = .init(&backing); try queue.putOne(io, value); // blocks if queue is full const val = try queue.getOne(io); // blocks if queue is empty
individual dispatch
The difference between io.async and io.concurrency:
io.aync():
- The function MAY run on a separate unit of concurrency or it MAY run immediately on the caller.
- Never fails - if no concurrency is available, it just runs the function right away.
- More portable, works with all Io backends.
io.concurrency():
- Guarantees a separate unit of concurrency.
- Can fail with error.ConcurrencyUnavailable if resources exhausted or the backend doesn't support it.
- Use when we need the task to run independently of the caller.
cancel protection
Sometimes a task has a critical section that MUST NOT be interrupted.
const old = io.swapCancelProtection(.blocked); defer _ = io.swapCancelProtection(old); // ... do some stuff safely ... io.checkCancel() return error.Canceled if a cancel request is pending
memory allocation (heap)
var area = std.heap.ArenaAllocator.init(std.heap.page.allocator); defer arena.deinit(); const allocator = arena.allocator(); const ptr = try allocator.create(i32); const slice_ptr = try allocator.alloc(f64, 5);
bit manipulation
<< >> & | ^
testing
const testing = std.testing; test "description" { try testing.expect(...); try testing.expectEqual(...); try testing.expectError(error.xxx, ...); }
packed
internal is integer!!
const foo = packed struct { ... }; const bar = packed union { ... }; const U = packed union(u2) { a: u2, b: i2, }; // we could leverage this same memory region for different purpose comptime { const u: U = .{ .a = 3 }; switch (u) { .{ .a = 3 } => {} // ok .{ .a = 2 }, .{ .a = 1 }, .{ .a = 0 }, } }
FFI
C
const c = @cImport({ @cInclude("unistd.h"); }); pub fn main() void { const c_res = c.write(2, "Hello C from Zig!", 17); }