Imports
Zig uses the @import built-in to bring code from other files:
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
print("Hello, World!\n", .{});
}
@import returns a struct representing the imported file, with all public declarations as fields.
Standard Library Import
The standard library is imported with @import("std"):
const std = @import("std");
const ArrayList = std.ArrayList;
const testing = std.testing;
const mem = std.mem;
The standard library provides modules for memory, collections, I/O, networking, and much more.
File Imports
Import other files in your project:
// main.zig
const utils = @import("utils.zig");
const parseU64 = @import("parser.zig").parseU64;
pub fn main() void {
const result = utils.add(5, 3);
_ = result;
}
// utils.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
fn privateFunction() void {
// Not accessible from other files
}
Exports
Public Declarations
Use pub to make declarations visible to importing files:
// math.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn sub(a: i32, b: i32) i32 {
return a - b;
}
pub const PI = 3.14159;
// Private - not accessible from imports
fn internalHelper() void {}
pub fn publicFunction() void {}
pub const PUBLIC_CONSTANT = 42;
pub const PublicStruct = struct {};
fn privateFunction() void {}
const private_constant = 42;
const PrivateStruct = struct {};
Exporting for C
Use export to make symbols available for C linking:
export fn add(a: i32, b: i32) i32 {
return a + b;
}
This creates a C-compatible symbol that can be called from C code.
export uses C ABI and naming. Use it only for C interoperability.
Package Structure
A typical Zig project structure:
my-project/
├── build.zig
├── build.zig.zon
├── src/
│ ├── main.zig
│ ├── lib.zig
│ ├── utils/
│ │ ├── math.zig
│ │ └── string.zig
│ └── core/
│ └── types.zig
└── tests/
└── tests.zig
Build System
The build.zig file defines how to build your project:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
}
Dependencies
Manage dependencies in build.zig.zon:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.@"some-package" = .{
.url = "https://example.com/some-package/archive/v1.0.0.tar.gz",
.hash = "1220...",
},
},
}
Then import in your code:
const some_package = @import("some-package");
Zig’s package manager is built into the build system and uses content hashing for reproducibility.
Module Organization
Single File Module
// math.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn multiply(a: i32, b: i32) i32 {
return a * b;
}
Multi-File Module
// utils.zig (module root)
pub const math = @import("utils/math.zig");
pub const string = @import("utils/string.zig");
// Usage:
const utils = @import("utils.zig");
const result = utils.math.add(1, 2);
Container-Level Variables
Modules can have container-level state:
// config.zig
var log_level: LogLevel = .info;
pub fn setLogLevel(level: LogLevel) void {
log_level = level;
}
pub fn getLogLevel() LogLevel {
return log_level;
}
Container-level mutable state is shared across all uses of the module. Use carefully.
Circular Dependencies
Zig handles circular dependencies:
// a.zig
const b = @import("b.zig");
pub fn funcA() void {
b.funcB();
}
// b.zig
const a = @import("a.zig");
pub fn funcB() void {
// Can reference a.funcA
}
Builtin Module
Access compiler information:
const builtin = @import("builtin");
const std = @import("std");
pub fn main() void {
std.debug.print("OS: {}\n", .{builtin.os.tag});
std.debug.print("Arch: {}\n", .{builtin.cpu.arch});
std.debug.print("Mode: {}\n", .{builtin.mode});
}
const builtin = @import("builtin");
const is_windows = builtin.os.tag == .windows;
const is_linux = builtin.os.tag == .linux;
const is_macos = builtin.os.tag == .macos;
Namespace Organization
Organize code with nested structs:
pub const math = struct {
pub const constants = struct {
pub const PI = 3.14159;
pub const E = 2.71828;
};
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
};
// Usage:
const pi = math.constants.PI;
const sum = math.add(1, 2);
Testing Modules
Tests can be organized in separate files:
// src/math.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add" {
const std = @import("std");
try std.testing.expect(add(1, 2) == 3);
}
// tests/math_test.zig
const math = @import("math");
const std = @import("std");
test "comprehensive add tests" {
try std.testing.expect(math.add(0, 0) == 0);
try std.testing.expect(math.add(-1, 1) == 0);
try std.testing.expect(math.add(100, 200) == 300);
}
Run tests with zig test src/main.zig or zig build test.
Library Root File
For libraries, create a root file that exports all public APIs:
// lib.zig
pub const math = @import("math.zig");
pub const string = @import("string.zig");
pub const collections = @import("collections.zig");
pub const version = "1.0.0";
test {
// Include all tests
@import("std").testing.refAllDecls(@This());
}
Best Practices
- Use
pub to explicitly mark public APIs
- Organize related functionality into modules
- Create a root module file for libraries
- Use the
builtin module for platform detection
- Keep modules focused and cohesive
- Use namespaces (nested structs) for organization
- Document public APIs with doc comments
- Include tests in the same file as implementation when appropriate
Zig’s module system is simple: files are modules, and @import brings them together. There are no complex visibility rules or module declarations.