C++なんもわからないです。
何もわからないんですが、高性能な硬いライブラリはC++で書かれていることが多く、PythonのバインディングはあってもRustからは利用できないものも多くあります。
Rustへの移行が始まっているライブラリもあれば、大規模すぎて移行が難しいライブラリもたくさんあり、そんな中で「必要な機能だけうまく抜き出して」バインディングが作れれば、フル機能の移植ができなくても場合によっては有効だなー。と思って色々ガチャガチャ試しているんですが、大体失敗します。
多分、C++で何かを作った経験がないため、ビルド周りとか綺麗なクラスの作り方とかそういったお作法が何もわからないことが原因なんだろうなと思ったので、今回は四則演算を行う程度のC++の関数を作って、それをRustから呼び出してみるテストをします。
AIくんが優秀なおかげでこういったことがやりやすくなったので、彼(?)のフルサポートでやっていきます。
以下のようなコマンドでRustのプロジェクトを始めます。
cargo init --lib cpp-binding-sample
最終的にはこんな感じのフォルダ構成になりました。
$ tree -L 3 --dirsfirst --gitignore
.
├── examples
│ └── demo.rs
├── src
│ ├── cpp
│ │ ├── calculator.cpp
│ │ ├── calculator.h
│ │ ├── calculator_cxx.cpp
│ │ ├── calculator_cxx.h
│ │ ├── test_calculator
│ │ └── test_calculator.cpp
│ └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── build.rs
C++のコードを書く
まずはC++側のコードを作成します。今回は四則演算を行うCalculatorクラスを作ってみました。
C++ではヘッダーファイル(.h)に関数やクラスの宣言を、実装ファイル(.cpp)に実際の処理を書くというルールがあるっぽいので、そうします。
Rustで例えるとトレイトの定義とその実装を別ファイルに分けるような感じですかね。
src/cpp/calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public:
Calculator();
~Calculator();
int add(int a, int b) const;
int subtract(int a, int b) const;
double divide(double a, double b) const;
void setMemory(double value);
double getMemory() const;
private:
double memory;
};
int factorial(int n);
#endif
src/cpp/calculator.cpp
ではいくつかの計算用の関数を用意しておきます。
#include "calculator.h"
#include <stdexcept>
Calculator::Calculator() : memory(0.0) {}
Calculator::~Calculator() {}
int Calculator::add(int a, int b) const {
return a + b;
}
int Calculator::subtract(int a, int b) const {
return a - b;
}
double Calculator::divide(double a, double b) const {
if (b == 0.0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
void Calculator::setMemory(double value) {
memory = value;
}
double Calculator::getMemory() const {
return memory;
}
int factorial(int n) {
if (n < 0) {
throw std::runtime_error("Negative factorial");
}
if (n == 0 || n == 1) return 1;
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
C++コードの動作確認
動作確認用のプログラムを作ってC++コード単体で動作確認をしてみました。
見るからにAIが書いたコード、という感じですが勉強用なので、むしろもっと丁寧にコメント書いて欲しいという感じです。
#include "calculator.h"
#include <iostream>
#include <iomanip>
int main() {
std::cout << "=== Calculator Test Program ===" << std::endl;
// Calculatorクラスのインスタンス作成
Calculator calc;
// 基本的な演算のテスト
std::cout << "\n基本演算:" << std::endl;
std::cout << " 5 + 3 = " << calc.add(5, 3) << std::endl;
std::cout << " 10 - 4 = " << calc.subtract(10, 4) << std::endl;
std::cout << " 20.0 / 4.0 = " << calc.divide(20.0, 4.0) << std::endl;
// 除算エラーのテスト
std::cout << "\n除算エラーのテスト:" << std::endl;
try {
double result = calc.divide(10.0, 0.0);
std::cout << " 10.0 / 0.0 = " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cout << " エラー: " << e.what() << std::endl;
}
// メモリ機能のテスト
std::cout << "\nメモリ機能:" << std::endl;
std::cout << " 初期メモリ値: " << calc.getMemory() << std::endl;
calc.setMemory(42.5);
std::cout << " メモリ設定後: " << calc.getMemory() << std::endl;
// 階乗関数のテスト
std::cout << "\n階乗計算:" << std::endl;
for (int i = 0; i <= 5; ++i) {
std::cout << " " << i << "! = " << factorial(i) << std::endl;
}
// 負の階乗エラーのテスト
std::cout << "\n負の階乗エラーのテスト:" << std::endl;
try {
int result = factorial(-1);
std::cout << " -1! = " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cout << " エラー: " << e.what() << std::endl;
}
std::cout << "\n=== テスト完了 ===" << std::endl;
return 0;
}
コンパイルして実行するとこんな感じになりました。
$ cd src/cpp
$ g++ -o test_calculator test_calculator.cpp calculator.cpp -std=c++11
$ ./test_calculator
=== Calculator Test Program ===
基本演算:
5 + 3 = 8
10 - 4 = 6
20.0 / 4.0 = 5
除算エラーのテスト:
エラー: Division by zero
メモリ機能:
初期メモリ値: 0
メモリ設定後: 42.5
階乗計算:
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
負の階乗エラーのテスト:
エラー: Negative factorial
=== テスト完了 ===
C++のプログラムがちゃんと動いている…感動…!
Rustバインディングの作成
次はRustからC++を呼び出せるようにします。今回はcxx
クレートを使いました。
プロジェクトの設定
Cargo.toml
[package]
name = "cpp-binding-sample"
version = "0.1.0"
edition = "2024"
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
C++ラッパーファイルの作成
cxxクレートを使用するために、C++側にラッパー関数を作成します。
すでに既存のC++ライブラリが存在していて、その中の一部だけ利用したい、みたいな場合はおそらくこのようにラッパーを作った方が良いかもしれません。
src/cpp/calculator_cxx.h
#ifndef CALCULATOR_CXX_H
#define CALCULATOR_CXX_H
#include "calculator.h"
#include "rust/cxx.h"
#include <memory>
// cxxブリッジ用のラッパー関数宣言
std::unique_ptr<Calculator> new_calculator();
int32_t calculator_add(const Calculator& calc, int32_t a, int32_t b);
int32_t calculator_subtract(const Calculator& calc, int32_t a, int32_t b);
rust::String calculator_divide(const Calculator& calc, double a, double b);
void calculator_set_memory(Calculator& calc, double value);
double calculator_get_memory(const Calculator& calc);
rust::String factorial_wrapper(int32_t n);
#endif // CALCULATOR_CXX_H
src/cpp/calculator_cxx.cpp
#include "calculator.h"
#include "calculator_cxx.h"
#include "rust/cxx.h"
#include <memory>
#include <stdexcept>
#include <string>
// cxxブリッジ用のラッパー関数実装
std::unique_ptr<Calculator> new_calculator() {
return std::make_unique<Calculator>();
}
int32_t calculator_add(const Calculator& calc, int32_t a, int32_t b) {
return calc.add(a, b);
}
int32_t calculator_subtract(const Calculator& calc, int32_t a, int32_t b) {
return calc.subtract(a, b);
}
rust::String calculator_divide(const Calculator& calc, double a, double b) {
try {
double result = calc.divide(a, b);
return std::to_string(result);
} catch (const std::runtime_error& e) {
std::string error_msg = "error:";
error_msg += e.what();
return error_msg;
}
}
void calculator_set_memory(Calculator& calc, double value) {
calc.setMemory(value);
}
double calculator_get_memory(const Calculator& calc) {
return calc.getMemory();
}
rust::String factorial_wrapper(int32_t n) {
try {
int result = factorial(n);
return std::to_string(result);
} catch (const std::runtime_error& e) {
std::string error_msg = "error:";
error_msg += e.what();
return error_msg;
}
}
ビルドスクリプトの設定
cxx-build
クレートが自動的にシステムのC++コンパイラを検出してコンパイルし、FFIブリッジコードも生成してくれます。便利ですねー。
build.rs
fn main() {
// cxxブリッジのビルド設定
let mut build = cxx_build::bridge("src/lib.rs");
// C++ソースファイルとインクルードディレクトリの設定
build
.file("src/cpp/calculator.cpp")
.file("src/cpp/calculator_cxx.cpp")
.include("src/cpp")
.std("c++14")
.compile("calculator");
// リンカー設定
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=src/cpp/calculator.h");
println!("cargo:rerun-if-changed=src/cpp/calculator.cpp");
println!("cargo:rerun-if-changed=src/cpp/calculator_cxx.h");
println!("cargo:rerun-if-changed=src/cpp/calculator_cxx.cpp");
}
Rustバインディングの実装
cxxを利用しているので、あんまりunsafeなコードを書かなくて済むらしいです。素晴らしい。
src/lib.rs
#[cxx::bridge]
pub mod ffi {
unsafe extern "C++" {
include!("calculator_cxx.h");
// C++ Calculator型の宣言
type Calculator;
// ラッパー関数の宣言
fn new_calculator() -> UniquePtr<Calculator>;
fn calculator_add(calc: &Calculator, a: i32, b: i32) -> i32;
fn calculator_subtract(calc: &Calculator, a: i32, b: i32) -> i32;
fn calculator_divide(calc: &Calculator, a: f64, b: f64) -> String;
fn calculator_set_memory(calc: Pin<&mut Calculator>, value: f64);
fn calculator_get_memory(calc: &Calculator) -> f64;
fn factorial_wrapper(n: i32) -> String;
}
}
// 安全なRust APIを公開
use cxx::UniquePtr;
pub struct SafeCalculator {
inner: UniquePtr<ffi::Calculator>,
}
impl SafeCalculator {
pub fn new() -> Self {
Self {
inner: ffi::new_calculator(),
}
}
pub fn add(&self, a: i32, b: i32) -> i32 {
ffi::calculator_add(&self.inner, a, b)
}
pub fn subtract(&self, a: i32, b: i32) -> i32 {
ffi::calculator_subtract(&self.inner, a, b)
}
pub fn divide(&self, a: f64, b: f64) -> Result<f64, String> {
let result = ffi::calculator_divide(&self.inner, a, b);
if result.starts_with("error:") {
Err(result[6..].to_string())
} else {
result.parse::<f64>()
.map_err(|_| "Failed to parse result".to_string())
}
}
pub fn set_memory(&mut self, value: f64) {
ffi::calculator_set_memory(self.inner.pin_mut(), value)
}
pub fn get_memory(&self) -> f64 {
ffi::calculator_get_memory(&self.inner)
}
}
pub fn factorial(n: i32) -> Result<i32, String> {
let result = ffi::factorial_wrapper(n);
if result.starts_with("error:") {
Err(result[6..].to_string())
} else {
result.parse::<i32>()
.map_err(|_| "Failed to parse result".to_string())
}
}
impl Default for SafeCalculator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operations() {
let calc = SafeCalculator::new();
assert_eq!(calc.add(5, 3), 8);
assert_eq!(calc.subtract(10, 4), 6);
}
#[test]
fn test_division() {
let calc = SafeCalculator::new();
assert_eq!(calc.divide(10.0, 2.0).unwrap(), 5.0);
assert!(calc.divide(10.0, 0.0).is_err());
}
#[test]
fn test_memory() {
let mut calc = SafeCalculator::new();
calc.set_memory(42.0);
assert_eq!(calc.get_memory(), 42.0);
}
#[test]
fn test_factorial() {
assert_eq!(factorial(5).unwrap(), 120);
assert!(factorial(-1).is_err());
}
}
実際に使ってみる
最後に、作成したバインディングを使ってみます。
examples/demo.rs
use cpp_binding_sample::{SafeCalculator, factorial};
fn main() {
println!("=== SafeCalculator Demo ===\n");
// 基本的な演算
let calc = SafeCalculator::new();
println!("基本演算:");
println!(" 5 + 3 = {}", calc.add(5, 3));
println!(" 10 - 4 = {}", calc.subtract(10, 4));
// 除算(エラー処理付き)
println!("\n除算:");
match calc.divide(20.0, 4.0) {
Ok(result) => println!(" 20.0 / 4.0 = {}", result),
Err(e) => println!(" エラー: {}", e),
}
match calc.divide(10.0, 0.0) {
Ok(result) => println!(" 10.0 / 0.0 = {}", result),
Err(e) => println!(" 10.0 / 0.0 => エラー: {}", e),
}
// メモリ機能
println!("\nメモリ機能:");
let mut calc = SafeCalculator::new();
println!(" 初期メモリ値: {}", calc.get_memory());
calc.set_memory(42.5);
println!(" メモリ設定後: {}", calc.get_memory());
// 階乗計算
println!("\n階乗計算:");
for n in 0..=5 {
match factorial(n) {
Ok(result) => println!(" {}! = {}", n, result),
Err(e) => println!(" {}! => エラー: {}", n, e),
}
}
match factorial(-1) {
Ok(result) => println!(" -1! = {}", result),
Err(e) => println!(" -1! => エラー: {}", e),
}
println!("\n=== デモ完了 ===");
}
実行するとこんな感じで動きました。
C++で書いた関数をRustから呼び出すことができていますね。
$ cargo run --example demo
=== SafeCalculator Demo ===
基本演算:
5 + 3 = 8
10 - 4 = 6
除算:
20.0 / 4.0 = 5
10.0 / 0.0 => エラー: Division by zero
メモリ機能:
初期メモリ値: 0
メモリ設定後: 42.5
階乗計算:
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
-1! => エラー: Negative factorial
=== デモ完了 ===
まとめ
今回はシンプルな四則演算を題材に、C++ライブラリのRustバインディングを作成してみました。実際のプロジェクトではもっと複雑になってしまうわけですが、とりあえず基本的なことは理解できたのかなと思います。
まだまだ難しいですが、C++大規模ライブラリのバインディングを作成できるように頑張ります!