Skip to content

Windows Support

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.zig

On Windows, this produces an .exe file:

main.exe

That file can be run directly from PowerShell, Command Prompt, Windows Terminal, or another shell.

.\main.exe

Windows 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.txt

Command-line shells are different:

.\program.exe

Newline conventions can be different:

\r\n

System 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.txt

But 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.zig

This builds for the current system.

You can also explicitly choose a Windows target:

zig build-exe main.zig -target x86_64-windows

This means: build a Windows executable for 64-bit x86 machines.

You may also see targets such as:

aarch64-windows
x86-windows
x86_64-windows

The 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-windows

This 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.exe

The 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.exe

Use:

.\main.exe

The .\ 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-windows

Run it on Windows:

.\main.exe hello "from windows"

Possible output:

Target OS: Windows
arg[0] = .\main.exe
arg[1] = hello
arg[2] = from windows

This 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.