この記事は Rustその3 Advent Calendarの20日目の記事です。
Rust を少しずつ触り始めたのですが、C++ のライブラリとリンクする必要が出てきてしまったので(その理由はまた別の機会で)、自分なりにまとめてみたことを書いておきます。
今回の環境はこんなところです。いつも夜ばかりですが、久々に stable にしてみました。
- macOS 10.14.6 (18G2022)
- rustc 1.39.0 (4560ea788 2019-11-04)
- cargo 1.39.0 (1c6ec66d5 2019-09-30)
Rust で C++ のクラスを扱うためには
Rust で C++ のクラスを直接扱うことはできないようですので、適当にラップして、ラッパー経由で扱います。ラップのしかたとしては、
のどちらかでしょう。そこで、軽く実装して比べてみました。また、ラッパーを生成するツール bindgen の使い勝手もみてみましょう。
C++ のサンプル
簡単なプログラムを用意してみました。(C++ のプログラムを書くのはいつ以来だろう... 完璧に忘れているので、密林でストローストラップ本を買ってしまいました。えっ、辞書並の厚さ...)
class Dog {
public:
enum status {
STATUS_WALKING,
STATUS_STOP,
STATUS_EATING
};
private:
const char *_name;
enum status _status;
public:
Dog(const char *name);
~Dog();
void walk();
void stop();
enum status status() const { return _status; }
};
#include "dog.hpp"
#include <cstdio>
Dog::Dog(const char *name) :
_name (name), _status (STATUS_STOP)
{
std::printf("%s: おはよう、わん!\n", _name);
}
Dog::~Dog()
{
std::printf("%s: おやすみ、ゎん.\n", _name);
}
void Dog::walk()
{
std::printf("%s: わんわん!\n", _name);
if (_status != STATUS_WALKING) _status = STATUS_WALKING;
}
void Dog::stop()
{
std::printf("%s: わん.\n", _name);
if (_status != STATUS_STOP) _status = STATUS_STOP;
}
方法1: C++ 側でラップする
こちらの方法は 秋津早苗さんの記事1を参考にさせていただきました。
C++ 側では class を struct でラップして、各メソッドを呼び出す関数を実装します。必要に応じてアクセサ関数も実装します。各関数は extern "C" とすることで、mangle されないようにしておきます。
#include "dog.hpp"
extern "C" {
typedef struct {
Dog impl;
} DogImpl;
DogImpl* Dog_Dog(const char* name) {
Dog *dog = new Dog(name);
return (DogImpl*)dog;
}
void Dog_Dog_destructor(DogImpl* di) {
di->impl.~Dog();
}
void Dog_walk(DogImpl* di) {
di->impl.walk();
}
void Dog_stop(DogImpl* di) {
di->impl.stop();
}
int Dog_status(DogImpl* di) {
return di->impl.status();
}
}
Rust 側で Rust らしい抽象化をちょっとしてみる
C++ の関数を直接呼び出しても良いのですが、ちょっとだけ Rustっぽくしてみます。
C++ のオブジェクトの中身は直接触りたくないので、バリアントなしの enum 型で表現し、触りたくても触れないようにします。オブジェクトがスコープから外れたら自動的にドロップされるように Drop トレイトを実装して、そこから C++ のデストラクタを呼び出すのが良さそうです。C++ の列挙体のうち "単なる enum" は、根底型が明示されていない場合、全ての列挙子が int や unsigned int で表現できる限り、sizeof(int) を超えない汎整数型で表現されます2(@SaitoAtsushiさん、コメントをありがとうございました)。また、Rust の enum 型はどんなバリアントがあるかによって整数型のサイズが異なり得ます。そこで、sizeof(enum Dog::status) を C++ コンパイラを通して調べたところ 4 でしたので、Rust では u32 型としています。Rust 上で #repr[(C)] を指定した enum 型を使うことも考えたのですが、C++ の "単なる enum" ではキャストすることで列挙子以外の値を代入するという技が使えるので、「混ぜるな危険」ということでやめました。各メソッドは少しでもオーバーヘッドを減らす意味で、#[inline] を指定します。
Rust 型のプログラムは以下のようになりました。
use std::os::raw::c_char;
pub enum DogImpl {}
extern {
pub fn Dog_Dog(name: *const c_char) -> *mut DogImpl;
pub fn Dog_Dog_destructor(dog: *mut DogImpl);
pub fn Dog_walk(dog: *mut DogImpl);
pub fn Dog_stop(dog: *mut DogImpl);
pub fn Dog_status(dog: *mut DogImpl) -> u32;
}
pub struct Dog {
raw: *mut DogImpl
}
impl Dog {
pub const STATUS_WALKING: u32 = 0;
pub const STATUS_STOP: u32 = 1;
pub const STATUS_EATING: u32 = 2;
#[inline]
pub fn new(name: *const c_char) -> Self {
unsafe { Dog { raw: Dog_Dog(name) } }
}
#[inline]
pub fn walk(&mut self) {
unsafe { Dog_walk(self.raw) }
}
#[inline]
pub fn stop(&mut self) {
unsafe { Dog_stop(self.raw) }
}
#[inline]
pub fn status(&mut self) -> u32 {
unsafe { Dog_status(self.raw) }
}
}
impl Drop for Dog {
#[inline]
fn drop(&mut self) {
unsafe { Dog_Dog_destructor(self.raw) }
}
}
ビルドまわりを指定する
C++ プログラムのコンパイルとリンクをするために、build.rs を書きます。
extern crate cc;
fn main() {
println!("cargo:rustc-link-lib=c++");
cc::Build::new()
.file("src/dog.cpp")
.file("src/wrapper.cpp")
.include("src")
.compile("dog");
}
[package]
name = "dog2"
version = "0.1.0"
authors = ["Satoshi Moriai <satoshi.moriai@gmail.com>"]
edition = "2018"
build = "build.rs"
[build-dependencies]
cc = "1.0"
[dependencies]
方法2: Rust 側でラップする
こちらの方法は@mod_poppoさんの記事3を参考にさせていただきました。
C++ をコンパイルすると extern "C" 宣言がない識別子は mangle されてしまいますので、Rust から呼び出すためには、どのように mangle されているかを Rust に教えてやる必要があります。ここでは、C++ をコンパイルしてできたオブジェクトファイルから nm でシンボル情報を取り出し、それを demangle した結果をもとにラッパーを書いていきます。
$ clang++ -c src/dog.cpp -o dog.o
$ nm dog.o | cut -c20- > dog.sym
$ c++filt @dog.sym | paste dog.sym -
__ZN3Dog4stopEv Dog::stop()
__ZN3Dog4walkEv Dog::walk()
__ZN3DogC1EPKc Dog::Dog(char const*)
__ZN3DogC2EPKc Dog::Dog(char const*)
__ZN3DogD1Ev Dog::~Dog()
__ZN3DogD2Ev Dog::~Dog()
...
コンストラクタとデストラクタの mangle 名については GCC のソースコード のコメントに下記の記述を見つけました(GitHub と Google 先生に感謝です)。今回はクラスの継承がないので、C1 と D1 を使えば良さそうです。
<special-name> ::= C1 # complete object constructor
::= C2 # base object constructor
::= C3 # complete object allocating constructor
Currently, allocating constructors are never used.
<special-name> ::= D0 # deleting (in-charge) destructor
::= D1 # complete object (in-charge) destructor
::= D2 # base object (not-in-charge) destructor
では、書いていきましょう。
use std::os::raw::c_char;
use std::mem::MaybeUninit;
#[repr(C)]
#[derive(Debug)]
pub struct Dog {
_name: *const c_char,
_status: u32
}
extern "C" {
#[link_name = "\u{1}__ZN3DogC1EPKc"]
pub fn Dog_Dog(this: *mut Dog, name: *const c_char);
#[link_name = "\u{1}__ZN3DogD1Ev"]
pub fn Dog_Dog_destructor(this: *mut Dog);
#[link_name = "\u{1}__ZN3Dog4walkEv"]
pub fn Dog_walk(this: *mut Dog);
#[link_name = "\u{1}__ZN3Dog4stopEv"]
pub fn Dog_stop(this: *mut Dog);
}
impl Dog {
pub const STATUS_WALKING: u32 = 0;
pub const STATUS_STOP: u32 = 1;
pub const STATUS_EATING: u32 = 2;
#[inline]
pub fn new(name: *const c_char) -> Self {
unsafe {
let mut dog = MaybeUninit::<Dog>::uninit();
Dog_Dog(dog.as_mut_ptr(), name);
dog.assume_init()
}
}
#[inline]
pub fn walk(&mut self) {
unsafe { Dog_walk(self) }
}
#[inline]
pub fn stop(&mut self) {
unsafe { Dog_stop(self) }
}
#[inline]
pub fn status(&mut self) -> u32 {
self._status
}
}
impl Drop for Dog {
#[inline]
fn drop(&mut self) {
unsafe { Dog_Dog_destructor(self) }
}
}
#[link_name = ...] によってリンクすべきシンボル名を指定します。シンボル名の頭に \u{1} というおまじないをつけると、LLVM が mangling や _ を頭につけたりなどのシンボル名の変換処理をしません。C++ のクラスは Rust の #[repr(C)] 付きの struct で表現します。enum については、方法1と同じです。コンストラクタの本体は C++ 側にありますが、Rust 側で未初期化領域を用意してから、C++ のコンストラクタを呼び出します(で、大丈夫かな?)。未初期化領域を扱うので、最近 stable でも使えるようになった std::mem::MaybeUninit を使います。他は方法1とだいたい同じです。
build.rs は次の通りです。
extern crate cc;
fn main() {
//println!("cargo:rustc-link-lib=c++");
cc::Build::new()
.file("src/dog.cpp")
.include("src")
.compile("dog");
}
Cargo.toml は方法1と同じです。C++ や C++ コンパイラのバージョンによっては、C++ ランタイムライブラリは不要となることもあるようです。リンク時にエラーになる場合には、println! のコメントアウトを外しましょう。
比較
以下のようなところでしょうか。
C++ でラップする | Rust でラップする | |
---|---|---|
オーバーヘッド | 少しあり | なし |
クラスメンバへのアクセス | 不可 | 可能 |
書きやすさ | 楽 | 少々めんどう |
メンテナンス性 | 良 | 良くない |
メンテナンス性については、C++ コードの変更への追従が楽かどうかで判断しましたが、C++ のヘッダファイルから、ラッパーを自動生成できるのであれば、あまり問題にならないでしょう。
bindgen を使ってみる
ということで、C/C++ のヘッダーファイルからラッパーを自動生成する bindgen というプログラムがありますので、使ってみることにします。こちらの方法は bindgen のドキュメント4を参考にしながら進めます。
インストール
macOS では次のような感じです。
$ brew install llvm
$ cargo install bindgen
ビルドまわりを指定する
「Rust でラップする」版の Cargo.toml に以下を加えます。
[build-dependencies]
bindgen = "0.52"
build.rs は次のようになります。
extern crate cc;
extern crate bindgen;
use std::env;
use std::path::PathBuf;
fn main() {
//println!("cargo:rustc-link-lib=c++");
println!("cargo:rerun-if-changed=src/dog.hpp");
cc::Build::new()
.file("src/dog.cpp")
//.flag("-std=c++17")
.include("src")
.compile("dog");
let bindings = bindgen::Builder::default()
.header("src/dog.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("dog.rs"))
.expect("Couldn't write bindings!");
}
前半で src/dog.cpp のコンパイルを行い、後半で src/dog.hpp を読み込んで、${OUT_DIR}/dog.rs に書き出します。
次に、src/main.rs の先頭に以下を追加します。
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/dog.rs"));
あとは、main.rs で変数名などを bindgen が吐くスタイルに修正して、ビルドしましょう。
bindgen が生成したコードをみてみると、方法2で述べた Rust でラップしたコードを生成していて、そのまま十分に使えるコードになっています。ただ、non_upper_case_globals、non_camel_case_types、non_snake_case を allow しないといけない識別子が生成されるので、ちょっと「うーん」という感じです。あくまでも個人の感想です。
おわりに
bindgen を色々な C/C++ のヘッダーファイルで試してみましたが、/usr/include や /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk の下にあるような複雑なヘッダーファイルでは Rust コード生成に失敗することがあります。必要な定義だけを抜き出したものを bindgen に食わせるなど、ちょっと工夫すれば、おおいに役立つと思います。
ただ、個人的には、C++ でラップしちゃった方がいいかな、せっかく本も買ったし、と思っています。
また、方法1と2の中間的なやり方もありますね。
という訳で、今回は以上です。
最後までお読みいただき、ありがとうございました。
-
秋津早苗さん: C++のライブラリをC言語でくるんでRustから使うまで. https://akitsu-sanae.hatenablog.com/entry/2016/12/21/010321 ↩
-
Bjarne Stroustrup: The C++ Programming Language, 4th ed., 2013. ↩
-
@mod_poppoさん: Rustから直接C++を叩いてみる. https://blog.miz-ar.info/2015/01/rust-cxx ↩
-
The "bindgen" User Guide. https://rust-lang.github.io/rust-bindgen ↩