# How Zig Integrates with the ESP-IDF Build System: A Complete Technical Guide

> Learn how Zig integrates with the ESP-IDF build system using CMake. Explore a hybrid workflow generating object files and linking Zig output with ESP-IDF libraries.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: how-to-guide
- Published: 2026-03-05

---

**Zig integrates with the ESP-IDF build system through a hybrid CMake-Zig workflow where `build.zig` generates object files and umbrella modules, while CMake orchestrates compilation and links the Zig output with ESP-IDF libraries.**

The `kassane/zig-esp-idf-sample` repository demonstrates a production-ready approach to using Zig for ESP32 firmware development. This integration allows developers to write idiomatic Zig code while maintaining full compatibility with the ESP-IDF ecosystem, enabling seamless use of Wi-Fi, GPIO, and other hardware features through a type-safe Zig interface.

## The Three-Layer Integration Architecture

Zig's integration with ESP-IDF relies on three coordinated components that bridge Zig's build system with ESP-IDF's CMake-based workflow.

### build.zig – The Zig Build Orchestrator

Located at the repository root, `build.zig` serves as the primary build script that manages Zig-specific compilation. It declares a Zig object module via `b.addObject`, selects the appropriate target using `b.standardTargetOptions`, and manages the `espressif_targets` list covering both `riscv_targets` (for RISC-V chips) and `xtensa_targets` (for legacy Xtensa chips).

The script detects Xtensa support through the `hasEspXtensaSupport` helper, automatically selecting the correct backend when available. It generates the umbrella `esp_idf` module that re-exports selected components, allowing Zig code to import ESP-IDF functionality through `@import("esp_idf")`.

### CMake Wrapper (main/CMakeLists.txt)

The [`main/CMakeLists.txt`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/CMakeLists.txt) file integrates Zig into the standard ESP-IDF component build flow. It defines `ZIG_EXAMPLE_ARG` to specify the Zig source file (defaulting to `main/app.zig` or a configurable example path), then includes `cmake/zig-config.cmake` to register the Zig compiler as a custom toolchain.

This CMake configuration treats the Zig object as an additional source file, ensuring that `zig build` runs during the compilation phase and that the resulting object links with ESP-IDF libraries declared in the component's `DEPS` list.

### imports/ Directory – C API Wrappers

The `imports/` directory contains thin Zig wrappers around ESP-IDF C APIs, such as `wifi.zig`, `gpio.zig`, and `nvs.zig`. These wrappers expose original C functions and types as native Zig symbols, enabling idiomatic Zig code while maintaining binary compatibility with ESP-IDF libraries.

Each wrapper is declared in `build.zig` through the `module_specs` table, which maps import names to source files and manages inter-module dependencies via the `deps` field. The `idf_wrapped_modules` function iterates over these specifications, constructing `std.Build.Module` instances with appropriate import lists.

## How the Build System Works Internally

### Target Selection and Multi-Architecture Support

The build system supports both Xtensa and RISC-V ESP32 chips through conditional target selection. In `build.zig`, the `espressif_targets` array combines `riscv_targets` and `xtensa_targets`, with the `hasEspXtensaSupport` helper detecting whether the Zig compiler includes the Espressif Xtensa backend. This allows the same build configuration to target ESP32-C3 (RISC-V) and ESP32 (Xtensa) without modification.

### Object Creation and Module Generation

The core build artifact is created via `b.addObject("app_zig", .{ .root_source_file = ... })`, producing a Zig object file that CMake can link. The root module points to the Zig entry file specified by the `-Dexample=` CMake variable, defaulting to `main/app.zig`.

The `idf_wrapped_modules` function processes `module_specs` to create individual Zig modules for each ESP-IDF component wrapper. These modules declare dependencies through the `deps` field, ensuring that importing `"wifi"` automatically resolves required base modules. Finally, the umbrella `"esp_idf"` module is constructed using `esp_idf_exports`, re-exporting selected components and injected into the object via `obj.root_module.addImport("esp_idf", ...)`.

### CMake Orchestration and Linkage

The [`main/CMakeLists.txt`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/CMakeLists.txt) file bridges Zig's build system with ESP-IDF's CMake infrastructure. It sets `ZIG_EXAMPLE_ARG` to pass the source file path to the Zig build, then includes `cmake/zig-config.cmake` to configure the Zig toolchain.

During the build, CMake invokes `zig build` to generate the object file, treating it as a standard source artifact. The Zig object is compiled with `link_libc = true`, ensuring compatibility with ESP-IDF's C libraries. The final linking stage combines the Zig object with ESP-IDF libraries (such as `esp_wifi`, `nvs_flash`, and `esp_netif`) to produce a standard ELF binary compatible with ESP-IDF's flashing and OTA mechanisms.

## Practical Example: Wi-Fi Station Implementation

The repository includes a complete Wi-Fi station example in `main/examples/wifi-station.zig` that demonstrates idiomatic Zig usage with ESP-IDF APIs. The implementation imports the ESP-IDF umbrella module and uses wrapped functions to initialize the network stack, configure Wi-Fi credentials from `sdkconfig` values, and handle connection events through Zig callbacks.

```zig
const std = @import("std");
const idf = @import("esp_idf");
const sys = idf.sys;
const log = std.log.scoped(.wifi);

// Event-group bits come from the C API
const CONNECTED_BIT = sys.WIFI_CONNECTED_BIT;
const FAILED_BIT = sys.WIFI_FAIL_BIT;

// Global handles used by callbacks
var g_event_group: sys.EventGroupHandle_t = null;
var g_retry_count: u32 = 0;

export fn app_main() callconv(.c) void {
    // Initialise NVS (required by the Wi-Fi driver)
    idf.nvs.flashInitOrErase() catch {
        log.err("NVS init failed", .{});
        return;
    };
    // Run the Wi-Fi logic; any error is logged
    wifiStation() catch |err| {
        log.err("Wi-Fi station failed: {s}", .{@errorName(err)});
    };
}

// Core Wi-Fi routine – all ESP-IDF calls are wrapped in Zig modules
fn wifiStation() !void {
    g_event_group = sys.xEventGroupCreate() orelse return error.EventGroupCreateFailed;
    defer sys.vEventGroupDelete(g_event_group);

    // ESP-IDF initialization steps (network stack, event loop, default interface)
    try idf.err.espCheckError(sys.esp_netif_init());
    try idf.event.loopCreateDefault();
    _ = sys.esp_netif_create_default_wifi_sta();

    // Initialise the Wi-Fi driver
    var wifi_cfg = idf.wifi.init_config_default();
    try idf.err.espCheckError(sys.esp_wifi_init(&wifi_cfg));

    // Register event callbacks (wrapped as Zig functions)
    const sta_evt = try idf.event.handlerInstanceRegister(
        sys.WIFI_EVENT, idf.event.ANY_ID, &onWifiEvent, null);
    defer idf.event.handlerInstanceUnregister(sys.WIFI_EVENT, idf.event.ANY_ID, sta_evt) catch {};

    const ip_evt = try idf.event.handlerInstanceRegister(
        sys.IP_EVENT, sys.IP_EVENT_STA_GOT_IP, &onIpEvent, null);
    defer idf.event.handlerInstanceUnregister(sys.IP_EVENT, sys.IP_EVENT_STA_GOT_IP, ip_evt) catch {};

    // Build a Wi-Fi configuration struct from sdkconfig values
    var cfg = idf.wifi.wifiConfig{
        .sta = .{
            .ssid = std.mem.zeroes([32]u8),
            .password = std.mem.zeroes([64]u8),
            .threshold = .{ .authmode = sys.WIFI_AUTH_WPA2_PSK },
        },
    };
    copyZ(&cfg.sta.ssid, sys.CONFIG_ESP_WIFI_SSID);
    copyZ(&cfg.sta.password, sys.CONFIG_ESP_WIFI_PASSWORD);

    // Apply and start the station
    try idf.wifi.setMode(.WIFI_MODE_STA);
    try idf.wifi.setConfig(.WIFI_IF_STA, &cfg);
    try idf.wifi.start();

    // Wait for connection or failure
    const bits = sys.xEventGroupWaitBits(
        g_event_group,
        CONNECTED_BIT | FAILED_BIT,
        0,
        0,
        sys.portMAX_DELAY,
    );
    if ((bits & CONNECTED_BIT) != 0) {
        log.info("Connected to SSID: {s}", .{sys.CONFIG_ESP_WIFI_SSID});
    } else {
        return error.WifiConnectionFailed;
    }
}

```

Key implementation details:

* All ESP-IDF functions (`esp_wifi_init`, `esp_netif_init`, etc.) are accessed through the `idf` umbrella module via `@import("esp_idf")`.
* The Zig code uses native features like `defer` for resource management and Zig error unions for error handling while respecting ESP-IDF's event-driven architecture.
* Wi-Fi credentials are pulled from `sdkconfig` values (`CONFIG_ESP_WIFI_SSID`, `CONFIG_ESP_WIFI_PASSWORD`) and copied into the configuration struct using standard Zig memory operations.

## Summary

- **Zig integrates with the ESP-IDF build system** through a hybrid architecture combining `build.zig` for Zig-specific compilation and CMake for ESP-IDF orchestration.
- The `build.zig` script in the repository root handles multi-architecture support (RISC-V and Xtensa) through `espressif_targets` and generates an umbrella `esp_idf` module for clean imports.
- CMake integration occurs through [`main/CMakeLists.txt`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/CMakeLists.txt), which invokes `zig build` via `cmake/zig-config.cmake` and links the resulting object file with ESP-IDF libraries like `esp_wifi` and `nvs_flash`.
- The `imports/` directory provides idiomatic Zig wrappers around C APIs, enabling type-safe access to ESP-IDF functionality through `@import("esp_idf")`.
- The final ELF binary is indistinguishable from pure C ESP-IDF applications, ensuring compatibility with existing flashing and OTA workflows.

## Frequently Asked Questions

### Can I use Zig with existing ESP-IDF components written in C?

Yes. The integration preserves full interoperability with C components. The `imports/` directory contains thin wrappers that expose C APIs as Zig modules, and the CMake build system links Zig object files alongside traditional C objects. You can call C functions from Zig and vice versa without modifying existing ESP-IDF components, as the final linking stage treats both Zig and C objects uniformly.

### Does this support both Xtensa and RISC-V ESP32 chips?

Yes. The `build.zig` script automatically detects architecture support through the `hasEspXtensaSupport` helper and configures targets accordingly. It maintains separate tables for `riscv_targets` (ESP32-C3, ESP32-C6) and `xtensa_targets` (ESP32, ESP32-S2, ESP32-S3), allowing the same build configuration to target multiple chip architectures without modification.

### How do I add a new ESP-IDF component wrapper in Zig?

To expose a new ESP-IDF component, create a Zig wrapper file in the `imports/` directory (e.g., `spi.zig` for SPI functionality). Then add an entry to the `module_specs` table in `build.zig`, mapping the import name to the source file and declaring any dependencies in the `deps` field. The `idf_wrapped_modules` function will automatically construct the module and make it available through the `esp_idf` umbrella import.

### Is the resulting firmware compatible with standard ESP-IDF tools?

Yes. The final ELF binary produced by this build system is indistinguishable from firmware built with pure C. Because the Zig object is compiled with `link_libc = true` and linked against standard ESP-IDF libraries, the output works with [`esptool.py`](https://github.com/kassane/zig-esp-idf-sample/blob/main/esptool.py), OTA updates, partition tables, and debugging tools exactly like traditional ESP-IDF applications.