Sentinel-Terminated Pointers
Sentinel-Terminated Pointers (센티널 종료 포인터)
C 함수처럼 "문자열이 없다는 의미로 0이 들어있는 배열"을 다룰 때, Zig는 언제 그 배열이 끝나는지를 알 수 없으면 곤란하죠. 이 문제를 해결해 주는 포인터 표기법이 있는데, 바로 [*:x]T예요. x는 배열의 끝을 알려주는 센티널(sentinel) 값이고, 이 문법은 "값이 x로 끝난다는 게 보장된, 길이가 정해지지 않은 포인터"를 말해요.
본문
구문 [*:x]T는 센티널 값에 의해 길이가 결정되는 포인터를 나타냅니다. 이렇게 함으로써 버퍼 오버플로(buffer overflow, 할당된 공간을 넘어 쓰는 것)와 오버리드(overread, 할당된 공간을 넘어 읽는 것)를 막아줍니다. 끝이 어디인지 타입이 스스로 알고 있으니, 그 너머를 건드리는 게 처음부터 불가능해지는 거죠.
대표적인 예가 null 종료 문자열이 필요한 C 표준 함수를 부를 때예요. std.c.printf와 같은 함수는 [*:0]const u8처럼 센티널이 0인 포인터를 받는데, 이 포인터가 가리키는 곳이 실제로 0으로 끝나는지 타입 체크가 보장합니다.
const std = @import("std");
// This is also available as `std.c.printf`.
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
pub fn main() anyerror!void {
_ = printf("Hello, world!\n"); // OK
const msg = "Hello, world!\n";
const non_null_terminated_msg: [msg.len]u8 = msg.*;
_ = printf(&non_null_terminated_msg);
}
여기서 msg는 처음부터 "Hello, world!\n"이라는 문자열 리터럴이라서 끝에 0이 붙어 있어요. 그래서 printf("Hello, world!\n")처럼 바로 넘기는 건 문제없이 컴파일됩니다 (첫 번째 호출의 // OK). 반면 두 번째는 msg를 복사해 만든 non_null_terminated_msg라는 배열을 넘기는데, 이 배열은 길이만큼만 있는 배열이고 끝에 0 센티널이 없어요. 그래서 [*:0]const u8을 요구하는 printf에 넘기면 컴파일 에러가 나죠.
$ zig build-exe sentinel-terminated_pointer.zig -lc
/home/ci/work/zig-bootstrap/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: error: expected type '[*:0]const u8', found '*const [14]u8'
_ = printf(&non_null_terminated_msg);
^~~~~~~~~~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: note: destination pointer requires '0' sentinel
/home/ci/work/zig-bootstrap/zig/doc/langref/sentinel-terminated_pointer.zig:4:34: note: parameter type declared here
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
^~~~~~~~~~~~~
referenced by:
callMain [inlined]: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64
callMainWithArgs [inlined]: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:729:20
main: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:754:28
1 reference(s) hidden; use '-freference-trace=4' to see all references
핵심 에러 메시지는 expected type '[*:0]const u8', found '*const [14]u8'예요. 예상 타입은 센티널 0이 붙은 포인터인데, 실제로 넘긴 건 그냥 길이 14짜리 배열을 가리키는 포인터라서 안 맞는다는 뜻이죠. note: destination pointer requires '0' sentinel이 "여기 도착해야 할 포인터는 0 센티널이 필요해요"라고 정확히 알려줍니다. 센티널이 보장되지 않는 포인터로는 종료 지점을 신뢰할 수 없으니, 이걸 컴파일 시점에 걸러내는 겁니다.
더 알아보기
센티널 개념은 포인터에만 있는 게 아니에요. 같은 생각이 슬라이스와 배열에도 적용됩니다.
- Sentinel-Terminated Slices — 길이 정보에 더해 센티널까지 갖는 슬라이스.
- Sentinel-Terminated Arrays — 끝에 센티널 값이 보장되는 배열.