C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]
이 글의 핵심
C++와 Rust의 상호 운용성(C ABI, FFI)과 메모리 안전성 논의가 실무에서 무엇을 의미하는지 정리합니다. 문제 시나리오, 완전한 예제, 자주 발생하는 에러, 프로덕션 패턴까지 C++ 실전 가이드 시리즈에서 다룹니다.
들어가며: “Rust로 갈아타야 하나요?"
"레거시 C++ 코드가 50만 줄인데, 새 기능을 Rust로 쓰고 싶어요”
Rust가 시스템·임베디드·웹Assembly 영역에서 대안으로 부상하면서, “C++ vs Rust”와 메모리 안전성(Memory Safety) 논의가 자주 나옵니다. 실무에서는 한쪽만 쓰기보다 기존 C++ 레거시를 유지하면서 새 모듈을 Rust로 쓰거나, C ABI(C 언어 호출 규약—함수 호출·구조체 레이아웃 등 이진 호환 규칙)로 경계를 나누어 FFI(Foreign Function Interface—다른 언어의 함수를 호출하거나 노출하는 인터페이스)로 상호 운용하는 경우가 많습니다.
이 글에서는 실제 겪는 문제 시나리오부터 시작해, C ABI·FFI로 C++와 Rust가 어떻게 경계를 나누는지, 완전한 예제, 자주 발생하는 에러, 프로덕션 패턴까지 단계별로 다룹니다.
이 글에서 다루는 것:
- 문제 시나리오: 레거시 연동·메모리 소유권·ABI 불일치 등 실무에서 겪는 상황
- C ABI 경계: C++와 Rust가 C 인터페이스로 맞대기
- 완전한 예제: C++ → Rust, Rust → C++ 양방향 호출, 구조체·문자열 전달
- 자주 발생하는 에러: 링크 에러·ABI 불일치·메모리 누수
- Memory Safety: 소유권·검사·C++에서의 완화 전략
- 프로덕션 패턴: 점진적 도입·경계 설계·팀 역량
목차
- 문제 시나리오: 왜 C++/Rust 연동이 필요한가
- C++와 Rust 상호 운용 (FFI)
- 완전한 C++/Rust 연동 예제
- 자주 발생하는 에러와 해결법
- Memory Safety 논의
- 실무에서의 선택
- 프로덕션 패턴
- 정리
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
1. 문제 시나리오: 왜 C++/Rust 연동이 필요한가
시나리오 1: “레거시 C++ 라이브러리를 Rust에서 써야 해요”
"10년 전에 만든 C++ 암호화 라이브러리가 있어요. 검증도 됐고, 재작성 비용이 너무 커요."
"새 프로젝트는 Rust로 하는데, 이 라이브러리를 그대로 쓰고 싶어요."
원인: C++ 라이브러리를 Rust로 완전히 재작성하면 시간·비용·버그 검증이 부담됩니다. FFI로 C ABI 경계만 두고 호출하면 기존 코드를 그대로 활용할 수 있습니다.
시나리오 2: “Rust로 만든 핵심 모듈을 C++ 앱에 넣고 싶어요”
"파서·시리얼라이저 같은 핵심 로직을 Rust로 새로 짰어요. 메모리 안전하고 테스트도 잘 됐어요."
"기존 C++ GUI 앱에서 이 모듈을 호출해야 해요."
원인: Rust가 메모리 안전성과 동시성에서 강점이 있어, 핫한 경로만 Rust로 옮기는 전략이 효과적입니다. C++ 앱은 동적 라이브러리(.so/.dll/.dylib) 로 로드해 호출하면 됩니다.
시나리오 3: “메모리 소유권이 FFI 경계에서 꼬여요”
"C++에서 할당한 버퍼를 Rust에 넘겼는데, C++에서 먼저 free 해버렸어요."
"Rust에서 반환한 문자열을 C++에서 쓰다가 use-after-free가 났어요."
원인: FFI 경계를 넘나드는 메모리 소유권이 문서화되지 않으면, 누가 할당·해제하는지 불명확해집니다. 계약을 명확히 하고, 가능하면 한쪽에서만 할당·해제하는 규칙을 두어야 합니다.
시나리오 4: “템플릿·예외·가상 함수를 그대로 노출하고 싶어요”
"C++의 std::vector<int>를 Rust에 넘기고 싶어요."
"C++ 예외를 Rust로 전달하고 싶어요."
원인: C++ ABI는 컴파일러·버전마다 다르고, 템플릿·예외·가상 함수는 이진 호환이 보장되지 않습니다. C 스타일 래퍼(포인터·길이·플래그)로만 경계를 두는 것이 안전합니다.
시나리오 5: “빌드 시스템이 C++와 Rust를 같이 돌리기 어려워요”
"CMake로 C++ 빌드하고, Cargo로 Rust 빌드하는데, 링크 순서가 맞지 않아요."
"크로스 컴파일할 때 두 툴체인이 충돌해요."
원인: C++와 Rust는 각각 다른 빌드 시스템을 쓰므로, 통합 빌드 스크립트나 CMake + Cargo 조합을 설계해야 합니다.
시나리오 6: “팀에 Rust 경험이 없는데 점진적으로 도입하고 싶어요”
"전면 이전은 위험하고, 새 기능만 Rust로 시험해보고 싶어요."
"경계를 어떻게 나누면 리스크를 줄일 수 있을까요?"
원인: 점진적 도입이 현실적입니다. C ABI로 경계를 명확히 하고, 소규모 모듈부터 Rust로 옮기면 학습 곡선을 관리할 수 있습니다.
2. C++와 Rust 상호 운용 (FFI)
C ABI가 경계다
- Rust는 extern “C” 로 C ABI를 노출·호출할 수 있습니다. C++ 쪽에서 extern “C” 로 내보낸 함수·구조체 레이아웃과 맞추면, Rust의 #[no_mangle] extern “C” 로 호출할 수 있습니다.
- 복잡한 C++ 타입(템플릿·예외·가상 함수)은 ABI가 불안정하므로 C 스타일 래퍼로만 경계를 두는 것이 안전합니다.
- 방향: C++ → Rust 호출(C++가 Rust로 만든 라이브러리 사용), Rust → C++ 호출(Rust가 기존 C++ 라이브러리 사용) 모두 C 레이어를 사이에 두고, 소유권 계약(누가 할당·해제하는지)을 문서화하는 것이 중요합니다.
- 도구: bindgen으로 C/C++ 헤더에서 Rust 바인딩을 생성할 수 있습니다. cxx 크레이트는 C++와 Rust 간 타입·예외 전달을 도와줍니다.
FFI 호출 시퀀스 (C++ → Rust)
sequenceDiagram
participant CPP as C++ 앱
participant C_ABI as C 래퍼
participant RUST as Rust 라이브러리
CPP->>C_ABI: extern "C" rust_add(3, 5)
C_ABI->>RUST: #[no_mangle] rust_add 호출
RUST->>RUST: a + b 계산
RUST-->>C_ABI: 8 반환
C_ABI-->>CPP: 8 반환
FFI 아키텍처 다이어그램
flowchart TB
subgraph cpp["C++ 영역"]
CPP_APP[C++ 애플리케이션]
CPP_LIB[C++ 라이브러리]
end
subgraph c_abi["C ABI 경계 (extern \"C\")"]
C_WRAPPER[C 스타일 래퍼]
end
subgraph rust["Rust 영역"]
RUST_LIB[Rust 라이브러리]
RUST_APP[Rust 애플리케이션]
end
CPP_APP -->|호출| C_WRAPPER
C_WRAPPER -->|호출| RUST_LIB
RUST_APP -->|호출| C_WRAPPER
C_WRAPPER -->|호출| CPP_LIB
기본 원리: extern “C”와 #[no_mangle]
C++ 에서 C ABI로 노출할 때는 extern “C” 로 이름 맹글링(name mangling)을 막고, C 호출 규약을 사용합니다.
// cpp_math.h - C++에서 C ABI로 노출
#ifdef __cplusplus
extern "C" {
#endif
// 이름 맹글링 없이 "add"로 심볼 노출
int add(int a, int b);
double compute_hash(const char* data, size_t len);
#ifdef __cplusplus
}
#endif
// cpp_math.cpp - 구현
#include "cpp_math.h"
extern "C" {
int add(int a, int b) {
return a + b;
}
double compute_hash(const char* data, size_t len) {
double result = 0.0;
for (size_t i = 0; i < len; ++i) {
result = result * 31.0 + static_cast<unsigned char>(data[i]);
}
return result;
}
}
Rust 에서는 #[no_mangle] extern “C” 로 같은 규약을 사용합니다.
// Rust에서 C ABI 함수 선언
#[link(name = "cpp_math")]
extern "C" {
fn add(a: i32, b: i32) -> i32;
fn compute_hash(data: *const u8, len: usize) -> f64;
}
fn main() {
let result = unsafe { add(3, 5) };
println!("3 + 5 = {}", result); // 8
let s = b"hello";
let hash = unsafe { compute_hash(s.as_ptr(), s.len()) };
println!("hash = {}", hash);
}
3. 완전한 C++/Rust 연동 예제
예제 1: C++ → Rust 호출 (Rust 라이브러리를 C++에서 사용)
목표: Rust로 만든 라이브러리를 C++ 앱에서 동적 로드해 호출합니다.
Rust 라이브러리 (cdylib):
// src/lib.rs - Rust cdylib
#![crate_type = "cdylib"]
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_greeting() -> *const u8 {
// 정적 문자열: 수명이 프로그램 전체이므로 안전
b"Hello from Rust!\0".as_ptr()
}
Cargo.toml:
[package]
name = "rust_ffi_lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
C++ 호출 측:
// main.cpp - C++에서 Rust 라이브러리 호출
#include <iostream>
#include <dlfcn.h> // Linux/macOS. Windows는 LoadLibrary
int main() {
void* handle = dlopen("./librust_ffi_lib.so", RTLD_LAZY); // macOS: .dylib
if (!handle) {
std::cerr << "dlopen failed: " << dlerror() << "\n";
return 1;
}
auto add_fn = (int(*)(int, int)) dlsym(handle, "rust_add");
auto greeting_fn = (const char*(*)()) dlsym(handle, "rust_greeting");
if (add_fn && greeting_fn) {
std::cout << "rust_add(3, 5) = " << add_fn(3, 5) << "\n";
std::cout << "greeting: " << greeting_fn() << "\n";
}
dlclose(handle);
return 0;
}
빌드:
# Rust 라이브러리 빌드
cargo build --release
# C++ 빌드 (Linux)
g++ -std=c++17 -o main main.cpp -ldl
# 실행
./main
예제 2: Rust → C++ 호출 (C++ 라이브러리를 Rust에서 사용)
C++ 라이브러리:
// libfoo.cpp
#include <cstdlib>
#include <cstring>
extern "C" {
int cpp_multiply(int a, int b) {
return a * b;
}
// 소유권: 호출자가 반환된 포인터를 free 해야 함 (C 스타일)
char* cpp_duplicate_string(const char* src) {
if (!src) return nullptr;
size_t len = strlen(src);
char* dst = (char*)malloc(len + 1);
if (dst) {
memcpy(dst, src, len + 1);
}
return dst;
}
void cpp_free_string(char* s) {
free(s);
}
}
Rust 호출 측:
// src/main.rs
use std::ffi::CString;
use std::os::raw::c_char;
#[link(name = "foo")]
extern "C" {
fn cpp_multiply(a: i32, b: i32) -> i32;
fn cpp_duplicate_string(src: *const c_char) -> *mut c_char;
fn cpp_free_string(s: *mut c_char);
}
fn main() {
let result = unsafe { cpp_multiply(4, 5) };
println!("4 * 5 = {}", result); // 20
let s = CString::new("hello").unwrap();
let ptr = unsafe { cpp_duplicate_string(s.as_ptr()) };
if !ptr.is_null() {
let rust_str = unsafe { std::ffi::CStr::from_ptr(ptr).to_str().unwrap() };
println!("duplicated: {}", rust_str);
unsafe { cpp_free_string(ptr) }; // 반드시 해제
}
}
예제 3: 구조체 전달 (레이아웃 일치 필수)
C/C++ 헤더:
// point.h - C와 C++ 모두에서 사용 가능한 구조체
#ifndef POINT_H
#define POINT_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
double x;
double y;
} Point;
Point point_add(Point a, Point b);
double point_distance(Point a, Point b);
#ifdef __cplusplus
}
#endif
#endif
C++ 구현:
// point.cpp
#include "point.h"
#include <cmath>
extern "C" {
Point point_add(Point a, Point b) {
return { a.x + b.x, a.y + b.y };
}
double point_distance(Point a, Point b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}
Rust 바인딩:
// Rust에서 동일한 레이아웃 구조체 정의
#[repr(C)]
#[derive(Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
#[link(name = "point")]
extern "C" {
fn point_add(a: Point, b: Point) -> Point;
fn point_distance(a: Point, b: Point) -> f64;
}
fn main() {
let a = Point { x: 1.0, y: 2.0 };
let b = Point { x: 3.0, y: 4.0 };
let sum = unsafe { point_add(a, b) };
let dist = unsafe { point_distance(a, b) };
println!("sum = ({}, {}), dist = {}", sum.x, sum.y, dist);
}
주의: #[repr(C)]를 반드시 사용해야 C와 레이아웃이 일치합니다. 필드 순서·타입·패딩이 정확히 맞아야 합니다.
예제 4: bindgen으로 헤더에서 바인딩 자동 생성
Cargo.toml:
[build-dependencies]
bindgen = "0.69"
build.rs:
// build.rs
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("bindgen failed");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("write bindings failed");
}
wrapper.h (C 헤더만 포함, C++ 직접 포함 시 주의):
#include "point.h"
lib.rs:
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
fn main() {
let a = Point { x: 1.0, y: 2.0 };
let b = Point { x: 3.0, y: 4.0 };
let sum = unsafe { point_add(a, b) };
println!("sum = ({}, {})", sum.x, sum.y);
}
예제 5: cxx 크레이트로 타입·예외 브리지
cxx는 C++와 Rust 간에 타입 브리지와 예외 전달을 지원합니다. C 스타일 포인터만 쓰는 것보다 편리합니다. C++ 클래스를 Rust에서 직접 사용할 수 있습니다.
Cargo.toml:
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
C++ 헤더 (include/calculator.h):
#pragma once
class Calculator {
public:
Calculator() : last_result_(0) {}
int add(int a, int b) {
last_result_ = a + b;
return last_result_;
}
int get_last_result() const {
return last_result_;
}
private:
int last_result_;
};
C++ 구현 (src/calculator.cpp):
#include "calculator.h"
// cxx가 자동 생성하는 브리지 코드와 링크
src/lib.rs:
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("rust_cxx_demo/include/calculator.h");
type Calculator;
fn new_calculator() -> UniquePtr<Calculator>;
fn add(self: Pin<&mut Calculator>, a: i32, b: i32) -> i32;
fn get_last_result(&self) -> i32;
}
}
fn main() {
let mut calc = ffi::new_calculator();
let result = calc.as_mut().unwrap().add(10, 20);
println!("10 + 20 = {}", result);
println!("last result = {}", calc.get_last_result());
}
build.rs:
fn main() {
cxx_build::bridge("src/lib.rs")
.file("rust_cxx_demo/src/calculator.cpp")
.flag("-std=c++17")
.compile("rust_cxx_demo");
}
cxx 장점: C++ 예외를 Rust Result로 변환, std::string·std::vector 브리지, UniquePtr·SharedPtr 지원. 단점: 빌드 설정이 복잡해지고, C++ 컴파일러가 필요합니다.
4. 자주 발생하는 에러와 해결법
에러 1: “undefined reference to add” (링크 에러)
원인: Rust가 extern "C" 함수를 선언했지만, C++ 라이브러리가 링크되지 않았거나, 라이브러리 이름이 잘못됐습니다.
해결법:
# Cargo.toml - 링크 검색 경로 지정
[build-dependencies]
cc = "1.0"
# build.rs
fn main() {
println!("cargo:rustc-link-search=native=/usr/local/lib");
println!("cargo:rustc-link-lib=foo");
}
# Linux: .so 경로 지정
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
./your_rust_binary
에러 2: “ABI 불일치” (잘못된 인자/반환값)
원인: C++와 Rust에서 구조체 레이아웃·타입 크기가 다릅니다. #[repr(C)] 누락, 필드 순서·패딩 차이.
해결법:
// ❌ 잘못된 예: repr(C) 없음
struct Point {
x: f64,
y: f64,
}
// ✅ 올바른 예
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
검증: std::mem::size_of::<Point>()와 C sizeof(Point)가 같아야 합니다.
에러 3: “dereferencing pointer to incomplete type”
원인: C++에서 불투명 포인터(Opaque Pointer) 를 사용할 때, Rust에서 해당 타입을 정의하지 않았거나, C++ 헤더가 불완전합니다.
해결법:
// C++: 불투명 포인터
typedef struct Handle* Handle;
extern "C" Handle create_handle();
extern "C" void destroy_handle(Handle h);
// Rust: opaque 타입
#[repr(C)]
pub struct Handle {
_private: [u8; 0],
}
extern "C" {
fn create_handle() -> *mut Handle;
fn destroy_handle(h: *mut Handle);
}
에러 4: “use after free” / “double free”
원인: FFI 경계에서 소유권이 불명확합니다. C++에서 malloc한 포인터를 Rust에 넘기고, Rust에서 free하지 않거나, 양쪽에서 모두 free하는 경우.
해결법: 계약 문서화 — “이 함수는 호출자가 반환 포인터를 free 해야 함” 등을 명시합니다.
/// # Safety
/// 반환된 *mut c_char는 cpp_free_string로 해제해야 함.
/// 호출자가 소유권을 가짐.
unsafe fn get_string() -> *mut c_char {
cpp_duplicate_string(...)
}
가능하면 한쪽에서만 할당·해제하는 규칙을 두세요.
에러 5: “panic in FFI” (Rust panic이 C++로 전파)
원인: Rust panic!이 FFI 경계를 넘으면 undefined behavior입니다. C ABI로 노출하는 Rust 함수에서는 panic을 잡아서 에러 코드로 반환해야 합니다.
해결법:
#[no_mangle]
pub extern "C" fn rust_safe_operation() -> i32 {
std::panic::catch_unwind(|| {
// 위험한 연산
do_something()
}).map(|r| r as i32).unwrap_or(-1)
}
에러 6: “symbol not found” (동적 로드 시)
원인: #[no_mangle] 누락, 또는 라이브러리 이름이 플랫폼마다 다름 (Linux: libfoo.so, macOS: libfoo.dylib, Windows: foo.dll).
해결법:
#[no_mangle] // 반드시 필요
pub extern "C" fn my_exported_fn() { ... }
# 심볼 확인
nm -D librust_ffi_lib.so | grep rust_add
에러 7: “alignment” / “unaligned access”
원인: C 구조체에 패딩이 있는데, Rust에서 #[repr(C)] 없이 정의하거나, 필드 순서가 다릅니다.
해결법: #[repr(C)] 사용, std::mem::align_of로 정렬 확인.
에러 8: “cxx-build failed” (cxx 크레이트)
원인: C++ 컴파일러 미설치, C++17 미지원, include 경로 오류.
해결법:
# Linux: build-essential
sudo apt install build-essential
# macOS: Xcode Command Line Tools
xcode-select --install
// build.rs - include 경로 추가
fn main() {
cxx_build::bridge("src/lib.rs")
.file("src/calculator.cpp")
.include("include") // -I include
.flag("-std=c++17")
.compile("demo");
}
플랫폼별 라이브러리 확장자
| 플랫폼 | 정적 라이브러리 | 동적 라이브러리 |
|---|---|---|
| Linux | .a | .so |
| macOS | .a | .dylib |
| Windows (MSVC) | .lib | .dll |
| Windows (MinGW) | .a | .dll |
Rust cdylib는 플랫폼에 맞게 자동 생성합니다. C++에서 dlopen/LoadLibrary 사용 시 확장자를 조건부로 선택해야 합니다.
5. Memory Safety 논의
소유권·검사 vs 수동·도구
- Rust: 소유권·빌림 검사로 데이터 경합·use-after-free·이중 해제 등을 컴파일 타임에 막는 것을 목표로 합니다. 대신 런타임 오버헤드 없이 안전한 코드를 유도합니다.
- C++: 수동 메모리 관리·스마트 포인터·RAII로 같은 목표를 관례와 도구로 추구합니다. 정적 분석(Clang-Tidy, 41-1)·Sanitizer(ASan, TSan, 41-2)·코드 리뷰로 실수를 줄이지만, 언어가 강제하지는 않습니다.
- 논쟁의 실체: “Rust가 안전하다”는 것은 언어가 보장하는 범위가 넓다는 뜻이고, “C++가 위험하다”는 것은 실수 가능성이 있고 도구와 습관으로 보완해야 한다는 뜻입니다. 실무에서는 둘 다 쓰이므로, 경계(FFI)를 잘 정하고 계약을 명확히 하는 것이 중요합니다.
FFI 경계에서의 안전성
flowchart LR
subgraph safe["Rust 안전 영역"]
R1[소유권 검사]
R2[빌림 검사]
end
subgraph unsafe["unsafe 경계"]
U[FFI 호출]
end
subgraph manual["C++ 수동 관리"]
C1[수동 할당/해제]
C2[RAII/스마트 포인터]
end
R1 --> U
U --> C1
FFI를 넘는 순간 Rust의 안전성 보장이 사라집니다. unsafe 블록 안에서는 수동으로 계약을 지켜야 합니다. C++ 쪽에서 RAII·스마트 포인터를 잘 쓰면, 경계에서의 실수를 줄일 수 있습니다.
6. 실무에서의 선택
레거시·성능·팀
- 레거시 C++가 많으면 전면 이전보다 새 기능만 Rust로 쓰거나 핫한 경로만 Rust로 옮기는 점진적 접근이 현실적입니다. C ABI로 경계를 나누면 됩니다.
- 성능: 둘 다 제로 코스트를 지향합니다. 세밀한 제어가 필요하면 C++가 여전히 강하고, 안전성 우선이면 Rust가 컴파일러가 더 많이 도와줍니다.
- 팀 역량: Rust 학습 곡선과 C++ 유지보수 비용을 저울질해서, 도입 속도와 교육을 함께 계획하는 것이 좋습니다.
7. 프로덕션 패턴
패턴 1: 경계 레이어 분리
원칙: C++/Rust 경계를 얇은 C 래퍼 레이어로만 두고, 복잡한 로직은 각 언어 내부에서 처리합니다.
[C++ 앱] → [C 래퍼 10줄] → [Rust 핵심 로직]
[Rust 앱] → [C 래퍼 10줄] → [C++ 레거시]
패턴 2: 소유권 계약 문서화
원칙: FFI 함수마다 할당·해제 책임을 주석으로 명시합니다.
/// @param buf 호출자가 할당. 이 함수는 해제하지 않음.
/// @return 호출자가 free 해야 함.
extern "C" char* process_buffer(const char* buf, size_t len);
패턴 3: 에러 코드 반환 (예외 금지)
원칙: FFI 경계에서는 예외를 넘기지 않습니다. 에러 코드·널 반환·out 파라미터로 전달합니다.
#[no_mangle]
pub extern "C" fn rust_parse(input: *const u8, len: usize, out: *mut i32) -> i32 {
if input.is_null() || out.is_null() {
return -1; // EINVAL
}
match parse_something(unsafe { std::slice::from_raw_parts(input, len) }) {
Ok(v) => {
unsafe { *out = v };
0
}
Err(_) => -2, // EPARSE
}
}
패턴 4: 통합 빌드 (CMake + Cargo)
# CMakeLists.txt
add_custom_target(rust_lib
COMMAND cargo build --release
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/rust_part
)
add_dependencies(my_app rust_lib)
target_link_libraries(my_app ${CMAKE_SOURCE_DIR}/rust_part/target/release/librust_ffi_lib.so)
패턴 5: 테스트 전략
- 단위 테스트: C 래퍼만 테스트 (각 언어 내부)
- 통합 테스트: C 래퍼를 경계로, 양쪽에서 호출해 검증
- Sanitizer: C++ 쪽에 ASan, TSan 적용해 FFI 호출 경로 검증
구현 체크리스트
- C ABI 경계에
extern "C"/#[no_mangle] extern "C"사용 - 구조체에
#[repr(C)]적용, 레이아웃 검증 - 소유권 계약 문서화 (할당/해제 책임)
- FFI 노출 Rust 함수에서
panic방지 (catch_unwind등) - 에러는 예외 대신 코드/널로 반환
- 빌드 시스템 통합 (CMake + Cargo)
- Sanitizer로 C++ 쪽 검증
8. 정리
| 항목 | 요약 |
|---|---|
| FFI | C ABI 경계·extern “C”·bindgen/cxx |
| Memory Safety | Rust는 언어가 강제, C++는 관례·도구·Sanitizer로 보완 |
| 실무 | 점진적 도입·경계 명확화·팀 역량 고려 |
| 에러 | 링크·ABI 불일치·소유권·panic 방지 |
| 프로덕션 | 얇은 C 래퍼·계약 문서화·에러 코드 반환 |
44-2로 C++와 Rust의 공존·상호 운용·메모리 안전성 논의의 실체를 정리했습니다.
참고 자료
- The Rustonomicon - FFI: Rust FFI 안전성 가이드
- cxx 공식 문서: C++/Rust 브리지
- bindgen: C/C++ 헤더 → Rust 바인딩
- C++ 시리즈 #41-1 Clang-Tidy: C++ 정적 분석으로 FFI 경로 검증
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
- Rust 메모리 안전성 완벽 가이드 | 소유권·Borrow checker·수명·unsafe 실전
이 글에서 다루는 키워드 (관련 검색어)
C++ Rust 연동, FFI, bindgen, cxx, C ABI 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 기존 C++ 레거시를 유지하면서 새 모듈을 Rust로 쓰거나, Rust 라이브러리를 C++ 앱에 통합할 때 FFI를 사용합니다. 문제 시나리오·완전한 예제·에러 해결법을 참고해 적용하면 됩니다.
Q. bindgen과 cxx 중 뭘 쓰나요?
A. 순수 C 또는 C 스타일 C++ 라이브러리면 bindgen이 적합합니다. C++ 클래스·예외·스마트 포인터를 넘나들어야 하면 cxx가 편리합니다. 복잡도가 낮으면 수동 extern "C" 선언만으로도 충분합니다.
| 상황 | 권장 도구 |
|---|
관련 글
- C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]
- C++ SFINAE 완벽 가이드 | enable_if·void_t
- C++ Fold Expression 완벽 가이드 | 단항·이항·쉼표 fold·커스텀 연산자 실전
- C++ 메타프로그래밍의 진화: Template에서 Constexpr, 그리고 Reflection까지
- C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전