Rust programming language

C/C++ Rust Tech

【Rust】RustからC++ を呼び出す方法

この記事でわかること

Rustに移行したいけど、全部を移行することはできない場合、一部のC++コードを呼び出したいと考える方も多いでしょう。

特に、外部のライブラリを使用する場合は、Rustのライブラリが用意されていないことが多いため、C++コードを使用する必要があります。

この記事では、RustからC++コードをビルドして、呼び出す方法を説明します。

cxx: RustとC++をつなぐライブラリ

C++コードを呼び出すためのライブラリの1つに、「cxx」があります。

このライブラリは、RustとC++のインタフェースを簡単に扱うことができます。

詳細は下記で参照ください。

https://docs.rs/cxx/latest/cxx/

https://cxx.rs

「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::bridghemod ffiを宣言することで、この中の関数やモジュールをC++の関数と関連づけることができます。


#[cxx::bridge]
mod ffi {
...
}

RustからC++を呼ぶ関数やモジュールは

unsafe extern "C++" {}

の中に記述します。

今回は、hello.hhello.cppを作成して、その中でhello()という関数を作ります。

なので、includeして、

nclude!("cxx_example/src/hello.h");

Rustから呼ぶ関数

fn hello();

を記述します。

4. C++側の実装

まずは、cxx_example/src直下にhello.hhello.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

-C/C++, Rust, Tech
-, , ,