원래 연재 이후 최신 Rust 2024 에디션과 새 Cargo 리졸버, 오류 처리, Makefile, Sanitizer 테스트, 현대적 CMake/Corrosion 통합까지 반영한 Rust/C++ 상호 운용 전체 예제와 프로젝트 구조를 소개합니다. 수동 FFI와 cxx 기반 접근을 한 워크스페이스에서 나란히 비교합니다.
거의 1년 전 제가 썼던 Rust/C++ 상호 운용성에 관한 원래 블로그 연재 이후로 시간이 흘렀습니다. 그 연재는 기초를 다루었고, 당시에는 코드를 공유할 수 없었습니다. 오늘은 개념뿐 아니라 실제로 Rust/C++ 상호 운용을 작동시키는 데 필요한 완전한 도구 체인과 프로젝트 구조까지 보여 주는 예제를 공유하게 되어 기쁩니다.
rust-cpp-interop-example 프로젝트는 원래 연재에서 다룬 두 가지 접근, 즉 수동 FFI와 cxx 기반 바인딩을 하나의 워크스페이스에서 빌드 도구, 테스트, 실전 예제와 함께 모두 보여 줍니다.
전체 예제는 제 블로그 저장소의 rust-cpp-interop-example에서 확인할 수 있습니다.
예제는 최신 Rust 기능을 활용합니다:
[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
version = "0.1.0"
edition = "2024"
새 Cargo 리졸버와 Rust 2024 에디션은 워크스페이스 전반의 의존성 해석과 기능 플래그 처리를 더 나아지게 해 줍니다.
Rust 2024 에디션에는 예제에 반영한 소소한 언어 변경도 있습니다. 이제 no_mangle 속성에 unsafe 표시가 필요합니다.
#[unsafe(no_mangle)]
rust-cpp-interop-example/
├── Makefile # 통합 빌드 시스템
├── examples/ # 완전한 C++ 사용 예제
│ ├── manual_ffi_example.cpp
│ └── cxx_example.cpp
└── crates/
├── robot_joint/ # 순수 Rust 라이브러리
├── robot_joint-cpp/ # 수동 FFI 바인딩
└── robot_joint-cxx/ # cxx 기반 바인딩
이 구조는 두 접근을 나란히 비교하고 트레이드오프를 이해하기 쉽게 해 줍니다.
이제 순수 Rust 라이브러리는 thiserror로 제대로 된 오류 처리를 합니다:
/// 로봇 조인트 연산의 오류 타입
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("잘못된 조인트 구성: {message}")]
InvalidConfiguration { message: String },
#[error("예상한 변수 개수는 {expected}개, 실제는 {actual}개")]
InvalidVariableCount { expected: usize, actual: usize },
#[error("수학적 오류: {message}")]
MathError { message: String },
}
이는 원래 예제의 기본 Result 타입에 비해 훨씬 더 나은 오류 메시지와 디버깅 정보를 제공합니다.
예제를 직접 시도하려면 저장소를 클론하고 rust-cpp-interop-example 디렉터리의 Makefile을 사용하면 됩니다. 환경에는 Rust와 C++ 툴체인, CMake, Eigen 같은 기본 의존성이 필요합니다.
make
예제에는 AddressSanitizer와 UndefinedBehaviorSanitizer를 사용한 테스트가 포함되어 있습니다. 이는 수동 FFI 코드에서 쉽게 지나칠 수 있는 메모리 안전성 문제를 잡아 냅니다.
이번 예제의 가장 큰 개선점은 수동 FFI와 cxx 기반 접근을 나란히 비교할 수 있다는 것입니다.
#include <robot_joint.hpp>
int main() {
// 직접적이고 일급(1급)인 C++ API
robot_joint::Joint joint("example_joint");
Eigen::VectorXd variables(1);
variables << M_PI / 2.0;
// 인터페이스에서 자연스러운 Eigen 타입 사용
auto transform = joint.calculate_transform(variables);
std::cout << transform.matrix() << std::endl;
// 단순한 메서드 호출
auto [min_limit, max_limit] = joint.limits();
return 0;
}
#include <robot_joint/robot_joint.hpp>
int main() {
// 불투명 타입에는 팩토리 함수가 필요함
auto joint = robot_joint::new_joint("cxx_example_joint");
rust::Vec<double> variables;
variables.push_back(M_PI / 2.0);
// 명시적 변환 필요
auto transform_vec = joint->calculate_transform(
robot_joint::to_rust_slice(variables));
auto transform = robot_joint::to_eigen_isometry3d(
std::move(transform_vec));
std::cout << transform.matrix() << std::endl;
return 0;
}
순수 Rust 라이브러리는 블로그 연재의 타입 골격 대신 더 현실적인 세부 사항을 담고 있습니다.
#[derive(Clone, Debug)]
pub struct Joint {
name: String,
parent_link_to_joint_origin: Isometry3<f64>,
parent_link_index: usize,
child_link_index: usize,
index: usize,
dof_index: usize,
axis: Vector3<f64>,
}
impl Joint {
/// 전체 구성을 받아 생성
pub fn new_with_config(
name: String,
parent_link_to_joint_origin: Isometry3<f64>,
parent_link_index: usize,
child_link_index: usize,
index: usize,
dof_index: usize,
axis: Vector3<f64>,
) -> Self;
/// FFI를 위해 평평한 배열로 변환 행렬 계산
pub fn calculate_transform_matrix(&self, variables: &[f64]) -> [f64; 16];
/// 조인트 한계 검사
pub fn is_within_limits(&self, position: f64) -> bool;
}
특히 calculate_transform_matrix 메서드는 C++이 기대하는 형식으로 행렬 데이터를 반환해 FFI 사용 사례에 최적화되어 있습니다.
CMake 통합은 의존성 처리를 더 잘하도록 다듬었습니다:
# 더 견고한 의존성 확인
find_package(Eigen3 REQUIRED)
# 더 나은 Corrosion 통합
include(FetchContent)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5)
FetchContent_MakeAvailable(Corrosion)
# 더 깔끔한 타깃 설정
corrosion_import_crate(MANIFEST_PATH Cargo.toml CRATES robot_joint-cpp)
또한 설치 타깃이 이제 다운스트림에서 사용하기 더 견고해졌습니다.
두 접근 모두 CMake의 FetchContent와 매끄럽게 동작하도록 설계되었습니다:
include(FetchContent)
FetchContent_Declare(
robot_joint
GIT_REPOSITORY https://github.com/tylerjw/tylerjw.dev
GIT_TAG main
SOURCE_SUBDIR "rust-cpp-interop-example/crates/robot_joint-cpp"
)
FetchContent_MakeAvailable(robot_joint)
target_link_libraries(your_target PRIVATE robot_joint::robot_joint)