# Migrating C/C++ ESP-IDF Code to Zig: A Complete Guide to the zig-esp-idf-sample Workflow

> Migrate C/C++ ESP-IDF code to Zig incrementally. Use a hybrid CMake build and auto-generated bindings to replace C logic file-by-file. Maintain full ESP-IDF compatibility.

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

---

**You can incrementally migrate C/C++ ESP-IDF components to Zig by using a hybrid CMake build system that auto-generates C bindings and provides idiomatic Zig wrappers, allowing you to replace C logic file-by-file while maintaining full compatibility with the ESP-IDF ecosystem.**

Migrating C/C++ ESP-IDF code to Zig enables you to leverage Zig’s compile-time metaprogramming, modern error handling, and memory safety features without abandoning the mature ESP-IDF hardware abstraction layer. The **kassane/zig-esp-idf-sample** repository demonstrates a production-ready workflow for writing ESP32 firmware entirely in Zig while reusing existing C/C++ components through automated binding generation.

## How the zig-esp-idf-sample Repository Enables Migration

The repository implements a **hybrid build architecture** that bridges the ESP-IDF CMake system with the Zig toolchain. This design allows Zig source files to coexist with C/C++ files, enabling incremental migration where you can port individual components while keeping the rest of the codebase in C.

### CMake Integration with Zig Toolchain

The build system uses two custom CMake modules to integrate Zig into the standard ESP-IDF workflow:

- **`cmake/zig-config.cmake`** – Discovers Zig source files (`.zig`) under `main/` and `imports/`, then adds them to the build targets using Zig’s cross-compilation capabilities.
- **`cmake/zig-download.cmake`** – Automatically downloads the correct Zig toolchain version, including the **zig-xtensa** fork required for ESP32 Xtensa targets.

This integration means you run standard commands like `idf.py build flash monitor` without manually invoking Zig—the CMake system handles compilation and linking automatically.

### Automatic C Binding Generation

The repository uses **`zig translate-c`** to generate Zig bindings from ESP-IDF C headers:

1. **Header stubs** in `include/*.h` (e.g., [`stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/stubs.h), [`wifi_stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/wifi_stubs.h)) declare the C APIs you need to access.
2. During the CMake configuration phase, `zig translate-c` processes these headers.
3. The generated bindings are written to **`imports/idf-sys.zig`**, exposing raw C functions and types to Zig.

This automated process ensures your Zig code stays synchronized with the ESP-IDF API as you add or remove headers from the `include/` directory.

### Idiomatic Zig Wrappers

Rather than using the raw C bindings directly, the repository provides **hand-written Zig façades** in `imports/*.zig` that wrap the low-level C API in Zig-idiomatic patterns:

- **`imports/gpio.zig`** – Wraps GPIO driver calls with Zig error unions and enums.
- **`imports/rtos.zig`** – Provides `Task` abstractions with `delayMs()` methods instead of raw `vTaskDelay` calls.
- **`imports/matter.zig`** – Wraps the C++ Matter API for Zig consumption.

These wrappers handle memory safety, convert C error codes to Zig errors, and use Zig’s `std.log` for output, allowing application code to remain purely idiomatic Zig while interfacing with C libraries.

## Step-by-Step Migration Workflow

When migrating C/C++ ESP-IDF code to Zig using this repository, follow this incremental workflow to minimize risk and maintain buildable states throughout the transition.

1. **Identify the component to migrate** – Select a C/C++ file (e.g., a driver or application module) that you want to rewrite in Zig.

2. **Add required headers** – Place any new ESP-IDF headers needed by your component into `include/` (or edit existing stub headers like [`stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/stubs.h) or [`wifi_stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/wifi_stubs.h)).

3. **Regenerate bindings** – Run `idf.py reconfigure` to trigger the CMake system to regenerate `imports/idf-sys.zig` via `zig translate-c`.

4. **Write a Zig wrapper** – Create an idiomatic Zig module in `imports/` (following the pattern of `gpio.zig` or `rtos.zig`) that exposes the C API with Zig-style naming, error handling, and logging.

5. **Port the implementation** – Replace the C source file with a Zig file (e.g., `main/your_feature.zig`) that imports your wrapper and implements the logic using Zig syntax and patterns.

6. **Update CMake (automatic)** – The custom `cmake/zig-config.cmake` module automatically discovers and compiles any `.zig` files under `main/` and `imports/`, so no manual CMake changes are typically required.

7. **Build and flash** – Use `idf.py build flash monitor` to compile the hybrid project and test on hardware.

## Porting Examples: From C to Zig

The repository provides concrete examples demonstrating how to transform typical ESP-IDF patterns from C into idiomatic Zig.

### Basic Hello World Migration

The transition from a standard C `app_main` to Zig illustrates the fundamental patterns: exporting the entry point, replacing C library calls with Zig equivalents, and routing logging through the ESP-IDF system.

**Original C implementation** ([`main/hello.c`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/hello.c)):

```c
#include "esp_log.h"
#include "esp_system.h"

void app_main(void) {
    ESP_LOGI("app", "Hello from C on ESP32!");
    printf("Zig version: %s\n", "unknown");
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

```

**Zig equivalent** (`main/app.zig`):

```zig
const std = @import("std");
const builtin = @import("builtin");
const idf = @import("esp_idf");
const log = std.log.scoped(.app);
const ver = idf.ver.Version;

comptime {
    // Export entry point expected by ESP-IDF
    @export(&app_main, .{ .name = "app_main" });
}

fn app_main() callconv(.c) void {
    // Use the Zig logger (forwarded to ESP-IDF log system)
    log.info("Hello from Zig on ESP32!", .{});

    // Show Zig compiler version
    log.info("Zig version: {s}", .{@as([]const u8, builtin.zig_version_string)});

    // Show ESP-IDF version
    var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();
    log.info("ESP-IDF version: {s}", .{ver.get().toString(allocator)});

    // Simple delay loop (FreeRTOS task)
    while (true) {
        idf.rtos.Task.delayMs(1000);
    }
}

pub const std_options: std.Options = .{
    .logFn = idf.log.espLogFn,
};
pub const panic = idf.esp_panic.panic;

```

Key migration patterns demonstrated:
- **`@export`** registers the Zig function with the C name `app_main` required by ESP-IDF.
- **`callconv(.c)`** ensures the function uses the C calling convention.
- **`idf.rtos.Task.delayMs`** replaces `vTaskDelay` with a Zig-idiomatic method.
- **`std.log.scoped`** combined with `idf.log.espLogFn` routes Zig logging to the ESP-IDF log system.

### Matter Device Migration

For complex C++ components like ESP-Matter, the repository demonstrates wrapping C++ APIs through C stubs and consuming them in Zig.

The C++ shim in [`main/matter_wrappers.cpp`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/matter_wrappers.cpp) bridges the Matter C++ API to C-compatible functions. To migrate fully to Zig:

1. Expose needed C functions in [`include/matter_stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/include/matter_stubs.h).
2. Regenerate bindings to update `imports/idf-sys.zig`.
3. Write a Zig façade in `imports/matter.zig` that wraps the Matter objects.
4. Replace C++ calls with Zig imports.

**Zig Matter example** (`main/examples/matter-light.zig`):

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

comptime {
    @export(&app_main, .{ .name = "app_main" });
}

fn app_main() callconv(.c) void {
    // Initialize Matter (wrapper handled in imports/matter.zig)
    const matter = idf.matter;
    const node = try matter.Node.init();
    defer node.deinit();

    // Create an On/Off Light endpoint
    const light = try node.addOnOffLightEndpoint(.{
        .name = "My Zig Light",
        .on_off = false,
    });
    defer light.deinit();

    log.info("Matter node ready – advertising...", .{});
    // The Matter stack runs its own task; the Zig app can idle
    while (true) {
        idf.rtos.Task.delayMs(1000);
    }
}

```

This example demonstrates how high-level C++ Matter concepts (nodes, endpoints) map to Zig structs and methods through the wrapper layer, allowing you to write Matter devices without directly handling C++ interop in application code.

## Key Files and Their Roles

Understanding the repository structure is essential for effective migration. The following files orchestrate the build process and runtime integration:

| File | Purpose |
|------|---------|
| **[`CMakeLists.txt`](https://github.com/kassane/zig-esp-idf-sample/blob/main/CMakeLists.txt)** (root) | Top-level CMake configuration that includes the Zig toolchain modules. |
| **`cmake/zig-config.cmake`** | Detects Zig sources and configures the build to compile `.zig` files alongside C/C++. |
| **`cmake/zig-download.cmake`** | Automatically downloads the Zig compiler, including the **zig-xtensa** fork for ESP32 targets. |
| **`include/*.h`** (e.g., [`stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/stubs.h), [`wifi_stubs.h`](https://github.com/kassane/zig-esp-idf-sample/blob/main/wifi_stubs.h)) | Header stubs fed to `zig translate-c` to generate bindings for the ESP-IDF APIs you need. |
| **`imports/idf-sys.zig`** | Auto-generated raw C bindings produced by `translate-c` during the CMake configuration phase. |
| **`imports/idf.zig`** | Hand-written façade that re-exports and organizes the generated bindings. |
| **`imports/*.zig`** (e.g., `gpio.zig`, `rtos.zig`, `matter.zig`) | Idiomatic Zig wrapper modules that provide type-safe, Zig-style APIs over the raw C bindings. |
| **`patches/*.zig`** | Post-processing patches applied to fix `translate-c` output issues (e.g., name collisions, struct layout problems). |
| **`main/app.zig`** | Reference implementation showing the entry point export, logging setup, and basic FreeRTOS integration. |
| **`main/examples/*.zig`** | Complete working examples demonstrating GPIO, Wi-Fi, HTTP, BLE, Matter, and DSP patterns. |

## Summary

Migrating C/C++ ESP-IDF code to Zig using the **zig-esp-idf-sample** repository provides a structured path to modern firmware development:

- **Hybrid build system** allows Zig and C/C++ to compile together via CMake modules (`cmake/zig-config.cmake`), enabling incremental migration without breaking existing code.
- **Automated binding generation** uses `zig translate-c` on headers in `include/` to produce `imports/idf-sys.zig`, ensuring Zig has access to the full ESP-IDF API.
- **Idiomatic wrappers** in `imports/*.zig` convert raw C bindings into Zig-friendly modules with proper error handling, logging integration, and naming conventions.
- **Entry point compatibility** requires exporting Zig functions with `@export(&func, .{ .name = "app_main" })` and `callconv(.c)` to maintain compatibility with the ESP-IDF startup code.
- **Memory safety** is achieved by using Zig’s `std.heap.c_allocator` (which wraps the ESP-IDF heap) or the specialized `idf.heap.*` allocators, ensuring compatibility with FreeRTOS memory management.

## Frequently Asked Questions

### How does the build system handle the Zig toolchain installation?

The build system automatically manages the Zig toolchain through **`cmake/zig-download.cmake`**, which downloads the appropriate Zig binary during the CMake configuration phase. For ESP32 Xtensa targets, it specifically fetches the **zig-xtensa** fork required to support the Xtensa instruction set architecture. You can also override this behavior by providing a local Zig installation via environment variables.

### Can I migrate only specific components while keeping the rest of my project in C++?

Yes, the **zig-esp-idf-sample** workflow supports incremental migration. You can port individual components (such as a GPIO driver or Wi-Fi manager) to Zig while retaining existing C++ code. The CMake system in `cmake/zig-config.cmake` automatically discovers and compiles `.zig` files alongside C/C++ sources, linking them into a single firmware image. This allows you to rewrite critical components in Zig for safety or performance while maintaining legacy C++ libraries.

### What is the purpose of the `patches/` directory in the repository?

The **`patches/`** directory contains small Zig source files that fix quirks in the output of `zig translate-c`. The automated binding generation process sometimes produces code with name collisions, incorrect struct layouts, or incompatible type definitions when processing complex ESP-IDF headers. The build system applies patches from this directory after generating `imports/idf-sys.zig` to correct these issues, ensuring the raw bindings are usable by the higher-level Zig wrappers in `imports/`.

### How do I handle ESP-IDF logging in Zig code?

Zig code integrates with the ESP-IDF logging system through the **`idf.log.espLogFn`** function. You configure Zig’s standard logging to route through ESP-IDF by setting `pub const std_options: std.Options = .{ .logFn = idf.log.espLogFn }` in your main application file. Then use `std.log.scoped(.module)` to create loggers that output through the ESP-IDF console, maintaining compatibility with existing C components that use `ESP_LOGI` and related macros.