Zenn
Open10

CXXを利用してRustからC++実装を呼び出す

shin-ueshin-ue

CXXを使ってRustからC++実装関数を呼び出すようにするまでのメモ

shin-ueshin-ue

以下のような構成とする。

rust_cxx
├─source
│  └─example.h/cpp
├─src
│  ├─cxx.rs    # RustとC++間のブリッジ用
│  └─main.rs
├─build.rs     # ビルドスクリプト (C++のビルド設定を記述)
├─Cargo.toml
shin-ueshin-ue

まずは雑にC++実装する。
グローバルなadd関数。

source/example.h
#pragma once

int add(int a, int b)
source/example.cpp
#include "example.h"

int add(int a, int b) {
    return a + b;
}
shin-ueshin-ue

Cargo.tomlにCXXを追加。
さらにビルド時の依存関係にcxx-buildを追加(build.rsでC++のコンパイルとリンクのために使う)。

Cargo.toml
[package]
name = "rust_cxx"
version = "0.1.0"
edition = "2021"

[dependencies]
cxx = "1.0.136"

[build-dependencies]
cxx-build = "1.0.136"

ビルドスクリプトは以下のように書く。

build.rs
fn main() {
    cxx_build::bridge("src/cxx.rs") // ブリッジを記述しているファイルを指定
        .file("source/example.cpp") // 対象のC++ソースファイル
        .include("source")          // ヘッダーファイルのインクルードパス
        .flag_if_supported("-std=c++14") // とりまC++14を使う
        .compile("rust_cxx");       // 出力されるライブラリ名

    println!("cargo:rerun-if-changed=source/example.h");
    println!("cargo:rerun-if-changed=source/example.cpp");
    println!("cargo:rerun-if-changed=src/cxx.rs");
}

cargo:rerun-if-changed=PATHは、対象ファイルに変更があったときにbuild.rsのスクリプトが実行されるやつ。

shin-ueshin-ue

ブリッジ記述する。
ドキュメントを参考。

src/cxx.rs
#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("example.h");

        fn add(a: i32, b: i32) -> i32;
    }
}

pub use ffi::*;

ちなみにRust⇔C++におけるプリミティブな型はもちろん、以下にあるような共通型が提供されている。後で使う。
https://cxx.rs/bindings.html

C++のadd関数を呼び出す実装。

src/main.rs
mod cxx;

fn main() {
    let result = cxx::add(1, 2);
    println!("Result: {}", result);
}
$ cargo run
Result: 3

ヨシッ

shin-ueshin-ue

次は画像データの輝度値を反転(255 - pixelValue)する処理を実装してみよう。

source/reverse.h
#pragma once
#include "rust/cxx.h"

rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data);

CXXが提供するRust⇔C++の共通型を利用するため、rust/cxx.hをインクルードした。
rust/cxx.hはcargo buildするとtarget/cxxbridgeフォルダ内に生成される。

source/reverse.cpp
#include "reverse.h"

rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data) {
    rust::Vec<uint8_t> output;
    output.reserve(img_data.size());

    for (const auto& pixel : img_data) {
        output.push_back(255 - pixel);
    }

    return output;
}

愚直にforループで各ピクセルの輝度を反転する🙄

shin-ueshin-ue

build.rsにreveseファイル追加。(exampleはとりあえず残しておく)

build.rs
fn main() {
    cxx_build::bridge("src/cxx.rs")
        .file("source/example.cpp")
+       .file("source/reverse.cpp")
        .include("source")
        .flag_if_supported("-std=c++14")
        .compile("rust_cxx");

    println!("cargo:rerun-if-changed=source/example.h");
    println!("cargo:rerun-if-changed=source/example.cpp");
+   println!("cargo:rerun-if-changed=source/reverse.h");
+   println!("cargo:rerun-if-changed=source/reverse.cpp");
    println!("cargo:rerun-if-changed=src/cxx.rs");
}

Rust側で画像データ読み込もうと思うので、imageクレートを追加しておく。

Cargo.toml
[package]
name = "rust_cxx"
version = "0.1.0"
edition = "2021"

[dependencies]
cxx = "1.0.136"
+ image = "0.25.5"

[build-dependencies]
cxx-build = "1.0.136"
shin-ueshin-ue

ブリッジにreverseのtransform関数を追加。

src/cxx.rs
#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("example.h");
+       include!("reverse.h");

        fn add(a: i32, b: i32) -> i32;
+       fn transform(img_data: &[u8]) -> Vec<u8>;
    }
}

pub use ffi::*;

画像を読み込み(グレースケールで)、reveseして、反転画像を書き込む処理を実装。

src/main.rs
mod cxx;

fn main() {
    let result = cxx::add(1, 2);
    println!("Result: {}", result);

+   let img = image::open("kuma.png").unwrap().into_luma8();
+   let (width, height) = img.dimensions();
+   let img_data = img.into_raw();
+
+   let reversed = cxx::transform(&img_data);
+   let img_reversed = image::GrayImage::from_raw(width, height, reversed).unwrap();
+   img_reversed.save("kuma_reversed.png").unwrap();
}
shin-ueshin-ue

Result型で結果を受け取ることもできる。
C++側でthrowされた例外は、Rust側で自動的にResultとして扱われる。

source/reverse.h
#pragma once
+ #include <stdexcept>
#include "rust/cxx.h"

rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data);
source/reverse.cpp
#include "reverse.h"

rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data) {
+   if (img_data.empty()) {
+       throw std::runtime_error("Empty image data");
+   }

    rust::Vec<uint8_t> output;
    output.reserve(img_data.size());

    for (const auto& pixel : img_data) {
        output.push_back(255 - pixel);
    }

    return output;
}

src/cxx.rs
#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("example.h");
        include!("reverse.h");

        fn add(a: i32, b: i32) -> i32;
-       fn transform(img_data: &[u8]) -> Vec<u8>;
+       fn transform(img_data: &[u8]) -> Result<Vec<u8>>;
    }
}

pub use ffi::*;
fn main() {
    // 省略

    match cxx::transform(&img_data) {
        Ok(reversed) => {
            let img_reversed = image::GrayImage::from_raw(width, height, reversed).unwrap();
            img_reversed.save("kuma_reversed.png").unwrap();
        }
        Err(e) => println!("Error: {}", e),
    }

    // 空のデータを渡せば `Error: Empty image data` とコンソールに表示される
    let empty_data: Vec<u8> = vec![];
    match cxx::transform(&empty_data) {
        Ok(_) => println!("Hoge"),
        Err(e) => println!("Error: {}", e),
    }
}

とりあえず今日はここまで

ログインするとコメントできます