My friend Christine has spoken well of Zig for a while now, and my interest was piqued when I read that it can integrate with C libraries without the use of FFI/bindings. In this article I will try to contrast Zig against my favorite language, Go.
(I will leave it to others to compare Zig with C++, D or Rust)
Similarities with Go…
Let’s start off by listing some of the characteristics shared between Go and Zig:
- Ahead-of-time (AOT) compiled
- Open Source
- Statically typed
- Cross-compilation
- No operator overloading
- Tool for one canonical code formatting style (
go fmt
andzig fmt
) - Errors are values
- WebAssembly as a compile target
- Multiline string literals
defer
- Source code is encoded in UTF-8
How is it different from Go?
Note: These are just a few of the things I’ve picked up when reading about Zig. I still have a lot to learn before I feel confident in using the language.
- Build modes on scope level
- Zig Build System
- Manual memory management
- Algebraic data types (Union and Enum types!)
- Optional type instead of null pointers
- Can output tiny binaries
- Generics and compile-time code execution
- First-class support for no standard library
- Somewhat easier to Google
- ~zero overhead when calling out to C
- “Zig is better at using C libraries than C is at using C libraries.”
- Enforcing the handling of returned errors
- Arbitrary bit-width integers (
i7
for example) - Undefined values
- Thread local variables
- No multiline comments
The Zen of Zig
- Communicate intent precisely.
- Edge cases matter.
- Favor reading code over writing code.
- Only one obvious way to do things.
- Runtime crashes are better than bugs.
- Compile errors are better than runtime crashes.
- Incremental improvements.
- Avoid local maximums.
- Reduce the amount one must remember.
- Minimize energy spent on coding style.
- Resource deallocation must succeed.
- Together we serve end users.
Installing Zig
If you are using macOS and have Homebrew installed, then you can install Zig like this:
$ brew install zig
The current version of zig
as of this writing is 0.13.0.
Make sure you also get zig.vim.
Hello, World!
The entry point of a Zig binary is the main
fn.
The binary is named after the .zig
file.
zighello/zighello.zig
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"world"});
}
This can be compiled with zig build-exe zighello.zig -O ReleaseSmall -fstrip -fsingle-threaded
and results in a ~10 KB
static executable.
(Not strictly true that you get a static executable under macOS, it is linked to libSystem.B.dylib
)
And as you probably know, the Go entry point is the main
func.
gohello/main.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
This can be compiled with go build -ldflags "-s -w" -trimpath main.go
and results in a ~1.6 MB
executable (when using Go version 1.15
under macOS)
Note: The size of your binaries doesn’t matter in most cases, but sometimes it matters a lot, for example when compiling to WebAssembly
Learn more about Zig
Bonus: Linking to the shared library from the article Go and Ruby-FFI
An example of importing a shared library from Zig, using the @cInclude
feature:
const std = @import("std");
const c = @cImport({
@cInclude("libsum.h");
});
pub fn main() void {
const sum: i64 = c.add(4, 2);
std.debug.warn("{}\n", .{sum});
}
Then you can compile this by calling zig -isystem . --library ./libsum.so build-exe zigsum.zig
Captain: Take off every ‘ZIG’!!