Zig getting started

Welcome to the quick zig walkthrough, where I’ll share the very basics on getting started with ziglang.

But take that with a pinch of salt, as I’m sure it’ll end up as a “this article is getting long, so I’ll write more about X later.”

Is that for low-level programming?#

yes

A high-level programming language, like C, that targets low-level programming.

Why?#

Andrew Kelley on Aug 29, 2016 posted in HN:

My plan to replace C is simple and pragmatic.

  1. Make a language and compiler that fills the same niche as C but does it significantly better, taking into account all the lessons we’ve learned in the last 44 years.
  2. Make a build tool that competes with autotools, cmake, make, etc. Similar to Python build.py, you’ll have a build.zig, import a library and use it. The library does everything configure/make does except better.
  3. Package management on par with NPM and Cargo. Minimal overhead to publishing reusable code. Although I don’t plan to have a central repository, rather the ability to put a URL or source control URL with a hash.
  4. Zig can import and export .h files, and you can target .o files that are compatible with the rest of your C codebase.
  5. Now there is minimal cost to replace bits and pieces of your codebase with Zig.

Why not Rust?#

aint-nobody-got-time-for-that

Andrew Kelley on Sep 22, 2020 posted on Twitter:

It’s my dream to raise software quality standards of the whole world. I want software to be known as reliable, productive, and serving humans rather than humans serving algorithms.

How to install?#

The installation process I’m sharing is for macOS and homebrew users, you’ll have to check the official docs for your operating system.

It’s preferred to install from latest HEAD

brew install zig --HEAD

Why –HEAD?#

If you want to use the zig language server, you’re likely to get the error reported in https://github.com/zigtools/zls/issues/222

Code highlight?#

To install the Zig language server (https://github.com/zigtools/zls) and the zls-vscode extension (https://github.com/zigtools/zls-vscode/releases)

git clone --recurse-submodules https://github.com/zigtools/zls
cd zls
zig build -Drelease-safe
# Configure ZLS
zig build config

How’s the Hello world?#

Create a new file main.zig and write the following:

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

  // Main fn
  pub fn main() void {
      print("Hello world!\n", .{});
  }

Execute it by running the cmd:

zig run main.zig

You’ll see the output:

Hello world!

What you did this far, was to write a program that outputs a text. The program uses the standard library, where we pick the print method from debug, which then we run inside the main function; main as it implies, is how zig knows what to execute first!

We use the print from debug, because print is a regular fn that might throw an error and prevent our program from continuing, the print fn from debug on failure continues without interrupting our program flow.

Have a quick look onto the documentation to find other alternatives for print https://ziglang.org/documentation/master/std/#std?print .

Oh! The second argument is an empty struct:

print("Hello {s}\n", .{"world"});

To find more about the formating options (specifiers), check the comments in the source-code .

How’s the function declarations?#

As the main fn demonstrated previously, the syntax is quite common among C syntax inspired languages:

fn fnNameCamelCase(variable: type) returnType {
  return computedValue;
}

We can call functions recursively, and values in the fn scope can be ignored:

_ = 1

There’s also defer, which is used to execute a statement when exiting. It’s like placing the statement in a defer stack, that is executed when the function terminates. The ziglearn.org provides a very good example of this:

var x: i16 = 5;
{
    defer x += 2;

    // The output is "Result 1 is 5!"
    print("Result 1 is {d}!\n", .{x});
}

// The output is "Result 2 is 7!"
print("Result 2 is {d}!\n", .{x});

Comments?#

Zig only supports single line comments. There are no multiple line comments (there’s no such thing as /* */ like we have in C). The reason is explained as:

This helps allow Zig to have the property that each line of code can be tokenized out of context.

Variable assignment?#

Zig supports the syntax var and const. A constant is immutable, and preferred over whenever possible.

var foobar: type = value;
const foobar: type = value;

The type can be inferred by the value.

var foobar = value;
const foobar = value;

Do I really need to use semicolons?#

yes

Plus, there are coding conventions, to know more about it, check the documention here .

How the primitive types look like?#

Here’s a brief example of some of its types, you can find more by clicking on the links.

TypeC EquivalentDescription
i8int8_tsigned 8-bit integer
i16int16_tsigned 16-bit integer

Up to 128 (e.g. i32, i64, i128) and also unsigned (e.g. u8, u16, u32, etc). In laymen’s terms it means that an unsigned integer does not have negative values, while a signed integer can be negative. An unsigned integer can hold a higher range of positive values. See integers .

In addition to the integer types above, arbitrary bit-width integers can be referenced by using an identifier of i or u followed by digits. For example, the identifier i7 refers to a signed 7-bit integer. The maximum allowed bit-width of an integer type is 65535.

TypeC EquivalentDescription
f16_Float1616-bit floating point (10-bit mantissa) IEEE-754-2008 binary16
f32float32-bit floating point (23-bit mantissa) IEEE-754-2008 binary32

Up to 128 (e.g f64, f128)! See floats .

TypeC EquivalentDescription
boolbooltrue or false
void(none)0 bit type
noreturn(none)the type of break, continue, return, unreachable, and while (true) {}

A complete list can be found:

https://ziglang.org/documentation/master/#toc-Primitive-Types

What about the primitive values?#

TypeDescription
true and falsebool values
nullused to set an optional type to null
undefinedused to leave a value unspecified

Does it support optional types?#

yes

You can convert a type to an optional type by putting a question mark in front of it:

const optional_int: ?i32 = 5678;

The syntax can look a bit different although, but you always put the question mark in fron of the type:

const foobar: ?[]const u8 = "Hello world!";

As you can probably guess by the example, a string literal is a pointer to an array literal!

const mem = @import("std").mem;

pub fn main() void {
    const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
    const same_message = "hello";

    if (mem.eql(u8, &message, same_message)) {
        print("Hello world!\n", .{});
    }
}

Don’t get scared, eql is simply doing a comparison between the two slices, see source code .

How do array’s look like?#

const a = [3]u8{ 1, 2, 3 };

When inferred:

const a = [_]u8{ 1, 2, 3 };

This reads as [Number of elements]Type. And the curly braces is that in Zig we can use the syntax T{} to construct values.

For the array length, as demonstrated, this can be inferred by using the _. But as far as I understand at time of writing, this makes it a Slice - The difference between an array and a slice is that the array’s lenght is part of the type and known at compile time, whereas the slice’s lenght is known at runtime (both have the interface .len although).

You can learn more about arrays, and in particular multidimensional arrays, here .

What about conditional statements?#

In Zig we have if statements, but these only accept the condition bool. Which means that there’s no such thing as truthy or falsy values.

value = true;

if (value) {
  // does X
} else {
  // does y
}

It also works as an expression:

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

pub fn main() void {
    const a = true;
    const x: u16 = if (a) 15 else 30;
    
    print("Result is {d}!\n", .{x});
}

Note: You can use “{d}” to print a base 10 representation of your number into the resulting string.

Could I convert an integer to string and print it?#

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

pub fn main() void {
    const user_input: u8 = 30;

    var buf: [2]u8 = undefined;

    const result = fmt.bufPrint(&buf, "{}", .{user_input});

    debug.print("Result is {s}!\n", .{result});
}

What’s that “{}”?#

“{}” is a format string. For example:

fmt.bufPrint(&buf, "x is: {}, y is {}\n", .{x, y});

The output will be written into the buffer you pass and a slice of the written output returned.

If you don’t want to supply a buffer manually, use fmt.allocPrint(). You’ll need to use a memory Allocator, since the Zig language does not perform memory management on your behalf, find more about it here . We’ll get more into this later…

const std = @import("std");
const fmt = std.fmt;
const debug = std.debug;
const Allocator = std.mem.Allocator;

pub fn main() void {
    const allocator = std.heap.page_allocator;
    const user_input: u8 = 30;

    const result = std.fmt.allocPrint(allocator, "{}", .{user_input});
 
    debug.print("Result is {s}!\n", .{result});
}

To learn the rules for format strings, read the doc comment of std.fmt.format() .

Do I need to check the source-code for documentation?#

yes

The goal is to have the documentation ready for std for version 1.0; but at the time of writing the tool is not ready yet, but once ready it’ll be available for everybody to use. So, if you stick with how to document a Zig program, you’ll get this for free, saving you a lot of time documenting your project.

To learn more about doc commnets, visit the docs .

Note: when checking the source-code, make sure the Zig version you have installed corresponds to it.

How’s iteration?#

The iteration control structures for Zig, are for and while loops.

To iterate a certain number of times, we use for :

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

pub fn main() void {
    var sum: u32 = 0;
    const items = [_]u8 { 5, 4, 3, 2, 1, 0, 10, 20, 30 };

    // For loops iterate over slices and arrays.
    for (items) |value| {
        // Break and continue are supported.
        if (value == 0) {
            break;
        }
        sum += value;
    }

    print("Result is {d}!\n", .{sum});
}

You can use slices, which provides you with a subset of a set starting from N (as in index, we’re dealing with zero-based indexing) and ending M-1 (see it as the quantity):

const items = [_]u8 { 5, 4, 3, 2, 1, 0, 10, 20, 30 };
var sum: u32 = 0;

// To iterate over a portion of a slice, reslice.
for (items[0..1]) |value| {
    sum += value;
}

// This results in 5
print("Result is {d}!\n", .{sum});

You can also omit the ending, to get to the item end.

The index of the iteration can be accessed by specifying the second capture value:

// To access the index of iteration, specify a second capture value.
for (items) |value, index| {
  print("Index {d} and value {d}!\n", .{index, value});
}

There’s some other concepts that I advise you to check in the docs , such as labeled, and expressions, similar to what I write about in the while loop.

To iterate until a certain condition is reached, we use the while loop:

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

pub fn main() void {
    var i: usize = 0;

    while (i < 10) {
        i += 1;
    }

    print("Result is {d}!\n", .{i});
}

Obs: you can use break and continue, see while . A few things you should read about is that break accepts a value parameter; and labeled while, which can be referenced from a break or continue.

outer: while (true) {
    while (true) {
        break :outer;
    }
}

outer: while (i < 10) : (i += 1) {
    while (true) {
        continue :outer;
    }
}

With while loops we can also capture the payload, take close attemption to the fn signature, as we have ?optional, and exiting the loop when encountering null:

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

pub fn main() void {
    var sum: u32 = 0;

    while (eventuallyNullSequence()) |value| {
        sum += value;
    }

    print("Result is {d}!\n", .{sum});
}

var numbers_left: u32 = 3;

fn eventuallyNullSequence() ?u32 {
    return if (numbers_left == 0) null else blk: {
        numbers_left -= 1;
        break :blk numbers_left;
    };
}

For each of these iterators, you have the option to run them inline at compile-time, inline for and inline while . Make sure you check the recommended use-case for inline loops, to avoid using it in the wild.

Can you import your own stuff?#

Absolutely! What you do is to create a separate file and expose the functions, or variables by prefixing it with the pub syntax.

In the example below, you can see a custom type Foobar, that is used as the type for the argument point in the function declaration.

// File foobar.zig
pub const Foobar = struct {
    x: i32,
    y: i32,
};

// File barfoo.zig
const foobar = @import("foobar.zig");

fn foo(point: foobar.Foobar) i32 {
    return point.x + point.y;
}

In Zig, you have to include the .zig filename extension.

Are there Constructor’s?#

Zig is a imperative programming language and unlike object oriented programming languages, Zig does not have classes and inheritance, etc.

Although, you can have a routing like Contructor as the following example demonstrates (I’ve used a common allocator pattern, see choosing an allocator ):

const print = @import("std").debug.print;
const heap = @import("std").heap;
const mem = @import("std").mem;
const expect = @import("std").testing.expect;

const Company = enum(u8) {
  honda,
  kawasaki,
  toyota,
  mitsubishi,
  panasonic,
  sony
};

const Person = struct {
  name: []const u8,
  age: u8,
  nickname: []const u8,
  company: Company,

  pub fn isAdult(self: *Person) bool {
    return self.age > 18;
  }

  pub fn setNickname(self: *Person, nickname: []const u8) void {
    self.nickname = nickname;
  }

  pub fn create(allocator: *mem.Allocator, name: []const u8, age: u8, companyNumber: u8) !*Person {
    var p = try allocator.create(Person);
    p.name = name;
    p.age = age;
    p.company = @intToEnum(Company, companyNumber);

    return p;
  }
};

test "creates person" {
  var arena = heap.ArenaAllocator.init(heap.page_allocator);
  defer arena.deinit();

  const allocator = &arena.allocator;

  const age: u8 = 32;
  var person = try Person.create(allocator, "John", age, 2);
  person.setNickname("Lizard");

  print("The person's name is {s}, {d} is an adult? {b}\n", .{person.name, person.age, person.isAdult()});
  print("The nickname's {s}\n", .{person.nickname});
  print("The Company {}\n", .{person.company});

  expect(person.age == age);
}

Does it have dictionary, tables or hashmaps?#

In Zig these are named “Hashmaps”, where at time of writing are not yet documented but can be found in the source-code .

A practical example of how to use follows (uses strings):

    var m = StringHashMap(ValueType).init(allocator);

    try m.put(key, value);

    var itr = m.iterator();
    
    while (itr.next()) |e| {
        // do something with 'e'
    }

    var value = m.get(key) orelse value_if_not_absent;

    if (m.contains(key)) {}

Find more details here in the implementation:

https://github.com/ziglang/zig/blob/master/lib/std/hash_map.zig#L812 https://github.com/ziglang/zig/blob/master/lib/std/hash_map.zig#L61

An example of it in practice (look at the fn signature, where the type is defined as std.StringHashMap([]const u8), if using the AutoHashMap that’d be AutoHashMap(u32, u32)):

  fn initKeywords(allocator: *std.mem.Allocator) !std.StringHashMap([]const u8) {
    var map = std.StringHashMap([]const u8).init(allocator);

    for (token.TokenKeywords) |keyword| {
      try map.put(keyword, "");
    }

    return map;
  }

Can I access an enum by a string literal?#

That’s a very dearing question to ask, as that’s a very common case for high-level languages but not the case for lower-level languages. Specially after we learned that a string literal, is really a pointer to an array literal. But here’s why Zig is awesome, check the example and find more in the source-code :

test "std.meta.stringToEnum" {
    const E1 = enum {
        A,
        B,
        FOOBAR
    };
    std.testing.expect(E1.A == std.meta.stringToEnum(E1, "A").?);
    std.testing.expect(E1.B == std.meta.stringToEnum(E1, "B").?);
    std.testing.expect(E1.FOOBAR == std.meta.stringToEnum(E1, "FOOBAR").?);
}

Do functions take functions as an argument?#

Zig supports it! So, you can get the function type by using @TypeOf(the_function) and use that when declaring your function.

pub fn myFunction(arg1: u8, callback: fn(u8) bool) void {
    if (!callback(arg1)) {
        ...
    }
}

Where fn(u8) bool is the return value of @TypeOf(the_function).

How to initialise a type that has a field Array of a given type?#

A bit tricky, as we have the concept of Array vs Slice, where the Array’s length is part of the type and known at compile-time.

const Foobar = struct {
  name: []const u8
};

const Thing = struct {
  foobars: [1]Foobar
};

test "thing init" {
  const t: Thing = .{
    .foobars = .{
      .{
        .name = "test"
      }
    }
  };
}

Whereas the slice’s length is known at runtime. See the type declaration []const Foobar and the address-of operator & that evaluates to the memory address of the operand.

const Foobar = struct {
  name: []const u8
};

const Thing = struct {
  foobars: []const Foobar
};

test "thing init" {
  const t: Thing = .{
    .foobars = &.{
      .{
        .name = "Helen"
      },
      .{
        .name = "Dolphin"
      },
      .{
        .name = "Margarida"
      }
    }
  };
}

Here’s an extended example, in which we deal with struct fn calls (notice we changed the type []const Foobar to []Foobar and we also moved the initialisation outside as &.{} creates a temporary variable that is a const breaking self):

const std = @import("std");

const thingErrors = error {
  failed
};

const Foobar = struct {
  name: []const u8,

  fn getName(self: *Foobar) []const u8 {
    return self.name;
  }
};

const Thing = struct {
  foobars: []Foobar,

  pub fn getFirst(self: *Thing) thingErrors![]const u8 {
    if (self.foobars.len > 0) {
      return self.foobars[0].getName();
    } else {
      return thingErrors.failed;
    }
  }
};

test "thing init" {
  var foobars = [_]Foobar {
    .{
      .name = "test"
    },
    .{
      .name = "someother"
    }
  };

  var t: Thing = .{
    .foobars = &foobars
  };

  std.debug.print("name is: {s}\n", .{ t.getFirst() });
}

How to handle errors?#

Error union type&rsquo;s , a function return type can be a comination of an error set type and a normal type, this forms an erro union type.

const myError = error {
    someError,
    otherError
}

pub fn sayHello() myError!void {
    return myError;
}

We can then catch it and provide a default:

const msg: []const u8 = sayHello() catch "Hello world!";

or, catch and return the error:

sayHello() catch |err| return err;

You can use the shortcut for this case of catching and returning err:

try sayHello();

And even ignore any errors, if you are absolutely certain it’ll never happen:

const msg: []const u8 = sayHello() catch unreachable;

How to construct Structs?#

The common pattern is to only use .init when you need to do some computation on the args before the struct construction e.g. allocate some things that will be need to be freed with a .deinit call.

Here’s an example, with .init as a convenient method:

const std = @import("std");

pub const Person = struct {
  alloc: *std.mem.Allocator,
  first_name: []const u8,
  second_name: []const u8,

  pub fn fullname(self: *Person) ![]const u8 {
    return std.mem.concat(self.alloc, u8, &[_][]const u8 { self.first_name, " ", self.second_name });
  }

  pub fn init(alloc: *std.mem.Allocator, fname: []const u8, sname: []const u8) Person {
    return Person {
      .alloc = alloc,
      .first_name = fname,
      .second_name = sname
    };
  }
};

test "Person" {
  var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
  defer arena.deinit();
  const allocator = &arena.allocator;

  var p = Person.init(allocator, "King", "Fish");
  const f = try p.fullname();

  std.testing.expectEqualStrings("King", p.first_name);
  std.testing.expectEqualStrings("Fish", p.second_name);
  std.testing.expectEqualStrings("King Fish", f);

  std.debug.warn("\nFullname: {s}\n", .{ f });
}

Since we’re not computing the args, or if all you’re doing is putting the fields in the struct, we don’t necessarily have to .deinit later - with that being said the following is more common use-case as a constructor:

test "Person" {
  var p = Person {
    .alloc = alloc,
    .first_name = fname,
    .second_name = sname
  }
}

Does it have support for Union types?#

Yes, through tagged union , here’s an example:

const std = @import("std");

const HtmlTags = enum {
  img,
  video
};

const ContentType = union(HtmlTags) {
  img: ImgContent,
  video: VideoContent
};

const ImgContent = struct {
  src: []const u8,
  alt: []const u8
};

const VideoContent = struct {
  src: []const u8,
  loop: bool,
  poster: []const u8
};

fn warnMediaSource(c: ContentType) void {
  switch (c) {
    .img => |content| {
      std.debug.warn("\nTypeOf imgTag: {s}\n", .{ content.src });
    },
    .video => |content| {
      std.debug.warn("\nTypeOf videoTag: {s}\n", .{ content.src });
    }
  }
}

test "Union" {
  const imgTag = ContentType {
    .img = ImgContent {
      .src = "http://placekitten.com/200/300",
      .alt = "Kitten!"
    }
  };

  const videoTag = ContentType {
    .video = VideoContent {
      .src = "http://www.ghibli.jp/nice-movie.mp4",
      .loop = true,
      .poster = "http://www.ghibli.jp/nice-movie.jpg"
    }
  };

  // The TypeOf is `ContentType`
  std.debug.warn("\nTypeOf imgTag: {s}\n", .{ @TypeOf(imgTag) });
  std.debug.warn("\nTypeOf videoTag: {s}\n", .{ @TypeOf(videoTag) });

  warnMediaSource(imgTag);
  warnMediaSource(videoTag);
}

Does it have support for Interfaces?#

It does not have “Interface” but you can achieve it through the following pattern:

const std = @import("std");

const Iface = struct {
    getMessageFn: fn(*Iface) []const u8
    pub fn getMessage(self: *Iface) []const u8 {
        return self.getMessageFn(self);
    }
    pub fn printMessage(self: *Iface) void {
        std.debug.print("{}\n", .{self.getMessage()});
    }
};

const Impl = struct {
    iface: Iface = .{
      .getMessageFn = getMessage,
    },
    message: []const u8,

    pub fn init(message: []const u8) Impl {
        return Impl{.message = message};
    }

    pub fn iface(self: *Impl) *Iface {
        return &self.iface;
    }

    fn getMessage(iface: *Iface) []const u8 {
        const self = @fieldParentPtr(Impl, "iface", iface);
        return message;
    }
};

test {
    var i = Impl.init("Hello, world!");
    var iface = i.iface();
    std.testing.expectEqual(iface.getMessage(), "Hello, world!");
    iface.printMessage(); // Outputs "Hello, world!"
}

Pass by *?#

A quick note on that, so by-* is usually referring to “pass-by-*”.

fn f(v: i32) void {} // by value
fn f(v: *i32) void {} // by ref/pointer

Also, “in-place” is more the opposite of “duplicate”. See in-place algorithm .

Run multiple tests?#

You should reference all test files inside your main test file so you only need to declare 1 step.

Let’s say this is main_test.zig:

test "All tests" {
    _ = @import("my_file.zig");
    _ = @import("my_other_file.zig");
}

In the build.zig (I picked part of this one from zig init-lib):

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

pub fn build(b: *Builder) void {
    const mode = b.standardReleaseOptions();

    var main_tests = b.addTest("src/main_test.zig");
    main_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&main_tests.step);
}

Finally, you can run zig build test

TO CONTINUE…

References:#

https://ziglang.org/learn/getting-started/

https://ziglang.org/documentation/master/

https://github.com/nrdmn/awesome-zig

https://erik-engheim.medium.com/is-zig-the-long-awaited-c-replacement-c8eeace1e692

https://ziglaunch.org/

comments powered by Disqus