// Make sure that an action (single instruction or block of code) is executed
// before the end of the scope (function, block of code).
// Even on error, that action will be executed.
// Useful for memory allocations, and resource management in general.
pub fn main() void {
// Should be executed at the end of the function.
defer print("third!\n", .{});
{
// Last element of its scope: will be executed right away.
defer print("first!\n", .{});
}
print("second!\n", .{});
}
fn hello_world() void {
defer print("end of function\n", .{}); // after "hello world!"
print("hello world!\n", .{});
}
// errdefer executes the instruction (or block of code) only on error.
fn second_hello_world() !void {
errdefer print("2. something went wrong!\n", .{}); // if "foo" fails.
defer print("1. second hello world\n", .{}); // executed after "foo"
try foo();
}
// Defer statements can be seen as stacked: first one is executed last.
```
### Memory allocators.
Memory isn't managed directly in the standard library, instead an "allocator" is asked every time an operation on memory is required.
Thus, the standard library lets developers handle memory as they need, through structures called "allocators", handling all memory operations.
**NOTE**: the choice of the allocator isn't in the scope of this document.
A whole book could be written about it.
However, here are some examples, to get an idea of what you can expect:
- page_allocator.
Allocate a whole page of memory each time we ask for some memory.
Very simple, very dumb, very wasteful.
- GeneralPurposeAllocator.
Get some memory first and manage some buckets of memory in order to
reduce the number of allocations.
A bit complex. Can be combined with other allocators.
Can detect leaks and provide useful information to find them.
- FixedBufferAllocator.
Use a fixed buffer to get its memory, don't ask memory to the kernel.
Very simple, limited and wasteful (can't deallocate), but very fast.
- ArenaAllocator.
Allow to free all allocted memory at once.
To use in combinaison with another allocator.
Very simple way of avoiding leaks.
A first example.
```zig
// "!void" means the function doesn't return any value except for errors.
// In this case we try to allocate memory, and this may fail.
fn foo() !void {
// In this example we use a page allocator.
var allocator = std.heap.page_allocator;
// "list" is an ArrayList of 8-bit unsigned integers.
// An ArrayList is a contiguous, growable list of elements in memory.
var list = try ArrayList(u8).initAllocated(allocator);
defer list.deinit(); // Free the memory at the end of the scope. Can't leak.
// "defer" allows to express memory release right after its allocation,
// regardless of the complexity of the function (loops, conditions, etc.).
list.add(5); // Some memory is allocated here, with the provided allocator.
for (list.items) |item| {
std.debug.print("item: {}\n", .{item});
}
}
```
### Memory allocation combined with error management and defer.
```zig
fn some_memory_allocation_example() !void {
// Memory allocation may fail, so we "try" to allocate the memory and
// in case there is an error, the current function returns it.
var buf = try page_allocator.alloc(u8, 10);
// Defer memory release right after the allocation.
// Will happen even if an error occurs.
defer page_allocator.free(buf);
// Second allocation.
// In case of a failure, the first allocation is correctly released.
var buf2 = try page_allocator.alloc(u8, 10);
defer page_allocator.free(buf2);
// In case of failure, both previous allocations are correctly deallocated.
try foo();
try bar();
// ...
}
```
### Memory allocators: a taste of the standard library.
```zig
// Allocators: 4 main functions to know
// single_value = create (type)
// destroy (single_value)
// slice = alloc (type, size)
// free (slice)
// Page Allocator
fn page_allocator_fn() !void {
var slice = try std.heap.page_allocator.alloc(u8, 3);
defer std.heap.page_allocator.free(slice);
// playing_with_a_slice(slice);
}
// GeneralPurposeAllocator
fn general_purpose_allocator_fn() !void {
// GeneralPurposeAllocator has to be configured.
// In this case, we want to track down memory leaks.
const config = .{.safety = true};
var gpa = std.heap.GeneralPurposeAllocator(config){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var slice = try allocator.alloc(u8, 3);
defer allocator.free(slice);
// playing_with_a_slice(slice);
}
// FixedBufferAllocator
fn fixed_buffer_allocator_fn() !void {
var buffer = [_]u8{0} ** 1000; // array of 1000 u8, all initialized at zero.
var fba = std.heap.FixedBufferAllocator.init(buffer[0..]);
// Side note: buffer[0..] is a way to create a slice from an array.
// Since the function takes a slice and not an array, this makes
// the type system happy.
var allocator = fba.allocator();
var slice = try allocator.alloc(u8, 3);
// No need for "free", memory cannot be freed with a fixed buffer allocator.
// defer allocator.free(slice);
// playing_with_a_slice(slice);
}
// ArenaAllocator
fn arena_allocator_fn() !void {
// Reminder: arena doesn't allocate memory, it uses an inner allocator.
// In this case, we combine the arena allocator with the page allocator.
var arena = std.heap.arena_allocator.init(std.heap.page_allocator);
defer arena.deinit(); // end of function = all allocations are freed.
var allocator = arena.allocator();
const slice = try allocator.alloc(u8, 3);
// No need for "free", memory will be freed anyway.
// playing_with_a_slice(slice);
}
// Combining the general purpose and arena allocators. Both are very useful,
// and their combinaison should be in everyone's favorite cookbook.
fn gpa_arena_allocator_fn() !void {
const config = .{.safety = true};
var gpa = std.heap.GeneralPurposeAllocator(config){};
defer _ = gpa.deinit();
const gpa_allocator = gpa.allocator();
var arena = arena_allocator.init(gpa_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var slice = try allocator.alloc(u8, 3);
defer allocator.free(slice);
// playing_with_a_slice(slice);
}
```
### Comptime.
```zig
// Comptime is a way to avoid the pre-processor.
// The idea is simple: run code at compilation.
inline fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
var res = max(u64, 1, 2);
var res = max(f32, 10.50, 32.19);
// Comptime: creating generic structures.
fn List(comptime T: type) type {
return struct {
items: []T,
fn init() ... { ... }
fn deinit() ... { ... }
fn do() ... { ... }
};
}
const MyList = List(u8);
// use
var list = MyList{
.items = ... // memory allocation
};
list.items[0] = 10;
```
### Conditional compilation.
```zig
const available_os = enum { OpenBSD, Linux };
const myos = available_os.OpenBSD;
// The following switch is based on a constant value.
// This means that the only possible outcome is known at compile-time.
// Thus, there is no need to build the rest of the possibilities.
// Similar to the "#ifdef" in C, but without requiring a pre-processor.
const string = switch (myos) {
.OpenBSD => "OpenBSD is awesome!",
.Linux => "Linux rocks!",
};
// Also works in this case.
const myprint = switch(myos) {
.OpenBSD => std.debug.print,
.Linux => std.log.info,
}
```
### Testing our functions.
```zig
const std = @import("std");
const expect = std.testing.expect;
// Function to test.
pub fn some_function() bool {
return true;
}
// This "test" block can be run with "zig test".
// It will test the function at compile-time.
test "returns true" {
expect(false == some_function());
}
```
### Compiler built-ins.
The compiler has special functions called "built-ins", starting with an "@".
There are more than a hundred built-ins, allowing very low-level stuff:
- compile-time errors, logging, verifications
- type coercion and convertion, even in an unsafe way
- alignment management
- memory tricks (such as getting the byte offset of a field in a struct)
- calling functions at compile-time
- including C headers to transparently call C functions
- atomic operations
- embed files into the executable (@embedFile)
- frame manipulations (for async functions, for example)
- etc.
Example: enums aren't integers, they have to be converted with a built-in.
```zig
const Value = enum { zero, stuff, blah };
if (@enumToInt(Value.zero) == 0) { ... }
if (@enumToInt(Value.stuff) == 1) { ... }
if (@enumToInt(Value.blah) == 2) { ... }
```
### A few "not yourself in the foot" measures in the Zig language.
- Namespaces: names conflicts are easily avoided.
In practice, that means an unified API between different structures (data types).
- Enumerations aren't integers. Comparing an enumeration to an integer requires a conversion.
- Explicit casts, coercion exists but is limited.
Types are slightly more enforced than in C, just a taste:
Pointers aren't integers, explicit conversion is necessary.
You won't lose precision by accident, implicit coercions are only authorized in case no precision can be lost.
Unions cannot be reinterpreted (in an union with an integer and a float, one cannot take a value for another by accident).
Etc.
- Removing most of the C undefined behaviors (UBs), and when the compiler encounters one, it stops.
- Slice and Array structures are prefered to pointers.
Types enforced by the compiler are less prone to errors than pointer manipulations.
- Numerical overflows produce an error, unless explicitly accepted using wrapping operators.
- Try and catch mechanism.
It's both handy, trivially implemented (simple error enumeration), and it takes almost no space nor computation time.
- Unused variables are considered as errors by the compiler.
- Many pointer types exist in order to represent what is pointed.
Example: is this a single value or an array, is the length known, etc.
- Structures need a value for their attributes, and it still is possible to give an undefined value (stack garbage), but at least it is explicitely undefined.
## Further Reading
For a start, some concepts are presented on the [Zig learn website][ziglearn].
The [official website][zigdoc] provides a reference documentation to the language.
For now, documentation for standard library is WIP.