러스트 스터디 막주 발표가 나라서 발제를 위한 정리함
러스트 프로그래밍 언어에서 외부 함수 인터페이스(FFI, Foreign Function Interface)를 활용하여러스트 코드에서 C로 작성된 함수와 일부 C++로 작성된 함
수를 호출할 수 있다.
러스트는 시스템 프로그래밍 언어로서 메모리 안전성을 강조하지만, 기존의 C/C++로 작성된 라이브러리를 활용하거나 시스템 호출을 직접 다루어야 하는 경우가 많다. 이때
FFI
는 러스트 코드가 외부 함수와 데이터를 교환할 수 있게 해주는 중요한 메커니즘이다.
외부 함수 인터페이스(FFI)
는 두 가지 이상의 프로그래밍 언어가 서로의 코드를 호출할 수 있도록 하는 메커니즘이다. 이는 주로 시스템 프로그래밍, 운영 체제 API 호출, 네트워크 통신, 하드웨어 제어, 고성능 연산이 필요한 분야에서 유용하다. 러스트는 다른 언어와의 상호작용을 위한 안전하고 효율적인 도구를 제공하며, FFI를 통해 기존 C/C++ 라이브러리와 쉽게 통합할 수 있다.
러스트는 메모리 안전성과 소유권 모델을 통해 안전한 코드 작성을 장려합니다. 그러나 FFI를 사용할 때는 이러한 안전성이 완전히 보장되지 않습니다. 외부 언어와의 상호작용에서 데이터 타입과 메모리 레이아웃의 호환성을 맞추는 것이 매우 중요합니다. 이를 소홀히 하면 메모리 손상, 충돌, 예기치 않은 동작이 발생할 수 있습니다.
러스트와 C는 서로 다른 메모리 표현 방식과 데이터 정렬 방식을 사용하기 때문에, 두 언어 간의 데이터 타입을 호환시키는 것이 필요합니다. 이를 위해 러스트에서는 libc
크레이트를 사용하여 C의 기본 데이터 타입과 호환되는 타입을 제공합니다. 예를 들어, C의 int
타입은 러스트에서 c_int
로, char
타입은 c_char
로 매핑됩니다.
extern crate libc;
use libc::{c_int, c_char};
#[repr(C)]
struct MyStruct {
field1: c_int,
field2: c_char,
}
여기서 #[repr(C)]
어트리뷰트는 구조체의 메모리 레이아웃을 C와 동일하게 맞추도록 합니다. 이 어트리뷰트를 사용하지 않으면 러스트 컴파일러는 구조체의 메모리 레이아웃을 최적화할 수 있으며, 이는 C 코드와의 호환성 문제를 일으킬 수 있습니다. 예를 들어, 멀티바이트 데이터가 포함된 구조체의 경우 데이터 정렬 방식에 따라 구조체의 크기와 메모리 배치가 달라질 수 있습니다. 따라서 #[repr(C)]
는 FFI에서 매우 중요합니다.
이 문서는 러스트와 C 언어 사이의 공통 데이터 표현에 대해 다룹니다. 러스트와 C는 시스템 프로그래밍 언어로서, 기계 수준에서 데이터 표현 방식이 유사한 경우가 많습니다. 이러한 공통점 덕분에 러스트 프로그램이 C 라이브러리와 상호작용할 수 있습니다. 그러나 두 언어 간의 데이터 타입, 메모리 레이아웃, 문자열 처리 방식 등이 다르기 때문에, 상호 운용성을 보장하려면 특정 규칙을 따라야 합니다. 본 문서는 이를 해결하기 위한 기법들을 설명합니다.
러스트와 C는 시스템 프로그래밍 언어로서 기계 수준에서 데이터가 메모리에 어떻게 배치되는지에 대한 유사점을 공유한다. 예를 들어, 러스트의 usize
와 C의 size_t
는 동일한 데이터 표현을 가진다. 또한, 두 언어 모두 구조체를 지원
하며, 구조체의 필드
가 메모리에 순서대로 배치된다.
러스트의 데이터 타입은 C와의 호환성을 위해 std::os::raw
모듈을 통해 매핑된다. 이 모듈에는 C와 동일한 표현을 보장하는 여러 타입이 정의되어 있다.
C 타입 | 러스트 std::os::raw 타입 |
---|---|
short | c_short |
int | c_int |
long | c_long |
long long | c_longlong |
unsigned short | c_ushort |
unsigned /unsigned int | c_uint |
unsigned long | c_ulong |
unsigned long long | c_ulonglong |
char | c_char |
signed char | c_schar |
unsigned char | c_uchar |
float | c_float |
double | c_double |
void * , const void * | *mut c_void , *const c_void |
c_void
를 제외한 모든 타입은 러스트의 기본 타입에 대한 별칭이다. 예를 들어, c_char
는 보통 i8
타입에 해당한다.즉, 이 설명은 std::os::raw 모듈에 정의된 타입들이 실제로는 러스트의 기본 타입에 대한 별칭(alias)이라는 뜻이다. 즉, 이들 타입은 새로운 타입이 아니라, 기존의 러스트 기본 타입에 이름만 다른 별칭을 붙인 것에 불과하다.
예를 들어, std::os::raw::c_char는 보통 i8 타입에 해당한다. 이는 c_char라는 이름을 통해 C 언어의 char 타입을 표현하는 것이며, 러스트에서는 실제로 i8 타입으로 구현된다는 의미이다. 따라서, C 언어와의 호환성을 위해 c_char라고 부르지만, 실제로는 러스트의 i8 타입을 사용하게 된다.
즉, c_char는 러스트에서 다음과 같은 식으로 정의될 수 있다:
type c_char = i8;
다만, c_void
의 경우는 예외로, 포인터로만 사용되며 실제 데이터 타입이 없는 타입이다.
이러한 별칭을 사용하는 이유는, 코드 작성 시 C와의 상호작용에서 타입의 의미를 더 명확하게 하기 위해서이다.
usize
와 isize
는 C의 size_t
와 ptrdiff_t
와 동일한 표현을 갖는다.러스트와 C는 기본 타입의 호환성을 제공하지만, 타입별로 다음과 같은 차이점이 존재할 수 있다:
int
는 대개 32비트이지만, 16비트나 64비트일 수도 있다. 따라서 c_int
와 같은 적절한 타입을 사용해 타입 호환성을 보장해야 한다.char
는 32비트 유니코드 문자로, C의 char
와는 다르다. C의 char
는 c_char
로 매핑되며, 보통 i8
또는 u8
타입을 사용한다.*mut T
또는 *const T
로 매핑된다.#[repr(C)]
어트리뷰트를 사용해야 한다. 이 어트리뷰트를 사용하면 러스트는 C와 동일한 메모리 배치를 따른다.#[repr(C)]
어트리뷰트 사용C와 호환되는 구조체를 정의할 때는
#[repr(C)]
어트리뷰트를 사용해야 한다.
이 어트리뷰트는 구조체의 필드를 메모리에 순서대로 배치하게 하여 C 컴파일러의 규칙을 따르게 한다. 이 어트리뷰트를 적용하면 러스트 컴파일러가 C 컴파일러와 동일한 방식으로 데이터를 메모리에 배치하게 된다. 이는 주로 FFI(Foreign Function Interface)를 통해 C와 데이터를 주고받을 때 호환성을 유지하기 위해 필요하다.
다음은 C 언어로 정의된 구조체와 이를 러스트에서 매핑하는 예제이다. 러스트에서는 기본적으로 구조체의 필드 순서가 컴파일러에 의해 최적화될 수 있다. 그러나 #[repr(C)]를 사용하면 구조체 필드가 선언된 순서대로 메모리에 배치되므로 C와 동일한 메모리 레이아웃을 보장할 수 있다.
C 코드:
typedef struct {
char *message;
int klass;
} git_error;
러스트 코드:
use std::os::raw::{c_char, c_int};
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int,
}
위 예제에서 git_error 구조체는 C에서 동일한 메모리 레이아웃을 가지므로 C 코드와 직접 상호작용할 수 있다.
enum
) 및 유니언(union
) 매핑러스트의 이넘과 C의 이넘도 #[repr(C)]
어트리뷰트를 통해 호환성을 보장할 수 있다. 러스트에서 이넘은 기본적으로 최적화된 표현을 사용하지만, #[repr(C)]
를 붙이면 C의 int
와 동일한 크기를 가진다.
C 코드:
enum git_error_code {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4
};
러스트 코드:
#[repr(C)]
#[allow(non_camel_case_types)]
enum git_error_code {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
}
이 코드에서
#[repr(C)]
는 러스트 이넘이 C의int
크기를 따르도록 한다. 이를 통해 C와 러스트 간의 데이터 교환이 가능하다.
참고로 이넘
은 이넘은 "열거형"이라고 불리며, 특정한 이름을 가진 여러 상수 값을 나열한 데이터 타입입니다. 이넘을 사용하면 코드의 가독성을 높이고, 값의 의미를 더 명확하게 표현할 수 있다. 이넘은 주로 상태, 옵션, 플래그 등을 나타내는 데 유용하다.
C 언어에서의 유니언
union Number {
int integer;
float floating_point;
};
러스트에서 유니언을 정의하고 사용하는 방법:
#[repr(C)]
union Number {
integer: i32,
floating_point: f32,
}
fn main() {
let num = Number { integer: 42 };
unsafe {
println!("Integer value: {}", num.integer);
}
}
유니언
은 여러 데이터 타입을 한 번에 저장할 수 있는 메모리 공간을 공유하는 방식이다. 유니언을 사용하면 여러 타입 중 하나의 값을 저장할 수 있다. 유니언의 주요 특징은 모든 필드가 동일한 메모리 위치를 공유한다는 점입이다. 따라서, 한 번에 하나의 값만 저장할 수 있으며, 어떤 필드가 유효한지 추적해야 한다.
러스트의 유니언은 unsafe 블록 안에서만 안전하게 접근할 수 있는 반면, 이넘은 안전하게 사용할 수 있다.
unsafe
블록을 사용하는 이유는 외부 함수 호출이 메모리 안전성을 보장하지 않기 때문이다.
C에서는 태그된 유니언이라는 패턴을 통해 다양한 타입의 데이터를 표현할 수 있다. 이는 유니언과 함께 해당 유니언이 어떤 데이터를 나타내는지를 가리키는 태그 값을 가진 구조체로 구성된다.
예제(C 코드):
enum tag {
FLOAT = 0,
INT = 1,
};
union number {
float f;
int i;
};
struct tagged_number {
enum tag t;
union number n;
};
러스트
#[repr(C)] // C 언어와 호환되는 메모리 레이아웃을 지정합니다.
enum Tag {
Float = 0, // 태그 값이 0인 경우, 값이 부동소수점(f32) 형식임을 나타냅니다.
Int = 1, // 태그 값이 1인 경우, 값이 정수(i32) 형식임을 나타냅니다.
}
#[repr(C)] // C 언어와 호환되는 메모리 레이아웃을 지정합니다.
union FloatOrInt {
f: f32, // 유니언의 첫 번째 필드: 32비트 부동소수점 숫자
i: i32, // 유니언의 두 번째 필드: 32비트 정수
}
#[repr(C)] // C 언어와 호환되는 메모리 레이아웃을 지정합니다.
struct Value {
tag: Tag, // 값의 타입을 나타내는 태그 필드 (Float 또는 Int)
union: FloatOrInt, // 실제 값을 저장하는 유니언 필드
}
fn is_zero(v: Value) -> bool {
unsafe { // 유니언의 데이터를 안전하지 않게 접근하므로 unsafe 블록을 사용해야 합니다.
match v {
// 태그가 Int이고, 유니언의 i 필드가 0일 때 true를 반환
Value { tag: Tag::Int, union: FloatOrInt { i: 0 } } => true,
// 태그가 Float이고, 유니언의 f 필드가 0.0일 때 true를 반환
Value { tag: Tag::Float, union: FloatOrInt { f: num } } => (num == 0.0),
// 그 외의 경우에는 false를 반환
_ => false,
}
}
}
위 예제에서 is_zero 함수는 태그에 따라 유니언 필드를 확인하여 값이 0인지 검사한다.
러스트와 C의 문자열 표현 방식은 다르다. C의 문자열은 null 문자로 끝나는 char*
이며, 러스트의 String
또는 &str
은 길이를 명시적으로 저장하는 UTF-8 인코딩 문자열이다. 이러한 차이 때문에 문자열을 서로 변환해야 할 때 주의가 필요하다.
러스트의 std::ffi
모듈은 이를 위한 CString
과 CStr
타입을 제공한다. CString
은 null 종단 문자열을 소유하는 타입이고, CStr
은 차용된 문자열을 나타낸다.
use std::ffi::CString; // CString 타입을 사용하기 위해 std::ffi 모듈을 가져옵니다.
use libc::c_char; // C 문자열 타입인 c_char를 사용하기 위해 libc 모듈을 가져옵니다.
// 외부 C 함수 선언
extern "C" {
fn some_c_function(input: *const c_char); // C 함수 some_c_function을 외부에서 가져옵니다.
// 이 함수는 C 문자열(*const c_char)을 인자로 받습니다.
}
fn main() {
let rust_string = "Hello, C"; // 러스트 문자열을 정의합니다.
let c_string = CString::new(rust_string).expect("CString::new failed");
// CString::new()를 사용해 러스트 문자열을 C에서 사용 가능한 CString으로 변환합니다.
// 문자열에 '\0'이 포함되어 있으면 변환에 실패하므로 expect()로 에러 처리를 합니다.
let c_ptr = c_string.as_ptr();
// C 문자열의 포인터를 가져옵니다. 이 포인터는 C 함수에서 사용할 수 있습니다.
unsafe {
some_c_function(c_ptr);
// C 함수 some_c_function을 호출합니다. 이때 unsafe 블록을 사용해야 합니다.
// 외부 C 코드를 호출하는 것은 러스트의 안전성 검사 범위를 벗어나기 때문입니다.
}
}
이 예제에서는 CString::new
를 사용하여 러스트 문자열을 null 종단 문자열로 변환한다. 그런 다음, as_ptr
로 C 함수에 전달할 수 있는 포인터를 얻는다.
FFI 사용 시 러스트의 안전성을 보장하기 위해 여러 기법을 사용해야 한다. 메모리 해제, 에러 처리, 라이프타임 관리 등에서 주의가 필요하다.
extern crate libc;
use libc::{c_char, malloc, free};
use std::ptr;
extern "C" {
fn some_c_function(input: *mut c_char);
}
fn main() {
unsafe {
let ptr: *mut c_char = malloc(100) as *mut c_char;
if ptr.is_null() {
panic!("Failed to allocate memory");
}
some_c_function(ptr);
free(ptr as *mut libc::c_void);
}
}
위 예제는 C 스타일의 메모리 할당과 해제를 올바르게 관리하는 방법을 보여준다.
extern
블록을 사용하여 러스트 코드에서 외부 C 함수와 전역 변수를 선언하고 사용하는 방법을 설명한다.
extern
블록을 사용한 외부 함수 선언
extern
블록은 러스트 실행 파일에 링크된 외부 라이브러리에 정의된 함수나 변수를 선언할 때 사용한다.
예를 들어, 표준 C 라이브러리에 정의된 strlen
함수를 러스트 코드에서 사용하려면
use std::os::raw::c_char;
extern {
fn strlen(s: *const c_char) -> usize;
}
extern
블록 안에서 strlen
함수의 시그니처를 선언하여 러스트가 이 함수의 이름과 타입을 알 수 있게 합니다. 함수의 정의는 이후에 링크된다.strlen
은 C에서 문자열의 길이를 반환하는 함수로, *const c_char
타입의 문자열 포인터를 인자로 받는다.unsafe
블록과 외부 함수 호출외부 함수는
unsafe
로 선언된 함수로 간주된다. 따라서 호출할 때도unsafe
블록을 사용해야 합니다. 이는 외부 함수 호출 시 메모리 안전성을 보장할 수 없기 때문이다.
예를 들어, strlen
을 호출하는 코드
use std::ffi::CString;
let rust_str = "I'll be back";
let null_terminated = CString::new(rust_str).unwrap(); // CString으로 변환하여 널 종료 문자열 생성
unsafe {
assert_eq!(strlen(null_terminated.as_ptr()), 12); // unsafe 블록 내에서 strlen 호출
}
CString::new
함수는 주어진 문자열이 널 문자를 포함하고 있지 않으면, 끝에 널 바이트를 추가하여 CString을 만든다. CString은 C 스타일 문자열을 표현하며, 이를 통해 외부 C 함수와 안전하게 상호 작용할 수 있다.as_ptr
메서드를 사용하여 C 문자열의 포인터를 얻는다.CString::new
의 동작과 비용CString::new
는 Into<Vec<u8>>
를 구현한 타입을 인수로 받는다. &str
를 전달하면 힙에 새로 할당된 문자열의 복사본을 만들어야 하므로 할당과 복사가 발생한다.String
을 값으로 넘기면 그 문자열을 소비하여 새로운 할당이 필요 없다.CString
은 CStr
로 역참조할 수 있으며, CStr
의 as_ptr
메서드를 사용해 C 스타일 문자열 포인터를 얻을 수 있다.extern
블록에서는 외부 전역 변수도 선언할 수 있다. 예를 들어, POSIX 시스템의 environ
변수는 환경 변수 목록을 가리키는 포인터 배열이다.
use std::os::raw::c_char;
extern {
static environ: *mut *mut c_char;
}
extern char **environ
과 대응된다.전역 변수 environ
의 첫 번째 환경 변수를 출력하는 예제
use std::ffi::CStr;
unsafe {
if !environ.is_null() && !(*environ).is_null() {
let var = CStr::from_ptr(*environ); // C 문자열을 CStr로 변환
println!("first environment variable: {}", var.to_string_lossy());
}
}
environ
이 null이 아닌지 확인한 후, CStr::from_ptr
을 사용해 C 문자열을 러스트의 CStr
로 변환한다.to_string_lossy
메서드는 CStr
을 UTF-8 문자열로 변환합니다. 만약 문자열이 유효한 UTF-8이면 그대로 반환하고, 그렇지 않으면 유니코드 대체 문자로 변환한다.이러한 방법으로
extern
블록을 통해 C 함수와 전역 변수를 선언하고, 러스트에서 이를 호출하여 사용할 수 있다. 이를 통해 러스트와 C 간의 상호 운용성을 확보할 수 있다.
러스트 프로그램에서 특정 C 라이브러리의 함수를 사용하려면, 해당 라이브러리를 러스트 코드에 링크해야 한다.
이를 위해
#[link]
어트리뷰트를 사용하고,extern
블록을 이용해 외부 함수를 선언할 수 있다.
libgit2
라이브러리 사용코드 예시
use std::os::raw::c_int;
#[link(name = "git2")] // "git2" 라이브러리를 링크
extern {
pub fn git_libgit2_init() -> c_int; // 외부 함수 선언
pub fn git_libgit2_shutdown() -> c_int; // 외부 함수 선언
}
fn main() {
unsafe {
git_libgit2_init(); // 외부 함수 호출
git_libgit2_shutdown(); // 외부 함수 호출
}
}
설명
#[link(name = "git2")]
어트리뷰트는 러스트가 git2
라이브러리를 최종 실행 파일에 링크하도록 한다.extern
블록 내부에서 외부 C 라이브러리 함수들을 선언한다.unsafe
블록에서 호출한다.libgit2
와 같은 C 라이브러리를 사용하려면, 먼저 라이브러리를 시스템에 설치하거나 직접 빌드해야 한다.libgit2
사용하기러스트 프로그램 생성
$ cargo new --bin git-toy
$ cd git-toy
cargo
명령어를 사용해 러스트 프로젝트를 생성프로그램 빌드 및 실행 시 문제
만약 cargo run
을 통해 빌드하면, libgit2
라이브러리를 찾지 못할 경우 아래와 같은 오류가 발생한다.
error: linking with 'cc' failed: exit status: 1
= note: /usr/bin/ld: error: cannot find -lgit2
빌드 스크립트 작성 (build.rs)
Cargo.toml
파일이 있는 디렉터리에 build.rs
파일을 생성하고 다음 내용을 추가한다.
fn main() {
println!("cargo:rustc-link-search=native=/path/to/libgit2/build");
}
이 스크립트는 cargo
가 빌드할 때 실행되며, 라이브러리의 검색 경로를 추가한다.
공유 라이브러리 설정
리눅스, 맥OS, 윈도우에서 각각의 환경 변수 설정을 통해 실행 시 라이브러리를 찾을 수 있도록 한다.
# 리눅스
$ export LD_LIBRARY_PATH=/path/to/libgit2/build:$LD_LIBRARY_PATH
# 맥OS
$ export DYLD_LIBRARY_PATH=/path/to/libgit2/build:$DYLD_LIBRARY_PATH
# 윈도우
> set PATH=C:\path\to\libgit2\build\Debug;%PATH%
-sys
접미사를 붙이는 것이 일반적이다. 예를 들어, git2
라이브러리에 대한 러스트 바인딩은 git2-sys
라는 이름을 사용할 수 있다.libgit2의 사용법을 제대로 이해하기 위해서는 다음의 두 가지 질문에 답해야 한다.
- 러스트에서 libgit2 함수를 쓰려면 무엇이 필요할까?
- 여기에 안전한 러스트 인터페이스를 구축하려면 어떻게 해야 할까?
libgit2
함수를 사용하기 위해 필요한 것libgit2
와 같은 외부 C 라이브러리를 러스트에서 사용하려면 몇 가지 준비 단계와 규칙을 따라야 한다.
FFI 설정:
extern
블록과 #[link]
어트리뷰트를 사용해 라이브러리 함수를 선언하고, 실행 파일을 빌드할 때 라이브러리를 링크해야 한다.라이브러리 링크 설정:
#[link(name = "libgit2")]
와 같은 어트리뷰트를 사용해 러스트가 외부 라이브러리를 연결하도록 지시할 수 있다. build.rs
파일을 작성하여 빌드 시점에 컴파일 플래그를 설정하거나 라이브러리 검색 경로를 추가할 수 있다.메모리 안전성 확보:
unsafe
블록 내에서 수행되며, 개발자가 메모리 관리를 명시적으로 처리해야 한다.데이터 정렬:
#[repr(C)]
어트리뷰트를 사용하여 구조체의 메모리 정렬을 C와 동일하게 설정해야 합니다. 이는 데이터 정렬 문제를 방지하기 위함이다.extern crate libc;
use libc::{c_int, c_char};
#[link(name = "git2")]
extern "C" {
fn git_libgit2_init() -> c_int;
fn git_libgit2_shutdown() -> c_int;
}
fn main() {
unsafe {
// libgit2 초기화 함수 호출
git_libgit2_init();
println!("libgit2 initialized");
// libgit2 종료 함수 호출
git_libgit2_shutdown();
println!("libgit2 shutdown");
}
}
위 코드는
libgit2
의 초기화 및 종료 함수를 호출하는 기본적인 FFI 사용 예시이다.unsafe
블록 내에서 외부 함수를 호출하며, FFI를 통해 외부 라이브러리와 상호작용할 수 있다.
러스트의 FFI는 기본적으로 안전하지 않으므로, 외부 라이브러리를 사용할 때 안전한 인터페이스를 구축하는 것이 중요하다. 이를 위해 몇 가지 권장 사항과 기법을 따를 수 있다.
extern crate libc;
use libc::{malloc, free, c_char};
use std::ptr;
extern "C" {
fn some_c_function(input: *mut c_char);
}
fn main() {
unsafe {
// C 스타일 메모리 할당
let ptr: *mut c_char = malloc(100) as *mut c_char;
if ptr.is_null() {
panic!("Failed to allocate memory");
}
// 외부 함수 호출
some_c_function(ptr);
// 메모리 해제
free(ptr as *mut libc::c_void);
}
}
위의 예제에서는 malloc
을 통해 메모리를 할당하고, 외부 C 함수 호출 후 free
를 사용하여 메모리를 해제한다. 메모리 해제 규칙을 준수하는 것이 중요하다.
'a
)와 PhantomData
를 활용할 수 있다.use std::marker::PhantomData;
#[repr(C)]
pub struct Repository {
raw: *mut libc::c_void, // libgit2의 git_repository 구조체 포인터
_marker: PhantomData<()>,
}
impl Drop for Repository {
fn drop(&mut self) {
unsafe {
// libgit2의 메모리 해제 함수 호출
raw::git_repository_free(self.raw);
}
}
}
Drop
트레이트를 구현하여 객체가 스코프에서 벗어날 때 메모리를 해제하도록 한다.
러스트의 에러 처리 방식을 활용하여, libgit2
와 같은 외부 라이브러리에서 발생하는 에러를 러스트다운 방식으로 처리할 수 있다.
#[derive(Debug)]
pub struct Error {
code: i32,
message: String,
class: i32,
}
pub type Result<T> = std::result::Result<T, Error>;
fn check(code: i32) -> Result<i32> {
if code >= 0 {
Ok(code)
} else {
Err(Error {
code,
message: "An error occurred".to_string(),
class: 0,
})
}
}
FFI
를 통해 호출되는 외부 함수의 스레드 안전성을 보장하기 위해 필수적이다.bindgen
을 사용한 자동 바인딩 생성bindgen
도구를 사용하면 C 헤더 파일을 분석하여 러스트 바인딩을 자동으로 생성할 수 있다. 이는 대규모 C 라이브러리와 연동 작업을 간소화한다.
bindgen
을 사용한 예제bindgen path/to/header.h -o bindings.rs
위 명령을 사용하여 C 헤더 파일에서 러스트 바인딩 파일(bindings.rs
)을 생성할 수 있다. 이를 통해 C 라이브러리의 구조체, 함수 등을 러스트에서 쉽게 사용할 수 있다.
FFI를 사용할 때 성능을 최적화하기 위해 다음과 같은 전략을 사용할 수 있다.
호출 빈도 줄이기:
메모리 정렬 최적화:
#[repr(C)]
를 사용하여 메모리 정렬을 일관되게 설정한다.배치된 데이터 처리:
배치(batch) 단위
로 데이터를 처리하여 호출 오버헤드를 줄이고 성능을 향상시킬 수 있다.러스트에서 libgit2
와 같은 외부 라이브러리를 FFI를 통해 사용하려면, 메모리 관리, 데이터 정렬, 라이프타임 규칙 등을 고려해야 한다. FFI의 비안전성을 다루기 위해 unsafe
블록 내에서 외부 함수를 호출하며, 데이터 정렬 및 메모리 해제 규칙을 준수해야 한다.
러스트의 타입 시스템과 메모리 안전성을 활용하여 FFI와의 상호작용에서 발생할 수 있는 잠재적인 문제를 예방할 수 있다. 이를 통해 안전하고 효율적인 외부 라이브러리 사용이 가능해진다.
러스트는 단순한 언어가 아니다. 러스트의 목표는 매우 다른 두 세계에 걸쳐 있다. 러스트는 안전하게 설계된 데다 클로저와 이터레이터 같은 편의성까지 갖춘 모던 프로그래밍 언어로, 실행 중인 머신이 가진 날것 그대로의 능력을 최소한의 실행 시점 오버헤드로 제어하게 하는 걸 목표로 한다.
언어의 윤곽은 이러한 목표에 의해서 결정된다. 러스트는 대부분의 빈틈을 안전한 코드로 메운다.차용 검사기와 무비용 추상화는 미정의 동작의 위험을 무릅쓰지 않고도 최대한 하드웨어에 가까이 다가설 수 있게 해준다.
이걸로 부족하거나 기존 C 코드를 활용하고 싶을 때를 위해서 안전하지 않은 코드와 외부 함수 인터페이스가 마련되어 있다.
그러나 다시 말하지만 러스트는 안전하지 않은 기능을 손에 쥐여주고는 그저 행운을 빈다는 말뿐인 그런 언어가 아니다.
목표는 늘 안전하지 않은 기능으로 안전한 API를 구축하는 것이다.
libgit2를 가지고 한 일이 바로 그것이다. 러스트 팀이 만든 Box, Vec, 기타 컬렉션, 채널 등도 같은 방식이 적용됐다. 표준 라이브러리를 가득 메운 안전한 추상화의 이면에는 안전하지 않은 코드로 된 구현이 자리 잡고 있다. 러스트와 같은 야망을 품은 언어라면 아무래도 단순한 도구가 되는 데서 그칠 순 없을 것이다.
그러나 러스트는 안전하고 빠를 뿐 아니라 동시적이고 효율적이다. 러스트로 하드웨어의 성능을 최대한 활용하는 크고, 빠르고, 안전하고, 견고한 시스템을 구축하자. 러스트로 더 나은 소프트웨어를 만들자