shankar.blog

Title

A Monologue on Zig Import

Part of Learning Zig series
Author
Shankar Murralitharan
Written
Category
Programming
Read
4 min

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:

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.