Imports
const std = @import("std");
const expect = std.testing.expect;
// A relative path imports another source file. The result is a struct type
// whose declarations are that file's `pub` declarations.
const shapes = @import("_shapes.zig");
test "use an imported declaration" {
try expect(shapes.double(21) == 42);
}
test "imported types work like any other" {
const c = shapes.Circle{ .radius = 2 };
try expect(c.area() > 12.5 and c.area() < 12.6);
}
test "a file is a struct" {
// `@This()` inside a file refers to that file's implicit struct type,
// which is why `@import` can return something you access with `.`.
try expect(@TypeOf(shapes) == type);
try expect(shapes.pi > 3.14);
}
test "std is just another import" {
// `@import("std")` is the same mechanism, resolved by the compiler
// rather than by path.
try expect(std.mem.eql(u8, "a", "a"));
}@import takes a path and returns that file as a struct type:
const shapes = @import("_shapes.zig");
shapes.Circle{ .radius = 2 };
There is no separate module system to learn. A file is a struct, @import
evaluates it, and you access its declarations with . like any other struct.
Because the result is a value, the name you bind it to is yours. const s = @import("std") works exactly as well as const std = @import("std").
Importing the same file twice gives you the same type both times, so there is
no duplicate-definition problem and no include guard.
pub is the boundary
Only pub declarations are visible to importers. Everything else is private
to the file:
pub const pi = 3.14159; // visible
const secret = 42; // not
This is the whole visibility system: no private, no header files, no export
lists. The unit of privacy is the file. So splitting a type into its own file
is also how you give it a private implementation, and merging two files is how
you let them see each other’s internals.
A file can be the type
Since a file is a struct, a file that declares fields at the top level is that struct, and importing it gives you the type directly:
// Point.zig
x: f32,
y: f32,
pub fn magnitude(self: @This()) f32 { ... }
const Point = @import("Point.zig");
const p: Point = .{ .x = 3, .y = 4 };
This is the convention behind the capitalised filenames in the standard
library. std.Io.Writer lives in Writer.zig, and the file has no wrapper
declaration around it. A lowercase filename means a namespace full of
declarations; a capitalised one means the file is a single type. Following
that convention is the difference between reading the standard library’s
layout and being confused by it.
@import("std") is not special
The standard library is imported by the same mechanism; the name is resolved
by the compiler rather than by path. Package dependencies declared in
build.zig.zon work the same way, imported by their package name.
A name resolves as a module when the build declared one, and as a file path
otherwise. So @import("foo") and @import("foo.zig") are different
questions: the first asks the build system, the second asks the filesystem,
relative to the importing file. Adding a dependency is a change to build.zig
and build.zig.zon, never a change to an include path.
Two more names are always available. @import("root") is the file at the root
of the compilation, which is how library code reaches a configuration constant
the application declared. @import("builtin") is generated by the compiler
and describes the build itself: the target, the optimisation mode, the Zig
version. Neither is a file you wrote.
Import cycles
Two files may import each other. Zig resolves declarations lazily, so this
works. What it will not accept is a real cycle in values: a const in A
whose initialiser needs a const in B, whose initialiser needs the one in A.
Types referring to each other is fine, which is what most cycles actually are.
A Node in one file can hold a ?*Tree from another while the Tree holds
[]Node, and it resolves without complaint. The size of a pointer does not
depend on what it points at. The cycle is only a problem when a value cannot
be computed without itself, and the error names the loop.
Bringing in data, not code
@embedFile("table.bin") reads a file at compile time and gives you a *const [N:0]u8: a pointer to the contents, with a zero byte after the last one. It is
the right tool for a lookup table, a small template, or a test fixture. It also
removes a whole class of “the data file was not next to the binary” failure. It
is the wrong tool for anything large, since the bytes end up in the binary.