Migrating C/C++ ESP-IDF Code to Zig: A Complete Guide to the zig-esp-idf-sample Workflow
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) undermain/andimports/, 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:
- Header stubs in
include/*.h(e.g.,stubs.h,wifi_stubs.h) declare the C APIs you need to access. - During the CMake configuration phase,
zig translate-cprocesses these headers. - 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– ProvidesTaskabstractions withdelayMs()methods instead of rawvTaskDelaycalls.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.
-
Identify the component to migrate – Select a C/C++ file (e.g., a driver or application module) that you want to rewrite in Zig.
-
Add required headers – Place any new ESP-IDF headers needed by your component into
include/(or edit existing stub headers likestubs.horwifi_stubs.h). -
Regenerate bindings – Run
idf.py reconfigureto trigger the CMake system to regenerateimports/idf-sys.zigviazig translate-c. -
Write a Zig wrapper – Create an idiomatic Zig module in
imports/(following the pattern ofgpio.zigorrtos.zig) that exposes the C API with Zig-style naming, error handling, and logging. -
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. -
Update CMake (automatic) – The custom
cmake/zig-config.cmakemodule automatically discovers and compiles any.zigfiles undermain/andimports/, so no manual CMake changes are typically required. -
Build and flash – Use
idf.py build flash monitorto 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):
#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):
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:
@exportregisters the Zig function with the C nameapp_mainrequired by ESP-IDF.callconv(.c)ensures the function uses the C calling convention.idf.rtos.Task.delayMsreplacesvTaskDelaywith a Zig-idiomatic method.std.log.scopedcombined withidf.log.espLogFnroutes 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 bridges the Matter C++ API to C-compatible functions. To migrate fully to Zig:
- Expose needed C functions in
include/matter_stubs.h. - Regenerate bindings to update
imports/idf-sys.zig. - Write a Zig façade in
imports/matter.zigthat wraps the Matter objects. - Replace C++ calls with Zig imports.
Zig Matter example (main/examples/matter-light.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 (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, 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-con headers ininclude/to produceimports/idf-sys.zig, ensuring Zig has access to the full ESP-IDF API. - Idiomatic wrappers in
imports/*.zigconvert 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" })andcallconv(.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 specializedidf.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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →