mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-11-22 12:32:09 +03:00
Merge pull request #4293 from KaneRoot/master
[zig/en] Introduce the Zig programming language.
This commit is contained in:
commit
b86facbef8
980
zig.html.markdown
Normal file
980
zig.html.markdown
Normal file
@ -0,0 +1,980 @@
|
||||
---
|
||||
language: zig
|
||||
filename: learnzig.zig
|
||||
contributors:
|
||||
- ["Philippe Pittoli", "https://karchnu.fr/"]
|
||||
---
|
||||
|
||||
|
||||
[Zig][ziglang] aims to be a replacement for the C programming language.
|
||||
|
||||
**WARNING**: this document expects you to understand a few basic concepts in computer science, such as pointers, stack and heap memory, etc.
|
||||
|
||||
**WARNING**: Zig isn't considered as ready for production. Bugs are expected.
|
||||
DO NOT TRY ZIG AS YOUR FIRST PROGRAMMING EXPERIENCE.
|
||||
The compiler, even the language and its libraries aren't ready, yet.
|
||||
You've been warned.
|
||||
|
||||
Prior knowledge of C is recommended.
|
||||
|
||||
|
||||
## Quick overview: Zig compared to C
|
||||
|
||||
- Syntax is mostly the same, with some improvements (less ambiguity).
|
||||
- Zig introduces namespaces.
|
||||
- Try and catch mechanism, which is both convenient, efficient and optional.
|
||||
- Most of the C undefined behaviors (UBs) are fixed.
|
||||
- Compared to C, raw pointers are safer to use and less likely to be needed.
|
||||
* The type system distinguishes between a pointer to a single value, or multiple values, etc.
|
||||
* Slices are preferred, which is a structure with a pointer and a runtime known size, which characterizes most uses of pointers in the first place.
|
||||
- Some arbitrary language limitations are removed. For example, enumerations, structures and unions can have functions.
|
||||
- Simple access to SIMD operations (basic maths on vectors).
|
||||
- Zig provides both low-level features of C and the one provided through compiler extensions.
|
||||
For example: packed structures.
|
||||
- An extensive standard library, including data structures and algorithms.
|
||||
- Cross-compilation capability is provided by default, without any dependency.
|
||||
Different libc are provided to ease the process.
|
||||
Cross-compilation works from, and to, any operating system and architecture.
|
||||
|
||||
## Zig language
|
||||
|
||||
|
||||
```zig
|
||||
//! Top-level documentation.
|
||||
|
||||
/// Documentation comment.
|
||||
|
||||
// Simple comment.
|
||||
```
|
||||
|
||||
|
||||
### Hello world.
|
||||
```zig
|
||||
// Import standard library, reachable through the "std" constant.
|
||||
const std = @import("std");
|
||||
|
||||
// "info" now refers to the "std.log.info" function.
|
||||
const info = std.log.info;
|
||||
|
||||
// Usual hello world.
|
||||
// syntax: [pub] fn <function-name>(<arguments>) <return-type> { <body> }
|
||||
pub fn main() void {
|
||||
// Contrary to C functions, Zig functions have a fixed number of arguments.
|
||||
// In C: "printf" takes any number of arguments.
|
||||
// In Zig: std.log.info takes a format and a list of elements to print.
|
||||
info("hello world", .{}); // .{} is an empty anonymous tuple.
|
||||
}
|
||||
```
|
||||
|
||||
### Booleans, integers and float.
|
||||
```zig
|
||||
// Booleans.
|
||||
// Keywords are prefered to operators for boolean operations.
|
||||
print("{}\n{}\n{}\n", .{
|
||||
true and false,
|
||||
true or false,
|
||||
!true,
|
||||
});
|
||||
|
||||
// Integers.
|
||||
const one_plus_one: i32 = 1 + 1;
|
||||
print("1 + 1 = {}\n", .{one_plus_one}); // 2
|
||||
|
||||
// Floats.
|
||||
const seven_div_three: f32 = 7.0 / 3.0;
|
||||
print("7.0 / 3.0 = {}\n", .{seven_div_three}); // 2.33333325e+00
|
||||
|
||||
// Integers have arbitrary value lengths.
|
||||
var myvar: u10 = 5; // 10-bit unsigned integer
|
||||
// Useful for example to read network packets, or complex binary formats.
|
||||
|
||||
// Number representation is greatly improved compared to C.
|
||||
const one_billion = 1_000_000_000; // Decimal.
|
||||
const binary_mask = 0b1_1111_1111; // Binary. Ex: network mask.
|
||||
const permissions = 0o7_5_5; // Octal. Ex: Unix permissions.
|
||||
const big_address = 0xFF80_0000_0000_0000; // Hexa. Ex: IPv6 address.
|
||||
|
||||
|
||||
// Overflow operators: tell the compiler when it's okay to overflow.
|
||||
var i: u8 = 0; // "i" is an unsigned 8-bit integer
|
||||
i -= 1; // runtime overflow error (unsigned value always are positive)
|
||||
i -%= 1; // okay (wrapping operator), i == 255
|
||||
|
||||
// Saturation operators: values will stick to their lower and upper bounds.
|
||||
var i: u8 = 200; // "i" is an unsigned 8-bit integer (values: from 0 to 255)
|
||||
i +| 100 == 255 // u8: won't go higher than 255
|
||||
i -| 300 == 0 // unsigned, won't go lower than 0
|
||||
i *| 2 == 255 // u8: won't go higher than 255
|
||||
i <<| 8 == 255 // u8: won't go higher than 255
|
||||
```
|
||||
|
||||
### Arrays.
|
||||
```zig
|
||||
// An array is a well-defined structure with a length attribute (len).
|
||||
|
||||
// 5-byte array with undefined content (stack garbage).
|
||||
var array1: [5]u8 = undefined;
|
||||
|
||||
// 5-byte array with defined content.
|
||||
var array2 = [_]u8{ 1, 2, 3, 4, 5 };
|
||||
// [_] means the compiler knows the length at compile-time.
|
||||
|
||||
// 1000-byte array with defined content (0).
|
||||
var array3 = [_]u8{0} ** 1000;
|
||||
|
||||
// Another 1000-byte array with defined content.
|
||||
// The content is provided by the "foo" function, called at compile-time and
|
||||
// allows complex initializations.
|
||||
var array4 = [_]u8{foo()} ** 1000;
|
||||
|
||||
// In any case, array.len gives the length of the array,
|
||||
// array1.len and array2.len produce 5, array3.len and array4.len produce 1000.
|
||||
|
||||
|
||||
// Modifying and accessing arrays content.
|
||||
|
||||
// Array of 10 32-bit undefined integers.
|
||||
var some_integers: [10]i32 = undefined;
|
||||
|
||||
some_integers[0] = 30; // first element of the array is now 30
|
||||
|
||||
var x = some_integers[0]; // "x" now equals to 30, its type is infered.
|
||||
var y = some_integers[1]; // Second element of the array isn't defined.
|
||||
// "y" got a stack garbage value (no runtime error).
|
||||
|
||||
// Array of 10 32-bit undefined integers.
|
||||
var some_integers: [10]i32 = undefined;
|
||||
|
||||
var z = some_integers[20]; // index > array size, compilation error.
|
||||
|
||||
// At runtime, we loop over the elements of "some_integers" with an index.
|
||||
// Index i = 20, then we try:
|
||||
try some_integers[i]; // Runtime error 'index out of bounds'.
|
||||
// "try" keyword is necessary when accessing an array with
|
||||
// an index, since there is a potential runtime error.
|
||||
// More on that later.
|
||||
```
|
||||
|
||||
### Multidimensional arrays.
|
||||
```zig
|
||||
|
||||
const mat4x4 = [4][4]f32{
|
||||
[_]f32{ 1.0, 0.0, 0.0, 0.0 },
|
||||
[_]f32{ 0.0, 1.0, 0.0, 1.0 },
|
||||
[_]f32{ 0.0, 0.0, 1.0, 0.0 },
|
||||
[_]f32{ 0.0, 0.0, 0.0, 1.0 },
|
||||
};
|
||||
|
||||
// Access the 2D array then the inner array through indexes.
|
||||
try expect(mat4x4[1][1] == 1.0);
|
||||
|
||||
// Here we iterate with for loops.
|
||||
for (mat4x4) |row, row_index| {
|
||||
for (row) |cell, column_index| {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strings.
|
||||
```zig
|
||||
|
||||
// Simple string constant.
|
||||
const greetings = "hello";
|
||||
// ... which is equivalent to:
|
||||
const greetings: *const [5:0]u8 = "hello";
|
||||
// In words: "greetings" is a constant value, a pointer on a constant array of 5
|
||||
// elements (8-bit unsigned integers), with an extra '0' at the end.
|
||||
// The extra "0" is called a "sentinel value".
|
||||
|
||||
print("string: {s}\n", .{greetings});
|
||||
|
||||
// This represents rather faithfully C strings. Although, Zig strings are
|
||||
// structures, no need for "strlen" to compute their size.
|
||||
// greetings.len == 5
|
||||
```
|
||||
|
||||
### Slices.
|
||||
```zig
|
||||
|
||||
// A slice is a pointer and a size, an array without compile-time known size.
|
||||
// Slices have runtime out-of-band verifications.
|
||||
|
||||
const array = [_]u8{1,2,3,4,5}; // [_] = array with compile-time known size.
|
||||
const slice = array[0..array.len]; // "slice" represents the whole array.
|
||||
// slice[10] gives a runtime error.
|
||||
```
|
||||
|
||||
### Pointers.
|
||||
```zig
|
||||
|
||||
// Pointer on a value can be created with "&".
|
||||
const x: i32 = 1;
|
||||
const pointer: *i32 = &x; // "pointer" is a pointer on the i32 var "x".
|
||||
print("1 = {}, {}\n", .{x, pointer});
|
||||
|
||||
// Pointer values are accessed and modified with ".*".
|
||||
if (pointer.* == 1) {
|
||||
print("x value == {}\n", .{pointer.*});
|
||||
}
|
||||
|
||||
// ".?" is a shortcut for "orelse unreachable".
|
||||
const foo = pointer.?; // Get the pointed value, otherwise crash.
|
||||
```
|
||||
|
||||
### Optional values (?<type>).
|
||||
```zig
|
||||
// An optional is a value than can be of any type or null.
|
||||
|
||||
// Example: "optional_value" can either be "null" or an unsigned 32-bit integer.
|
||||
var optional_value: ?u32 = null; // optional_value == null
|
||||
optional_value = 42; // optional_value != null
|
||||
|
||||
// "some_function" returns ?u32
|
||||
var x = some_function();
|
||||
if (x) |value| {
|
||||
// In case "some_function" returned a value.
|
||||
// Do something with 'value'.
|
||||
}
|
||||
```
|
||||
|
||||
### Errors.
|
||||
```zig
|
||||
// Zig provides an unified way to express errors.
|
||||
|
||||
// Errors are defined in error enumerations, example:
|
||||
const Error = error {
|
||||
WatchingAnyNetflixTVShow,
|
||||
BeOnTwitter,
|
||||
};
|
||||
|
||||
// Normal enumerations are expressed the same way, but with "enum" keyword.
|
||||
const SuccessStory = enum {
|
||||
DoingSport,
|
||||
ReadABook,
|
||||
};
|
||||
|
||||
|
||||
// Error union (!).
|
||||
// Either the value "mylife" is an an error or a normal value.
|
||||
var mylife: Error!SuccessStory = Error.BeOnTwitter;
|
||||
// mylife is an error. Sad.
|
||||
|
||||
mylife = SuccessStory.ReadABook;
|
||||
// Now mylife is an enum.
|
||||
|
||||
|
||||
// Zig ships with many pre-defined errors. Example:
|
||||
const value: anyerror!u32 = error.Broken;
|
||||
|
||||
|
||||
// Handling errors.
|
||||
|
||||
// Some error examples.
|
||||
const Error = error {
|
||||
UnExpected,
|
||||
Authentication,
|
||||
};
|
||||
|
||||
// "some_function" can either return an "Error" or an integer.
|
||||
fn some_function() Error!u8 {
|
||||
return Error.UnExpected; // It returns an error.
|
||||
}
|
||||
|
||||
// Errors can be "catch" without intermediate variable.
|
||||
var value = some_function() catch |err| switch(err) {
|
||||
Error.UnExpected => return err, // Returns the error.
|
||||
Error.Authentication => unreachable, // Not expected. Crashes the program.
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// An error can be "catch" without giving it a name.
|
||||
const unwrapped = some_function() catch 1234; // "unwrapped" = 1234
|
||||
|
||||
// "try" is a very handy shortcut for "catch |err| return err".
|
||||
var value = try some_function();
|
||||
// If "some_function" fails, the current function stops and returns the error.
|
||||
// "value" can only have a valid value, the error already is handled with "try".
|
||||
```
|
||||
|
||||
### Control flow.
|
||||
|
||||
```zig
|
||||
// Conditional branching.
|
||||
|
||||
if (condition) {
|
||||
...
|
||||
}
|
||||
else {
|
||||
...
|
||||
}
|
||||
|
||||
// Ternary.
|
||||
var value = if (condition) x else y;
|
||||
|
||||
// Shortcut for "if (x) x else 0"
|
||||
var value = x orelse 0;
|
||||
|
||||
// If "a" is an optional, which may contain a value.
|
||||
if (a) |value| {
|
||||
print("value: {}\n", .{value});
|
||||
}
|
||||
else {
|
||||
print("'a' is null\n", .{});
|
||||
}
|
||||
|
||||
// Get a pointer on the value (if it exists).
|
||||
if (a) |*value| { value.* += 1; }
|
||||
|
||||
|
||||
// Loops.
|
||||
|
||||
// Syntax examples:
|
||||
// while (condition) statement
|
||||
// while (condition) : (end-of-iteration-statement) statement
|
||||
//
|
||||
// for (iterable) statement
|
||||
// for (iterable) |capture| statement
|
||||
// for (iterable) statement else statement
|
||||
|
||||
// Note: loops work the same way over arrays or slices.
|
||||
|
||||
// Simple "while" loop.
|
||||
while (i < 10) { i += 1; }
|
||||
|
||||
// While loop with a "continue expression"
|
||||
// (expression executed as the last expression of the loop).
|
||||
while (i < 10) : (i += 1) { ... }
|
||||
// Same, with a more complex continue expression (block of code).
|
||||
while (i * j < 2000) : ({ i *= 2; j *= 3; }) { ... }
|
||||
|
||||
// To iterate over a portion of a slice, reslice.
|
||||
for (items[0..1]) |value| { sum += value; }
|
||||
|
||||
// Loop over every item of an array (or slice).
|
||||
for (items) |value| { sum += value; }
|
||||
|
||||
// Iterate and get pointers on values instead of copies.
|
||||
for (items) |*value| { value.* += 1; }
|
||||
|
||||
// Iterate with an index.
|
||||
for (items) |value, i| { print("val[{}] = {}\n", .{i, value}); }
|
||||
|
||||
// Iterate with pointer and index.
|
||||
for (items) |*value, i| { print("val[{}] = {}\n", .{i, value}); value.* += 1; }
|
||||
|
||||
|
||||
// Break and continue are supported.
|
||||
for (items) |value| {
|
||||
if (value == 0) { continue; }
|
||||
if (value >= 10) { break; }
|
||||
// ...
|
||||
}
|
||||
|
||||
// For loops can also be used as expressions.
|
||||
// Similar to while loops, when you break from a for loop,
|
||||
// the else branch is not evaluated.
|
||||
var sum: i32 = 0;
|
||||
// The "for" loop has to provide a value, which will be the "else" value.
|
||||
const result = for (items) |value| {
|
||||
if (value != null) {
|
||||
sum += value.?; // "result" will be the last "sum" value.
|
||||
}
|
||||
} else 0; // Last value.
|
||||
```
|
||||
|
||||
### Labels.
|
||||
```zig
|
||||
|
||||
// Labels are a way to name an instruction, a location in the code.
|
||||
// Labels can be used to "continue" or "break" in a nested loop.
|
||||
outer: for ([_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }) |_| {
|
||||
for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
|
||||
count += 1;
|
||||
continue :outer; // "continue" for the first loop.
|
||||
}
|
||||
} // count = 8
|
||||
outer: for ([_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }) |_| {
|
||||
for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
|
||||
count += 1;
|
||||
break :outer; // "break" for the first loop.
|
||||
}
|
||||
} // count = 1
|
||||
|
||||
|
||||
// Labels can also be used to return a value from a block.
|
||||
var y: i32 = 5;
|
||||
const x = blk: {
|
||||
y += 1;
|
||||
break :blk y; // Now "x" equals 6.
|
||||
};
|
||||
// Relevant in cases like "for else" expression (explained in the following).
|
||||
|
||||
// For loops can be used as expressions.
|
||||
// When you break from a for loop, the else branch is not evaluated.
|
||||
// WARNING: counter-intuitive.
|
||||
// The "for" loop will run, then the "else" block will run.
|
||||
// The "else" keyword has to be followed by the value to give to "result".
|
||||
// See later for another form.
|
||||
var sum: u8 = 0;
|
||||
const result = for (items) |value| {
|
||||
sum += value;
|
||||
} else 8; // result = 8
|
||||
|
||||
// In this case, the "else" keyword is followed by a value, too.
|
||||
// However, the syntax is different: it is labeled.
|
||||
// Instead of a value, there is a label followed by a block of code, which
|
||||
// allows to do stuff before returning the value (see the "break" invocation).
|
||||
const result = for (items) |value| { // First: loop.
|
||||
sum += value;
|
||||
} else blk: { // Second: "else" block.
|
||||
std.log.info("executed AFTER the loop!", .{});
|
||||
break :blk sum; // The "sum" value will replace the label "blk".
|
||||
};
|
||||
```
|
||||
|
||||
### Switch.
|
||||
```zig
|
||||
|
||||
// As a switch in C, but slightly more advanced.
|
||||
// Syntax:
|
||||
// switch (value) {
|
||||
// pattern => expression,
|
||||
// pattern => expression,
|
||||
// else => expression
|
||||
// };
|
||||
|
||||
// A switch only checking for simple values.
|
||||
var x = switch(value) {
|
||||
Error.UnExpected => return err,
|
||||
Error.Authentication => unreachable,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// A slightly more advanced switch, accepting a range of values:
|
||||
const foo: i32 = 0;
|
||||
const bar = switch (foo) {
|
||||
0 => "zero",
|
||||
1...std.math.maxInt(i32) => "positive",
|
||||
else => "negative",
|
||||
};
|
||||
```
|
||||
|
||||
### Structures.
|
||||
```zig
|
||||
|
||||
// Structure containing a single value.
|
||||
const Full = struct {
|
||||
number: u16,
|
||||
};
|
||||
|
||||
// Packed structure, with guaranteed in-memory layout.
|
||||
const Divided = packed struct {
|
||||
half1: u8,
|
||||
quarter3: u4,
|
||||
quarter4: u4,
|
||||
};
|
||||
|
||||
// Point is a constant representing a structure containing two u32, "x" and "y".
|
||||
// "x" has a default value, which wasn't possible in C.
|
||||
const Point = struct {
|
||||
x: u32 = 1, // default value
|
||||
y: u32,
|
||||
};
|
||||
|
||||
// Variable "p" is a new Point, with x = 1 (default value) and y = 2.
|
||||
var p = Point{ .y = 2 };
|
||||
|
||||
// Fields are accessed as usual with the dot notation: variable.field.
|
||||
print("p.x: {}\n", .{p.x}); // 1
|
||||
print("p.y: {}\n", .{p.y}); // 2
|
||||
|
||||
|
||||
// A structure can also contain public constants and functions.
|
||||
const Point = struct {
|
||||
pub const some_constant = 30;
|
||||
|
||||
x: u32,
|
||||
y: u32,
|
||||
|
||||
// This function "init" creates a Point and returns it.
|
||||
pub fn init() Point {
|
||||
return Point{ .x = 0, .y = 0 };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// How to access a structure public constant.
|
||||
// The value isn't accessed from an "instance" of the structure, but from the
|
||||
// constant representing the structure definition (Point).
|
||||
print("constant: {}\n", .{Point.some_constant});
|
||||
|
||||
// Having an "init" function is rather idiomatic in the standard library.
|
||||
// More on that later.
|
||||
var p = Point.init();
|
||||
print("p.x: {}\n", .{p.x}); // p.x = 0
|
||||
print("p.y: {}\n", .{p.y}); // p.y = 0
|
||||
|
||||
|
||||
// Structures often have functions to modify their state, similar to
|
||||
// object-oriented programming.
|
||||
const Point = struct {
|
||||
const Self = @This(); // Refers to its own type (later called "Point").
|
||||
|
||||
x: u32,
|
||||
y: u32,
|
||||
|
||||
// Take a look at the signature. First argument is of type *Self: "self" is
|
||||
// a pointer on the instance of the structure.
|
||||
// This allows the same "dot" notation as in OOP, like "instance.set(x,y)".
|
||||
// See the following example.
|
||||
pub fn set(self: *Self, x: u32, y: u32) void {
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
}
|
||||
|
||||
// Again, look at the signature. First argument is of type Self (not *Self),
|
||||
// this isn't a pointer. In this case, "self" refers to the instance of the
|
||||
// structure, but can't be modified.
|
||||
pub fn getx(self: Self) u32 {
|
||||
return self.x;
|
||||
}
|
||||
|
||||
// PS: two previous functions may be somewhat useless.
|
||||
// Attributes can be changed directly, no need for accessor functions.
|
||||
// It was just an example.
|
||||
};
|
||||
|
||||
// Let's use the previous structure.
|
||||
var p = Point{ .x = 0, .y = 0 }; // "p" variable is a Point.
|
||||
|
||||
p.set(10, 30); // x and y attributes of "p" are modified via the "set" function.
|
||||
print("p.x: {}\n", .{p.x}); // 10
|
||||
print("p.y: {}\n", .{p.y}); // 30
|
||||
|
||||
// In C:
|
||||
// 1. We would have written something like: point_set(p, 10, 30).
|
||||
// 2. Since all functions are in the same namespace, it would have been
|
||||
// very cumbersome to create functions with different names for different
|
||||
// structures. Many long names, painful to read.
|
||||
//
|
||||
// In Zig, structures provide namespaces for their own functions.
|
||||
// Different structures can have the same names for their functions,
|
||||
// which brings clarity.
|
||||
```
|
||||
|
||||
### Tuples.
|
||||
```zig
|
||||
// A tuple is a list of elements, possibly of different types.
|
||||
|
||||
const foo = .{ "hello", true, 42 };
|
||||
// foo.len == 3
|
||||
```
|
||||
|
||||
### Enumerations.
|
||||
```zig
|
||||
|
||||
const Type = enum { ok, not_ok };
|
||||
|
||||
const CardinalDirections = enum { North, South, East, West };
|
||||
const direction: CardinalDirections = .North;
|
||||
const x = switch (direction) {
|
||||
// shorthand for CardinalDirections.North
|
||||
.North => true,
|
||||
else => false
|
||||
};
|
||||
|
||||
// Switch statements need exhaustiveness.
|
||||
// WARNING: won't compile. East and West are missing.
|
||||
const x = switch (direction) {
|
||||
.North => true,
|
||||
.South => true,
|
||||
};
|
||||
|
||||
|
||||
// Switch statements need exhaustiveness.
|
||||
// Won't compile: East and West are missing.
|
||||
const x = switch (direction) {
|
||||
.North => true,
|
||||
.South => true,
|
||||
.East, // Its value is the same as the following pattern: false.
|
||||
.West => false,
|
||||
};
|
||||
|
||||
|
||||
// Enumerations are like structures: they can have functions.
|
||||
```
|
||||
|
||||
### Unions.
|
||||
```zig
|
||||
|
||||
const Bar = union {
|
||||
boolean: bool,
|
||||
int: i16,
|
||||
float: f32,
|
||||
};
|
||||
|
||||
// Both syntaxes are equivalent.
|
||||
const foo = Bar{ .int = 42 };
|
||||
const foo: Bar = .{ .int = 42 };
|
||||
|
||||
// Unions, like enumerations and structures, can have functions.
|
||||
```
|
||||
|
||||
### Tagged unions.
|
||||
```zig
|
||||
|
||||
// Unions can be declared with an enum tag type, allowing them to be used in
|
||||
// switch expressions.
|
||||
|
||||
const MaybeEnum = enum {
|
||||
success,
|
||||
failure,
|
||||
};
|
||||
|
||||
const Maybe = union(MaybeEnum) {
|
||||
success: u8,
|
||||
failure: []const u8,
|
||||
};
|
||||
|
||||
// First value: success!
|
||||
const yay = Maybe{ .success = 42 };
|
||||
switch (yay) {
|
||||
.success => |value| std.log.info("success: {}", .{value}),
|
||||
.failure => |err_msg| std.log.info("failure: {}", .{err_msg}),
|
||||
}
|
||||
|
||||
// Second value: failure! :(
|
||||
const nay = Maybe{ .failure = "I was too lazy" };
|
||||
switch (nay) {
|
||||
.success => |value| std.log.info("success: {}", .{value}),
|
||||
.failure => |err_msg| std.log.info("failure: {}", .{err_msg}),
|
||||
}
|
||||
```
|
||||
|
||||
### Defer and errdefer.
|
||||
```zig
|
||||
|
||||
// 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.
|
||||
|
||||
[ziglang]: https://ziglang.org
|
||||
[ziglearn]: https://ziglearn.org/
|
||||
[zigdoc]: https://ziglang.org/documentation/
|
Loading…
Reference in New Issue
Block a user