Windows is one of Zig’s main supported platforms. You can write Zig programs on Windows, build Windows executables, call Windows system APIs, link with C libraries, and...
Windows is one of Zig’s main supported platforms. You can write Zig programs on Windows, build Windows executables, call Windows system APIs, link with C libraries, and cross-compile Windows programs from Linux or macOS.
For beginners, the most important idea is simple: Zig does not treat Windows as a second-class target. Windows support is part of the normal Zig workflow.
You can write:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello from Windows!\n", .{});
}Then build it:
zig build-exe main.zigOn Windows, this produces an .exe file:
main.exeThat file can be run directly from PowerShell, Command Prompt, Windows Terminal, or another shell.
.\main.exeWindows Is Different from Unix
Many beginner programmers first learn programming on Linux-like systems. But Windows has its own rules.
Paths are different:
C:\Users\alice\Desktop\file.txtCommand-line shells are different:
.\program.exeNewline conventions can be different:
\r\nSystem APIs are different. Linux has POSIX APIs such as fork, exec, and file descriptors. Windows has Win32 APIs such as CreateFileW, ReadFile, WriteFile, and handles.
Zig helps you write portable code through the standard library, but it also lets you call platform-specific APIs when you need them.
Prefer std for Portable Code
Most ordinary programs should start with Zig’s standard library.
For example, instead of manually calling Windows APIs to read a file, use std.fs:
const std = @import("std");
pub fn main() !void {
const data = try std.fs.cwd().readFileAlloc(
std.heap.page_allocator,
"hello.txt",
1024 * 1024,
);
defer std.heap.page_allocator.free(data);
std.debug.print("{s}\n", .{data});
}This code uses std.fs.cwd() to access the current working directory. It can work on Windows, Linux, and macOS because std.fs hides many platform differences.
That does not mean the differences disappear. It means Zig gives you a common interface for common tasks.
Use the standard library first. Drop down to Windows-specific APIs only when you need behavior that std does not provide.
Path Handling on Windows
A common beginner mistake is treating paths as plain strings and assuming every operating system uses /.
Windows commonly uses backslashes:
C:\Users\alice\Documents\note.txtBut in Zig strings, backslash starts an escape sequence. So this is wrong:
const path = "C:\Users\alice\Documents\note.txt";Zig may interpret parts like \U, \a, or \n as escapes.
You can escape the backslashes:
const path = "C:\\Users\\alice\\Documents\\note.txt";Or use a raw multiline string style when appropriate:
const path =
\\C:\Users\alice\Documents\note.txt
;For portable code, avoid hardcoding platform paths when possible. Use std.fs.path helpers and let the standard library handle path rules.
Example:
const std = @import("std");
pub fn main() void {
const joined = std.fs.path.join(
std.heap.page_allocator,
&.{ "data", "users", "alice.txt" },
) catch return;
defer std.heap.page_allocator.free(joined);
std.debug.print("{s}\n", .{joined});
}On Windows, the resulting path follows Windows conventions. On Unix-like systems, it follows Unix conventions.
Windows Uses UTF-16 Internally
Windows has an important Unicode detail: many Windows APIs use UTF-16 strings.
Zig source code strings are usually UTF-8 byte sequences. This is normal:
const name = "hello.txt";But some low-level Windows APIs expect wide strings, usually represented as UTF-16.
If you stay inside Zig’s standard library, you usually do not need to worry about this. std.fs and related APIs handle many of these details.
But if you call Win32 APIs directly, string encoding matters. You may need to convert UTF-8 to UTF-16 before calling a Windows API.
This is one reason beginners should avoid calling raw Windows APIs too early. Learn the standard library first, then learn the Windows layer when you need exact operating system behavior.
Calling Windows APIs
Zig can call Windows APIs directly.
For example, Zig exposes Windows definitions through the standard library:
const std = @import("std");
const windows = std.os.windows;From there, you can use Windows types and functions.
A simplified example might look like this:
const std = @import("std");
const windows = std.os.windows;
pub fn main() void {
_ = windows;
std.debug.print("Windows APIs are available through std.os.windows\n", .{});
}For real Windows API calls, you often need to understand handles, error codes, UTF-16 strings, and calling conventions.
Windows programming is a subject of its own. Zig gives you access to it, but it does not remove the need to understand the Windows API model.
Building a Windows Executable
On Windows, the simplest command is:
zig build-exe main.zigThis builds for the current system.
You can also explicitly choose a Windows target:
zig build-exe main.zig -target x86_64-windowsThis means: build a Windows executable for 64-bit x86 machines.
You may also see targets such as:
aarch64-windows
x86-windows
x86_64-windowsThe target describes the CPU architecture and operating system.
This is one of Zig’s strengths. The compiler has built-in cross-compilation support, so building for another platform is a normal workflow.
Cross-Compiling to Windows
You can build a Windows program from Linux or macOS:
zig build-exe main.zig -target x86_64-windowsThis produces a Windows .exe file even though you are not running Windows.
That is useful for release builds, CI systems, and open source projects.
For example, a project may produce binaries like:
mytool-linux-x86_64
mytool-macos-aarch64
mytool-windows-x86_64.exeThe same Zig source code can be compiled for all of them, as long as the code does not depend on platform-specific APIs without checks.
Detecting Windows at Compile Time
Sometimes your program needs different code on Windows.
Zig lets you check the target operating system at compile time:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() void {
if (builtin.os.tag == .windows) {
std.debug.print("Running on Windows\n", .{});
} else {
std.debug.print("Running on another operating system\n", .{});
}
}This check happens using compile-time target information.
You can use this pattern to write platform-specific code:
if (builtin.os.tag == .windows) {
// Windows implementation
} else {
// Unix-like implementation
}This is useful when the operating systems truly require different behavior.
Do not overuse it. If std already gives you a portable API, use that instead.
File Paths and Command-Line Arguments
Windows command-line behavior has its own edge cases. Quoting rules, Unicode behavior, and shell behavior can differ between PowerShell, Command Prompt, Git Bash, and MSYS2.
For ordinary command-line arguments, use Zig’s standard library:
const std = @import("std");
pub fn main() !void {
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
while (args.next()) |arg| {
std.debug.print("arg: {s}\n", .{arg});
}
}Run it:
.\main.exe hello "two words"The program receives the arguments after shell parsing.
Beginners should remember this distinction:
The shell parses the command line first.
Your Zig program receives the parsed arguments.
Different shells may parse quotes and escapes differently.
Console Output
For simple programs, this works:
std.debug.print("Hello\n", .{});For many tools, that is enough while learning.
But production command-line programs often distinguish between standard output and standard error.
You can write to stdout like this:
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("normal output\n", .{});
}And stderr like this:
const std = @import("std");
pub fn main() !void {
const stderr = std.io.getStdErr().writer();
try stderr.print("error output\n", .{});
}This matters for command-line tools. Users may pipe stdout into another program while still seeing error messages separately.
Windows GUI vs Console Programs
A Windows program can be a console program or a GUI program.
A console program opens or uses a terminal. Most beginner Zig examples are console programs.
A GUI program uses the Windows graphical subsystem. It usually has a different entry style and links differently.
For this book, start with console programs. They are easier to build, run, test, and debug.
Once you understand Zig basics, you can use Windows APIs or external libraries to build GUI applications.
Linking Libraries on Windows
Windows libraries often appear as .lib files and .dll files.
A .dll is a dynamic library loaded at runtime.
A .lib file may be an import library used at link time.
If your Zig program calls a Windows system library or a third-party C library, you may need to link it.
With build.zig, linking is usually expressed in the build script rather than typed manually every time.
A simplified build script may include logic such as:
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
exe.linkSystemLibrary("user32");The exact build API can change between Zig versions, so treat build scripts as version-specific code. The important idea is stable: Windows programs often need explicit library linking when they call system or third-party APIs.
Common Windows Problems
One common problem is running the wrong executable path.
In PowerShell, this usually does not work:
main.exeUse:
.\main.exeThe .\ means “run the program from the current directory.”
Another common problem is path escaping:
const bad = "C:\new\test.txt";This contains escape sequences. Use:
const good = "C:\\new\\test.txt";Another common problem is assuming Unix APIs exist:
// Do not assume every POSIX API exists on Windows.Windows is not POSIX. Some APIs exist only on Unix-like systems. Use std when possible, and use compile-time OS checks when needed.
A Good Beginner Rule
When writing Zig programs for Windows, follow this order:
Use std first.
Use builtin.os.tag when behavior must differ by operating system.
Use std.os.windows when you need Windows-specific APIs.
Use raw C or Win32 interop only after you understand the data types, string encoding, error model, and linking requirements.
That order keeps your code simpler.
Complete Example
Here is a small Windows-friendly program that reads command-line arguments and prints them:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
if (builtin.os.tag == .windows) {
try stdout.print("Target OS: Windows\n", .{});
} else {
try stdout.print("Target OS: not Windows\n", .{});
}
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
var index: usize = 0;
while (args.next()) |arg| {
try stdout.print("arg[{d}] = {s}\n", .{ index, arg });
index += 1;
}
}Build it for Windows:
zig build-exe main.zig -target x86_64-windowsRun it on Windows:
.\main.exe hello "from windows"Possible output:
Target OS: Windows
arg[0] = .\main.exe
arg[1] = hello
arg[2] = from windowsThis small example shows three important ideas: Zig can target Windows directly, the standard library handles normal command-line work, and platform checks are available when you need them.
Windows support in Zig is practical because Zig combines portable APIs with direct operating system access. Start portable, then become platform-specific only where the program requires it.