Story
C is the first programming language I’ve learned, all the way back in . However I recently started learning Zig. Zig is similar to C in may ways. It is a low-level language destined for systems programming and hardware interaction. And I absolutely love it! I’ve implemented a clone of the GNU ’ . $f->code(‘test’) . ’ command to learn it. This language fixes many of the problems and shortcomings of C.
Generics
Generics are a pain to do in C. There are multiple approaches: void pointers, macros, passing the type size. But there are very few guarantees of correctness and the syntax is tedious.
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#define vector_MIN_CAP 32
#define vector_struct(T) \
typedef struct T##_vector { \
T *buf; \
size_t capacity; \
size_t size; \
} T##_vector;
#define vector_init(T) \
void T##_vector_init(T##_vector *vec) { \
vec->capacity = vector_MIN_CAP; \
vec->buf = malloc(sizeof(T) * vec->capacity); \
vec->size = 0; \
}
#define vector_get(T) \
void *T##_vector_get(T##_vector *vec, size_t idx) { return vec->buf + idx; }
#define vector_set(T) \
void T##_vector_set(T##_vector *vec, size_t idx, T data) { \
vec->buf[idx] = data; \
}
#define vector_push(T) \
void T##_vector_push(T##_vector *vec, T data) { \
if (vec->size == vec->capacity) { \
vec->capacity *= 2; \
vec->buf = realloc(vec->buf, sizeof(T) * vec->capacity); \
} \
T##_vector_set(vec, vec->size++, data); \
}
#define vector(T) \
vector_struct(T); \
vector_init(T) vector_get(T) vector_set(T) vector_push(T)In Zig, generics are easy as pie. You can pass type arguments to functions as first-class citizens. You can even store types in constants.
What about generic data structures? Well, just make a generic function that defines the structure locally using the type arguments and returns an instance of it.
pub fn Binary(comptime T: type) type {
return struct {
left: T,
right: T,
};
}Explicit and exhaustive error handling
printf("Hello world");const stdout = std.io.getStdOut().writer();
try stdout.print("Hello world\\n");try indicates that in case of an error, the error is returned. If we’re in the main function, it will crash the program.According to printf’s man page, if an output error is encountered, a negative value is returned
. This means that to be 100% correct and handle all possible failures, we must enclose every call to printf (or any other function of the family) in an if statement and handle the failure appropriately.
However who does that? The return value of printf is almost always ignored. The reason is that we don’t know have anything relevant to do on failure. If printf has failed, something must have gone very wrong, and the program is probably going to crash. Maybe we’re out of memory or in the a kernel panic is occuring. Exiting seems like a good choice; but that is not the behavior expressed when you implicitly ignore printf’s return value; instead, the error is ignored and the program continues, as if nothing happened.
Zig’s explicit error handling model is a must-have. It also makes use of the errors as values
pattern with error sets. There are also compile-time invariants on the handling of every error.
Ignorance is no longer the default. Together, we can ascend and build safer sofware.
Explicit number sizes
Zig does not have a generic integer or floating-point type whose size is determined by the compiler and architecture you’re using. Instead, all number sizes are explicity indicated in the code: u32 for a 32 bit unsigned integer, for instance.
This annoyed me at first; but it makes sense if you think about it: you give a variable its value, so you should decide on its size, as it determines the range of values it supports.
It’s possible this explicit approach would have prevented Ariane 5’s failure due to an integer overflow[2]
defer: the best thing since sliced bread
defer is such a simple concept, but it solves so many problems. It brings related statements close to each other in the code, even when they need to be executed at different times.
Its initial purpose is for memory allocation, but it can be used for other things too. It helps to ensure that resources are cleaned up when they are no longer needed. Instead of needing to remember to manually free up the resource, you can add a defer statement right next to the statement that allocates the resource.
var i: u8 = 1;
while (i < 100) {
defer i++;
// i's value hasn't changed
// Do some work
// ...
}defer for incrementing a loop variant. The incremantion is done at the start of the loop, close to the condition and the variant declaration. It is actually executed at the end of the loop block.A richer standard library
C’s standard library is quite rudimentary. You often need to download an STB library[3] when you want an arena allocator or an hash table.
You can also learn to implement these data structures and algorithms yourself, which is why I think learning C as my first language has really helped me become a better programmer.
Zig solves this problem by having a richer standard library. This isn’t unique to Zig, though. Similar languages like Go or Rust also have rich standard libraries.
A standard build system
C doesn’t have an official
build system; third-party build systems like Make or CMake are used instead.
The Zig build system is shipped with the compiler. It uses a declarative approach. What I especially like about it that it does not introduce a new language. Instead, build commands are expressed in Zig.
Namespaces
Ah, the joy of prefixing every symbol in a C program to avoid name conflicts with users of your library!
Namespaces have become the de facto standard of modern programming languages. It is insanely useful and idiomatic to be able to manipulate a library or a module as what it is: a bag of names.
Encapsulation
No need to prefix internal functions with an underscore anymore! pub is all you’ll need.
Seriously though, encapsulation is non-existent in C. Although the convention of if it starts with an underscore, don’t touch it
has been around for a while now, it’s always safer when the language enforces the invariant of this thing can’t be accessed outside this module / structure
.
In C, you have to get creative. In order to avoid any name conflicts with internals from other librairies the user might implemenent, your internal prefix should not only start with an underscore, but also contain an (hopefully) unique string. In addition, the underscore may not be followed by an uppercase letter unless you want to violate the standard and open yourself to name conflics with compiler or standard library internals.
For instance, Cori uses the prefix _cori_ on internal macros, functions, structures, type aliases, and so on.
In Zig, you don’t need any of this madness. Private is the default. Export functions, structures and methods using the pub keyword.
If it compiles, it works
Zig is much, much closer than C to the ideal of if it compiles, it works
.
It is true that everything that can be done in Zig can also be done in C, but the invariants and restrictions Zig enforces is stuff you don’t have to worry about getting wrong, unlike when you do it manually in C.
I believe this is the goal of any language; to provide an expressive syntax in order to give as many compile-time guarantees of well-formedness and correctness as posssible.
Overall I feel C has run its course as a language. Of course, it will be around forever since tons of software is written in it and it would be ludicrous to expect everything to be rewritten in Rust or Zig. Though I wouldn’t expect it to be used for any greenfield project.
Nonetheless I think C is an invaluable language to learn. It forces you to leave your comfort zone of abstactions and face the actual challenge of implementing stuff that we use every day as programmers, such a vectors or hash tables.
Conclusion
While C has been a cornerstone language for decades, the emergence of Zig presents a promising alternative. As a programmer, my journey began with C, a language that has not only shaped my understanding of programming fundamentals but also played a pivotal role in the history of computer programming. Its simplicity, efficiency, and portability have made it a go-to language for system programming since its inception in .
However, as we move towards a future where safety, performance, and developer productivity are paramount, Zig emerges as a strong contender. Zig’s design philosophy, which emphasizes maintaining the simplicity and low-level control of C while providing modern features to ensure compile-time correctness and enforce invariants, makes it an attractive choice.
While C may not be as trendy or feature-rich as newer languages, its influence and importance cannot be understated. However, the tide seems to be turning with languages like Zig, which offer the best of both worlds — the power of C and the safety of modern languages. As we continue to strive for better and safer code, I anticipate that Zig will gradually phase out C in many domains, offering a new standard for system programming languages.
In the end, the choice between C and Zig will depend on the specific needs of the project and the familiarity of the developer. However, one thing is clear: Zig is a language worth keeping an eye on, and I am excited to see how it will shape the future of programming.