Will Zig be the "sweet spot" I am looking for?

Let’s continue our exploration of novel kinda-low-level languages. After a quick exploration of Crystal, it is time to look at another language that is constantly popping up on my sources: Zig.

It will be my new love? It will be the perfect tool I was looking for? It is the beginning of a new programming langue star? Let’s find out that together.

What is Zig?

Zig is a compiled general-purpose programming language. Syntax wide is something in between Rust and Kotlin but, in its core, it is just an improved C.

One of the design’s cornerstone is to be extremely predictable. As they state, Zig do not perform any hidden, implicit, control flows or memory allocation. That means no macros, but also no exceptions (exceptions change the execution flow), no operator overloads (operator overloads are just a hidden function call), no properties (properties are another hidden function call), no allocations during coroutines, and so on. You got the point.

Other than that the language really is a “modern C with a sound type system” and, how we will see, this comes with pros and cons.

(I also want to add that Andrew Kelley, Zig’s creator, is a cool guy and the Zig development is very open. If you will like this language, you will have fun.)

Zig: The Good

Zig is clear

When I say that Zig is clear, I am not only talking about its syntax. I am talking about the perceived “character” of the language. Many languages fail because they look like “generic language Num. 2943”. Zig, on the other hand, is very opinionated. It has a clear goal, a clear design direction, and it knows what it wants to achieve.

And let’s be honest: a C without nullable types is already a good thing.

Zig syntax is familiar

Note: it is a personal first impression. Of course, if you only programmed in Python is definitely not familiar.

Zig syntax is very similar for people coming from C, but especially from Rust. Look at this example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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={d})", id);
        errdefer allocator.free(device.name);

        if (id == 0) return error.ReservedDeviceId;

        return device;
    }
};

pub, fn, struct, types defined after a colon (:), const, lines ending with semicolons (;) they are all things very gentle to the eyes of a Rust programmer and not alien to a C programmer.

Flawless C integration

Every time we start with a new kinda-low-level language we start thinking about interfacing it with something else, especially with the old rock-solid C libraries. However, this part is always more complicated than expected.

Not with Zig.

One of the biggest selling points of Zig is C interoperability. You do not need to wrap anything into anything else: you can just import the C library. Do you want to import soundio? No worries: just do it.

1
const c = @cImport(@cInclude("soundio/soundio.h"));

How it’s possible? Because Zig can also directly compile C code.

1
zig build-exe --c-source hello.c --library c

That’s a big selling point for Zig. It also greatly expands its “libraries” pool. Now you can use C libraries as Zig libraries (with minimal changes in the variables types).

More info here.

Zig: The Goodish

Zig has not Memory Management

For a long time, we got used to languages where memory is automatically managed (via garbage collector, reference counting or some other mechanism). Then came Rust: a language where memory is not managed, but memory safety is guaranteed at compile time.

We thought that there is no coming back.

So, you can imagine my surprise when I saw that Zig is old school: memory is not managed, and you have to worry about it.

Before you run away screaming, let’s be more precise. There are three big categories of memory safety:

  • Spacial Memory Safety: you are reading/writing where you shouldn’t. For instance, out of bound access to arrays.
  • Temporal Memory Safety: you are reading/writing when you shouldn’t. For instance, reading a block of memory after a free.
  • Data Race Safety: let’s not open this box. It is related concurrent read/write in a multi-threading setting.

For what concerns spacial memory safety, Zig and Rust have a similar behavior: check this at runtime (there is not really a way to avoid this). However, Rust is capable of asserting temporal memory safety and data race safety at compile time (and that’s the biggest killer feature of Rust). However, this has a price: a substantial increase in language complexity.

Zig avoid such complexity by doing nothing about them at compile time. I can hear you: what about safety!? Do not worry, Zig provides a runtime validation that is sufficient in many situations. Then, after you tested the software you can ship the release version disabling those checks. At least for temporal memory safety.1

Is it enough? Depends. For instance, some application can achieve temporal memory safety in an elementary way: never calling free. If you are developing simple command line tools that run and die in a matter of seconds, there is no point in freeing memory during the execution.

On the other hand, compile validation is huge for a good deal of other complex, heavy, continuously running applications. Especially for data race safety. Therefore, in the end, I am not totally sold on this additional cognitive load I need to carry around during development. But it has its uses.

Zig: The Baddish

No strings attached. Literally

In its frenzy to compete with C, Zig did another peculiar choice: it has no string type defined in the language. Instead, any string literal is a *const [5:0]u8, i.e., a null-terminated array in memory. In some sense, instead of making strings special citizens of the language, the entire language encourages you to use a sentinel-terminated chunk of memory.

The syntax [N:x]T is the core of Zig array/slices/pointers. It defines a piece of memory of type T, of size N terminated by the value x. For example, const array = [_:0]u8 {1, 2, 3, 4}; define an array of u8 terminated by 0 (Zig infers the array’s size from the in-line initialization).

In theory, this makes perfectly sense, however, as a new user, this is somewhat confusing because strings are a special citizen. Strings are a much more complex beast than arrays of bytes. Strings require dedicated functions. For instance, reversing UTF-8 encoded strings is not equal to reversing the code-points stored in an array. You need to reverse the graphemes of the string. If I reverse an “string” array in Zig, I end up with an invalid string.

By design, contrary to most other languages, Zig decided to leave all this complexity to third-party libraries. Personally, I do not think it is a very good idea (Haskell has a similar problem, and it is considered one of its biggest early mistakes).

Still in a 0.x release

At the time I am writing, Zig’s last version is the 0.7.1. As a consequence, the language is considered still pretty unstable. Many part of the languages, especially the standard library, gave me the feeling that the language is still immature.

As a consequence, adoption is little and slow and there are no big solid “framework” or “killer app”.

Yet.

I think the language is fascinating to follow. But there is still a lot of road to do.

Conclusions

Let’s recap.

Pro:

  • Familiar syntax.
  • Very interesting design decisions.
  • Actively developed by a non-profit foundation.
  • Superb C interoperability.
  • A solid (but kinda low level) async/await support.

Cons:

  • Still in pre-1.0 state.
  • Mostly unsupervised memory management (this is a pro or con depending on the application)
  • The absence of built-in string functionalities is not new-user friendly. And a bit weird. And a bit annoying.

In the end. Zig got me excited way more than Crystal, however, I quickly realized that the language still need active development and, in some sense, is more low-level than I was looking for.

I have good filing for this language, and I am sure it will cross my road in the future. But for now, I am not in love. My quest continues.


  1. From what I read, it seems that they are trying to add runtime checks also for Data Race Safety. For now, though, those are totally on you. ↩︎

Header Image
Will Crystal be the "sweet spot" I am looking for?

The Crystal programming language recently reached version 1.0. As a modern compiled language, it caught my attention. It is time to spend some time playing with it to have a better idea of …

Read
Header Image
Go is still fighting over generics. In 2019.

I dislike Go. I dislike it a lot. Nevertheless, I usually do not bash on it because I am deeply convinced that people should use whatever they want and they like. Many people I respect use …

Read
Header Image
Kotlin Development in VS Code

Kotlin is a really sweet language. It is the perfect thing in between a “super-powerful and but difficult language” like Rust or Modern C++, and a “super-easy but that seems to be designed …

Read
comments powered by Disqus