๐Ÿ”ง nativetoolchainzig

August 13, 2026 ยท View on GitHub

Pub Version Dart CI License: MIT

Zig support for Dart's build hooks. Automatically builds and bundles your Zig code with your Dart/Flutter application.

Note

Zig 0.14 and 0.15 do not currently provide a reliable -femit-h path for generating C headers from exported Zig declarations. This package includes a source-first bindings generator so package authors do not need to maintain a handwritten C header just to feed ffigen.

Prerequisites

Install Zig 0.15.0+ on your development machine.

Installation

dart pub add hooks native_toolchain_zig

Project Setup

  1. Create your Zig project in my_project/zig/:
my_package/
โ”œโ”€โ”€ hook/
โ”‚   โ””โ”€โ”€ build.dart
โ”œโ”€โ”€ lib/
โ”‚   โ””โ”€โ”€ my_package.dart
โ”œโ”€โ”€ zig/
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ””โ”€โ”€ lib.zig
โ”‚   โ”œโ”€โ”€ build.zig
โ”‚   โ””โ”€โ”€ build.zig.zon
โ””โ”€โ”€ pubspec.yaml
  1. Create hook/build.dart:
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_zig/native_toolchain_zig.dart';

Future<void> main(List<String> arguments) async {
  await build(arguments, (input, output) async {
    await ZigBuilder(
      assetName: 'my_package.dart',
      zigDir: 'zig/',
    ).run(input: input, output: output);
  });
}
  1. Create zig/build.zig:
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const lib = b.addLibrary(.{
        .name = "my_package",
        .linkage = .dynamic,
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/lib.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(lib);
}
Build both static and dynamic libraries

To produce both library types in a single build:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const root_module = b.createModule(.{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Dynamic library (.so, .dylib, .dll)
    const dynamic_lib = b.addLibrary(.{
        .name = "my_package",
        .linkage = .dynamic,
        .root_module = root_module,
    });

    b.installArtifact(dynamic_lib);

    // Static library (.a, .lib)
    const static_lib = b.addLibrary(.{
        .name = "my_package",
        .linkage = .static,
        .root_module = root_module,
    });

    b.installArtifact(static_lib);
}
Select linkage via command line

To control linkage type via a custom -Dlinkage option, first accept it in your build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const linkage = b.option(
        std.builtin.LinkMode,
        "linkage",
        "Library linkage type",
    ) orelse .dynamic;

    const lib = b.addLibrary(.{
        .name = "my_package",
        .linkage = linkage,
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/lib.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(lib);
}

Then pass the flag from Dart via extraArguments in hook/build.dart:

import 'package:hooks/hooks.dart';
import 'package:native_toolchain_zig/native_toolchain_zig.dart';

Future<void> main(List<String> arguments) async {
  await build(arguments, (input, output) async {
    await ZigBuilder(
      assetName: 'my_package.dart',
      zigDir: 'zig/',
      // Forward the linkage option to zig build
      extraArguments: ['-Dlinkage=static'],
    ).run(input: input, output: output);
  });
}
  1. Create zig/build.zig.zon:
.{
    .name = .my_package,
    .version = "0.1.0",
    .minimum_zig_version = "0.15.0",
    .fingerprint = 0x..........,
    .paths = .{
        "src",
        "build.zig",
        "build.zig.zon",
    },
}

Important

The paths field drives incremental build tracking. ZigBuilder parses build.zig.zon and registers every file and directory listed in paths as a build dependency. When any of those files change, Dart's build system automatically re-triggers the Zig build. Make sure paths includes all source directories and files your build depends on (e.g. "src", C headers, embedded data files).

The name field (.my_package) is a comptime enum literal and is ignored.

  1. Create zig/src/lib.zig:
export fn add(a: i32, b: i32) i32 {
    return a + b;
}
  1. Generate Dart bindings in lib/my_package.dart:
dart run native_toolchain_zig:zig bindings --output lib/my_package.dart

The generator discovers exported Zig declarations and emits Dart @Native bindings that use the same native asset ID registered by ZigBuilder.

You can also pass explicit paths:

dart run native_toolchain_zig:zig bindings \
  --zig-dir zig \
  --root-source-file src/lib.zig \
  --output lib/my_package.dart

During development, use watch mode:

dart run native_toolchain_zig:zig bindings --watch
  1. Run your app:
dart run

Example

An example can be found in /example/.

License

MIT License - see LICENSE for details.