アダコテック技術ブログ

アダコテックのエンジニアが発信する技術ブログです

CXXを利用してC++とRustをつなげよう

こんにちは!社内で最もタコスを食べていると噂のエンジニアの井上です。

前回は、Rust+WebAssemblyによるWebアプリでの高速画像処理を紹介しました。 今回は、RustからC++関数を呼び出すCXXについて紹介します!

要約

  • C++で実装された自社ML(機械学習)ライブラリのリアーキテクチャに伴い、RustへのリプレイスおよびC++コード資産をCXXを利用して呼び出すようにしたよ。
  • RustからC++の関数を安全に呼び出す方法を、加算処理や画像輝度反転処理といった例を通して紹介するよ。
  • CXXを活用することで、既存資産を活かしながら段階的にRustに移植できるよ。

背景

  • 自社MLライブラリは、コアロジックとインターフェースが密結合であることから再利用性や機能拡張性が低いという課題が存在していました。
  • MLライブラリのアーキテクチャ改善と処理高速化機能を導入するにあたり、直近アダコテック内で勢いのあるRust言語へのリプレイスプロジェクトが2024年末に発足しました。
    • 社内ではRust経験者 > C++経験者な状況でもあるため、Rust化によってより多くのメンバーがメンテナンス可能になり、属人性の低減にもつながりました。
  • ただし、既存のC++実装では一部機能にSIMD関数を利用しており、この部分のRust化は厳しい(2025年1月時点でRustのSIMD APIサポートは完全ではない)と判断し、該当箇所のみ既存のC++コードを活かしつつRustから呼び出す形で移行を進めることにしました。
    • そのために採用したクレートが「CXX」です。

CXXとは?

  • RustとC++間で安全にバインディングを行うためのFFI(Foreign Function Interface)クレートです。以下のような特徴があります。
    • Rustから、C++の関数を安全に呼び出せる
    • C++から、Rustの関数を呼び出すことも可能
    • Rust ↔ C++で、プリミティブな型はもちろん、文字列(std::string/CxxString)、スライス、VecUniquePtr などの標準的な型も扱うことができます。

  • Rust ↔ C++を橋渡しするために、#[cxx::bridge]マクロを利用します。
    • このマクロによって、Rust側でバインディングコードが自動生成され、C++関数をあたかもRust関数のように安全かつ自然に呼び出すことができます。
    • 同様にC++側にもバインディングコードが生成されるため、Rustで定義した関数や型をC++からも違和感なく利用できます。
  • この仕組みにより、今回のようにSIMDなどの既存C++資産を活かしたいケースでも、Rust実装との共存が可能になります。

環境構築

  • 任意ディレクトリでcargo initまたはcargo newをして、Rustプロジェクトを作成します。
  • 今回は以下のような構造とします。C++コードはsource/にまとめました。
rust_project
├─source
│  └─example.h/cpp  # C++コード
├─src
│  ├─bridge.rs      # RustとC++間のブリッジ用
│  └─main.rs
├─build.rs          # ビルドスクリプト (C++のビルド設定を記述)
├─Cargo.toml
  • cargo add cxxでCXXを追加します。
  • Rustビルド用スクリプトbuild.rsにC++コンパイルとリンクを記述するため、cxx-buildをbuild-dependenciesに追加します。
[package]
name = "cxx_example"
version = "0.1.0"
edition = "2024"

[dependencies]
cxx = "1.0.161"

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

動作確認

C++コード

  • source/example.hおよびexample.cppに「2つの値の加算処理」を実装してみましょう。
// 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;
}

CXXブリッジ

  • RustとC++実装を繋げる役割を持たせるsrc/bridge.rsに#[cxx::bridge]マクロを利用してブリッジ記述します。
  • 今回のように#[cxx::bridge]を別ファイルに分離しておくことで、FFI関連の定義とアプリケーションロジックを分離できます。大規模になるとbridgeが肥大化するため、このような構造を取っておくと保守性を高めることができます。
// src/bridge.rs
#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("example.h");          // sourceフォルダにあるヘッダファイル

        fn add(a: i32, b: i32) -> i32;  // 呼び出すC++関数
    }
}

pub use ffi::*;

ビルドスクリプト

  • Rustビルド時のスクリプトコードをbuild.rsに実装します。
// build.rs
fn main() {
    cxx_build::bridge("src/bridge.rs")    // Rust <-> C++ バインディングの定義ファイル
        .file("source/example.cpp")       // C++関数の実装ファイル
        .include("source")                // ヘッダーファイル格納ディレクトリ
        .std("c++20")                     // コンパイルオプション(C++20準拠ビルド)
        .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/bridge.rs");
}
  • 今回の例では c++20 を指定していますが、flag_if_supported を使うことで以下のような任意コンパイルオプションを適用できます。

    オプション 内容
    .flag_if_supported("-O2") 最適化レベル2でビルド
    .flag_if_supported("-march=native") 使用しているCPUアーキテクチャに最適化
    .flag_if_supported("-mavx2") AVX2命令セットを使用したSIMD命令を有効化

RustからC++関数を呼ぶ

  • それではRust側からC++関数を呼んでみましょう。
// src/main.rs
mod bridge;

fn main() {
    let result = bridge::add(1, 2);
    println!("Result: {}", result);
}
  • cargo runするとRustおよびC++のコードがコンパイルされ、加算処理が実行されます!
$ cargo run
Result: 3

画像処理をしてみる

  • 我々アダコテックは画像データを主に扱うので、画像データを利用した例(輝度反転)も示しましょう。
  • sourceフォルダに以下を追加します。
// 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をインクルードしています。
// source/reverse.cpp
#include "reverse.h"

// 各ピクセル値 x に対して 255 - x を適用し、輝度を反転
rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data) {
    rust::Vec<uint8_t> output;
    output.reserve(img_data.size());

      for (size_t i = 0; i < img_data.size(); ++i) {
            auto pixel = img_data[i];
            output.push_back(255 - pixel);
      }

    return output;
}
  • Vec<u8>相当のデータをC++で受け取るためにrust::Slice<const uint8_t>を使用し、C++からRustへ返すデータにはrust::Vec<uint8_t>を使います。

  • 次に、ブリッジにreverseのtransform関数を追加します。
// src/bridge.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>;
   }
}
  • build.rsにreverse.hおよびreverse.cppを追加します。
// build.rs
fn main() {
    cxx_build::bridge("src/bridge.rs")
        .file("source/example.cpp")
        .file("source/reverse.cpp")       // 新たに追加した実装ファイル
        .include("source")
        .std("c++20")
        .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/bridge.rs");
}
  • Rust側で画像ファイルを読み込むので、imageクレートを追加します。
[package]
name = "cxx_example"
version = "0.1.0"
edition = "2024"

[dependencies]
cxx = "1.0.161"
image = "0.25.6"

[build-dependencies]
cxx-build = "1.0.161"
  • 最後にmain.rsで、画像をグレースケールで読み込み、輝度反転関数を呼んでみましょう。
// src/main.rs
mod bridge;

use image::{GrayImage, ImageError};

// 画像を読み込みグレースケール化
fn load_grayscale_image(path: &str) -> Result<GrayImage, ImageError> {
    let img = image::open(path)?;
    Ok(img.into_luma8())
}

fn main() -> Result<(), ImageError> {
    let img = load_grayscale_image("kuma.png")?;
    let (width, height) = img.dimensions();
    let img_data = img.into_raw();

        // 輝度反転処理を実行
    let reversed = bridge::transform(&img_data);
    let img_reversed = GrayImage::from_raw(width, height, reversed).expect("Failed");

    img_reversed.save("kuma_reversed.png")?;

    Ok(())
}
  • cargo runで、以下のように反転された画像が出力されました!

入力画像
入力画像

C++の例外をRustのResultで受け取る

  • C++のthrowによる例外処理を、Rust側で自動的にResultとして扱うことができます。
  • 例えば、画像データが空である場合に例外を投げたいときは、以下のようにstd::runtime_errorを使います。
// source/reverse.cpp
#include "reverse.h"
#include <stdexcept>

rust::Vec<uint8_t> transform(rust::Slice<const uint8_t> img_data)
{
        // データが空のときのエラー処理
      if (img_data.empty()) {
            throw std::runtime_error("データが空です");
      }
    
      rust::Vec<uint8_t> output;
      output.reserve(img_data.size());
    
      for (size_t i = 0; i < img_data.size(); ++i) {
            auto pixel = img_data[i];
            output.push_back(255 - pixel);
      }
    
      return output;
}
  • Rust側のブリッジ定義をResultに書き換えることで、C++例外をエラーとして受け取れるようになります。
// src/bridge.rs
#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("reverse.h");

        fn transform(img_data: &[u8]) -> Result<Vec<u8>>; // Result追加
    }
}

pub use ffi::*;
  • 呼び出し側では、成功時と失敗時の処理を match で分岐します。
// src/main.rs
mod bridge;

use image::{GrayImage, ImageError};

fn load_grayscale_image(path: &str) -> Result<GrayImage, ImageError> {
    let img = image::open(path)?;
    Ok(img.into_luma8())
}

fn main() -> Result<(), ImageError> {
    let img = load_grayscale_image("kuma.png")?;
    let (width, height) = img.dimensions();
    // let img_data = img.into_raw();
    let empty_data: Vec<u8> = vec![]; // 空のデータ

    match bridge::transform(&empty_data) {
        Ok(reversed) => {
            let img_reversed = image::GrayImage::from_raw(width, height, reversed).expect("Failed");
            img_reversed.save("kuma_reversed.png")?;
        }
        Err(e) => println!("C++側エラー: {}", e),
    }

    Ok(())
}
  • cargo runすると、以下のように出力されます!
C++側エラー: データが空です

まとめ

  • CXXを活用して、Rustと既存のC++コードを連携させる方法を紹介しました。
  • このようにして、既存資産(特にSIMD処理など)を活かしながら、Rustの安全性の高いコードベースに徐々に移行するという選択肢が現実的であることを実感しました。
  • 是非、皆さんも段階的にRust化する際にはCXXを活用してみてください!