C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]

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++에서의 완화 전략
  • 프로덕션 패턴: 점진적 도입·경계 설계·팀 역량

목차

  1. 문제 시나리오: 왜 C++/Rust 연동이 필요한가
  2. C++와 Rust 상호 운용 (FFI)
  3. 완전한 C++/Rust 연동 예제
  4. 자주 발생하는 에러와 해결법
  5. Memory Safety 논의
  6. 실무에서의 선택
  7. 프로덕션 패턴
  8. 정리

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


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가 경계다

  • Rustextern “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. 정리

항목요약
FFIC ABI 경계·extern “C”·bindgen/cxx
Memory SafetyRust는 언어가 강제, C++는 관례·도구·Sanitizer로 보완
실무점진적 도입·경계 명확화·팀 역량 고려
에러링크·ABI 불일치·소유권·panic 방지
프로덕션얇은 C 래퍼·계약 문서화·에러 코드 반환

44-2로 C++와 Rust의 공존·상호 운용·메모리 안전성 논의의 실체를 정리했습니다.

참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • 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 실전