rokdocs
v0.1

1.7. FFI - extern "C" / CStr / CString / CPtr

rok::bare::ffi 모듈은 C ABI 와 호환되는 외부 함수 및 데이터 정의 메커니즘을 제공합니다. 이는 refs-lang 5.9 extern 블록 이 정의하는 extern "C" { ... } 블록과 함께 동작합니다.

본 장에서는 전통적인 C FFI 만 다루며, WASM Component Model 기반의 FFI 는 1.8 wit 에서 별도로 서술합니다.

1.7.1. extern "C" block

가장 기본적인 형태는 다음과 같습니다.

extern "C" {
    // 함수 선언
    fn malloc(size: usize) -> CPtr<u8>;
    fn free(ptr: CPtr<u8>);
    fn strlen(s: ConstPtr<c_char>) -> usize;

    // 글로벌 (read-only)
    static errno: i32;

    // 글로벌 (mutable) - `mut` 키워드 _이 자리에서만_ 허용
    static mut atexit_count: u64;
}

여기에서 등장하는 mut 키워드는 refs-lang 1.5 키워드 에서 정의되며, 오직 이 자리에서만 의미를 가집니다. 일반 변수에는 사용할 수 없다는 점에 유의해야 합니다.

#[link("name")] - library 지정

라이브러리를 명시적으로 지정하려면 #[link] 어트리뷰트를 활용합니다.

#[link("c")]
extern "C" {
    fn malloc(size: usize) -> CPtr<u8>;
    fn free(ptr: CPtr<u8>);
}

#[link("m")]
extern "C" {
    fn cos(x: f64) -> f64;
    fn sin(x: f64) -> f64;
}

linker 가 -lc 또는 -lm 같은 플래그를 자동으로 부착합니다.

호출 규약 (calling convention)

플랫폼과 인터페이스에 따라 다양한 호출 규약을 선택할 수 있습니다.

extern "C" { ... }              // System V (Unix) / Microsoft x64 (Windows)
extern "system" { ... }         // Windows: stdcall on x86, C on x64 / aarch64
extern "fastcall" { ... }       // x86 fastcall
extern "stdcall" { ... }        // x86 stdcall
extern "aapcs" { ... }          // ARM AAPCS
extern "thiscall" { ... }       // C++ this pointer (x86 only)

대부분의 사용자는 기본값인 "C" 만 사용해도 충분합니다. 그 밖의 규약은 주로 Windows API 나 C++ 인터페이스를 다룰 때 등장합니다.

str 매개변수와 반환

extern "C" 함수의 str 매개변수는 NUL 로 끝나는 char* 로 전달되고, 반환된 char* 는 strlen 으로 길이를 재어 str 이 됩니다 (refs-lang 5.9.4). (ptr, len) 전달은 런타임 ABI extern "rok" 의 것입니다 (5.9.2).

extern "C" {
    fn puts(s: str) -> i32;          // "hi" → NUL 로 끝나는 char*
    fn getenv(name: str) -> CPtr<c_char>;   // null 가능 - CPtr 로 받음
}

1.7.2. CStr / CString - null-terminated string

C 문자열을 다루는 빌림(borrow) 형태의 타입이 CStr 입니다.

struct CStr {
    // 내부: null-terminated UTF-8 또는 raw bytes (Unix 은 bytes, Windows 는 UTF-16 별도)
}

impl CStr {
    fn from_ptr(p: ConstPtr<c_char>) -> CStr | none;
                                    // null pointer → none
                                    // null-terminator 없으면 - undefined (호출자 책임)

    fn from_bytes_with_nul(bytes: Slice<u8>) throws (FromBytesWithNulError) -> CStr;
                                    // 검증 - 마지막이 0, 중간 0 없음
    fn from_bytes_with_nul_unchecked(bytes: Slice<u8>) -> CStr;

    fn as_ptr(self) -> ConstPtr<c_char>;
    fn as_bytes(self) -> Slice<u8>;            // null-terminator 제외
    fn as_bytes_with_nul(self) -> Slice<u8>;   // null-terminator 포함
    fn to_str(self) throws (Utf8Error) -> str;  // UTF-8 변환

    fn len(self) -> usize;                     // null-terminator 제외
}

from_ptr 는 C 문자열을 복사해 들입니다. CStr / CString 은 바이트와 NUL 을 GC 객체 하나에 담으며, as_ptr 는 str::as_ptr 와 같은 동안 유효합니다 (2.15.2): 포인터 값이 나올 수 있는 함수에서 그 CStr / CString 을 담은 변수가 scope 안에 있는 동안입니다.

CString - owned, null-terminated

소유권을 가지는 형태가 필요하다면 CString 을 사용합니다.

struct CString { /* GC heap, null-terminated */ }

impl CString {
    fn new(bytes: Slice<u8>) throws (NulError) -> CString;
                                        // 중간 null byte 거부
    fn from_str(s: str) throws (NulError) -> CString;

    fn as_c_str(self) -> CStr;
    fn as_ptr(self) -> ConstPtr<c_char>;
    fn into_bytes(self) -> List<u8>;     // owned bytes (null 제외)
    fn into_bytes_with_nul(self) -> List<u8>;
}

사용 예 - strlen

extern "C" {
    fn strlen(s: ConstPtr<c_char>) -> usize;
}

let s: str = "hello";
let cs = try CString::from_str(s);
let n = unsafe { strlen(cs.as_ptr()) };       //→ 5

참고

str 와 CStr 사이의 변환에는 할당이 필요합니다 (null-terminator 를 추가해야 하기 때문입니다). str 는 일반적으로 non-null-terminated 형태이므로 FFI 자리에서만 명시적으로 변환하는 편이 권장됩니다.

1.7.3. CPtr<T> 와 nullable

권위 있는 정의는 1.1.5 CPtr 에 있습니다. C 함수가 T* 를 반환할 때에는 null 가능성을 명시할 필요가 있는데, 그 자리에 등장하는 타입입니다.

extern "C" {
    fn fopen(path: ConstPtr<c_char>, mode: ConstPtr<c_char>) -> CPtr<FILE>;
    fn fclose(file: CPtr<FILE>) -> i32;
}

struct FILE;        // opaque

let path = try CString::from_str("data.txt");
let mode = try CString::from_str("r");
let f: CPtr<FILE> = unsafe { fopen(path.as_ptr(), mode.as_ptr()) };

match f.as_ptr() {
    file: Ptr<FILE> => {
        // 사용
        unsafe { fclose(f) };
    }
    none => panic("fopen failed"),
}

1.7.4. using 블록 - RAII

GC 는 finalizer 를 보장하지 않습니다. 이는 모든 GC 언어를 학습할 때 짚게 되는 원칙으로, 외부 자원의 RAII 는 명시적인 using 으로 처리해야 합니다.

using f = try rok::bare::ffi::fopen(path, "r") {
    let buf = try f.read_to_end();
    process(buf);
}     // ← scope 끝에서 자동 fclose (Drop)

// without using - 자원 누수 가능
let f = try rok::bare::ffi::fopen(path, "r");
try process(f);                                // 에러 전파 시 fclose 안 됨
rok::bare::ffi::fclose(f);                        // _explicit_ 만 보장

using 구문 자체는 refs-lang 3.17 using 표현식 이 권위를 가지며, 2.13 marker 의 Drop 트레잇과 통합되어 작동합니다.

1.7.5. 안전 wrapper 패턴 - sys + safe 분리

Rust 생태계의 표준 관용구는 *-sys 크레이트(raw FFI) 와 동명 크레이트(safe wrapper) 를 분리하는 것입니다. rok 도 같은 패턴을 권장합니다.

// libc-sys 모듈 - raw FFI
mod c {
    #[link("c")]
    extern "C" {
        pub fn malloc(size: usize) -> CPtr<u8>;
        pub fn free(ptr: CPtr<u8>);
        pub fn strdup(s: ConstPtr<c_char>) -> CPtr<c_char>;
    }
}

// libc - safe wrapper
pub fn alloc_buffer(size: usize) throws (AllocError) -> List<u8> {
    let p = unsafe { c::malloc(size) };
    match p.as_ptr() {
        ptr: Ptr<u8> => {
            // unsafe → safe 경계
            let bytes = unsafe { Slice::from_raw_parts(ptr, size).to_list() };
            unsafe { c::free(p) };
            bytes
        }
        none => { throw AllocError::OutOfMemory; }
    }
}

이러한 분리는 unsafe 코드를 좁은 영역에 가두는 효과를 가집니다. 그 결과 사용자 코드에서는 safe wrapper 만 호출하게 되어 안전성이 자연스럽게 확보됩니다.

1.7.6. 자동 binding 생성

rokc bindgen (refs-lang 9.1) 은 libclang 으로 C 헤더에서 rok 선언을 생성합니다. libclang 은 toolchain 이 그 lib/ 에 함께 배포하며, 이 명령만 로드합니다.

rokc bindgen libfoo.h > libfoo_sys.rok
rokc bindgen libfoo.h --target x86_64-pc-windows-msvc -o libfoo_sys.rok -- -I include -DFOO=1

출력은 target 하나 (--target, 기본값은 host) 를 위한 것입니다: 크기, 레이아웃, 정수 폭은 그 target 에 대한 clang 의 것입니다. -- 뒤의 인자는 clang 에 넘깁니다. 기계의 시스템 헤더는 그 기계 자신의 플랫폼, Apple target (SDK), MSVC host 의 MSVC target 에 씁니다. 그 밖의 target 에서는 --sysroot 를 주지 않는 한 clang 이 자기 내장 헤더만 받으며, target 의 헤더는 -- 뒤에 (-isystem <dir>) 넘깁니다. 헤더와 그것이 include 하는 시스템 아닌 헤더의 선언을 씁니다 (시스템 디렉토리의 헤더라면 자기 디렉토리 트리에서 include 하는 헤더의 선언도). 그것들이 쓰는 다른 헤더의 선언도 함께 씁니다. clang 이 거부하는 헤더는 clang 의 진단과 함께 에러 (exit 1) 입니다.

매핑 규약은 다음과 같습니다.

  • C 의 정수와 부동소수 타입은 그 target 에서 같은 크기의 rok 원시 타입이 됩니다 (int → i32, long → i32 또는 i64, char → c_char, _Bool → bool). size_t / uintptr_t → usize, ssize_t / intptr_t / ptrdiff_t → isize. 시스템 헤더가 그런 타입에 붙인 typedef 는 그 타입 자체입니다.
  • typedef 는 rok 의 type alias 로 변환됩니다.
  • struct 와 union 은 #[repr(C)] struct / union 에 대응합니다 (clang 의 레이아웃이 필요로 하면 #[repr(C, align(N))] 이나 #[repr(packed)]). 연속된 bitfield 는 그 저장소를 덮는 바이트 배열 필드 _bitfieldN 하나가 됩니다. rok 가 표현할 수 없는 레이아웃 (N > 1 인 #pragma pack(N), union 안의 bitfield) 은 같은 크기와 정렬로 _opaque: [u8; N] 을 담은 struct 가 됩니다. 선언만 되고 정의되지 않은 struct 는 struct Name; (opaque) 가 됩니다.
  • enum 은 그 정수 타입의 alias 와 enumerator 마다 const 하나가 됩니다 (pub type color = u32; pub const RED: color = 0;). 이름 없는 enum 은 그 상수들입니다.
  • 객체 포인터 (T*, const T*) 는 CPtr<T> 가 되며, void* 는 CPtr<u8> 가 됩니다.
  • 함수는 extern "C" fn 이 됩니다 (C variadic 은 ...). 32-bit x86 Windows 의 stdcall 함수는 extern "system" 에 들어갑니다. 전역 변수는 static 이 됩니다 (const 가 아니면 static mut). static 과 inline 함수는 건너뜁니다 (링크할 심볼이 없음).
  • 함수 포인터는 unsafe extern "C" fn(...) -> R | none 이 됩니다 (refs-lang 5.9.4; NULL = none).
  • 정수, 부동소수, 문자열 상수로 평가되는 object-like 매크로는 const 항목이 됩니다.
  • rok 키워드나 원시 타입 이름인 C 이름에는 뒤에 _ 가 붙습니다. 함수와 static 은 #[link_name] 으로 C 심볼을 유지합니다.
  • 그 밖의 것 (128-bit 정수, _Float16, 8 바이트보다 넓은 long double, vector 와 block 타입, variadic 함수 포인터, thread-local 변수, 다른 호출 규약) 은 // skipped ... 주석과 함께 건너뜁니다. 바이트로 적은 struct (_opaque) 는 결코 값으로 넘기지 않습니다: 그것을 직접 또는 다른 struct 안에 담아 받거나 돌려주는 함수나 함수 포인터는 건너뜁니다. C 는 그것을 필드로 분류하기 때문입니다.

1.7.7. 호출 안전 책임 분배

호출 자리에서 누가 무엇을 책임지는지 정리하면 다음과 같습니다.

항목책임자
Pointer null 검사C → rok 시 rok 코드
Buffer 길이 검증rok 코드 (length 알고 있어야)
String null-terminationCString 생성 시 자동
GC pinrok 코드 (long-running FFI 시)
메모리 해제 (free)rok 코드 또는 safe wrapper
호출 규약 일치extern 선언자
Thread safetyC 라이브러리 문서 따름
Errno / GetLastError호출 직후 즉시 read

1.7.8. 다른 언어와의 비교

비교의 시야를 넓히면 rok 의 위치가 더 분명해집니다.

측면CRustZigGorok
외부 함수(직접)extern "C" { ... }extern "..."import "C" (cgo)extern "C" { ... }
호출 규약(one default)extern "system" etc.extern "C" etc."C" only다중 ("C"/"system"/etc.)
C stringchar*CStr / CString[*:0]u8C.CString / C.GoStringCStr / CString
nullable ptr(default)Option<*const T>?*T*C.struct_T (always nullable)CPtr<T>
binding 도구nonebindgen / cbindgen@cImportcgorokc bindgen
GC pinN/AN/AN/Aruntime.Pinner (1.21+)rok::bare::gc::pin
RAII / using(manual)Dropdeferdeferusing block (Drop)

rok 의 가장 큰 차이점은 다음 세 가지로 압축됩니다.

  1. GC 와 FFI 의 통합. Pinned<T> guard 와 using 블록을 함께 사용함으로써 자원 누수와 pin 누락을 동시에 차단합니다.
  2. 엄격한 포인터 모델. Ptr<T> / ConstPtr<T> / ManyPtr<T> / CPtr<T> 가 분리됩니다 (1.1).
  3. safe wrapper 강제. as cast 를 거부하고 named cast 만 허용합니다 (1.2).

GitHub에서 이 페이지 고치기