最近流行りのGhosttyというターミナルエミュレータではSwiftでMacのネイティブUIを書きつつ、Coreのエミュレータ部分はlibghosttyというZigで書かれたライブラリです。
どうやってSwiftからZigのライブラリを使っているかというと、このAboutに書いてある通り、Zigの関数をC APIとして公開し、SwiftからはC APIを呼び出すことでinteropを実現しています。
Zigではexoprtキーワードを使うことで簡単に関数をCのAPIとして公開できます。 ZigではCとの互換性を保つための型も用意されているので、それらの型を使って関数を定義してexportするだけです。
最近、GUIのDBクライアントを作りたい欲があり、同じ構成で出来ないかと実際に試してみたので、備忘録としてまとめておきます。
Zigのライブラリを用意する
プロジェクトルートで zig init コマンドを実行してZigのプロジェクトを作成します。
build.zigを以下のように修正して、ライブラリとしてビルドできるようにします。 (zig build-lib でも問題ないです)
// addExecutable -> addLibrary に変更
const lib = b.addLibrary(.{
.name = "ziglib",
.root_module = b.createModule(.{
// src/root.zig をエントリーポイントに設定
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "ziglib", .module = mod },
},
}),
});
zig build コマンドでビルドすると zig-out/lib/libziglib.a という静的ライブラリが生成されます。
ZigでC APIを定義する
これに関しては公式ドキュメントを参照すれば特に問題なくできるはずです。
headerファイルを作成します。これはSwiftからC APIを呼び出すときに必要になります。
完了したら再度ビルドして、ライブラリを生成します。
XCodeで新しくプロジェクトを作成する
XCodeで新しくプロジェクトを作成します。 macOSアプリケーションのテンプレートを選択します。
XcodeにZigのライブラリをxcframeworkとして追加する
ここらへんはGhosttyのレポジトリを参考にしました。 Ghosttyではここら辺のビルドコマンドは全部Zigのビルドスクリプトとして書いてあります。 src/build/
ビルドスクリプトもただのZigなのがZigのいいところですね。 ビルドやマクロ(厳密にはcomptime)含めてZigという言語で簡潔しているのが非常にシンプルで好きです。
以下のコマンドを実行してxcframeworkとしてビルドします。
オプション
-library: 生成されたZigの静的ライブラリへのパス-headers: Zigのヘッダーファイルを置いているディレクトリへのパス-output: 出力先のxcframeworkのパス。どこでもよいですが、Xcodeプロジェクトのルートに置くと管理しやすいです。
$ xcodebuild -create-xcframework -library ./zig-out/lib/libziglib.a -headers ./include -output ./<path-to-your-xcode-project-root>/ZigLib.xcframework
xcframework successfully written out to: xxx/ZigLib.xcframework
次にXCodeプロジェクトにこのフレームワークを登録します。
XCodeのプロジェクト設定でGeneralタブを開き、 Frameworks, Libraries, and Embedded Contentセクションに先ほど作成したZigLib.xcframeworkを追加します。
これでSwiftからZigのライブラリを使えるようになりました。
あとは普通にSwiftからC APIを呼び出すだけです。
import Foundation
import ZigLib
// Zigで定義したC APIを呼び出す
let result = zig_function_name()
ここまでは割とすんなり出来ました。
しかし当初Zigのライブラリ側でlibpqに依存していたために色々とハマりました。
Zig側でCライブラリに依存している場合
Zig側でCのライブラリに依存している場合、Swiftから使う際に(Swiftに限らずですが)、そのCライブラリもリンクする必要があります。
やり方は主に2つです。
- Zig側でCライブラリを静的にリンクする
- Swift側でCライブラリを動的にリンクする
1ができるのであれば単純で、ZigのライブラリをビルドすればそこにCライブラリも含まれるのでSwift側で特に意識する必要はありません。
当初は1の方法でlibpqを静的にリンクしようとしたのですが、そうするとlibpqが依存している他のライブラリも全部静的にリンクする必要があり、非常に面倒でした。
otoolでlibpq.dylibの依存関係を確認すると以下のようになっています。
~/dev/tdir main !5 ?2 ❯ otool -L /opt/homebrew/opt/libpq/lib/libpq.dylib Go 1.25.1 Node v24.3.0 22:33:50
/opt/homebrew/opt/libpq/lib/libpq.dylib:
/opt/homebrew/opt/libpq/lib/libpq.5.dylib (compatibility version 5.0.0, current version 5.18.0)
/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib (compatibility version 3.0.0, current version 3.0.0)
/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib (compatibility version 3.0.0, current version 3.0.0)
/opt/homebrew/opt/krb5/lib/libgssapi_krb5.2.2.dylib (compatibility version 2.0.0, current version 2.2.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.120.2)
ここでlibgssapi_krb5というライブラリが動的ライブラリのみ提供されており、静的リンクするには自分でビルドする必要があります。
一回ビルドしようとしたのですが、あまりうまくいかなかったので断念しました。
Web開発だけやっているとこういう依存関係とか動的ライブラリとか静的ライブラリとか、正直あんまり気にしたことがないのでいい勉強になりました。
本当にいろんなレイヤーで抽象化されていたり、必要なものがほとんどが用意されている環境で開発できている現代のエンジニアはだいぶSpoiledされてますね…
結局1の方法は諦めて、2の方法でSwift側でlibpqを動的にリンクすることにしました。
これはそんなに難しくなくやったことは以下です。
Build Settings>Library Search Pathsに/opt/homebrew/opt/libpq/libを追加Build Settings>Other Linker Flagsに-lpqを追加
これでSwift側でlibpqがリンクされるようになり、Zigのライブラリも問題なく動作しました。(ちょくちょくSandboxとか署名系でエラーが出ましたが、すぐに解決しました)
まとめ: SwiftとZigでMacOSのアプリを作る
基本的には上記のようなことをやればGhosttyでやっているようなMacアプリの作り方をできるはずです。
ただ依存するライブラリは気をつけた方がいいなと思いました。
基本的にはデスクトップアプリとして配布する以上、ほとんどの依存ライブラリは静的にリンクするのが望ましいと思ってます(たぶん。ここら辺は経験があまりないので他の方法があるかもまだわかってないです)
自分たちで完全にコントロールできる環境であれば自分たちでインストールすればいいので別にいいのですが、各ユーザーの環境に依存してしまう部分はなるべく小さくしたい。
アプリのインストーラーとかがそういう所の面倒をみてるんですかね?ここら辺の解像度があまり高くないので、今後もう少し調べてみたいと思います。
今回libpqを使っていたのはZig nativeなPostgreSQLのクライアントがあんまりなかったからなのですが、言語Nativeの実装というもののありがたみを感じました。
PostgreSQLのプロトコルもそこまで複雑ではなさそうなので、練習がてら実装してみるのも面白そう。
以上、SwiftからZigのライブラリを使う方法についてでした。