Go のコードを Rust から呼び出す

近年のツールは Go で実装されていることが多く [要出典]、CLI だけでなくライブラリとして API も公開されていることも多くてそのツールを自作のツールに組み込むことも容易になっている。 しかしそれはあくまで自作のツールも Go で書いている場合で、言語の壁を超えることは難しい。

と思ってたんだけど、cgo を使えば Go でわりと手軽に C ライブラリを作ることができて、C ライブラリの呼び出しは色々な言語でサポートされているので、C のインターフェイスを経由すれば思ってたよりは手軽に他言語から Go のコードを呼び出せることに気付いた。

github.com

これは https://pkg.go.dev/cuelang.org/go/cue を使って cue export 相当を Rust からできるようにした例。 C の貧弱な表現に合わせたりメモリ管理の手間があったり Rust の文字列へのコピーのオーバーヘッドがあったりするけど、bindgen を使えば FFI の宣言を自動生成できるので、これくらいのコード量で Rust から Go のコードを呼び出せるようにできた。

やってることとしては、build.rs の中で go build -buildmode=c-archive を実行して .a と .h を生成し、.a を静的リンク対象に追加しつつ .h から bindgen で FFI の宣言を生成しているというかんじ。 Go の世界には C における const の概念が無いので、const char * を受け取るような関数を定義するときは typedef const char *const_string_t みたいな定義を preamble に書いておいて C.const_string_t を受け取る関数として定義して C の世界に export すればいい。 preamble とか export とかを特殊なコメントとしてやる設計もやはりあまり好きになれない……

ここまでで crate として動くものはできるんだけど、この crate を publish しようとすると docs.rs の環境でビルドできないという問題がある。docs.rs のビルド環境ではネットワークアクセスが禁止されていて、build.rs の中から Go module のダウンロードができないからだ。 これを回避するには go mod vendor で依存モジュールのコードも含めてパッケージングして publish するしかない、と思う。 あと地味な問題として go build がデフォルトでビルドに使うディレクトリが docs.rs の環境では書き込みを禁じられているので、if std::env::var("DOCS_RS").is_ok() { cmd.env("XDG_CACHE_HOME", "/tmp/.cache"); } みたいに XDG_CACHE_HOME を変えておく必要がある。