Zigで組込み開発を始める方法——Cを「そのまま」置き換えられる言語がマイコン開発を変える

はじめに——なぜ今、Zigが組込み界隈で注目されるのか

マイコン開発といえば、長年C言語が事実上の標準でした。しかしCでの開発には、ターゲットごとにarm-none-eabi-gccのようなクロスコンパイラを別途用意するツールチェイン構築の面倒さ、未定義動作に起因する潜在的なバグ、プリプロセッサマクロが生む読みにくいコードといった課題がつきまといます。モダンな代替としてRustも注目されていますが、所有権やライフタイムといった独自概念の学習コストの高さに、二の足を踏むエンジニアも少なくありません。

こうした中で注目されているのが、システムプログラミング言語のZigです。Zigは既存のC/C++コードベースを壊さずに段階的に改善できる言語として設計されており、公式サイトでは「堅牢、最適、再利用可能なソフトウェア」のための言語兼ツールチェインとして紹介されています。隠れた制御フローや隠れたメモリ割り当てを排したシンプルな設計でありながら、zig ccとしてクロスコンパイル対応のドロップインC/C++コンパイラにもなれるため、C資産と相互運用しながら段階的に導入しやすいのが大きな特徴です。Zennの解説では「Cの50年分の進化を2020年代の知見で再構成した言語」と表現されており(5分でざっと理解するZig言語)、Cとの相互運用の簡単さとRustより低い学習コストも、比較記事で繰り返し指摘されています。

組込み分野では、Zig Embedded Groupが開発するMicroZigを中心にエコシステムが育ちつつあり、Raspberry Pi Pico向けのファームウェアをZigだけで書けるようになっています。なお、RTOSをベースにしたモダンな組込み開発の別の選択肢については、Zephyr RTOSとRISC-Vマイコンではじめるモダン組込み開発もあわせてご覧ください。

本記事では、Zigが組込み開発に適する理由、C・Rustとの比較、そしてRaspberry Pi PicoでLチカ(LED点滅)を実現する手順までを、ステップバイステップで解説します。「Cから脱却したいけれど、Rustまで学び直すのは大変」という方は、ぜひ最後までご覧ください。

Zigが組込みに適する3つの理由+全体図解

「Zigで組込み開発」と聞くと、専用ツールや複雑な環境構築を想像するかもしれません。しかし実際には、Zigの言語とツールチェインの設計そのものが組込みに向いています。鍵となる3つの理由を順に見ていきましょう。

理由1: クロスコンパイルが標準装備

Cによるマイコン開発では、ターゲットごとにarm-none-eabi-gccのようなクロスツールチェインを用意するのが定番の悩みでした。Zigはクロスコンパイル機能が本体に組み込まれており、ターゲットを指定するだけでRaspberry Pi Pico(Arm Cortex-M0+)のような異なるアーキテクチャ向けバイナリを、コンパイラ単体で生成できます。zig buildの一発ビルドがそのままクロスコンパイルになります。MicroZigは、この仕組みの上に各マイコン向けのターゲット定義を構築したフレームワークです。

ただし「マイコン開発に必要なツールがすべてZig一本で完結する」という意味ではありません。ベンダーSDKの対応状況、リンカ設定の要否、フラッシュへの書き込みやオンターゲットデバッグに必要なツールは、ターゲットとプロジェクトに依存します(詳細はFAQ「開発環境には何を用意すればいいの?」も参照)。

理由2: Cヘッダを直接importできる相互運用性

Zigでは@cImportを使ってCのヘッダファイルをそのまま取り込み、型や関数をFFIオーバーヘッドなしで呼び出せます。ベンダー提供のSDKやドライバなど、Cで書かれた既存資産を「捨てずに」活かせるのが最大の強みです。さらにZigコンパイラはzig ccとしてC/C++コンパイラにもなれるため、「既存プロジェクトのビルドだけZigに置き換える」という低リスクな段階的移行が現実的になります(Zig公式)。

理由3: ランタイムゼロ・隠れた動作なし

Zigの設計原則は「隠れた制御フローがない」「隠れたメモリ割り当てがない」の2本柱です。例外や演算子オーバーロードで制御が見えなくなることも、暗中で動くガベージコレクタもありません。メモリ確保を行う処理にはアロケータを明示的に引数として渡す規約があり、このおかげでOSを持たないフリースタンディング環境でも、標準ライブラリの大半をそのまま利用できます。さらにCマクロの代わりとなる「comptime(コンパイル時実行)」により、型安全なメタプログラミングが可能です。これはレジスタ定義の自動生成など、組込み特有のボイラープレート削減にも直結します。

全体図解: Zigの組込み開発スタック

3つの理由がどう組み合わさるのか、スタック全体を図にまとめました。

flowchart TD
    A["開発者: zig build"] --> B["Zigコンパイラ<br/>クロスコンパイル標準装備"]
    B --> C["MicroZig<br/>・レジスタ定義からMMIOバインディング自動生成<br/>・リンカスクリプト自動生成<br/>・HAL/ポート定義"]
    C --> D["マイコン<br/>Raspberry Pi Pico / Pico 2 ほか"]

既存のC資産は、次の流れでZigからそのまま取り込めます。

flowchart LR
    subgraph CA["既存C資産"]
        H["SDK・ヘッダファイル"]
        S["Cソースコード"]
    end
    H -->|"@cImport<br/>FFIオーバーヘッドゼロ"| Z["Zigで書く新規コード"]
    S -->|"zig ccでそのままビルド"| Z

このようにZigは、「新しく書くコード」も「既存のC資産」も1つのツールチェインで扱える言語です。次の章では、C・Zig・Rustを具体的に比較していきます。

C・Zig・Rust比較表——どれを選ぶべきか

「結局、C・Zig・Rustのどれを選べばいいのか?」——組込み開発の言語選定で必ず議論になるポイントです。結論から言えば、この問いに単純な優劣はありません。3言語は「何を重視するか」によって得意分野がはっきり分かれています。まずは組込み開発の主要な観点で比較表を見てみましょう。

観点 C Zig Rust
クロスコンパイル ターゲットごとにツールチェインが必要(arm-none-eabi-gcc等) 本体に標準装備。コンパイラ単体で完結(書き込み・デバッグ等はターゲット依存) ターゲット追加で可能だが、リンカ等の追加設定が挟みやすい
Cとの相互運用 C自身 @cImportでCヘッダをそのままimport、FFIオーバーヘッドゼロ。zig ccでCコードのビルドも可能 FFIは可能だがbindgen等の補助ツールが一般的。unsafeが必要な場面も多い
ランタイム・隠れた動作 最小だが未定義動作・隠れた挙動が多い ランタイムなし。隠れた制御フロー・隠れたメモリ割り当てなし ランタイムは最小だが、言語機構が複雑
メモリ安全性 保証なし(未定義動作多数) Rustのようなコンパイル時保証はない。未定義動作の検出はビルドモード・操作に依存し、危険な操作は明示的に目立つ設計 コンパイル時に保証(借用チェッカー)
学習コスト 文法は単純だが落とし穴が多い Cより小さくシンプル。comptimeが新しい概念 高い(所有権・ライフタイム・トレイト)
メタプログラミング プリプロセッサ(型安全でない) comptime(型安全なコンパイル時実行) ジェネリクス+マクロ
組込みエコシステム 圧倒的(HAL/BSPが豊富) MicroZig等、発展途上だが活発 embedded-hal等、比較的成熟
成熟度・安定性 安定 1.0未満。破壊的変更あり 安定(1.0超え、editionsで進化)

この表が示す通り、3言語の得手不得手は明確です。選び方の指針をケース別に整理しましょう。

実績とエコシステムを最優先するならC。 半世紀にわたる資産と、あらゆるマイコンをカバーするHAL/BSPは他の追随を許しません。チームの習熟度や調達要件で「絶対的な安定」が求められる現場では、依然として第一選択肢であり続けます。

既存C資産を活かしつつモダン化したいならZig。 @cImportによるCヘッダの直接importとFFIオーバーヘッドゼロの相互運用は、3言語の中で際立って簡単です。Cとの相互運用の簡単さとRustより低い学習コストは、Qiitaの解説やZennの記事でも共通する論調です。ただし同じQiita記事で「ZigにはRustほど強い静的メモリ安全性はなく、ポインタ操作を誤れば落ちうる」点も紹介されており、安全性の立ち位置を一律な順位で語ることはできません。既存プロジェクトなら、まずzig ccでビルドだけ置き換える低リスクな導入から始められます。

コンパイル時の安全性保証が最優先ならRust。 借用チェッカーによるメモリ安全性のコンパイル時保証は3言語随一で、embedded-halを中心に組込みエコシステムも比較的成熟しています。ただし所有権・ライフタイムの学習コストは覚悟が必要です。Rustがシステム開発の現場に広がりつつある流れは、LinuxカーネルはなぜRustを受け入れ始めたのか——Rust for Linuxの現在地でも解説しています。

一方で、公平な目も忘れてはいけません。Zigはまだ1.0未満で、言語仕様やMicroZigのAPIには破壊的変更が含まれます。公式のGetting Startedも「新しく破壊的変更のある言語に基づくalphaプロジェクト」と明言しており、企業採用実績や人材市場の規模もC・Rustに及びません。したがって現実的な戦略は「今すぐすべてを置き換える」ことではなく、zig ccでのビルド導入→@cImportで新規コードだけZig化、という段階的な移行です。Cの50年の資産を捨てずに、2020年代の言語設計の恩恵を受けられる——それがZigという第三の選択肢の価値です。

実践ステップ——Raspberry Pi PicoでLチカする

理屈の説明はここまでにして、実際にZigでマイコンを動かしてみましょう。用意するのはRaspberry Pi Pico(またはPico 2)とMicro USBケーブルだけ。ARMクロスコンパイラのインストールは不要です。手順はMicroZig公式のGetting Startedに沿っています。

検証時点について(2026年9月7日): 本記事の手順とコードは、この日時点のMicroZig公式Getting Started(参照時点でZig 0.17.0-dev.1158+1d1193aa7 の例を掲載)に基づきます。MicroZigはZigの開発版(master)に追随して開発されており、Zig・MicroZigのバージョンが変わるとAPIも変わります。実際に試す際は、zig fetch --saveで固定したmicrozigのリビジョンと、手元のzig versionの完全な識別子(例: 0.17.0-dev.1158+1d1193aa7)を照合し、最新の公式Getting Startedを必ず参照してください。

Step 1: Zigをインストールする

MicroZigはZigの開発版に追随して開発されているため、Zig公式からmaster版(0.17.0-dev系)を入手します。インストール後、ターミナルで確認しましょう。

$ zig version
0.17.0-dev.xxxx

Step 2: プロジェクトを作成し、microzigを追加する

zig initでプロジェクトの雛形を作り、zig fetchでmicrozigを依存関係に追加します。パッケージマネージャの追加設定は不要です。

$ zig init
$ zig fetch --save 'git+https://github.com/ZigEmbeddedGroup/microzig'

Step 3: build.zigにファームウェアを定義する

Getting Startedの現行手順では、MicroBuildでビルドを初期化し、add_firmwareでファームウェアを定義して、install_firmwareでインストール対象に登録します。

const std = @import("std");
const microzig = @import("microzig");

const MicroBuild = microzig.MicroBuild(.{
    .rp2xxx = true,
});

pub fn build(b: *std.Build) void {
    const mz_dep = b.dependency("microzig", .{});
    const mb = MicroBuild.init(b, mz_dep) orelse return;

    const firmware = mb.add_firmware(.{
        .name = "blinky",
        .target = mb.ports.rp2xxx.boards.raspberrypi.pico,
        .optimize = .ReleaseSmall,
        .root_source_file = b.path("src/main.zig"),
    });

    // RP2040のデフォルト出力はUF2。ELFも併せて出力する
    mb.install_firmware(firmware, .{});
    mb.install_firmware(firmware, .{ .format = .elf });
}

ターゲットに「Raspberry Pi Pico」ボードを指定するだけで、リンカスクリプトやスタートアップコードはmicrozigが自動生成します。ファームウェア定義にはroot_source_file(エントリポイントとなるソースファイル)の指定が必須です。Pico 2(RP2350)で動かす場合は、.targetをmb.ports.rp2xxx.boards.raspberrypi.pico2_armに変えるだけです。

Step 4: Lチカコードを書く

src/main.zigに、本体LED(GPIO25)を点滅させるコードを書きます。ピンの設定はcomptime(コンパイル時実行)で宣言的に行えるのがZigらしいところです。

const std = @import("std");
const microzig = @import("microzig");
const rp2xxx = microzig.hal;
const time = rp2xxx.time;

pub const panic = microzig.panic;
pub const std_options = microzig.std_options(.{});

comptime {
    _ = microzig.export_startup();
}

// コンパイル時のピン設定
const pin_config = rp2xxx.pins.GlobalConfiguration{
    .GPIO25 = .{
        .name = "led",
        .direction = .out,
    },
};

pub fn main() !void {
    const pins = pin_config.apply();

    while (true) {
        pins.led.toggle();
        time.sleep_ms(250);
    }
}

冒頭のpanic・std_options・export_startup()はフリースタンディング環境で動かすための前提となる宣言です。pin_config.apply()でピンを初期化し、pins.led.toggle()でON/OFFを反転。250ミリ秒ごとに点滅を繰り返します。実装はGitHubのmicrozigリポジトリでも確認できます。

Step 5: ビルドする

$ zig build

これだけです。クロスコンパイルは自動で行われ、zig-out/firmware/以下にblinky.uf2とblinky.elfが生成されます。

Step 6: Picoに書き込む

BOOTSELボタンを押しながらPicoに電源を供給すると、UF2ブートローダーが起動してPCにストレージとして認識されます。blinky.uf2をそのドライブにコピーすれば、即座にファームウェアが起動。Picoでは書き込み専用ツールもIDEも不要です(他のマイコンでは書き込みツールの要否が変わることがあります)。

なお、Zigとmicrozigのバージョンが異なると、build.zigのAPI(MicroBuildの初期化やadd_firmwareのシグネチャ)やピン設定の書き方が大きく変わることがあります。サンプルコードを参考にする際は、冒頭の「検証時点について」のとおりバージョンを揃えるか、利用するリビジョンの公式ドキュメントを必ず確認してください。

動いたら最後にひとつ、公式が強調しているポイントを紹介します。「標準ライブラリの大半はMCU上のZigでもそのまま使える」のです。これはアロケータが明示的に渡される設計だからで、OSなしの環境でもヒープを使わない関数なら通常どおり呼び出せます。これこそが「ランタイムゼロ」言語の実力です。

ケーススタディ——既存CプロジェクトをZigでどう改善するか

こうして動かせると分かったところで、次に気になるのは実務への取り入れ方です。いきなりコードを全部書き換える必要はありません。Zigの強みは、既存のC資産を壊さずに段階的に移行できることです。実務では次の3パターンが現実的です。

パターン1: zig ccを「ビルドツール」としてだけ導入する

最初の一歩は、ソースコードを書き換えないことです。Zigは公式がうたうとおりzig ccとしてC/C++コンパイラ兼クロスコンパイラとして動作するため、MakefileやCMakeのコンパイラ指定を差し替えるだけで、arm-none-eabi-gccなどターゲットごとのクロスコンパイラのインストールと管理をzig1本に集約できます。チーム全員の環境構築が統一され、クロスコンパイルの再現性という大きな効果が得られます。ただしこれもコンパイルの話であり、フラッシュへの書き込みやオンターゲットデバッグなど、ビルド以外の工程で必要なツールはターゲットとプロジェクトに依存します。

パターン2: @cImportで既存C SDKをラップし、新規コードだけZigで書く

次の段階は相互運用です。Zigは@cImportで既存のCヘッダをそのまま取り込め、FFIオーバーヘッドなしで型・関数を呼び出せます。ベンダー提供のSDKやドライバはCのまま活かし、アプリケーション層など新しく書くコードだけをZigに置き換えていく——これが「C資産を捨てない」移行の中間形です(Qiita「Zigのすすめ Rustとの比較」も「Cとのインターフェイスが非常にシンプル」と評しています)。

パターン3: MicroZigで新規ファームウェアをフルZig構成にする

新規プロダクトやリニューアル部分には、Zig Embedded Groupが開発するMicroZigが使えます。レジスタ記述からMMIOバインディングを自動生成し、マイコン向けのリンカスクリプトも自動生成してくれるため、Cでは手作業になりがちな下回りの設定をビルドシステムが担います。対応状況はRaspberry Pi Pico/Pico 2が最も手厚く、Getting Startedでファームウェア生成からUF2書き込みまで確認できます。

MicroZigの設計思想も注目に値します。公式は「ゼロコストな抽象化はないが、適切なレイヤー配置を見つけた」と説明し、高レベルに見えるMMIOインターフェースの各呼び出しが、レジスタへの1回のリード・モディファイ・ライトに帰着する低コスト抽象化を実現しています。さらに「Bring your own hardware」の思想のもと、未対応マイコン向けに自前のレジスタ定義を差し込んだり、カスタムPCBでもフレームワークと汎用ドライバを流用したりできます。

このように「zig ccでビルド統一 → @cImportで共存 → MicroZigで新規開発」という3段階を踏めば、Cの50年分の資産を活かしながら、リスク最小でZigへ段階的に移行できます。

FAQ——Zig組込み開発のよくある6つの質問

Q. Zigは本番の組込み開発に使えるの?

使えますが、条件付きと考えるのが正直なところです。Zigはまだバージョン1.0未満で、言語仕様や標準ライブラリ、MicroZigのような主要フレームワークのAPIには破壊的変更が入る可能性があるため、プロジェクトごとにバージョンを固定する運用が事実上必須です(MicroZig公式自身もalphaプロジェクトであることを明言しています)。ここからは著者の助言ですが、個人開発・技術検証・新規の小規模ファームウェアから始めるのが現実的で、長期保守が必要な量産製品への採用は、1.0の動向だけでなく長期保守・認証・サプライチェーンの要件も踏まえて判断するのが安全だと考えます。

Q. ZigはRustの代わりになる?

「完全な代わり」ではなく、得意分野の異なる隣人です。メモリ安全性について「Rust > Zig > C」といった一律の順位で語られることもありますが、正式な比較評価が存在するわけではありません。ZigにはRustの借用チェッカーのようなコンパイル時のメモリ安全性保証はなく、未定義動作を検出できる範囲はビルドモードや操作の内容に依存します。コンパイル時の安全性保証を最優先するならRustが有利、既存のC資産(ヘッダやSDK)を活かしながら段階的に移行したい場面では、Cとの相互運用性に優れるZigが有利、という使い分けです。

Q. Zigはどのマイコンに対応している?

MicroZigエコシステムではRaspberry Pi Pico/Pico 2のサポートが最も手厚く、ATSAMD51J19などの他チップ向けサポートも進んでいます。対応がまだのチップでも、レジスタ定義を自前で書けばZig本体のクロスコンパイルやランタイムゼロの性質はそのまま活用できます。まずはPicoあたりで試すのが最も情報が多くておすすめです。

Q. 既存のCライブラリはそのまま使える?

はい、@cImportでCのヘッダを直接インポートでき、FFI(外部関数インターフェース)のオーバーヘッドなしに呼び出せます。さらにZigのコンパイラ自体がzig ccとしてCコードをビルドできるため、既存のCソース一式をZigのビルドシステムに取り込むことも可能です。ベンダー提供のC SDKを捨てずに使えるのが大きな強みです。

Q. 開発環境には何を用意すればいいの?

コンパイラとしては、Zig本体のインストールだけでクロスコンパイルまで完結します。クロスコンパイル機能が内蔵されているため、arm-none-eabi-gccのようなターゲット別クロスコンパイラの追加インストールは不要です。一方で、ベンダーSDKの対応状況やリンカ設定の要否、フラッシュへの書き込み、オンターゲットデバッグに必要なツールはターゲットとプロジェクトに依存します。IDEの補完やデバッガ連携は発展途上なので、エディタはZig公式のzls(Zig Language Server)対応品を選ぶとよいでしょう。

Q. デバッグやテストはどうすればいいの?

Zigには標準でzig testが用意されており、PC上でロジックの単体テストを書けます。さらにcomptime(コンパイル時実行)を利用すれば、型や値の整合性をビルド段階で検証でき、実行前のバグ潰しに役立ちます。マイコン実機のデバッグは、利用するボードやデバッグプローブ、ターゲットの公式資料に応じて、デバッグ情報付きのELFと既存のデバッグツールを組み合わせて構成するのが一般的です。

まとめ——CからZigへの移行チェックリスト

Zigの強みは、C資産と相互運用しながら段階的に導入しやすい設計にあります。クロスコンパイルをコンパイラ本体が担い、@cImportで既存のC資産を活かせ、ランタイムや隠れた動作がないため、制約の厳しいマイコン環境でも素直に動きます。これらは、長年Cで苦労してきた組込み開発の体験を確実に変える要素です。

とはいえ、いきなり全面移行する必要はありません。まずは次のチェックリストに沿って、小さな一歩から試してみてください。

  • [ ] zig ccで既存ビルドを試す(リスク最小の第一歩)
  • [ ] @cImportで既存のCヘッダを取り込めるか検証する
  • [ ] Raspberry Pi PicoなどでMicroZigのLチカを動かす
  • [ ] Zigバージョンの固定と破壊的変更への追従方針を決める
  • [ ] 本番採用は1.0の動向と長期保守・認証などの要件で判断する(著者の助言)

おすすめの進め方は、まず「zig cc」を既存プロジェクトのコンパイラとしてだけ導入し、クロスコンパイル環境を統一することです。次に@cImportで既存ドライバやSDKをラップしながら、新規コードだけZigで書き始めると、学習コストを抑えつつ段階的に移行できます。検証が進んだら、MicroZigで新規ファームウェアをフルZig構成で作ってみましょう。

なお、Zigはまだ1.0未満であり、バージョンアップに伴う破壊的変更がある点には注意が必要です。個人プロジェクトや技術検証、新規の小規模開発から始めるのが現実的な選択だと考えます(著者の助言として)。

Cの50年にわたる資産を捨てずに、2020年代の言語設計の恩恵を受けられる——それがZigによる組込み開発です。まずはチェックリストの最初の1項目「zig ccで既存ビルドを試す」から、今日始めてみてはいかがでしょうか。

関連記事