ZigC-Interop

Zig FFI: Calling Zig from C

TT
TopicTrick Team
Zig FFI: Calling Zig from C

Zig FFI: Calling Zig from C

The ultimate power of a systems language is its ability to "Play well with others." If you write a high-performance "Image Processor" in Zig, you don't want to rewrite it in Python just so a Python developer can use it.

You want to Export it. Zig allows you to create .dll (Windows) or .so (Linux) files that look exactly like C libraries. This 1,500+ word guide explores the export keyword, the extern struct, and the architecture of "Global Compatibility."


1. The export Keyword: Visibility

To make a function visible to the "Outside World" (C), you must use export.

zig

Architecture Note: Notice that the function is Not pub. pub is for other Zig files. export is for the CPU's linker. When you use export, the name add is saved directly into the binary file's symbol table.


2. The Physics of the Export: Symbol Visibility and Linker Tables

When you export a function, you are talking to the Linker, not just the compiler.

The Symbol Mirror

  • The Concept: Every binary file (like an .so or .dll) has a Dynamic Symbol Table.
  • The Physics: By default, functions are "Hidden" within the binary to optimize file size. export flips a bit in the ELF or PE header, making the function's entry point address public.
  • The Result: When a Python script "Loads" your Zig library, it searches this table for the name add. The linker then maps the Python function call directly to your Zig function's physical starting address in RAM, allowing for zero-latency execution.

3. Calling Convention: callconv(.C)

Different languages pass arguments to functions in different ways (on the Stack, in Registers, etc.).

  • Zig uses its own "Fast" way by default.
  • C uses the "C Standard" way.
  • The Rule: If you use export, Zig automatically uses the C calling convention. You don't have to worry about it!

3. Extern Structs: Matching the Memory

If you pass a struct between Zig and C, you must use extern struct.

  • A normal Zig struct can have its fields re-ordered by the compiler to save space.
  • An extern struct is Guaranteed to stay in the exact order you wrote it. This is mandatory so C knows where the id ends and the name begins.

4. The C-ABI Reality: Register Pressure and Argument Passing

Zig's internal calling convention is aggressive and can change between versions. To "Play with C," you must freeze your logic into the Standard C-ABI.

The ABI Mirror

  • The Process: For a function with 4 integers, the System V ABI (Linux) requires arguments to be placed in %rdi, %rsi, %rdx, and %rcx.
  • The Physics: If you pass a large struct by value, the ABI may require it to be copied onto the stack. This Stack Traffic is the "Physics" of your performance bottleneck.
  • Mastery: By using extern struct and passing by pointer, you ensure that Zig only moves a single 64-bit address through a register, bypassing the expensive stack-copying logic of the legacy C style.

5. Error Wrapping: c_int results

C does not understand Zig's "Error Union" (!u32). If a Zig function returns an error, C will just see a weird, random number. The Fix: You must "Wrap" your logic.

zig

5. Building the Library: .so and .dll

In build.zig, you must change your project type from an "Executable" to a "Shared Library."

zig

Run zig build. You now have a file that a Python developer can import using ctypes or a C++ developer can link via a header.


FFI turns your Zig code into a "Universal Plugin." By mastering the export boundary and the discipline of extern-memory matching, you gain the ability to power libraries in Python, Ruby, Rust, and C++ with the extreme speed of Zig. You graduate from "Building an app" to "Architecting the Foundation."


Phase 22: FFI Architecture Checklist

  • Audit your Exported Names: Ensure they don't conflict with standard C library names (e.g., don't export a function called malloc).
  • Implement Sentinel Strings: When exporting strings, ensure you append a :0 null terminator so C can read them using strlen().
  • Setup extern struct for every data transfer object to ensure bit-identical layout between Zig and the host language.
  • Profile your Boundary Crossing: Use a performance profiler to measure the "FFI Overhead" in your host language (e.g., Python's ctypes cost).
  • Verify Symbol Visibility: Use the nm -D command (Linux) or dumpbin /exports (Windows) to verify that your Zig functions are correctly exposed in the final .so or .dll.

Read next: Zig as a C/C++ Compiler: The Zero-Legacy Orchestrator →


Part of the Zig Mastery Course — engineering the export.