이전 글에서는 WebAssembly(WASM)로 러스트 코드를 브라우저 환경에서 실행하는 방법을 다뤘습니다. 이제는 러스트와 C++를 연동하는 FFI(Foreign Function Interface)를 살펴보며, 기존 C++ 코드베이스에 러스트 코드를 통합하거나, 러스트 라이브러리를 C++ 애플리케이션에서 사용하는 방법을 예제로 구현해보겠습니다.
이러한 혼합 언어 접근법은 다음과 같은 상황에 유용합니다.
- 점진적 전환: 기존 C++ 프로젝트 일부를 러스트로 작성해 메모리 안전성과 생산성 향상
- 특정 기능 모듈화: C++로 작성된 대규모 코드베이스에서 성능 크리티컬 모듈을 러스트로 대체, 혹은 안전성을 확보
- 라이브러리 확장: 러스트 크레이트 생태계를 활용해 C++ 프로젝트에 빠르게 기능 추가
C++에서는 헤더 파일과 소스 파일을 통해 ABI를 노출하고, 러스트는 extern "C"을 통한 C ABI를 사용하여 상호 운용할 수 있습니다. 또 cxx 크레이트를 사용하면 C++와 러스트 간 상호 연동을 더 편리하게 구현할 수 있습니다.
프로젝트 개요
이번 예제는 간단한 수학 연산 라이브러리를 러스트로 구현하고, C++ 애플리케이션에서 이를 호출하는 흐름을 시연하겠습니다.
- Rust 라이브러리: 복잡한 수학 연산(예: 피보나치 수 계산, 소수 판별)을 구현한 함수들을 노출
- C++ 프로그램: Rust 라이브러리를 로드해 해당 함수를 호출하고 결과를 출력
이를 통해 FFI를 통한 혼합 언어 프로젝트 구성을 이해하고, C++와 Rust가 어떻게 안전한 경계를 형성할 수 있는지 경험할 수 있습니다.
준비: cxx 크레이트 사용
cxx 크레이트는 Rust와 C++ 사이에 안전한 FFI 바인딩을 생성하는데 도움을 주는 라이브러리입니다. C ABI로만 접근하는 것보다 더 안정적으로 타입, 함수 바인딩을 관리할 수 있습니다.
Rust 쪽에서 cxx를 사용하면, .rs 코드와 .h, .cc 파일을 통해 C++ 타입과 함수를 정의하고 연결할 수 있습니다.
Rust 라이브러리 생성
먼저 러스트 라이브러리를 만듭니다.
cargo new --lib ffi_demo
cd ffi_demo
Cargo.toml:
[package]
name = "ffi_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
cxx = "1.0"
src/lib.rs:
// cxx 브리지 선언
#[cxx::bridge]
mod ffi {
// Rust에서 C++로 공개하고 싶은 함수/타입, 또는 C++에서 Rust로 호출하고 싶은 것들을 정의
extern "Rust" {
fn fibonacci(n: u32) -> u64;
fn is_prime(n: u64) -> bool;
}
}
// 피보나치 수 계산 함수 (단순 재귀는 비효율적이니 반복문으로 구현)
pub fn fibonacci(n: u32) -> u64 {
let (mut a, mut b) = (0, 1);
for _ in 0..n {
let temp = a + b;
a = b;
b = temp;
}
a
}
// 소수 판별 함수 (간단한 방식)
pub fn is_prime(n: u64) -> bool {
if n < 2 {
return false;
}
for i in 2..=((n as f64).sqrt() as u64) {
if n % i == 0 {
return false;
}
}
true
}
이로써 fibonacci와 is_prime 함수가 Rust 라이브러리로 정의되고 cxx 브리지를 통해 C++에서 호출 가능한 ABI로 노출됩니다.
빌드를 하면 target/debug/libffi_demo.a 또는 target/release/libffi_demo.a 형태의 정적 라이브러리가 생성됩니다(플랫폼에 따라 다를 수 있음).
C++ 프로젝트 구성
Rust 라이브러리(ffi_demo)를 C++ 프로젝트에서 링크하고 호출하기 위해 C++ 프로젝트를 생성합니다. 예를 들어 cpp_app 디렉토리를 만들고 main.cpp를 작성합니다.
프로젝트 구조 예:
ffi_demo/ # Rust 라이브러리
cpp_app/
├─ main.cpp
├─ bridge.rs (C++ <-> Rust 연결을 위한 브리지 RS파일, 선택사항)
├─ Cargo.toml (ffi_demo 경로 참조)
├─ Makefile or CMakeLists.txt
실제론 cxx는 빌드 과정에서 bridge 파일을 이용해 자동으로 C++ 헤더와 소스 코드를 생성합니다. 여기서는 단순화를 위해 ffi_demo 라이브러리를 직링크하는 과정을 예제로 보여주겠습니다.
ffi_demo 라이브러리 빌드 후, cxx 빌드 아티팩트(.h, .cc)를 이용해 C++ 쪽에 함수 선언을 제공합니다. 실제 프로덕션 예제에서는 cxx 가이드에 따라 bridge.rs와 include! 매크로를 활용해 자동 코드 생성을 합니다. 여기서는 개념 시연을 위해 약간 단순화합니다.
참고: 실제 구현 시 cxx 문서를 참조해 cxxbridge 명령어로 생성한 C++ 헤더/소스와 Rust 라이브러리를 연동해야 합니다. 여기서는 개념에 집중하기 위해 간략히 설명합니다.
cxxbridge 통한 헤더/소스 생성 (개념)
cxxbridge 명령을 통해 ffi_demo의 cxx::bridge 모듈에서 선언한 함수에 대응하는 C++ 헤더와 소스 파일을 생성할 수 있습니다.
cxxbridge src/lib.rs --header > ffi_demo.h
cxxbridge src/lib.rs --impl > ffi_demo.cc
이렇게 생성된 ffi_demo.h와 ffi_demo.cc를 cpp_app 디렉토리로 복사합니다.
C++ 코드에서 Rust 함수 호출
cpp_app/main.cpp:
#include <iostream>
#include "ffi_demo.h" // cxxbridge로 생성한 헤더
int main() {
uint32_t n = 10;
uint64_t fib = ffi_demo::fibonacci(n);
std::cout << "fibonacci(" << n << ") = " << fib << "\n";
uint64_t x = 37;
bool prime = ffi_demo::is_prime(x);
std::cout << x << (prime ? " is prime\n" : " is not prime\n");
return 0;
}
이제 ffi_demo.a 정적 라이브러리(또는 dylib)를 링크하고, ffi_demo.cc를 컴파일한 뒤 main.cpp를 빌드하면 C++ 프로그램이 Rust 함수를 호출하게 됩니다.
빌드 과정 (개념 예시)
cpp_app/Makefile 예시:
CXX=g++
CXXFLAGS=-std=c++17 -I. -Wall
LDFLAGS=-L../ffi_demo/target/debug -lffi_demo -lpthread
all: main
main: main.cpp ffi_demo.cc ffi_demo.h
$(CXX) $(CXXFLAGS) -c ffi_demo.cc
$(CXX) $(CXXFLAGS) -c main.cpp
$(CXX) $(CXXFLAGS) -o main main.o ffi_demo.o $(LDFLAGS)
clean:
rm -f *.o main
ffi_demo.a는 미리 cargo build로 생성해두었다고 가정합니다. 플랫폼에 따라 라이브러리 이름이 libffi_demo.a일 수도 있으니 적절히 수정하세요.
make 명령을 통해 빌드하고, ./main 실행 시 Rust 함수가 정상적으로 동작하는 것을 확인할 수 있습니다.
확장 아이디어
- C++에서 Rust로 콜백: cxx를 사용하면 C++ 함수를 Rust에서 호출하거나, 반대로 Rust 함수를 C++에서 콜백 형태로 호출하는 것도 가능합니다.
- 복잡한 타입 교환: 단순한 숫자 외에 문자열, 구조체, 벡터 등 복잡한 타입을 교환할 수 있으며 cxx가 이를 안전하게 관리하도록 돕습니다.
- 메모리 관리 문제 최소화: C++는 수동 메모리 관리가 필요하나, cxx를 통해 스마트 포인터나 Rc/Cow 등을 사용해 안전하게 자원 관리 가능.
C++ 대비 장점 재확인
C++에서 Rust 라이브러리를 사용하면 러스트가 제공하는 메모리 안전성, 강력한 타입 시스템, 풍부한 크레이트 생태계를 활용할 수 있습니다. 러스트 코드 내에서 메모리 에러를 사전에 차단하고 C++로는 단순한 ABI를 통해 안전한 함수 호출만 수행하면, 전체 시스템 안정성과 생산성이 향상됩니다.
결론
이번 글에서는 Rust와 C++를 연결하는 FFI(특히 cxx 크레이트)를 통해 혼합 언어 프로젝트를 구성하는 방법을 살펴보았습니다. 이를 통해 기존 C++ 코드베이스에 러스트 모듈을 도입하거나, 러스트 라이브러리를 C++에서 활용할 수 있는 기반을 마련할 수 있습니다.
다음 글에서는 마이크로서비스 아키텍처 적용, Docker 컨테이너화, CI/CD 파이프라인 구축, 성능 최적화 등 실전 프로젝트 운영 단계에서 고려해야 할 주제를 다루며, 러스트를 전체 개발 생태계 속에서 어떻게 활용할 수 있는지 더욱 폭넓게 살펴보겠습니다.
유용한 링크와 리소스
- cxx 크레이트 문서: https://cxx.rs/
- Rust FFI Guide: https://michael-f-bryan.github.io/rust-ffi-guide/
- C++ and Rust Interoperability: https://doc.rust-lang.org/nomicon/ffi.html