LoginSignup
26
13

More than 3 years have passed since last update.

GoでRustを呼ぶ。そしてRustでGoを呼ぶ。

Last updated at Posted at 2020-12-16

はじめに

Question. GoとRustどちらが優れている?

Answer. どちらも優れてる!

この記事ではそれぞれの特徴には言及しません。この記事では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関数です。

lib/rustaceanize/src/lib.rs
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言語側でのコンパイルにて関数シンボルが必要になるため以下のようなヘッダーファイルを用意します。

lib/rustaceanize.h
char* rustaceanize(char *name);

Go側のコード

上述のRustでできるバイナリとビルドするためにGo側では以下の実装になります。

main.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.CStringC.GoStringはそれぞれ文字列をCの構造、Goの構造に変換させるメソッドです。コメントとしても記載していますが、C.CStringで作られるポインタはGo言語の管理化ではないです。つまりGoのガベージコレクション対象外のポインタになります。そのため上記の例ではC言語のようにメモリ解放を実装しています。

Build

上述のRustとGoのコードをビルドするために、RustではCargo.tolmに下記のようにcrate-type = ["cdylib"]を指定することでよそ行きのバイナリを作成することができます。

lib/rustaceanize/Cargo.toml
[package]
name = "rustaceanize"
version = "0.1.0"

[lib]
crate-type = ["cdylib"]

[dependencies]
libc = "0.2.2"

ビルドするためのMakefileが以下になります。2

Makefile
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関数になります。

golib/main.go
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側のコードは以下のようになります。

src/main.rs
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言語のメモリレイアウトにさせるマクロです。CStrCStringはRust側でCメモリからRustで扱える形で処理するためのもので、C側に渡す場合は#[repr(C)]がFFIになるようです。

Gophernizeexternな外部の関数なので"unsafe function"にあたります。なので呼び出しもunsafeスコープで囲う必要があります。

CStr::from_ptrはGoでRustを呼ぶときと同様な理由でunsafeスコープで囲まれる必要があります。

Build

Go言語の場合はgo buildにLDFALGオプションを指定してビルド時にリンカへ指示を与えることができましたが、Rustの場合はbuild.rsというファイルにてリンクさせるための情報を記載する必要があります。 4

今回の場合は以下のような内容です。

build.rs
fn main() {
    let path = "./golib";
    let lib = "gophernize";

    println!("cargo:rustc-link-search=native={}", path);
    println!("cargo:rustc-link-lib=static={}", lib);
}

Makefileの中身は以下のようになっています。

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も使っていこう。

参考


  1. この表現は誤りかもしれません。指摘いただければと思います。 

  2. 注意点として、Mac OSでは.dylibという拡張子ですがLinuxでは.soになります。 

  3. ここの内容を非常に参考にしました。 

  4. 公式ドキュメント 

26
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
13