はじめに
Question. GoとRustどちらが優れている?
Answer. どちらも優れてる!
Indeed!
— Go (@golang) July 25, 2019
この記事ではそれぞれの特徴には言及しません。この記事ではGo側からRustのライブラリを呼ぶ方法と、逆にRust側からGoのライブラリを呼ぶ方法を紹介します。
FFI (バインディング)
あるプログラミング言語から別のプログラミング言語で定義された関数などを利用するための仕組みをFFI(Foreign Function Interface)またはバインディングと言います。
FFIはその言語のライブラリ・ツールとして実現されていることがほとんどです。Go言語の場合はcgoという実装、Pythonではctypesという実装がそれぞれの代表的なFFIということになります。
少なくともGoとRustに関してはそれぞれのFFIはC言語のオブジェクトとのみやり取りができ、その部分は処理系(コンパイラとリンカ)がABI(Application Binary Interface)での呼出規約に基づいて実施します。1
そのため、呼ばれる側の言語は呼ぶ側の言語のFFIが呼べるようにC言語オブジェクトな形にする必要があります。
GoでRustを呼ぶ
サンプルプログラムの内容
Rust側のライブラリは入力された文字列の後ろに(V)[0-0](V)
というカニの顔文字を追加した文字列を返す関数でGo側はその関数を呼びます。
構成
以下のようになっています。
.
├── Makefile
├── lib
│ ├── rustaceanize
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ └── rustaceanize.h
└── main.go
Rust側(ライブラリ)の実装
入力文字列へ文字追加して返す関数が以下のrustaceanize
関数です。
extern crate libc;
use std::ffi::{CStr, CString};
#[no_mangle] // no_mangle はRustコンパイラが関数名を変えたり削除しないように必要
pub extern "C" fn rustaceanize(name: *const libc::c_char) -> *const libc::c_char {
let cstr_name = unsafe { CStr::from_ptr(name) };
let mut str_name = cstr_name.to_str().unwrap().to_string();
println!("Rustaceanizing \"{}\"", str_name);
let r_string: &str = " (V)[0-0](V)";
str_name.push_str(r_string);
CString::new(str_name).unwrap().into_raw()
}
*const libc::c_char
型はC言語(GoもRustもC言語型を通したFFIである)が用意した生のcharポインタです。重要なのはこれがC言語側のポインタなのでRustのメモリ管理領域からは外れているという点です。それに対して、CStr::from_ptr
関数を通してstd::ffi::CStr
というRust側で定義したC言語文字列用型のポインタに変換させています。この処理はunsafe
スコープ内で処理される必要があります。その理由は公式ドキュメントから以下になります。
(筆者訳)
-
*const libc::c_char
の値が有効(validity)である保証がない。 - 返り値のライフタイムが実際のポインタのライフタイムである保証がない。
-
*const libc::c_char
ポインタへのメモリが有効なnul終端文字を含んでいる保証がない。 -
*const libc::c_char
ポインタへのメモリがCStr
が消える前に変更されることはない、という保証がない。
関数の最後でCString::new(str_name).unwrap().into_raw()
のようにしてC言語が扱えるように生のポインタに変換しています。
また、後述のGo言語側でのコンパイルにて関数シンボルが必要になるため以下のようなヘッダーファイルを用意します。
char* rustaceanize(char *name);
Go側のコード
上述のRustでできるバイナリとビルドするためにGo側では以下の実装になります。
package main
/*
#cgo LDFLAGS: -L./lib -lrustaceanize
#include <stdlib.h>
#include "./lib/rustaceanize.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
s := "I'm a Gopher"
input := C.CString(s) // Goの管理化のポインタではなくなる。
defer C.free(unsafe.Pointer(input)) // そのためメモリ解放を実装する必要がある。
// 以下の場合はinputのメモリはGoの管理化である。
// このときGo側でGCが働くのでこのプログラムではランタイムエラーが発生する!
// data := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
// input := (*C.char)(unsafe.Pointer(data))
o := C.rustaceanize(input)
output := C.GoString(o)
fmt.Printf("%s\n", output)
}
cgoがFFIとして働いています。LDFLAGS: -L./lib -lrustaceanize
はRustが作るバイナリをビルド時にリンク(go tool link)させるためのオプションです。
C.CString
とC.GoString
はそれぞれ文字列をCの構造、Goの構造に変換させるメソッドです。コメントとしても記載していますが、C.CString
で作られるポインタはGo言語の管理化ではないです。つまりGoのガベージコレクション対象外のポインタになります。そのため上記の例ではC言語のようにメモリ解放を実装しています。
Build
上述のRustとGoのコードをビルドするために、RustではCargo.tolm
に下記のようにcrate-type = ["cdylib"]
を指定することでよそ行きのバイナリを作成することができます。
[package]
name = "rustaceanize"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
libc = "0.2.2"
ビルドするためのMakefileが以下になります。2
ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
build:
cd lib/rustaceanize && cargo build --release
cp lib/rustaceanize/target/release/librustaceanize.dylib lib/
echo 'ROOT_DIR is $(ROOT_DIR)'
go build -ldflags="-r $(ROOT_DIR)lib" main.go
Rustが生成したバイナリに対してgo buildで-ldflags="-r $(ROOT_DIR)lib"
のオプションをつけることでGoビルドでのリンカでGoからRustへの呼び出しを紐付けることができます。
実行
実行すると期待どおりに動きます。
$ make build
$ ./main
Rustaceanizing "I'm a Gopher"
I'm a Gopher (V)[0-0](V)
RustでGoを呼ぶ
サンプルプログラムの内容
GoでRustを呼ぶのときと同じ内容です。こっちではGo側はゴーファー君顔文字 ʕ ◔ϖ◔ʔ
を追加して返す関数にします。
構成
構成は以下のようになっています。
.
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── build.rs
├── golib
│ └── main.go
└── src
└── main.rs
Go側(ライブラリ)の実装
入力文字列へ文字追加して返す関数が以下のGophernize
関数になります。
package main // ①
import "C" // ②
// ③
//export Gophernize
func Gophernize(name string) *C.char {
str := name + " ʕ ◔ϖ◔ʔ"
return C.CString(str)
}
func main() {} // ④
Goが他言語が利用できる関数があるバイナリを作成するためにソースコードでは以下を守る必要があります。3
- ①
main
package を利用すること。Goコンパイラはmain
packageをビルドしすべての依存モジュール含めてシングルバイナリとして生成される。 - ② コードでは必ず
"C"
をインポートする必要がある。 - ③ 他言語からアクセスさせるために対象の関数上に
//export
のコメントを注釈づけ(annotate)させる。 - ④ 空の
main
関数を宣言しておく必要がある。
Rust側のコード
Rust側のコードは以下のようになります。
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn Gophernize(name: GoString) -> *const c_char;
}
#[repr(C)]
struct GoString {
a: *const c_char,
b: i64,
}
fn main() {
let s = CString::new("I'm a Rustacean").expect("CString::new failed");
let ptr = s.as_ptr();
let input = GoString {
a: ptr,
b: s.as_bytes().len() as i64,
};
let result = unsafe { Gophernize(input) };
let c_str = unsafe { CStr::from_ptr(result) };
let output = c_str.to_str().expect("to_str failed");
println!("{}", output);
}
Go関数への引数となるGoString
型にある#[repr(C)]
はC言語のメモリレイアウトにさせるマクロです。CStr
とCString
はRust側でCメモリからRustで扱える形で処理するためのもので、C側に渡す場合は#[repr(C)]
がFFIになるようです。
Gophernize
はextern
な外部の関数なので"unsafe function"にあたります。なので呼び出しもunsafe
スコープで囲う必要があります。
CStr::from_ptr
はGoでRustを呼ぶときと同様な理由でunsafe
スコープで囲まれる必要があります。
Build
Go言語の場合はgo buildにLDFALGオプションを指定してビルド時にリンカへ指示を与えることができましたが、Rustの場合はbuild.rs
というファイルにてリンクさせるための情報を記載する必要があります。 4
今回の場合は以下のような内容です。
fn main() {
let path = "./golib";
let lib = "gophernize";
println!("cargo:rustc-link-search=native={}", path);
println!("cargo:rustc-link-lib=static={}", lib);
}
Makefile
の中身は以下のようになっています。
build:
cd golib && go build -buildmode=c-archive -o libgophernize.a main.go
cargo build
Go側にて-buildmode=c-archive
のオプションをつけることがポイントになります。これはRustの場合でのcrate-type = ["cdylib"]
と同様によそ行き用のバイナリにするために必要なオプションになります。
実行
$ make build
$ ./target/debug/call-go-from-rust
I'm a Rustacean ʕ ◔ϖ◔ʔ
おわりに
FFIの部分やリンカについて個人的に色々勉強になりました。これからもGoもRustも使っていこう。
参考
- Software Design 2020年12月号 "作品でみせるGoプログラミング"
- https://qiita.com/yugui/items/e71d3d0b3d654a110188
- https://blog.arranfrance.com/post/cgo-sqip-rust/
- https://github.com/vladimirvivien/go-cshared-examples
- https://github.com/mediremi/rust-plus-golang
- https://github.com/arranf/responsive-image-to-hugo-shortcode
- https://github.com/arranf/sqip-ffi
- https://www.altoros.com/blog/golang-internals-part-3-the-linker-object-files-and-relocations/
- https://stackoverflow.com/questions/47074919/how-can-i-call-a-rust-function-from-go-with-a-slice-as-a-parameter
- https://speakerdeck.com/filosottile/calling-rust-from-go-without-cgo-at-gothamgo-2017