rokdocs
v0.1

1.1. Pointer 계열 - Ptr / ConstPtr / ManyPtr / CPtr

rok::bare::ptr 모듈은 raw pointer 다섯 종을 정의합니다. 설계 기조는 Zig 식 arity-encoded 모델을 따르는 것으로, 가리키는 개수 (arity) 와 mutability 를 타입 자체에 인코딩하여 silent coercion 을 원천 차단합니다.

1.1.1. 5 pointer 종류

다섯 종류가 어떻게 갈라지는지는 다음 표에 한눈에 들어옵니다.

타입의미C 대응nullable
Ptr<T>mutable, 정확히 1 개 TT*✗ (Ptr<T> | none 사용)
ConstPtr<T>const, 정확히 1 개 Tconst T*✗
ManyPtr<T>mutable, 연속 N개 T (길이 모름)T* (배열)✗
Slice<T>mutable, 연속 N개 T (길이 포함)(T*, len)✗ (별도 niche)
CPtr<T>C 호환 nullable + 1 개T* (nullable)✓ (자동)

Ptr, ConstPtr, ManyPtr, CPtr 은 #[repr(transparent)] 입니다: extern "C" 를 건너면 C 포인터입니다.

이 가운데 Slice<T> 는 2.9 collections 에서 정의되는 안전한 view 이며, 본 모듈은 거기에서 underlying ptr 을 추출하는 경로만 정의합니다.

Send / Sync

원시 포인터 Ptr<T> 와 CPtr<T> 는 !Send, !Sync 입니다 (2.13.9). 포인터를 담은 타입을 스레드 사이로 넘기려면 작성자가 unsafe impl Send 로 보증해야 합니다.

자동 niche

Ptr<T>, ConstPtr<T>, ManyPtr<T> 는 null pointer 자리를 niche 로 쓸 수 있어, none 을 끼워 넣어도 추가 태그가 필요 없습니다. 다만 이것은 최적화이며 보장이 아닙니다. 구현은 Ptr<T> | none 을 일반 태그 합 타입으로 표현해도 됩니다.

sizeof::<Ptr<i32>>()         //→ 8 (64-bit target)
sizeof::<Ptr<i32> | none>() //→ 8 (niche 를 쓰는 구현 - 보장 아님)

한편 CPtr<T> 는 그 자체로 nullable 이라 별도의 sentinel 이 필요 없고, none 표현은 단순히 CPtr::null() 입니다.

자세한 niche 메커니즘은 2.2 non-zero 에서 다룹니다.

1.1.2. Ptr<T> - strict provenance API

가장 기본이 되는 Ptr<T> 의 공개 API 는 다음과 같습니다.

struct Ptr<T> { /* opaque - (address, provenance) pair */ }

impl<T> Ptr<T> {
    // 생성
    fn dangling() -> Ptr<T>;           // valid alignment, invalid address (unique)
    fn null() -> Ptr<T> | none;         // 항상 none

    // provenance-preserving (strict provenance, 안전 우선)
    fn addr(self) -> usize;             // 주소만 (provenance 손실)
    fn with_addr(self, addr: usize) -> Ptr<T>;
                                        // provenance 보존, address 변경
    fn map_addr(self, f: fn(usize) -> usize) -> Ptr<T>;

    // 산술 (provenance 보존)
    fn offset(self, count: isize) -> Ptr<T>;       // count * sizeof(T) byte offset
    fn add(self, count: usize) -> Ptr<T>;
    fn sub(self, count: usize) -> Ptr<T>;
    fn byte_offset(self, count: isize) -> Ptr<T>;  // byte 단위 offset

    fn distance(self, origin: Ptr<T>) -> isize;     // self.addr() - origin.addr()
    fn is_aligned(self) -> bool;
    fn align_offset(self, align: usize) -> usize;

    // dereference (UB if invalid)
    unsafe fn read(self) -> T;
    unsafe fn write(self, value: T);
    unsafe fn read_volatile(self) -> T;
    unsafe fn write_volatile(self, value: T);
}
// module functions of rok::bare::ptr
fn addr_of<T>(place: T) -> Ptr<T>;
fn many_addr_of<T>(place: T) -> ManyPtr<T>;
fn expose_provenance<T>(p: Ptr<T>) -> usize;
unsafe fn from_exposed_provenance<T>(addr: usize) -> Ptr<T>;

addr_of / many_addr_of 는 place 식을 받습니다: 지역 변수나 static (extern "C" static 포함), 또는 그 필드나 원소. 지역 변수를 가리키는 포인터는 그 변수가 스코프에 있는 동안, static 을 가리키는 포인터는 실행 내내 유효합니다 (static 은 data section 에 있고, lazy 초기화 static 은 주소를 취하기 전에 초기화되며, static 이 가진 GC 참조는 루트입니다). mut 가 아닌 static 을 가리키는 포인터로 쓰면 undefined behavior 입니다. 주소를 취한 변수는 그 스코프 동안 GC 루트로 남고, 루트는 pin 되므로 가리키는 대상은 움직이지 않습니다. coroutine 본체에서는 포인터가 다음 중단 지점까지만 유효합니다. 그런 포인터를 중단 지점 뒤에 쓰면 컴파일 에러입니다.

Strict provenance - escape hatch 분리

usize → Ptr<T> 변환은 기본적으로 거부됩니다. provenance 가 손실된 ptr 을 dereference 하는 순간 UB 가 발생하기 때문입니다. 그래서 정상 경로와 escape hatch 가 명확하게 분리됩니다.

// 권장 - provenance 보존
let p: Ptr<i32> = ...;
let q = p.with_addr(p.addr() + 4);     // 4 byte 다음 - provenance 그대로

// 비권장 - provenance 새로 (FFI / hardware register 자리에서만)
use rok::bare::ptr::{expose_provenance, from_exposed_provenance};

let exposed: usize = expose_provenance(p);   // p 의 provenance 를 "노출"
let r: Ptr<i32> = unsafe { from_exposed_provenance(exposed) };
                                              // 새로 만든 ptr 이 노출된 provenance 중
                                              // 하나를 가진다고 _가정_

이 설계의 권위 있는 출발점은 Ralf Jung 의 "Pointers Are Complicated" 글이며, 2024 년 12 월에 안정화된 Rust 1.84 의 rok::core::ptr::with_exposed_provenance 와 동형 모델입니다.

사용 예 - pointer 산술

실제로 산술이 어떻게 쓰이는지는 다음 짧은 예제에서 확인할 수 있습니다.

let arr: [i32; 5] = [10, 20, 30, 40, 50];
let p: Ptr<i32> = rok::bare::ptr::addr_of(arr[0]);

let p3 = p.add(3);                    // arr[3] 의 ptr
unsafe {
    let v = p3.read();                //→ 40
}

// byte 산술 (struct field offset)
struct Header { magic: u32; size: u64; }
let h: Ptr<Header> = ...;
let size_ptr: Ptr<u64> = h.byte_offset(4).cast::<u64>();
                                      // Header.size 의 위치
                                      // (정렬 검증 필요 - 1.2 cast 참조)

1.1.3. ConstPtr<T> - read-only

읽기 전용 변종은 write 계열이 빠진 형태입니다.

struct ConstPtr<T> { /* opaque */ }

impl<T> ConstPtr<T> {
    fn dangling() -> ConstPtr<T>;

    fn addr(self) -> usize;
    fn with_addr(self, addr: usize) -> ConstPtr<T>;
    fn offset(self, count: isize) -> ConstPtr<T>;
    fn add(self, count: usize) -> ConstPtr<T>;
    fn sub(self, count: usize) -> ConstPtr<T>;

    unsafe fn read(self) -> T;
    unsafe fn read_volatile(self) -> T;
    // write 메서드 없음 - read-only
}

Ptr<T> 와의 변환은 방향에 따라 안전성이 다릅니다. const 를 부여하는 쪽은 자유롭지만, 제거하는 쪽은 이름 있는 cast rok::bare::const_cast 를 거쳐야 합니다 (호출은 안전하고, 결과를 통한 쓰기가 unsafe).

let m: Ptr<i32> = ...;
let c: ConstPtr<i32> = m.as_const();        // Ptr → ConstPtr (안전)

let c: ConstPtr<i32> = ...;
let m: Ptr<i32> = rok::bare::const_cast(c);    // ConstPtr → Ptr (호출은 안전, m 을 통한 쓰기는 unsafe - 1.2 cast)

따라서 const 제거는 오로지 명시적 경로로만 일어나며, silent 변환은 받아들여지지 않습니다.

1.1.4. ManyPtr<T> - 연속 N개, 길이 모름

다음으로 길이 정보 없이 여러 개를 가리키는 타입입니다.

struct ManyPtr<T> { /* opaque */ }

impl<T> ManyPtr<T> {
    fn addr(self) -> usize;
    fn with_addr(self, addr: usize) -> ManyPtr<T>;

    fn offset(self, count: isize) -> ManyPtr<T>;
    fn add(self, count: usize) -> ManyPtr<T>;

    unsafe fn read(self, idx: usize) -> T;
    unsafe fn write(self, idx: usize, value: T);

    // 길이 정보 없음 - index 호출자가 보장
    unsafe fn slice(self, len: usize) -> Slice<T>;        // length 부착하여 Slice 만듬
}

C 의 T* (배열) 을 받아 들일 때 쓰이며, 길이 정보가 빠져 있으므로 OOB 회피 책임은 호출자에게 있습니다. GC heap 밖의 메모리에 대한 slice 의 원소가 GC 참조를 가지면 undefined behaviour 입니다.

extern "C" {
    fn read_buffer(out: ManyPtr<u8>, len: usize) -> usize;
}

let buf: [u8; 1024] = [0; 1024];
let read_count = unsafe {
    read_buffer(rok::bare::ptr::many_addr_of(buf[0]), 1024)
};

1.1.5. CPtr<T> - C 호환 nullable

C ABI 와 직결되는 nullable 변종은 다음과 같습니다.

struct CPtr<T> { /* opaque - nullable */ }

impl<T> CPtr<T> {
    fn null() -> CPtr<T>;
    fn from_ptr(p: Ptr<T>) -> CPtr<T>;       // non-null
    fn from_const(p: ConstPtr<T>) -> CPtr<T>;

    fn is_null(self) -> bool;
    fn as_ptr(self) -> Ptr<T> | none;        // null → none
    fn as_const(self) -> ConstPtr<T> | none;

    fn addr(self) -> usize;                  // null = 0
}

C 함수가 nullable T* 를 주고 받는 경계에서 자연스럽게 자리 잡으며, 전형적인 사용은 다음과 같이 흘러갑니다.

extern "C" {
    fn malloc(size: usize) -> CPtr<u8>;       // 실패 시 null
    fn free(ptr: CPtr<u8>);
}

let buf: CPtr<u8> = unsafe { malloc(1024) };
match buf.as_ptr() {
    p: Ptr<u8> => use(p),
    none       => panic("malloc failed"),
}
unsafe { free(buf) };

Ptr<T> | none 은 자동 niche 덕분에 ABI 가 호환되지만, -1 처럼 명시 sentinel 이 별도로 필요한 자리에는 CPtr 만으로 부족합니다. 그러한 경우에는 newtype 에 #[niche(-1)] 를 붙여 별도로 모델링하는 것이 정석입니다.

1.1.6. 변환 매트릭스

이상의 다섯 타입 사이를 오가는 길은 다음 매트릭스 한 장으로 정리됩니다.

from \ toPtr<T>ConstPtr<T>ManyPtr<T>CPtr<T>Slice<T>usize
Ptr<T>-.as_const() 안전.as_many() 안전CPtr::from_ptr()rok::bare::ptr::slice(p, len).addr() provenance loss
ConstPtr<T>rok::bare::const_cast-.as_many()CPtr::from_const()rok::bare::ptr::slice.addr()
ManyPtr<T>.as_one() 안전.as_const()-CPtr::from_ptr(...).slice(len) unsafe.addr()
CPtr<T>.as_ptr()? .as_const()?manual-manual.addr()
Slice<T>(안전 view 의 raw 추출).as_ptr() (ConstPtr).as_many_ptr()manual-.as_ptr().addr()
usizefrom_exposed_provenance ⚠from_exposed_provenance ⚠manualCPtr::from_addrmanual-

⚠ 표시는 provenance 를 새로 만들어 내는 escape hatch 임을 뜻합니다.

1.1.7. 다른 언어와의 비교

같은 문제를 풀어 온 다른 언어들과 견주어 보면 rok 의 위치가 분명해집니다.

측면CRustZigrok
1 개 ptrT**const T / *mut T*T / *const TPtr<T> / ConstPtr<T>
다수 ptrT* (모호)*const T (모호)[*]TManyPtr<T>
length-aware없음&[T][]TSlice<T>
nullable(모든 ptr)Option<*const T>?*TCPtr<T> 또는 Ptr<T> | none
arity 분리없음부분 (slice 별도)✓ (*T / [*]T 분리)✓ (Zig 모델)
strict provenance없음✓ (1.84+)없음✓ (Rust 모델)
as cast 거부허용허용 (warning 만)명시 @ptrCast완전 거부

표가 보여 주듯 rok 의 결정 가운데 가장 무게가 큰 것은 다음 셋입니다. 첫째로 Zig 식 arity 분리를 받아들여 다섯 종을 명확히 갈라 놓았고, 둘째로 strict provenance 에서 Rust 2024 모델을 채택했으며, 셋째로 명시 cast 를 강제하여 pointer 자리에서는 as 를 아예 쓸 수 없게 만들었습니다. 이 세 결정이 맞물려 C 와 Rust 양쪽이 안고 있던 pointer 함정 대부분을 회피합니다.

GitHub에서 이 페이지 고치기