この記事でわかること
Rustに移行したいけど、全部を移行することはできない場合、一部のC++コードを呼び出したいと考える方も多いでしょう。
特に、外部のライブラリを使用する場合は、Rustのライブラリが用意されていないことが多いため、C++コードを使用する必要があります。
この記事では、RustからC++コードをビルドして、呼び出す方法を説明します。
cxx: RustとC++をつなぐライブラリ
C++コードを呼び出すためのライブラリの1つに、「cxx」があります。
このライブラリは、RustとC++のインタフェースを簡単に扱うことができます。
詳細は下記で参照ください。
https://docs.rs/cxx/latest/cxx/
「cxx」でRustからC++を呼び出す方法
では、C++でClassを作り、それをRustから呼び出しています。
ここでは、もう少し簡素化して、単純にC++の関数を呼び出す方法を示したいと思います。
段階を踏んで、まずは単純に呼び出す方法、次に引数を渡す方法について説明します。
1. プロジェクトの作成
まずはプロジェクトを作成していきましょう。
ここで、「cxx_example」という名前でプロジェクトを作成します。
cargo new cxx_example
cd cxx_example
2. 設定ファイルに「cxx」を追加
まずは、Rustの設定ファイルであるtomlに今回利用する「cxx」を追加します。
// cxx_example/Cargo.toml
[package]
name = "cxx_example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
/***** 追加 *****/
cxx = "1.0"
/***************/
3. Rust側の実装
src
直下にmain.rs
というファイルがあるので、その中でC++の関数を呼び出す実装をしていきます。
引数を渡す方法は後述しますが、ここでは単純にC++から呼び出したい関数を記述します。
// cxx_example/src/main.rs
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("cxx_example/src/hello.h");
fn hello();
}
}
fn main() {
ffi::hello();
}
#[cxx::bridghe
とmod ffi
を宣言することで、この中の関数やモジュールをC++の関数と関連づけることができます。
#[cxx::bridge]
mod ffi {
...
}
RustからC++を呼ぶ関数やモジュールは
unsafe extern "C++" {
}
の中に記述します。
今回は、hello.h
とhello.cpp
を作成して、その中でhello()
という関数を作ります。
なので、includeして、
nclude!("cxx_example/src/hello.h");
Rustから呼ぶ関数
fn hello();
を記述します。
4. C++側の実装
まずは、cxx_example/src
直下にhello.h
とhello.cpp
を新規に作成します。
// cxx_example/src/hello.h
#pragma once
#include "rust/cxx.h"
void hello();
// cxx_example/src/hello.cpp
#include "hello.h"
#include
void hello() { std::cout << "Hello" << std::endl; }
基本的には、通常のC++の実装と同じで、特殊な点としては、ヘッダーに
include "rust/cxx.h"
を追加しておく必要がある点です。
5. ビルド設定
C++をRustで利用するには、
C++ビルド → Rustビルド
の順でビルドを行う必要があります。(C++のソースは一度ライブラリにして、Rustに読み込まれるため)
そのために、cxx用のビルド設定を行う必要があります。
まずは、cxx用のビルドツールの追加。
// cxx_example/Cargo.toml
[package]
name = "cxx_example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cxx = "1.0"
/***** 追加 *****/
[build-dependencies]
cxx-build = "1.0"
/***************/
続いて、ビルドの設定を、
cxx_example/build.rs
を作成して行います。
build.rs
は、通常のRustのビルドに追加で処理を行いたい場合に記述します。
例えば、今回のようにC++のビルドを事前に行っておきたい時など。
今回は下記のように記述しています。
// cxx_example/build.rs
fn main() {
cxx_build::bridge("src/main.rs")
.file("src/hello.cpp")
.flag_if_supported("-std=c++20")
.compile("cxx-example");
println!("cargo:rerun-if-changed=/src/*");
println!("cargo:rerun-if-changed=/build.rs");
}
まずは、C++を呼び出すRust側のファイルを登録。
cxx_build::bridge("src/main.rs")
次に、C++側のコードを登録。
.file("src/hello.cpp")
環境にもよりますが、僕の環境では、C++11以降でないと、ビルドが通らなかったので、
.flag_if_supported("-std=c++20")
を追加。
最後に、ビルドで生成されるライブラリ名(.a
や.lib
などは省略)を決めます。
.compile("cxx-example");
実行ファイルまで生成されるので、実際にはこのライブラリを触ることはほとんどないと思います。
println!("cargo:rerun-if-changed=〇〇");
というのも記述されていますが、こちらは、〇〇が変わったら再度ビルドをしますという宣言になります。
これを忘れて、ビルド結果が変わらないという沼にハマってしまうので、気をつけましょう。
6. 実行
cargo run
// output: Hello
cargo run
を実行して、「Hello」と表示されれば成功です。
また、cargo run
、または、cargo build
で、実行ファイルが下記に生成されます。
cxx_example/target/debug/cxx_example(.exe)
cargo build
はビルドまで、cargo run
はさらに実行してくれます
引数/戻り値の追加して呼び出す方法
7. Rust側の実装
RustからC++に文字列を渡して、C++でそれに文字列を追加してRustに返却し、それを表示するということを行います。
Rust側のコードの変更はこちら。
// cxx_example/src/main.rs
/***** 追加 *****/
use cxx::let_cxx_string;
/***************/
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("cxx_example/src/hello.h");
/***** 変更 *****/
fn hello(name: &CxxString) -> &CxxString;
/***************/
}
}
fn main() {
/***** 変更 *****/
let_cxx_string!(name = "Taro");
let message = ffi::hello(&name);
println!("{}", message);
/***************/
}
まずは、C++の関数hello()
に引数と戻り値を追加します。
fn hello(name: &CxxString) -> &CxxString;
ちなみに、RustのCxxString
が、C++ではstd::string
として扱われます。
詳しくはこちら:RustとC++の文字列型の関係
続いて、CxxStringの変数を定義していきます。
こちらのモジュールはマクロで初期化する必要があるので、下記を追記します。
use cxx::let_cxx_string;
let_cxx_string!(name = "Taro");
name
が変数になります。
この変数を、helloに入れ、messageを受け取り表示します。
let message = ffi::hello(&name);
8. C++側の実装
まずは、ヘッダーをRustに合わせて修正。
// cxx_example/src/hello.h
#pragma once
#include "rust/cxx.h"
/***** 変更 *****/
const std::string& hello(const std::string& name);
/***************/
CxxStringはstd::stringと対応するので、そのまま置換し、&
もつけます。
(ちなみに、参照でないと受け渡しはできないようです。)
Rustはmut
をつけなければ、デフォルトで不変値となるので、C++側にはconst
をつけてあげる必要があります。
続いて、C++ソースファイルの修正。
// cxx_example/src/hello.cpp
#include "hello.h"
#include
/***** 変更 *****/
const std::string& hello(const std::string& name) {
static std::string message = "Hello " + name + "!";
return message;
}
/***************/
cxxの縛りから参照で返す必要があるため、C++側で静的なメモリの確保をしてあげる必要があります。(or new
でのメモリ確保)
なので、message
にはstatic
をつけて、hello()
関数を抜けても、メモリが解放されないようにしています。
9. 実行
cargo run
// output: Hello Taro!
cargo run
を実行して、「Hello Taro!」と表示されれば成功です。
終わりに
今回は、RustからC++を呼び出す方法を説明しました。
他にも呼び出す手段はありますので、興味がある方は調べてみてください。
また、この記事でも使っているサンプルコードをGitHubで公開していますので、ご参照ください。
https://github.com/ishikawa-takumi/cxx_example/tree/rust_to_cpp