Skip to content

Working with Zig Integration

Use Zig deliberately through explicit Janus/Zig boundaries.

Time: 50 minutes Level: Intermediate Prerequisites: Tutorials 1-3 (Hello World, CLI Tool, Error Handling) What you’ll learn: explicit bridge modules, generated wrappers, allocators, and when use zig is appropriate

Important: this tutorial is being migrated to Janus’ stricter boundary doctrine. The canonical rule is: ordinary .jan application code prefers use std.*, while use zig is reserved for explicit bridge modules or compiler-generated wrappers. Treat broad use zig "std/..." examples below as legacy patterns under replacement, not recommended end-state Janus style.


Most languages make you choose: clean syntax or systems power. Janus keeps the power, but it now draws a much harder language boundary.

Janus compiles through Zig — not beside it, not to it. That does not mean ordinary .jan code should directly mirror Zig source. The intended end state is Janus application code on one side, explicit bridge modules on the other, with the compiler and toolchain handling the lowering.

// Janus do..end blocks wrapping Zig's Ed25519 primitives.
// This is NOT FFI. It compiles as one unit.
pub fn sign(self: *const SoulKey, msg: []const u8) [64]u8 do
const kp = Ed25519.KeyPair
.generateDeterministic(self.seed) catch return .{0} ** 64;
return kp.sign(msg, null).toBytes();
end

This is NOT:

  • Foreign Function Interface (FFI)
  • C bindings or wrappers
  • Code generation or transpilation

This IS:

  • Zig’s type system with Janus control flow
  • One compilation unit, one binary
  • Zero overhead — the same machine code Zig would produce
  • Battle-tested crypto, I/O, networking from day one

// Import an explicit bridge module
use zig "std/bridge/process_bridge.zig"

What happens:

  1. Janus compiler finds the explicit bridge module
  2. Imports it natively into the same build
  3. Keeps the foreign implementation isolated at a named boundary
  4. Type-checks the exposed surface at compile time
ModulePurposeExample
std/bridge/process_bridge.zigProcess bridgeExecute subprocess work behind a boundary
std/bridge/json_bridge.zigJSON bridgeHandle-based JSON parsing behind a boundary
std/bridge/http_bridge.zigHTTP/socket bridgeSocket and HTTP primitives behind a boundary
bridge/net_bridge.zigProject-local bridgeIsolate project-specific Zig interop

Step 2: Memory Management (Allocators) (10 min)

Section titled “Step 2: Memory Management (Allocators) (10 min)”

In Janus + Zig, memory is explicit. No hidden allocations.

The Rule: Functions that allocate memory take an Allocator parameter.

use zig "std/ArrayList"
func create_list(allocator: Allocator) ![]i64 do
var list = zig.ArrayList(i64).init(allocator)
defer list.deinit()
try list.append(1)
try list.append(2)
try list.append(3)
return try list.toOwnedSlice()
end
func main() !void do
let allocator = std.heap.page_allocator
let numbers = try create_list(allocator)
defer allocator.free(numbers)
for i in 0..<numbers.len do
print_int(numbers[i])
print(" ")
end
println("")
end

Key Concepts:

  1. Allocator - The type that manages memory
  2. std.heap.page_allocator - General-purpose allocator
  3. defer list.deinit() - Always clean up!
  4. toOwnedSlice() - Transfer ownership from list to slice
// General-purpose (use for most things)
let allocator = std.heap.page_allocator
// Arena allocator (fast, batch-free everything at once)
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator)
defer arena.deinit()
let allocator = arena.allocator()
// Fixed buffer (no heap allocations)
var buffer: [1024]u8 = undefined
var fba = std.heap.FixedBufferAllocator.init(&buffer)
let allocator = fba.allocator()

use zig "std/fs"
func read_entire_file(path: []const u8, allocator: Allocator) ![]u8 do
// Open file
let file = try zig.fs.cwd().openFile(path, .{})
defer file.close()
// Read up to 10MB
let content = try file.readToEndAlloc(allocator, 10 * 1024 * 1024)
return content
end
func main() !void do
let allocator = std.heap.page_allocator
let content = try read_entire_file("README.md", allocator)
defer allocator.free(content)
print("File size: ")
print_int(content.len)
println(" bytes")
end
use zig "std/fs"
func write_log(message: []const u8) !void do
// Create/open file (truncate if exists)
let file = try zig.fs.cwd().createFile("app.log", .{})
defer file.close()
// Write message
try file.writeAll(message)
end
func main() !void do
try write_log("Application started\n")
println("Log written!")
end
use zig "std/fs"
func file_exists(path: []const u8) -> bool do
zig.fs.cwd().access(path, .{}) catch do
return false
end
return true
end
func main() do
if file_exists("config.txt") do
println("Config found!")
else
println("Config missing!")
end
end

use zig "std/ArrayList"
func process_numbers(allocator: Allocator) !void do
var numbers = zig.ArrayList(i64).init(allocator)
defer numbers.deinit()
// Add items
try numbers.append(10)
try numbers.append(20)
try numbers.append(30)
// Access items
print("First: ")
print_int(numbers.items[0])
println("")
print("Length: ")
print_int(numbers.items.len)
println("")
// Iterate
println("All numbers:")
for i in 0..<numbers.items.len do
print(" ")
print_int(numbers.items[i])
println("")
end
// Remove last
let last = numbers.pop()
print("Popped: ")
print_int(last)
println("")
end
func main() !void do
let allocator = std.heap.page_allocator
try process_numbers(allocator)
end
use zig "std/HashMap"
use zig "std/AutoHashMap"
func count_words(text: []const u8, allocator: Allocator) !void do
var counts = zig.AutoHashMap([]const u8, i64).init(allocator)
defer counts.deinit()
// Simplified word counting (split by spaces)
var word_start = 0
var i = 0
while i < text.len do
if text[i] == ' ' or text[i] == '\n' do
if i > word_start do
let word = text[word_start..i]
let entry = try counts.getOrPut(word)
if entry.found_existing do
entry.value_ptr.* = entry.value_ptr.* + 1
else
entry.value_ptr.* = 1
end
end
word_start = i + 1
end
i = i + 1
end
// Print results
var iter = counts.iterator()
while iter.next() do |kv| do
print(kv.key_ptr.*)
print(": ")
print_int(kv.value_ptr.*)
println("")
end
end
func main() !void do
let allocator = std.heap.page_allocator
let text = "hello world hello janus world"
try count_words(text, allocator)
end

use zig "std/process"
func main() !void do
let allocator = std.heap.page_allocator
// Get arguments
let args = try zig.process.argsAlloc(allocator)
defer zig.process.argsFree(allocator, args)
println("Program arguments:")
for i in 0..<args.len do
print(" [")
print_int(i)
print("] ")
println(args[i])
end
end

Run it:

Terminal window
janus build args.jan -o args
./args hello world 123

Output:

Program arguments:
[0] ./args
[1] hello
[2] world
[3] 123
use zig "std/process"
func get_env(key: []const u8, allocator: Allocator) ![]const u8 do
let value = try zig.process.getEnvVarOwned(allocator, key)
return value
end
func main() !void do
let allocator = std.heap.page_allocator
let home = get_env("HOME", allocator) catch |err| do
println("HOME not set")
return
end
defer allocator.free(home)
print("Home directory: ")
println(home)
end
use zig "std/process"
func main() !void do
let success = check_preconditions()
if not success do
println("Error: Preconditions not met")
zig.process.exit(1) // Exit with error code
end
println("Success!")
zig.process.exit(0)
end

  • use zig "module/path" for native imports
  • Zero-cost integration (not FFI)
  • Type-safe Zig stdlib access
  • Compile-time verification
  • Explicit allocators (no hidden allocations)
  • std.heap.page_allocator for general use
  • defer for cleanup
  • toOwnedSlice() for ownership transfer
  • std/fs for file operations
  • openFile(), createFile(), readToEndAlloc()
  • Always use defer file.close()
  • ArrayList for dynamic arrays
  • HashMap/AutoHashMap for key-value storage
  • defer .deinit() for cleanup
  • std/process for args, env, exit codes
  • argsAlloc() for command-line arguments
  • getEnvVarOwned() for environment variables

  1. Write a program that reads a file and counts the number of lines
  2. Create a simple key-value store that saves to a JSON file
  3. Build a directory lister that prints all files in the current directory
  1. Implement a text search tool (like grep) that finds patterns in files
  2. Create a file backup tool that copies files to a backup directory
  3. Build a CSV parser that reads tabular data into a HashMap
  1. Design a simple database (key-value store) with persistence
  2. Implement a log rotation system (delete old logs when they get too large)
  3. Create a file watcher that monitors changes and triggers actions
  4. Build a concurrent file processor using Zig’s threading primitives

use zig "std/fs"
use zig "std/HashMap"
use zig "std/AutoHashMap"
struct Config do
settings: zig.AutoHashMap([]const u8, []const u8)
allocator: Allocator
end
func Config.load(path: []const u8, allocator: Allocator) !Config do
var settings = zig.AutoHashMap([]const u8, []const u8).init(allocator)
// Read config file
let file = try zig.fs.cwd().openFile(path, .{})
defer file.close()
let content = try file.readToEndAlloc(allocator, 1024 * 1024)
defer allocator.free(content)
// Parse simple key=value format
var line_start = 0
for i in 0..<content.len do
if content[i] == '\n' do
let line = content[line_start..i]
if line.len > 0 do
// Find '=' separator
for j in 0..<line.len do
if line[j] == '=' do
let key = line[0..j]
let value = line[j+1..line.len]
try settings.put(key, value)
break
end
end
end
line_start = i + 1
end
end
return Config {
.settings = settings,
.allocator = allocator
}
end
func Config.deinit(self: *Config) do
self.settings.deinit()
end
func Config.get(self: *Config, key: []const u8) -> ?[]const u8 do
return self.settings.get(key)
end
func main() !void do
let allocator = std.heap.page_allocator
var config = try Config.load("app.config", allocator)
defer config.deinit()
if config.get("debug") do |value| do
print("Debug mode: ")
println(value)
end
if config.get("port") do |value| do
print("Port: ")
println(value)
end
end

You now know:

  • How to write clean Janus code ([Tutorial 1]/tutorials/hello-to-production/)
  • How to build CLI tools ([Tutorial 2]/tutorials/cli-tool/)
  • How to handle errors safely ([Tutorial 3]/tutorials/error-handling/)
  • How to leverage Zig’s stdlib (Tutorial 4)
  • Explore the [Error Handling Doctrine]/reference/errors/
  • Read the [ASTDB Architecture]/architecture/astdb/
  • Study the Zig Standard Library
  • Join the community (coming soon!)

Congratulations! You’re now a Janus + Zig power user!

Build something amazing with your new superpowers!