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

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

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, 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, OTA updates, partition tables, and debugging tools exactly like traditional ESP-IDF applications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →