Introduction
I am learning zig and writing here is a way of me documenting what I learned from various sources. I may have made mistakes. Please contact me at [email protected] if you find any error. Lets learn together.
I intend to keep this post and subsequent posts on zig updated. If at any point the examples don’t work or the concepts get outdated please contact me at the email provided above.
This is not going to be a zig tutorial. I am not going to talk about what zig language is or how to install it or even the syntax. It is going to be concepts of how zig works. So lets dive in.
Here is the obligatory hello world program.
hello.zig
1const std = @import("std");
2
3pub fn main() void {
4std.debug.print("Hello, World!!\n"), .{});
5}
Saving the above file and running the program using the command zig run hello.zig
will predictably print Hello, World!!
. In the first line the program is importing code from standard library. In this post we are going to appreciate how @import
function works from the perspective of a programmer.
To understand how importing works we need to first understand namespaces.
Namespace
A namespace is a container for related declarations. Anything within two curly braces is a namespace. The only exception to this rule is zig files.
@import function
@import is a builtin funtion.
When we need access to the declarations in another file we import that file using the @import
function. @import
function takes a filepath or package name string literal as argument and returns a struct type
. Here the file path cannot reach above the directory of the root file. Root file is the entry point file to the program or library.
Providing a constant as argument for @import function will throw an error. It can accept only string literal.
1const standard = "std";
2const std = @import(standard);
3...
If we make the above changes to the hello.zig file. The compiler will not compile and throw error: @import operand must be a string literal
.
There are 3 packages that are always available:
- std
- builtin
- root
Explaining these packages is beyond the scope of this post.
Remember @import function returns a struct ? let us write some code to demonstrate that.
hello.zig
1const print = @import("std").debug.print;
2const Student = @import("student.zig");
3
4pub fn main() void {
5 print("Hello {s}!!\n", .{Student.fullname});
6 var kid: Student = .{.roll_number = 32};
7 print("Roll number: {d}\n", .{kid.getRoll()});
8 print("Today your age is {d}\n", .{Student.getAge()});
9 print("Happy Birthday!!\n", .{});
10 Student.addAge();
11 print("Now your age is {d}\n", .{Student.getAge()});
12}
student.zig
1// These two are container level variables
2pub const fullname = "John Doe";
3//Cannot be accessed by any other file directly
4var age: u8 = 18;
5
6// This is a struct field
7roll_number: u8,
8
9pub fn getAge() u8 {
10 return age;
11}
12
13pub fn addAge() {
14 age += 1;
15}
16
17pub fn getRoll(student: @This()) u8 {
18 return student.roll_number;
19}
@import supports circular imports. Again lets write some code to demonstrate this.
main.zig
1const one = @import("one.zig");
2pub fn main() void {
3 one.func();
4}
one.zig
1const std = @import("std");
2const two = @import("two.zig");
3
4pub fn func() void {
5 std.debug.print("This is one.zig file\n", .{});
6 two.func();
7}
two.zig
1const std = @import("std");
2const one = @import("one.zig");
3
4var counter: u8 = 1;
5
6pub fn func() void {
7 std.debug.print("Count: {d}\n", .{counter});
8 if (counter == 10) return;
9 counter += 1;
10 one.func();
11}
If we run the program with zig run main.zig
, it will print the below output with increasing count value by 1 till it reaches 10
This is one.zig file Count: 1
To demonstrate circular import the one.zig
file imports and runs func()
from two.zig
file and two.zig
file imports and runs func()
from one.zig
.
Pub keyword
You would have perceptively noticed the keyword pub
in the code. The pub
keyword decides which declarations in a file are exposed when that file is imported by another file. Enough talk lets code
expose.zig
1pub const shankar = "Shankar Murralitharan";
2
3pub const MyPublicStruct = struct {
4 pub const nested_public = "Public const in MyPublicStruct";
5};
6
7const MyPrivateStruct = struct {
8 pub const nested_public = "Public const in MyPrivateStruct";
9};
10
11pub fn getMyPrivatStruct() type {
12 return MyPrivateStruct;
13}
main.zig
1const std = @import("std");
2const expose = @import("expose.zig");
3
4pub fn main() void {
5 const ExposedPrivateStruct = expose.getMyPrivatStruct();
6 const MyPublicStruct = expose.MyPublicStruct;
7 std.debug.print("{s}\n", .{expose.shankar});
8 std.debug.print("{s}\n", .{MyPublicStruct.nested_public});
9 std.debug.print("{s}\n", .{ExposedPrivateStruct.nested_public});
10}
In the file expose.zig the struct MyPrivateStruct
is not exposed by pub
keyword but since it is the return value of public function getMyPrivatStruct()
, it gets exposed in the main.zig
file when the fuction is called.
The use of pub
keyword is quite straightforward. Any declaration that has to be exposed should have pub
keyword preceeding it or should be returned by an exposed function.