Rust와 C 연결: Cbindgen 및 Cargo-c를 이용한 C 바인딩 및 헤더 생성
Min-jun Kim
Dev Intern · Leapcell

소개
성능, 메모리 안전성 및 동시성에 중점을 둔 Rust는 시스템 프로그래밍부터 웹 어셈블리까지 다양한 분야에서 빠르게 주목받고 있습니다. 그러나 세상은 여전히 C로 돌아가고 있으며, 기존 C 코드베이스와의 상호 운용성은 Rust 라이브러리를 더 큰 프로젝트에 통합하는 데 종종 중요한 요구 사항입니다. Rust의 애플리케이션 이진 인터페이스(ABI)를 직접 사용하는 것은 끊임없는 진화로 인해 불안정하고 복잡합니다. 여기서 안정적인 C 애플리케이션 프로그래밍 인터페이스(API)의 필요성이 대두됩니다. C 호환 바인딩을 통해 Rust 기능을 노출함으로써, 우리는 Rust의 강점을 활용하는 동시에 방대한 C 및 C 호환 언어 생태계와의 호환성을 유지할 수 있습니다. 이 글에서는 두 가지 강력한 도구인 cbindgen
과 cargo-c
를 사용하여 Rust 라이브러리에 대한 C 헤더 파일 및 바인딩을 생성하는 과정을 안내하여 원활한 통합을 가능하게 하고 Rust 창작물에 대한 새로운 가능성을 열어줍니다.
교차 언어 상호 운용성을 위한 핵심 개념
실제적인 측면을 자세히 살펴보기 전에, Rust와 C를 연결하는 데 관련된 핵심 개념에 대한 기초적인 이해를 확립해 보겠습니다.
- 외부 함수 인터페이스(FFI): FFI는 한 프로그래밍 언어로 작성된 프로그램이 다른 프로그래밍 언어로 작성된 함수를 호출하거나 서비스를 사용할 수 있도록 하는 메커니즘입니다. 우리 맥락에서 Rust의 FFI는 C 코드와 상호 작용하고 그 반대도 가능하게 합니다.
- C ABI (애플리케이션 이진 인터페이스): C ABI는 함수 호출 방법, 메모리 내 데이터 레이아웃, 함수 간 매개변수 및 반환 값 전달 방법을 정의합니다. Rust 함수를 C에 노출할 때, 정의되지 않은 동작 및 충돌을 방지하기 위해 C ABI 규칙을 준수해야 합니다.
no_mangle
속성: Rust에서 함수 이름은 오버로딩 및 고유 심볼 식별을 지원하기 위해 컴파일러에 의해 "매글링"됩니다.pub extern "C"
함수 앞에 배치된#[no_mangle]
속성은 이 이름 매글링을 방지하여 컴파일된 출력에서 함수의 이름이 소스 코드 이름과 일치하도록 하여 C 컴파일러가 이를 발견할 수 있도록 합니다.extern "C"
호출 규칙: 이는 함수가 C 호출 규칙을 사용해야 함을 지정하며, 이는 스택에 인수가 푸시되는 방법, 반환 값이 처리되는 방법, 호출 후 스택이 정리되는 방법을 결정합니다. FFI 상호 작용이 올바르게 이루어지려면 이를 준수하는 것이 중요합니다.cbindgen
:pub extern "C"
함수 및 C 호환 데이터 구조로 주석 처리된 Rust 코드에서 C/C++ 헤더 파일을 자동으로 생성하는 도구입니다. 오류가 발생하기 쉽고 지루할 수 있는 헤더 파일 수동 작성 프로세스를 단순화합니다.cargo-c
: Rust 프로젝트에서 C 호환 라이브러리를 만드는 데 도움이 되는 Cargo 하위 명령입니다. 빌드 프로세스를 자동화하여 필요한 C 헤더, 정적 라이브러리 및 동적 라이브러리를 생성하며, C 빌드 시스템과의 통합을 용이하게 하는pkg-config
파일을 생성할 수도 있습니다.
C 헤더 및 바인딩 생성
cbindgen
과 cargo-c
가 실제 작동하는 방식을 보여주는 예를 통해 단계별로 진행해 보겠습니다. 피보나치 수열을 계산하고 이를 C에 노출하는 간단한 Rust 라이브러리를 만들 것입니다.
1단계: Rust 라이브러리 초기화
먼저 새 Rust 라이브러리 프로젝트를 만듭니다.
cargo new --lib fib_lib cd fib_lib
2단계: Rust 기능 구현
src/lib.rs
를 편집하여 피보나치 함수를 포함하고 extern "C"
및 no_mangle
을 사용하여 노출합니다. 간단한 구조체도 정의합니다.
// src/lib.rs #[derive(Debug)] #[repr(C)] // C 호환 메모리 레이아웃 보장 pub struct MyData { pub value: i32, pub name: *const std::os::raw::c_char, // C 호환 문자열 포인터 } /// nth 피보나치 수를 계산합니다. /// # 안전 /// 이 함수는 음수가 아닌 n에 대해 안전하게 호출할 수 있습니다. #[no_mangle] pub extern "C" fn fibonacci(n: i32) -> i32 { if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) } } /// 제공된 데이터를 사용하여 메시지를 출력합니다. /// # 안전 /// `data` 포인터는 유효해야 하며 `MyData` 구조체를 가리켜야 합니다. /// `MyData` 내의 `name` 필드는 유효한 C 스타일 문자열 포인터 또는 null이어야 합니다. #[no_mangle] pub extern "C" fn greet_with_data(data: *const MyData) { if data.is_null() { println!("Received null data."); return; } unsafe { let actual_data = *data; let c_str = std::ffi::CStr::from_ptr(actual_data.name); let name_str = c_str.to_string_lossy(); println!("Hello, {}! Your value is {}", name_str, actual_data.value); } }
FFI에 대한 주요 고려 사항:
#[repr(C)]
: 이 속성은 구조체에 중요합니다. Rust 컴파일러에게 C 컴파일러가 메모리 내 구조체 필드를 구성하는 방식과 정확히 동일하게 구성하도록 지시하여 예상치 못한 패딩 또는 재정렬로 인해 ABI 호환성 문제가 발생하는 것을 방지합니다.*const std::os::raw::c_char
: Rust의String
및str
타입은 C와 호환되지 않습니다. FFI 경계를 넘어 문자열을 전달하려면 C 스타일의 null로 종료되는 문자열을 사용하는데, 이는*const std::os::raw::c_char
(불변 문자열의 경우) 또는*mut std::os::raw::c_char
(가변 문자열의 경우)로 표현됩니다. Rust의 소유권 규칙이 원시 포인터에는 적용되지 않으므로 양측에서 신중한 메모리 관리가 필요하다는 점을 기억하십시오. 이 예에서는name
필드에 대한 메모리를 C 측에서 소유하고 관리한다고 가정합니다.unsafe
블록: 원시 포인터 및 C FFI를 다룰 때unsafe
블록을 자주 접하게 됩니다. 이러한 블록은 내부 코드가 원시 포인터 역참조 또는 C 함수 호출과 같이 Rust 컴파일러가 메모리 안전성을 보장할 수 없는 작업을 수행함을 나타냅니다. 이러한 작업의 안전성을 보장하는 것은 개발자의 책임입니다.
3단계: cbindgen
통합
Cargo.toml
에 cbindgen
을 빌드 종속성으로 추가합니다.
# Cargo.toml [package] name = "fib_lib" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "staticlib"] # 동적 및 정적 C 라이브러리 생성 [build-dependencies] cbindgen = "0.24" # 최신 안정 버전 사용
build.rs
빌드 스크립트를 만들어 cbindgen
을 실행합니다.
// build.rs extern crate cbindgen; use std::env; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .generate() .expect("Unable to generate bindings") .write_to_file("target/fib_lib.h"); // target 디렉토리에 출력 }
이제 cargo build
를 실행하면 cbindgen
이 자동으로 target/fib_lib.h
를 생성합니다.
4단계: cargo-c
통합
cargo-c
는 Rust 라이브러리를 C 호환 라이브러리로 패키징하는 것을 단순화합니다. 먼저 설치합니다.
cargo install cargo-c
이제 cargo cbuild
를 실행하여 C 라이브러리를 빌드하기만 하면 됩니다. 이 명령은 다음을 수행합니다.
- Rust 코드를 정적 라이브러리(
.a
) 및 동적 라이브러리(.so
- Linux,.dylib
- macOS,.dll
- Windows)로 컴파일합니다. fib_lib.h
를 생성하는build.rs
스크립트를 실행합니다.- 이러한 출력 아티팩트들을 구조화된 디렉토리(예:
target/release/capi/
)에 배치합니다.
정규 빌드 중에 헤더가 target
디렉토리에 직접 출력되도록 build.rs
를 약간 수정해 보겠습니다. 그러면 cargo-c
가 이를 표준 C 호환 출력 구조로 이동시킵니다.
cargo cbuild --release
를 실행한 후 target/release/capi
에서 생성된 파일을 찾을 수 있습니다.
target/release/capi/
├── include/
│ └── fib_lib.h
├── lib/
│ ├── libfib_lib.a
│ └── libfib_lib.so (또는 .dylib/.dll)
└── pkgconfig/
└── fib_lib.pc
fib_lib.h
파일은 다음과 유사하게 보일 것입니다(간소화됨).
// fib_lib.h (cbindgen으로 생성됨) #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> typedef struct MyData { int32_t value; const char *name; } MyData; int32_t fibonacci(int32_t n); void greet_with_data(const struct MyData *data);
5단계: C에서 라이브러리 사용
Rust 라이브러리를 사용하기 위해 C 파일(예: main.c
)을 만듭니다.
// main.c #include <stdio.h> #include <stdlib.h> // malloc, free용 #include <string.h> // strdup용 #include "fib_lib.h" // 생성된 헤더 포함 int main() { // fibonacci 함수 테스트 int n = 10; int result = fibonacci(n); printf("Fibonacci(%d) = %d\n", n, result); // greet_with_data 함수 테스트 MyData my_c_data; my_c_data.value = 42; my_c_data.name = strdup("World from C"); // C 스타일 문자열 할당 greet_with_data(&my_c_data); free((void*)my_c_data.name); // 할당된 C 스타일 문자열 해제 return 0; }
생성된 Rust 라이브러리로 C 프로그램을 컴파일하고 링크합니다.
# main.c 및 fib_lib.h, .a/.so 파일이 포함된 디렉토리에 있다고 가정 # (target/release/capi를 가리키도록 경로 조정) # 동적 링크용 (Linux/macOS) gcc main.c -o my_c_app -Itarget/release/capi/include -Ltarget/release/capi/lib -lfib_lib # 정적 링크용 (Linux/macOS) # gcc main.c -o my_c_app -Itarget/release/capi/include target/release/capi/lib/libfib_lib.a # C 애플리케이션 실행 ./my_c_app
다음과 유사한 출력이 표시되어야 합니다.
Fibonacci(10) = 55
Hello, World from C! Your value is 42
이는 성공적인 상호 운용성을 보여줍니다. C 프로그램이 Rust 함수를 호출하고 Rust 정의 데이터 구조를 원활하게 사용하고 있습니다.
애플리케이션 시나리오
- 기존 C/C++ 코드베이스에 Rust 통합: Rust의 메모리 안전성 및 성능을 전체 애플리케이션을 다시 작성하지 않고 중요한 구성 요소에 활용합니다.
- 임베딩 가능한 모듈 생성: C 또는 C FFI 지원이 있는 다른 언어로 작성된 애플리케이션에서 로드하여 사용할 수 있는 Rust 기반 플러그인 또는 확장을 구축합니다.
- 저수준 OS 구성 요소 개발: 드라이버, 커널 모듈 또는 부트로더와 같이 C 인터페이스와 상호 작용해야 하는 구성 요소를 위해 Rust를 사용합니다.
- 교차 언어 개발: Rust와 C로 작업하는 팀이 두 언어를 철저히 배울 필요 없이 협업을 촉진합니다.
결론
cbindgen
과 cargo-c
를 능숙하게 사용하면 Rust 프로젝트에서 C 호환 헤더 및 라이브러리를 쉽게 생성할 수 있습니다. 이 강력한 조합은 무한한 가능성을 열어주며, 견고하고 안전한 Rust 코드를 방대한 C 생태계에 원활하게 통합할 수 있도록 합니다. 이 연결 기능은 Rust가 다양한 소프트웨어 환경 전반에서 도달 범위와 영향을 확장할 수 있는 실용적이고 효율적인 경로를 제공합니다.