こんにちは!スタッフエンジニアの @kenkoooo です!Rust から Go で書かれたコードを呼び出す方法を紹介します。
日々ソフトウェアを開発するにあたって、我々開発者は多種多様なツールを利用しています。それらのツールはそれぞれ様々なプログラミング言語で作られていて、その中には Go 言語で書かれたものも多くあります。例えば、自分の AWS アカウントに紐づく権限情報を使って、ローカルからクラウド上で動いているリソースにアクセスすることができるツールとして session-manager-plugin があります。plugin の名の通り、単体で使うことは想定されておらず、AWS CLI で Systems Manager の機能を利用可能にするものです。AWS CLI は内部的にこのコマンドを呼び出して使っています。estie でも内部的に session-manager-plugin を呼び出している Rust 製の社内ツールが存在します。
上記のようなユースケースでは、社内ツールを使ってもらうにあたって、ユーザーに session-manager-plugin を事前にインストールしてもらう必要があり、ツール自体も含めて2つのバイナリのインストールをお願いしなければなりません。Homebrew などのパッケージマネージャを導入して依存関係を解決してもらうこともできますが、1つのバイナリに固めてしまうことはできないのでしょうか?
C 言語から Go 言語で書かれた関数を呼び出す
Go 言語には cgo というツールが存在し、C 言語から Go 言語で書かれた関数を呼び出せるようにできます。「Rust はどこいった?」と思われるかもしれませんが、いったん気にしないでください。
次のような Go のコードを考えます。
package main import "fmt" func Println(p string) { fmt.Println(p) }
ここに以下のように追記することで、Println という関数を C 言語から呼び出せるようになります。
package main import "C" import "fmt" //export Println func Println(p string) { fmt.Println(p) } func main() {}
このコードを main.go という名前で保存し、次のコマンドでビルドすると、C 言語から呼び出し可能な静的ライブラリを生成できます。
go build -buildmode=c-archive -o out/libppsys.a main.go
./out/libppsys.a
という静的ライブラリが生成されたことを確認できます。
Rust から C 言語で書かれた関数を呼び出す
先ほどビルドされた libppsys.a
は C 言語にすると以下のようなインターフェイスをもっています。
extern void Println(GoString p);
これを Rust から呼び出すコードは次のようになります。
use std::ffi::CString; use std::os::raw::c_char; extern "C" { fn Println(args: GoString); } #[repr(C)] struct GoString { a: *const c_char, b: i64, } pub fn println(s: &str) { let args = CString::new(s).expect("failed to convert args to CString"); unsafe { Println(GoString { a: args.as_ptr(), b: args.as_bytes().len() as i64, }); } }
このコードだけでは、cargo が libppsys.a をビルド時にリンクしてくれません。cargo に読み出すべき静的ライブラリと、それが ./out/
にあるということを伝えるために、次のような build.rs
を書きます。
use std::{env, path::Path, process::Command}; enum Target { Linux, Macos, } fn main() { // main.go や ./out に変化があった時、再度ビルドを走らせる println!("cargo::rerun-if-changed=main.go"); println!("cargo::rerun-if-changed=out"); // ビルドする環境を設定する let target = env::var("TARGET").expect("TARGET env var not found"); let target = match target.as_str() { "x86_64-unknown-linux-gnu" => Target::Linux, "aarch64-apple-darwin" => Target::Macos, target => panic!("Unsupported target: {}", target), }; // 環境に合わせてビルドするコマンドを組み立てる let mut cmd = Command::new("go"); cmd.arg("build") .arg("-buildmode=c-archive") .arg("-o") .arg("out/libppsys.a") .arg("main.go"); match target { Target::Linux => cmd.envs([("GOOS", "linux"), ("GOARCH", "amd64")]), Target::Macos => cmd.envs([("GOOS", "darwin"), ("GOARCH", "arm64")]), }; // コマンドを実行する cmd.status().expect("Failed to build"); // ビルドしたライブラリをリンクするための情報を出力する let dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env var not found"); println!( "cargo::rustc-link-search=native={}", Path::new(&dir).join("out").display() ); println!("cargo::rustc-link-lib=static=ppsys"); }
ついでに先ほどの go build コマンドも組み込みました。Apple シリコンの Mac と x86_64 Linux の両対応になっています。これで Rust から C で書かれたライブラリ(実態は Go で書かれている)の関数を呼び出すことができます。
まとめ
- Go 言語で書かれた関数を、C 言語から呼び出せるように C 言語のインターフェイスをつけた上で、静的ライブラリにビルドすることができます。
- C 言語で書かれた静的ライブラリを Rust から呼び出すことができます。
これらを2つを組み合わせることで、Rust から Go で書かれた関数を呼び出せるようになり、静的ライブラリなので最終的に1つのバイナリに固めることができます。
おわかりいただけただろうか…
誰も C 言語を使っていないのにプロトコルとして利用されていて、面白いですね。
最後に
estie では複数のプロダクトを同時並行で開発していますが、お客様にご期待いただいていることもあり、それぞれの開発がどんどん加速しています。技術的な課題も同時並行で湧き出していて、その内容も様々で、大変解きがいがあります。
ぜひ来て、力を貸していただければと思います。