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 개 T | T* | ✗ (Ptr<T> | none 사용) |
ConstPtr<T> | const, 정확히 1 개 T | const 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 \ to | Ptr<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() |
usize | from_exposed_provenance ⚠ | from_exposed_provenance ⚠ | manual | CPtr::from_addr | manual | - |
⚠ 표시는 provenance 를 새로 만들어 내는 escape hatch 임을 뜻합니다.
1.1.7. 다른 언어와의 비교
같은 문제를 풀어 온 다른 언어들과 견주어 보면 rok 의 위치가 분명해집니다.
| 측면 | C | Rust | Zig | rok |
|---|---|---|---|---|
| 1 개 ptr | T* | *const T / *mut T | *T / *const T | Ptr<T> / ConstPtr<T> |
| 다수 ptr | T* (모호) | *const T (모호) | [*]T | ManyPtr<T> |
| length-aware | 없음 | &[T] | []T | Slice<T> |
| nullable | (모든 ptr) | Option<*const T> | ?*T | CPtr<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 함정 대부분을 회피합니다.