Chapter 5: Hosted Services
5부 — 운영체제와 대화하기. 선행 조건: 2부 (1, 2, 3). 이 장을 마치면 충돌 시에도 데이터를 잃지 않고 파일을 읽고 쓰며, 입력을 한 줄씩 읽고, 작업에 맞는 무작위성을 생성하고, 벽시계 시간과 경과 시간을 구별할 수 있다.
이 장에서는 fs.h, sysio.h, mmap.h, time.h, stream.h, random.h를 다룬다. 이 안의 모든 것은 운영체제를 필요로 한다: 이 장은 프리스탠딩(freestanding) 빌드에 적용되지 않는 매뉴얼의 유일한 부분이다.
이 API들은 호스티드 플랫폼 지원이 필요하며, 현재의 freestanding 하위 집합에서는 제외된다.
목차
1. 파일시스템 API
파일시스템 계층은 플랫폼 파일 핸들, 경로, 디렉터리 목록, 메타데이터, 권한, 링크, 잠금을 감싼다.
원시 파일시스템 헬퍼는 신뢰할 수 없는 경로를 정제하지 않고, 루트 감금(root confinement)을 강제하지 않으며, symlink-race TOCTOU를 방어하지 않는다. 신뢰할 수 없는 경로를 받아들이는 호출자는 API를 사용하기 전에 그것을 검증해야 한다.
구조체와 열거형
typedef struct {
union {
void *ptr;
int fd;
} internal;
} proven_fs_handle_t;
typedef proven_fs_handle_t proven_file_t;의도: POSIX나 Win32 세부사항을 상위 계층에 노출하지 않으면서 플랫폼 파일 핸들을 표현한다.
typedef enum {
PROVEN_FS_READ = 1 << 0,
PROVEN_FS_WRITE = 1 << 1,
PROVEN_FS_APPEND = 1 << 2,
PROVEN_FS_CREATE = 1 << 3,
PROVEN_FS_TRUNC = 1 << 4,
PROVEN_FS_CREATE_NEW = 1 << 5
} proven_fs_mode_t;파일 모드 플래그는 조합할 수 있다. 예:
PROVEN_FS_READPROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNCPROVEN_FS_WRITE | PROVEN_FS_APPEND | PROVEN_FS_CREATE
typedef enum {
PROVEN_FS_TYPE_FILE,
PROVEN_FS_TYPE_DIR,
PROVEN_FS_TYPE_OTHER
} proven_fs_type_t;typedef struct {
proven_u8str_t name;
proven_fs_type_t type;
proven_size_t size;
} proven_fs_entry_t;디렉터리 엔트리는 name을 소유(owned)로 소유한다. proven_fs_list()가 반환한 배열에는 proven_fs_list_destroy()를 사용하라.
typedef struct {
proven_err_t err;
proven_file_t value;
} proven_result_file_t;typedef enum {
PROVEN_FS_PERM_OWNER_R = 1 << 8,
PROVEN_FS_PERM_OWNER_W = 1 << 7,
PROVEN_FS_PERM_OWNER_X = 1 << 6,
PROVEN_FS_PERM_GROUP_R = 1 << 5,
PROVEN_FS_PERM_GROUP_W = 1 << 4,
PROVEN_FS_PERM_GROUP_X = 1 << 3,
PROVEN_FS_PERM_OTHER_R = 1 << 2,
PROVEN_FS_PERM_OTHER_W = 1 << 1,
PROVEN_FS_PERM_OTHER_X = 1 << 0,
PROVEN_FS_PERM_DEFAULT = PROVEN_FS_PERM_OWNER_R | PROVEN_FS_PERM_OWNER_W |
PROVEN_FS_PERM_GROUP_R | PROVEN_FS_PERM_OTHER_R
} proven_fs_perms_t;typedef enum {
PROVEN_FS_LOCK_SHARED,
PROVEN_FS_LOCK_EXCLUSIVE,
PROVEN_FS_LOCK_UNLOCK
} proven_fs_lock_type_t;typedef struct {
proven_size_t size; /* size in bytes (0 for directories on some hosts) */
proven_fs_type_t type; /* PROVEN_FS_TYPE_FILE / _DIR - there is no symlink type */
proven_fs_perms_t perms; /* the nine permission bits only; the file type is in `type` */
proven_i64 created_at; /* always 0: no birth time is queried - see below */
proven_i64 modified_at; /* last-modification time, seconds since the Unix epoch */
unsigned long long dev; /* device id (POSIX st_dev; 0 where unavailable) */
unsigned long long ino; /* inode number (POSIX st_ino; 0 where unavailable) */
unsigned long long uid; /* owner id (POSIX st_uid; 0 on Windows — no uid/gid) */
unsigned long long gid; /* group id (POSIX st_gid; 0 on Windows) */
} proven_fs_stat_t;uid/gid(v26.06.22a에서 추가됨)는 POSIX 소유자 및 그룹 id를 담는다. 둘 다 Windows에서는 0인데, Windows에는 uid/gid 개념이 없기 때문이다. ls -l 스타일의 소유자/그룹 열을 표시할 때는 호스트의 getpwuid/getgrgid로 이들을 이름으로 해석하라.
한 필드는 보이는 것보다 좁고, 다른 한 필드에는 날카로운 모서리가 있다:
created_at은 항상 0이다. 평범한stat()에는 이식 가능한 birth time이 없으며, PAL은 그것을 요청하지 않는다.modified_at만이 실제 타임스탬프를 담는다.type은 일반 파일이면FILE, 디렉터리면DIR, 그 밖의 모든 것—FIFO, 소켓, 디바이스, 끊어진(dangling) symlink—에 대해서는PROVEN_FS_TYPE_OTHER이다. (v26.07.13a 이전까지는 이 모든 것이FILE로 stat되어, 호출자에게 그것들을 열어 바이트를 읽을 수 있다고 알려주었다. 끊어진 symlink는 애초에 열 수 없다.)
type은 여기서도, 그리고 디렉터리 순회에서도 symlink를 따라간다. 그것이 두 값을 일치시키는 원리다: 일반 파일을 가리키는 symlink는 FILE이고, 디렉터리를 가리키는 symlink는 DIR이다. 이어지는 모서리는 실재한다—재귀 순회기는 루프에 빠질 수 있다. 조상을 가리키는 symlink는 사이클이고 그 type이 DIR이라고 말하기 때문이다. 깊이 제한을 두거나, (dev, ino) 쌍을 기억해 두고 이미 본 것으로는 내려가기를 거부하라.
proven_fs_stat_t st;
if (proven_is_ok(proven_fs_stat(scratch, PROVEN_LIT("/etc/hosts"), &st))) {
proven_println("size={} uid={} gid={}",
PROVEN_ARG(st.size), PROVEN_ARG(st.uid), PROVEN_ARG(st.gid));
}매크로
| 매크로 | 의도 |
PROVEN_FS_PATH_SEP | 라이브러리 수준에서 선호되는 구분 문자, 현재는 '/'. |
파일 함수
| API | 의도 | 반환 |
proven_fs_open(scratch, path, mode) | 파일 경로를 연다. scratch는 경로 변환에 사용된다. | proven_result_file_t. |
proven_fs_close(file) | 파일 핸들을 닫는다. | 에러를 반환한다. 쓰기를 한 파일에서는 close *자체*가 쓰기의 일부다: NFS, CIFS 및 쿼터를 강제하는 파일시스템은 write-back 실패를 오직 이곳에서만 보고한다. 읽기만 한 파일에서는 (void)로 무시해도 괜찮다. |
proven_fs_read(file, dest) | 가변 슬라이스로의 단일 읽기 시도. | proven_result_size_t. |
proven_fs_write(file, src) | 바이트 view로부터의 단일 쓰기 시도. | proven_result_size_t. |
proven_fs_write_all(file, src) | 모든 바이트가 쓰이거나 에러가 발생할 때까지 재시도. | proven_err_t. |
proven_fs_size(file) | 열린 파일의 크기를 조회. | proven_result_size_t. |
proven_fs_rename(scratch, src, dest) | 경로 이름 변경 또는 이동. | proven_err_t. |
proven_fs_remove(scratch, path) | 파일 제거. | proven_err_t. |
proven_fs_copy(temp_alloc, src, dest) | 임시 버퍼 할당을 사용해 파일 복사. | proven_err_t. |
proven_fs_mkdir(scratch, path) | 디렉터리 생성. | proven_err_t. |
proven_fs_rmdir(scratch, path) | 빈 디렉터리 제거. | proven_err_t. |
proven_fs_list(alloc, path) | 디렉터리를 proven_fs_entry_t의 proven_array_t로 나열. | proven_result_array_t. |
proven_fs_list_destroy(alloc, list) | 디렉터리 목록과 엔트리 이름을 파괴. | void. |
proven_fs_chmod(scratch, path, perms) | 권한 설정. | proven_err_t. |
proven_fs_lock(file, type, wait) | 파일 잠금 획득/해제. | proven_err_t. |
proven_fs_stat(scratch, path, out_stat) | 메타데이터 채우기. | proven_err_t. |
proven_fs_stat()은 perms에 아홉 개의 권한 비트만 보고하므로, stat의 perms를 그대로 proven_fs_chmod()에 다시 넘길 수 있다. 예전에는 원시 POSIX st_mode를 담았는데, chmod가 거부하는 파일 타입 비트가 포함되어 있어서—이 필드의 자명한 용도인 그 왕복(round-trip)이 실제 파일마다 PROVEN_ERR_INVALID_ARG로 실패했다. 파일 타입은 type에서 읽어라.
위치, 그리고 atomic과 durable의 차이
seek할 수 없는 핸들은 그렇다고 말한다. 파이프, FIFO, 터미널은 proven_fs_seek에서 PROVEN_ERR_IO가 아니라 PROVEN_ERR_UNSUPPORTED를 반환한다. seek 불가능은 호출의 실패가 아니라 그 대상의 속성이며, 그것에 적응하는 코드—스캐너가 그렇다—는 둘을 구별할 수 있어야 한다.
pread와 pwrite는 위치를 이동하지 않는다. 그것이 바로 그들의 목적이다: 하나의 핸들을 공유하는 두 읽기 스트림(reader)는 어느 쪽도 이동시키지 않는 커서를 두고 경합할 수 없다.
atomic과 durable은 서로 다른 약속이며, 이 둘을 혼동하는 것이 데이터가 손실되는 방식이다.
proven_fs_write_file_atomic은 *reader*가 절반만 쓰인 파일을 결코 보지 않도록 보장한다. 전원 차단에 대해서는 아무것도 말하지 않는다: 커널이 여전히 당신의 바이트를 붙들고 있을 수 있고, rename이 그것이 가리키는 데이터보다 먼저 디스크에 도달할 수 있다.proven_fs_write_file_durable은 그 창(window)을 닫는다. 오직 작동하는 하나의 순서로: 임시 파일을 fsync하고, 그 다음 rename하고, 그 다음 디렉터리를 fsync한다. 파일은 sync하되 디렉터리는 하지 않으면, 바이트는 안전하지만 그것을 가리키는 이름은 안전하지 않은 크래시 창이 남는다—그것이 바로 atomic 쓰기가 막고자 존재하는 그 손상이다.
durable 형태는 저장 장치를 두 번 기다린다. 쓰기를 잃는 것이 기다림보다 나쁠 때는 이것을, 그렇지 않을 때는 atomic 형태를 사용하라.
예전에는 여기에 proven_sysio_flush가 있었는데, 그것은 이 어느 것도 아니었다: 존재하지 않는 버퍼를 flush한다고 주장했다. 그것은 삭제되었다. 버퍼드 쓰기 스트림(writer)의 바이트를 OS로 밀어 넣는 것은 proven_writer_flush이고, OS의 바이트를 디스크로 밀어 넣는 것은 proven_fs_sync이다. 이 둘은 서로 다른 연산이며, 한 단어가 정직하게 둘 모두를 뜻할 수는 없었다.
중요한 동작:
proven_fs_read()와proven_fs_write()는 단일 연산 API이며 요청된 것보다 적은 바이트를 처리할 수 있다.- 파일 끝에서의 읽기는 0바이트 성공이 아니라
PROVEN_ERR_EOF를 반환한다. 자명한 방식으로 작성된 루프—if (r.value == 0) break;—는 결코 그 분기를 타지 않으며, 파일의 끝을 I/O 실패로 취급한다.PROVEN_ERR_EOF를 명시적으로 검사하라. 이 장 끝의 완성 예제가 그 형태를 보여준다. - 모든 바이트를 반드시 써야 할 때는
proven_fs_write_all()을 사용하라. - 크기 0의 읽기/쓰기 요청은 non-null 버퍼를 요구하지 않고 0바이트를 처리한 채로 성공해야 한다.
proven_fs_is_absolute()는 POSIX 절대 경로, Windows 드라이브-루트 경로, UNC 경로, 확장된 Windows 경로 형식을 인식한다.proven_fs_read_all()은 EOF까지 읽는다. 미리 측정된 크기까지 읽는 것이 아니다. 파일이 보고한 크기는 초기 용량의 씨앗(seed)으로만 쓰이므로, 일반 파일은 여전히 한 번의 할당과 한 번의 패스로 읽힌다. 이것이 중요한 이유는proven_fs_size()가 일반 파일이 아닌 모든 것에 대해 0을 보고하기 때문이다: FIFO, 캐릭터 디바이스,/proc엔트리에는 미리 알 수 있는 크기가 없으며, EOF까지 읽는 것만이 그 내용을 얻는 유일한 방법이다. 또한 이는 읽히는 도중 커지는 파일이 조용히 절단되지 않음을 뜻한다.value.size는 언제나 실제 바이트 수이며, 빈 소스는PROVEN_OK와 함께{ .ptr = NULL, .size = 0 }을 낳는다.proven_fs_read_all()과proven_fs_read_all_u8str()은 소스가 보고된 크기를 초과해 커질 경우realloc_fn을 가진 할당자(allocator)를 필요로 한다. 그렇지 않은(non-growing) allocator는 그 경우PROVEN_ERR_UNSUPPORTED를 반환한다.proven_fs_read_all_u8str()은 대부분의 호출자가 원하는 파일 전체 읽기다: 결과가 NUL로 종료되므로proven_u8str_as_view()와proven_u8str_as_cstr()가 두 번째 복사 없이 그 위에서 동작한다. 종료 슬롯은 미리 예약되므로 추가 할당 비용이 들지 않는다. 내용은 UTF-8로 검증되지 않는다.proven_u8str_destroy()로 해제하라.proven_fs_write_file()은 atomic이 아니다: reader가 부분적으로 쓰인 파일을 관찰할 수 있고, 쓰기 도중의 실패는 파일을 절단된 채로 남긴다.proven_fs_write_file_atomic()은 형제 임시 파일을 쓰고 그것을 대상 위로 rename하므로, 동시 reader는 전체 이전 파일 또는 전체 새 파일 둘 중 하나를 본다. 이는 reader에 대해 atomic이지만 전원 손실에 대해 durable하지는 않다: rename이 데이터보다 먼저 디스크에 도달할 수 있다. durability가 필요할 때는 위에서 설명한proven_fs_write_file_durable(또는proven_fs_sync)로 명시적으로 요청하라.
읽기 전용 대상은 넷 다 거절한다. proven_fs_write_file, proven_fs_write_file_atomic, proven_fs_write_file_durable, proven_fs_copy는 대상의 소유자 쓰기 비트가 꺼져 있으면 PROVEN_ERR_PERMISSION을 돌려주고 파일을 그대로 둔다. 그 비트가 두 플랫폼이 "이 파일에 쓰지 말라"를 기록하는 유일한 자리다 — POSIX에서는 모드 0200, Windows에서는 READONLY 속성이 같은 비트로 보고된다.
하나의 규칙인 이유는, 한 플랫폼 안에서 같은 질문에 세 가지 답이 나왔기 때문이다. write_file은 거절했다. 대상을 쓰기로 열기 때문이다. write_file_atomic은 성공했다. rename은 파일이 아니라 *디렉터리*에 권한을 묻기 때문에 파일의 모드를 아예 보지 않는다. copy는 성공했고 그 뒤 파일을 쓰기 가능한 상태로 남겼다. 원본의 모드를 옮겨 오기 때문이다. 호출자가 걸어 둔 보호가 사라졌는데 아무것도 그 사실을 말하지 않았다. 어느 함수를 골랐는지는 권한에 대한 결정이 아니다.
보호된 파일을 정말로 바꾸려는 호출자는 표시를 먼저 푼다 — 한 줄이고, 눈에 보인다. 거절은 되돌릴 수 있는 쪽이고, 실수로 덮어쓴 것은 되돌릴 수 없다. 보안 경계가 아니라 사고 방지 장치다: 모드는 작업 전에 읽고 작업 후에 반영되며, chmod를 할 수 있는 사람은 표시를 풀 수 있다.
proven_fs_rename도 그 목록에 있고, 있어야 한다. 대상을 대체하는 함수이고, atomic 쓰기가 바로 그 위에 지어져 있다 — 이것이 빠지면 한 함수에서 거절당한 호출자가 다른 함수로 같은 결과를 얻는다. proven_fs_remove는 일부러 빠져 있다: 이름을 지우는 것은 디렉터리 연산이고, POSIX는 파일 자신의 모드에 발언권을 준 적이 없다. 이 지점에서 두 플랫폼은 실제로 다르다 — Windows는 읽기 전용 파일을 지우지 않는다 — 그래서 proven_fs_remove는 그 차이를 입출력 오류 뒤에 숨기지 않고 PROVEN_ERR_PERMISSION으로 보고한다.
거절은 이제 어떤 거절인지 말한다. proven_fs_open, proven_fs_rename, proven_fs_remove는 예전에 전부 PROVEN_ERR_IO로 답하던 자리에서 PROVEN_ERR_NOT_FOUND, PROVEN_ERR_PERMISSION, PROVEN_ERR_BUSY를 답한다. 사용자에게 묻기, 다시 시도하기, 포기하기는 서로 다른 세 가지 답이고, 오류 코드 하나로는 그중 어느 것도 할 수 없다.
누가 읽고 있는 파일도 바꿔 넣는다. 다른 프로세스가 대상을 열어 두고 있어도 그 프로세스가 삭제 공유(delete sharing)를 허용했다면 — proven_fs_open이 그렇게 연다 — atomic 쓰기는 성공하고, 읽던 쪽은 열어 둔 핸들로 옛 내용을 끝까지 읽는다. POSIX가 원래 그렇고, Windows 10 1809 이후에서는 POSIX 방식 rename으로 같은 동작을 한다. 그 rename을 모르는 곳 — 1809 이전 Windows, FAT32·exFAT, 네트워크 드라이브 — 에서는 MoveFileExW로 물러서며, 거기서는 누가 열어 두기만 해도 교체가 거절되고 PROVEN_ERR_BUSY가 온다. 삭제 공유를 허용하지 않고 연 파일은 어느 Windows에서든 PROVEN_ERR_BUSY다. 보호가 아니라 사용 중이므로, 다시 시도하면 될 수 있다.
예:
proven_result_file_t of = proven_fs_open(
alloc,
PROVEN_LIT("out.txt"),
PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC
);
if (proven_is_ok(of.err)) {
proven_err_t e = proven_fs_write_all(
of.value,
proven_mem_view_from_u8(PROVEN_LIT("hello\n"))
);
/* The close is part of the write. Close on the failure path too - the handle is
* ours either way - but do not throw the answer away: on a network filesystem or
* over quota, close() is the only place the failure appears. */
proven_err_t ce = proven_fs_close(of.value);
if (proven_is_ok(e)) e = ce;
if (!proven_is_ok(e)) {
proven_eprintln("writing out.txt failed");
}
}2. 시스템 입출력과 환경변수
표준 스트림
proven_file_t proven_sysio_stdin(void);
proven_file_t proven_sysio_stdout(void);
proven_file_t proven_sysio_stderr(void);목적: 표준 스트림을 proven_file_t 핸들로 노출한다. 이들은 또한 writer와 reader이기도 하다— 아래 표준 스트림을 보라. 그것이 stdin을 한 줄씩 읽고, stdout을 버퍼링하며, 둘 중 어느 쪽으로든 직접 포매팅할 수 있게 해준다.
proven_sysio_scanner_t
typedef struct {
proven_file_t file;
proven_allocator_t alloc;
proven_u8 *buffer;
proven_size_t capacity;
proven_size_t cursor;
proven_size_t length;
bool eof;
} proven_sysio_scanner_t;목적: 경계가 있는 스트림 입력을 위한 버퍼드 스캐너로, seek 가능한 파일뿐 아니라 파이프와 stdin에도 안전하다. 토큰이 EOF 전에 현재 로드된 조각의 끝에 도달하면, 스캐너는 버퍼를 다시 채우고 재시도한다. 다시 채운 후에도 버퍼 전체에 들어갈 수 없는 토큰만이 절단되어 받아들여지는 대신 PROVEN_ERR_OUT_OF_BOUNDS로 거부된다.
Sysio 함수와 매크로
| API | 의도 | 반환 |
proven_sysio_scanner_init(scanner, file, alloc, buffer_capacity) | 스캐너 버퍼를 할당하고 스트림을 바인딩. | proven_err_t. |
proven_sysio_scanner_deinit(scanner) | 스캐너 버퍼를 해제. | void. |
proven_sysio_scanner_scan_impl(scanner, fmt, args, args_count) | 내부 스캐너 엔진. | proven_err_t. |
proven_sysio_scanner_scan(scanner, fmt, ...) | 타입 안전 버퍼드 스캔 매크로. | proven_err_t. |
proven_sysio_print_impl(handle, fmt, args, args_count) | 내부 출력 엔진. | proven_err_t. |
proven_sysio_scan_chunk_impl(handle, fmt, args, args_count) | 한-청크 스캔 엔진. | proven_err_t. |
proven_print(fmt, ...) | stdout으로 출력. | proven_err_t. |
proven_println(fmt, ...) | 개행과 함께 stdout으로 출력. | proven_err_t. |
proven_eprint(fmt, ...) | stderr로 출력. | proven_err_t. |
proven_eprintln(fmt, ...) | 개행과 함께 stderr로 출력. | proven_err_t. |
proven_scan_fmt_from_file(file, fmt, ...) | 파일의 고정 크기 청크 하나에서 스캔. | proven_err_t. |
proven_scan_fmt_from_stdin(fmt, ...) | stdin에서 고정 크기 청크 하나를 스캔. | proven_err_t. |
proven_env_get(alloc, key) | 환경 변수를 owned U8 문자열로 읽음. | proven_result_u8str_t. |
proven_sysio_scan_chunk_impl()은 seek 가능한 파일 입력을 위한 것이다. 최대 하나의 고정 크기 청크를 읽는다. 핸들을 되감을 수 없으면, 읽기 전에 PROVEN_ERR_UNSUPPORTED를 반환한다. 완전한 토큰이 준비되기 전에 청크가 가득 차면, PROVEN_ERR_OUT_OF_BOUNDS를 반환하고 파일 커서를 청크의 시작으로 되감는다. 문자열 뷰(view) 목적지도 읽기 전에 PROVEN_ERR_UNSUPPORTED로 거부한다. 그런 view는 이 함수의 지역 청크 버퍼를 빌리므로 반환 즉시 무효가 되기 때문이다. 반복적인 버퍼드 스캔이나 빌린 문자열 결과에는 호출자가 버퍼를 소유하는 proven_sysio_scanner_t를 사용하라. 그 스캐너가 반환한 문자열 view는 다음 scan 호출이나 스캐너 해제 전까지만 유효하다. 어느 쪽이든 공유 버퍼를 다시 채우거나 압축하거나 해제할 수 있기 때문이다.
예:
proven_println("answer={}", PROVEN_ARG(42));
proven_eprintln("warning: {}", PROVEN_ARG(PROVEN_LIT("low memory")));환경 변수 예. proven_env_get은 owned 문자열을 돌려주므로 반드시 파괴해야 한다:
proven_result_u8str_t env = proven_env_get(alloc, PROVEN_LIT("PATH"));
if (proven_is_ok(env.err)) {
proven_u8str_view_t path = proven_u8str_as_view(&env.value);
proven_println("PATH is {} bytes", PROVEN_ARG(path.size));
proven_u8str_destroy(alloc, &env.value);
}3. 메모리 매핑
문제: 복사하고 싶지 않은 파일 읽기
proven_fs_read_all_u8str는 파일을 당신이 소유하는 메모리로 읽어 들인다. 설정 파일이라면 그것이 정확히 옳다. 하지만 4 GB짜리 데이터베이스, 메모리 매핑된 인덱스, 또는 두 프로세스가 동시에 봐야 하는 파일이라면 세 가지 면에서 틀렸다: 4 GB의 RAM이 필요하고, 읽든 읽지 않든 모든 바이트를 복사하며, 그 복사본은 오직 당신만의 것이다.
메모리 매핑은 대신 운영체제에게 파일의 내용이 어떤 주소에 나타나게 해 달라고 요청한다. 미리 복사되는 것은 없다. 당신이 건드리는 페이지만 필요할 때 디스크에서 읽히고, 결코 건드리지 않는 페이지는 결코 읽히지 않는다. 두 프로세스가 같은 파일을 PROVEN_MMAP_SHARED로 매핑하면 하나의 페이지 집합을 보게 되므로, 한쪽에서의 쓰기가 다른 쪽에서 보인다.
이것의 비용, 그리고 쓰지 말아야 할 때
실패 양상이 오류처럼 보이지 않기 때문에, 이 절은 매뉴얼에서 득실이 가장 첨예한 부분이다:
- 읽기가 fault를 일으킬 수 있다. 파일이 메모리에 있으면 I/O 오류는 반환값이다. 매핑에서는, 디스크 읽기가 실패하는 페이지를 건드리면 오류 코드가 아니라 시그널(
SIGBUS)이 전달된다. 이를 우회하도록 작성할 수 있는if는 없다. - 잘림(truncation)은 지뢰다. 매핑한 뒤에 다른 프로세스가 파일을 줄이면, 새 끝을 넘어선 페이지를 건드리는 것 역시
SIGBUS다. 다른 무언가가 잘라낼 수 있는 파일을 매핑하는 것은 어떤 API도 대신 고쳐 줄 수 없는 방식으로 안전하지 않다. - 공짜가 아니다. 매핑을 설정하는 것은 syscall이자 페이지 테이블 작업이며, 첫 접촉마다 페이지 fault가 난다. 작은 파일이라면
proven_fs_read_all_u8str가 그냥 더 빠르다. proven_mmap_sync가 지속성의 지점이다. 공유 매핑에 대한 쓰기는 결국 파일에 도달한다. 지금 디스크에 있어야 한다면, 요청하라.
큰 파일을 드문드문 읽을 때, 프로세스 간에 공유하는 읽기 전용 데이터에, 그리고 큰 파일에 대한 임의 접근에 매핑을 쓰라. 그 밖의 모든 것에는 평범한 읽기를 쓰라.
매핑은 호출자가 소유하는 상태다: proven_mmap_as_view는 매핑 안을 가리키는 view를 건네주므로, 그 view는 proven_mmap_destroy가 실행되는 순간 죽는다.
잘못된 예 — 매핑을 파괴한 뒤에 view를 사용하기:
proven_u8str_view_t data = proven_mmap_as_view(m);
proven_err_t e = proven_mmap_destroy(&m);
(void)e;
parse(data); /* wrong: those addresses are no longer mapped - SIGSEGV */잘못된 예 — 매핑을 키울 수 있는 버퍼처럼 취급하기:
/* wrong: a mapping is a window onto a file of a fixed size at map time.
Appending means changing the file and mapping it again. */구조체와 열거형
typedef enum {
PROVEN_MMAP_READ = 0x01,
PROVEN_MMAP_WRITE = 0x02,
PROVEN_MMAP_EXEC = 0x04
} proven_mmap_prot_t;
typedef enum {
PROVEN_MMAP_PRIVATE = 0x01,
PROVEN_MMAP_SHARED = 0x02
} proven_mmap_flags_t;
typedef struct {
void *ptr;
proven_size_t size;
proven_fs_handle_t file;
void *internal_handle;
} proven_mmap_t;
typedef struct {
proven_err_t err;
proven_mmap_t value;
} proven_result_mmap_t;함수
| API | 의도 | 반환 |
proven_mmap_create(file, offset, size, prot, flags) | 파일 영역을 매핑. size == 0은 EOF까지 매핑한다. 오프셋은 플랫폼의 매핑 세분성(granularity)과 일치해야 한다. | proven_result_mmap_t. |
proven_mmap_destroy(mmap) | 영역을 언매핑하고 상태를 정리. | proven_err_t. |
proven_mmap_sync(mmap) | 공유된 쓰기 가능 변경분을 저장소로 flush. | proven_err_t. |
proven_mmap_as_view(mmap) | 매핑된 메모리를 U8 view로 빌림(borrow). | proven_u8str_view_t. |
예:
proven_result_file_t f = proven_fs_open(alloc, PROVEN_LIT("data.bin"), PROVEN_FS_READ);
if (proven_is_ok(f.err)) {
proven_result_mmap_t mr = proven_mmap_create(
f.value,
0,
0,
PROVEN_MMAP_READ,
PROVEN_MMAP_PRIVATE
);
if (proven_is_ok(mr.err)) {
/* The view borrows the mapping: it dies when the mapping is destroyed. */
proven_u8str_view_t bytes = proven_mmap_as_view(mr.value);
proven_println("mapped {} bytes", PROVEN_ARG(bytes.size));
(void)proven_mmap_destroy(&mr.value);
}
(void)proven_fs_close(f.value);
}4. 시간 API
문제: 서로 다른 두 질문, 하나의 단어
"시간"은 서로 닮았지만 하는 짓은 전혀 다른 두 가지를 뜻한다.
벽시계(wall clock)는 *지금 몇 시인가?*에 답한다 — 사용자가 읽는 그것이다. 이 시계는 튀어도 된다: NTP가 보정하고, 서머타임이 옮기고, 관리자가 설정한다. 이것으로 경과 시간을 재면 음수가 나올 수 있는데, 이는 윤초가 있는 날에 나타나고 테스트 중인 누구의 노트북에서도 나타나지 않는 버그다.
단조(monotonic) 시계는 *언제부터 얼마나 지났는가?*에 답한다. 앞으로만 움직이며 어떤 달력과도 관계가 없다. 연산의 시간을 잴 때 쓰는 것이 이것이다.
libc는 이 구분을 흐린다. time()은 벽시계의 초 단위 정수를 준다 — 무언가를 재기에는 너무 거칠다. clock()은 CPU 시간을 재므로, 소켓을 기다리는 프로그램은 시간이 전혀 걸리지 않은 것처럼 보인다. 어느 이름도 자신이 어느 질문에 답하는지 말해 주지 않으며, 둘 다 서로의 용도로 흔히 쓰인다.
이 라이브러리는 대신 무엇을 하는가
proven_time_now()는 Unix epoch 이후의 나노초를 부호 있는 64비트 값으로 반환한다. 빼면 경과 시간이 되고, 분해하면 달력 날짜가 되는 하나의 숫자다 — 실제 작업의 시간을 잴 만큼 충분히 고운 해상도로.
proven_time_breakdown()은 그 숫자를 proven_datetime_t로 바꾸며, 그 필드는 사람이 쓰는 것들이다: month는 libc의 0-11이 아니라 1-12이고, year는 1900년 이후의 햇수가 아니라 실제 연도다. struct tm의 그 두 가지 off-by-N 관례는 의도적으로 갈라설 만큼 충분히 많은 버그를 낳았다.
포맷팅은 3장과 같은 {} 자리표시자를 쓴다: 이름이 필드를 고르고 스펙이 자리를 채우므로, {month:0>2}는 너비 2로 0을 채운다. 월과 요일 이름은 proven_time_locale_t에서 오므로, 다른 언어로 렌더링하는 것은 전역을 설정하는 것이 아니라 다른 locale을 넘기는 일이다.
잘못된 예 — 벽시계로 시간을 재고 부호를 믿기:
proven_time_t t0 = proven_time_now();
do_work();
proven_time_t t1 = proven_time_now();
proven_u64 ns = (proven_u64)(t1 - t0); /* wrong: an NTP step back makes this enormous */뺄셈은 괜찮다. 캐스트가 문제다. 차이를 부호 있는 값으로 유지하고, 음수 경과 시간은 지속 시간이 아니라 "시계가 움직였다"로 취급하라.
잘못된 예 — sleep이 정확하다고 가정하기:
proven_time_sleep(15);
/* wrong to assume exactly 15ms have passed: sleep guarantees AT LEAST that long,
and the scheduler decides when you actually run again. */완성 예제: 경과 시간, 날짜, 그리고 포맷된 타임스탬프
테스트 스위트가 컴파일하고 실행한다. 이 예제가 무엇을 단언하지 *않는지*에 주목하라 — sleep의 상한이다. 그것을 단언하면 바쁜 기계에서 실패하는 테스트가 되기 때문이다.
/*
* 시간에는 똑같이 생겼지만 같지 않은 두 갈래가 있고, 잘못 고르는 것이 시간 재기의
* 고전적 버그다.
*
* - *벽시계*는 "지금 몇 시인가" 에 답한다. 사용자가 보고 싶어 하는 것이고, 뛰는 것이
* 허용된다 - NTP 가 고치고, 서머타임이 옮기고, 관리자가 맞춘다. 그것으로 기간을
* 재면 음수 경과 시간이 나올 수 있고, 윤초가 있던 날에 실제로 유명하게 그랬다.
*
* - *단조* 시계는 "그때로부터 얼마나" 에 답한다. 앞으로만, 고른 속도로 가고, 어떤
* 달력과도 관계가 없다. 어떤 작업의 시간을 잴 때 쓰는 것이 이것이다.
*
* libc 는 이 구분을 흐린다. time() 은 초 단위 벽시계라 재는 데는 쓸모없다. clock() 은
* 경과 시간이 아니라 CPU 시간을 재므로, 잠든 프로그램은 즉시 끝난 것처럼 보인다. 어느
* 이름도 자기가 둘 중 어느 물음에 답하는지 말해 주지 않는다.
*
* proven_time_now() 는 유닉스 기점부터의 나노초다. 날짜로 형식화되기도 하고 기간으로
* 빼지기도 하는 수 하나이며, 실제 작업의 시간을 재기에 충분히 고운 해상도를 갖는다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* --- 기간으로 쓰기 -------------------------------------------------- */
proven_time_t start = proven_time_now();
proven_time_sleep(15); /* 밀리초 */
proven_time_t end = proven_time_now();
proven_i64 elapsed_ns = end - start;
EXAMPLE_REQUIRE(elapsed_ns > 0, "time must move forward across a sleep");
/* sleep 은 청한 시간 *이상*을 보장하지, 이하를 보장하지 않는다. 언제 다시 돌지는
* 스케줄러가 정한다. 여기서 상한을 단언하면 바쁜 기계에서 실패하는 시험이 되고,
* 그래서 이 예제는 그러지 않는다. */
EXAMPLE_REQUIRE(elapsed_ns >= 10 * 1000 * 1000,
"sleeping 15ms must take at least ~10ms of wall time");
/* --- 날짜로 쓰기 ---------------------------------------------------- */
proven_datetime_t dt = proven_time_breakdown(start);
EXAMPLE_REQUIRE(dt.year >= 2020 && dt.year < 3000, "the epoch breakdown gives a plausible year");
EXAMPLE_REQUIRE(dt.month >= 1 && dt.month <= 12, "month is 1-12, not 0-11 as in libc's tm");
EXAMPLE_REQUIRE(dt.day >= 1 && dt.day <= 31, "day is 1-31");
EXAMPLE_REQUIRE(dt.hour <= 23 && dt.min <= 59 && dt.sec <= 60, "sec allows 60 for leap seconds");
EXAMPLE_REQUIRE(dt.weekday <= 6, "weekday is 0-6 with 0 = Sunday");
/* proven_time_now_datetime() 은 위의 두 호출을 하나로 합친 것이다. 달력 꼴만
* 필요할 때 쓴다. */
proven_datetime_t now = proven_time_now_datetime();
EXAMPLE_REQUIRE(now.year == dt.year, "both routes read the same clock");
/* --- 시각을 글자로 ------------------------------------------------- */
proven_result_u8str_t s = proven_u8str_create(alloc, 64);
EXAMPLE_REQUIRE(proven_is_ok(s.err), "a 64-byte string is enough for a timestamp");
/* 달 이름과 요일 이름은 로케일이 준다. proven_time_locale_en 이 내장된 영어
* 로케일이다. 다른 언어로 찍으려면 여러분의 것을 건넨다. */
proven_err_t err = proven_time_u8_fmt(alloc, &s.value, dt, &proven_time_locale_en,
"{year}-{month:0>2}-{day:0>2} {hour:0>2}:{min:0>2}:{sec:0>2}");
EXAMPLE_REQUIRE(proven_is_ok(err), "formatting a datetime should succeed");
proven_u8str_view_t out = proven_u8str_as_view(&s.value);
EXAMPLE_REQUIRE(out.size == 19, "year-month-day hour:min:sec is exactly 19 characters");
EXAMPLE_REQUIRE(out.ptr[4] == '-' && out.ptr[7] == '-' && out.ptr[13] == ':',
"the separators land where the pattern put them");
proven_println("formatted: {}", PROVEN_ARG(out));
proven_u8str_destroy(alloc, &s.value);
return EXAMPLE_OK();
}타입
typedef proven_i64 proven_time_t;UNIX epoch 이후 나노초.
typedef struct {
proven_i32 year;
proven_u8 month;
proven_u8 day;
proven_u8 hour;
proven_u8 min;
proven_u8 sec;
proven_u32 ms;
proven_u8 weekday;
} proven_datetime_t;필드 범위:
month: 1 ~ 12.day: 1 ~ 31.hour: 0 ~ 23.min: 0 ~ 59.sec: 0 ~ 60.ms: 0 ~ 999.weekday: 0 ~ 6, 일요일이 0.
typedef struct {
const proven_u8str_view_t *month_names;
const proven_u8str_view_t *month_short_names;
const proven_u8str_view_t *weekday_names;
const proven_u8str_view_t *weekday_short_names;
} proven_time_locale_t;proven_time_locale_en은 기본 영어 로케일이다.
함수
| API | 의도 | 반환 |
proven_time_u8_fmt(alloc, str, dt, locale, fmt) | 포맷된 날짜시간을 U8 문자열에 덧붙임. | proven_err_t. |
proven_time_u16_fmt(alloc, str, dt, locale, fmt) | U16이 비활성화되지 않은 한 포맷된 날짜시간을 U16 문자열에 덧붙임. | proven_err_t. |
proven_time_now() | 현재 타임스탬프(나노초). | proven_time_t. |
proven_time_breakdown(time_ns) | epoch 나노초를 분해된 UTC 시간으로 변환. | proven_datetime_t. |
proven_time_now_datetime() | 현재 로컬 분해 시간. | proven_datetime_t. |
proven_time_sleep(ms) | 밀리초 동안 sleep. | void. |
날짜시간 포맷 키:
{year},{month},{day},{hour},{min},{sec},{ms},{wday_num}- 로케일을 사용하는
{Month},{mon} - 로케일을 사용하는
{Weekday},{wday}
예:
proven_result_u8str_t r = proven_u8str_create(alloc, 32);
if (proven_is_ok(r.err)) {
proven_u8str_t s = r.value;
proven_datetime_t now = proven_time_now_datetime();
proven_err_t e = proven_time_u8_fmt(
alloc,
&s,
now,
&proven_time_locale_en,
"{year}-{month:0>2}-{day:0>2} {hour:0>2}:{min:0>2}:{sec:0>2}"
);
if (proven_is_ok(e)) {
proven_println("{}", PROVEN_ARG(proven_u8str_as_view(&s)));
}
proven_u8str_destroy(alloc, &s);
}5. 예제와 오용 사례
단일 쓰기는 부분적일 수 있다
잘못됨(Wrong):
proven_result_size_t w = proven_fs_write(file, data);
/* wrong: one write may be partial, and w.value is never looked at */올바름(Correct):
proven_result_file_t f = proven_fs_open(alloc, PROVEN_LIT("out.txt"),
PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
if (proven_is_ok(f.err)) {
proven_mem_view_t data = proven_mem_view_from_u8(PROVEN_LIT("payload"));
proven_err_t e = proven_fs_write_all(f.value, data); /* loops until done */
proven_err_t ce = proven_fs_close(f.value); /* and the close can fail too */
if (proven_is_ok(e)) e = ce;
(void)e;
}디렉터리 목록에는 특별한 파괴가 필요하다
잘못됨(Wrong):
proven_result_array_t r = proven_fs_list(alloc, PROVEN_LIT("."));
PROVEN_ARRAY_DESTROY(&r.value); /* wrong: entry names leak */올바름(Correct):
proven_result_array_t r = proven_fs_list(alloc, PROVEN_LIT("."));
if (proven_is_ok(r.err)) {
proven_fs_list_destroy(alloc, &r.value);
}destroy 이후에 매핑을 사용하지 말라
잘못됨(Wrong):
proven_mmap_destroy(&map);
use_bytes(map.ptr, map.size); /* wrong: mapping has been released */스트림에는 버퍼드 sysio 스캔을 사용하라
stdin이나 파이프에서 반복적으로 읽을 때는 다음을 선호하라:
proven_sysio_scanner_t scanner = {0};
proven_err_t e = proven_sysio_scanner_init(&scanner, proven_sysio_stdin(), alloc, 4096);
if (proven_is_ok(e)) {
int value = 0;
e = proven_sysio_scanner_scan(&scanner, "{}", PROVEN_SCAN_ARG(&value));
proven_sysio_scanner_deinit(&scanner);
}환경 값은 owned 문자열이다
잘못됨(Wrong):
proven_result_u8str_t env = proven_env_get(alloc, PROVEN_LIT("PATH"));
use_path(proven_u8str_as_view(&env.value)); /* wrong if env.err was not checked, and it leaks */올바름(Correct):
proven_result_u8str_t env = proven_env_get(alloc, PROVEN_LIT("HOME"));
if (proven_is_ok(env.err)) {
proven_u8str_view_t home = proven_u8str_as_view(&env.value);
proven_println("HOME={}", PROVEN_ARG(home));
proven_u8str_destroy(alloc, &env.value);
}완성 예제: 파일 전체를 읽고 쓰기
테스트 스위트에 의해 컴파일되고 실행됨. 이것은 대부분의 호출자가 실제로 원하는 파일 전체 API이며, 대상의 권한을 보존하는 atomic 재작성을 포함한다.
/*
* 파일 통째 API: 한 번 부르면 들어가고, 한 번 부르면 나온다. 이것이 있는 이유는 열고 -
* 읽는 반복문 - 닫는 그 춤사위가 파일 다루기 버그 대부분이 사는 자리이기 때문이다.
* 잊은 close, EOF 로 오해한 부분 읽기, 실패한 쓰기가 남긴 잘린 파일. 파일을 통째로 읽거나
* 쓰는 것이라면 이것이 그 API 다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* 지금 디렉터리의 상대 경로다. 이 예제가 쓸 수 있는 /tmp 에 기대면 안 되고, 만든
* 것은 돌아가기 전에 지운다. */
proven_u8str_view_t path = PROVEN_LIT("proven_example_wholefile.tmp");
proven_u8str_view_t text = PROVEN_LIT("first line\nsecond line\n");
/* --- 한 번의 호출로 쓰기 ------------------------------------------------ */
/* 원자적이지 않다. 동시에 읽는 쪽은 이 파일이 반쯤 쓰인 것을 볼 수 있다. 여기서는
* 괜찮다. 아직 아무도 이 파일을 보고 있지 않기 때문이다. */
proven_err_t err = proven_fs_write_file(alloc, path, proven_mem_view_from_u8(text));
EXAMPLE_REQUIRE(proven_is_ok(err), "writing the whole file should succeed");
if (!proven_is_ok(err)) return 1;
/* --- 날바이트로 되읽기 -------------------------------------------------- */
/* proven_fs_read_all 은 미리 잰 크기까지가 아니라 EOF 까지 읽는다. 그래서 크기를
* 미리 알 수 없는 파이프나 /proc 항목에서도 돈다. */
proven_result_mem_mut_t raw = proven_fs_read_all(alloc, path);
EXAMPLE_REQUIRE(proven_is_ok(raw.err), "reading the whole file should succeed");
if (proven_is_ok(raw.err)) {
EXAMPLE_REQUIRE(raw.value.size == text.size, "read_all should return every byte written");
/* 그 덩이는 그냥 할당자의 기억이다 - 그것을 내준 할당자에게 돌려줄 것.
* proven_fs_read_all_destroy 같은 것은 없다. */
alloc.free_fn(alloc.ctx, raw.value.ptr);
}
/* --- 문자열로 되읽기 ---------------------------------------------------- */
/* 대개의 부르는 쪽이 원하는 것이 이것이다. 결과가 NUL 로 끝나므로 뷰에도, as_cstr
* 에도, 스캐너에도 두 번째 복사 없이 건넬 수 있다. 종결자 자리는 미리 잡아 두므로
* 할당이 더 들지 않는다. */
proven_result_u8str_t s = proven_fs_read_all_u8str(alloc, path);
EXAMPLE_REQUIRE(proven_is_ok(s.err), "reading the whole file as a string should succeed");
if (!proven_is_ok(s.err)) {
(void)proven_fs_remove(alloc, path);
return 1;
}
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&s.value), text),
"the file's contents should come back unchanged");
printf("read back %zu bytes: %s", (size_t)proven_u8str_as_view(&s.value).size,
proven_u8str_as_cstr(&s.value));
proven_u8str_destroy(alloc, &s.value);
/* --- stat, 그리고 권한의 왕복 ------------------------------------------ */
proven_fs_stat_t st = {0};
err = proven_fs_stat(alloc, path, &st);
EXAMPLE_REQUIRE(proven_is_ok(err), "stat on a file we just wrote should succeed");
EXAMPLE_REQUIRE(st.type == PROVEN_FS_TYPE_FILE, "a regular file should stat as a FILE");
EXAMPLE_REQUIRE(st.size == text.size, "stat should report the size we wrote");
/* `perms` 는 권한 비트 아홉 개만 담고 그 밖의 것은 담지 않는다. 그래서 그대로
* chmod 에 도로 먹일 수 있다. 이 필드의 요점이 그것이다 - 파일의 모드를 읽어 두었다가
* 나중에 되돌리는 것. (예전에는 날 POSIX st_mode 를 담았는데, 그 파일 종류 비트를
* chmod 가 거부해서 이 뻔한 왕복이 실패했다.) */
err = proven_fs_chmod(alloc, path, st.perms);
EXAMPLE_REQUIRE(proven_is_ok(err), "a stat's perms must be accepted back by chmod");
/* 이제 파일을 소유자 전용으로 바꾼다. 그래야 다음 검사가 증명할 것이 생긴다. */
proven_fs_perms_t private_perms = PROVEN_FS_PERM_OWNER_R | PROVEN_FS_PERM_OWNER_W;
err = proven_fs_chmod(alloc, path, private_perms);
EXAMPLE_REQUIRE(proven_is_ok(err), "restricting the file to its owner should succeed");
/* --- 원자적으로 다시 쓰기 ----------------------------------------------- */
/* 형제 임시 파일 하나에 rename 하나. 동시에 읽는 쪽은 옛 파일 전체이거나 새 파일
* 전체를 보지, 반쯤 섞인 것을 보지 않는다. 읽는 쪽에게 원자적이지, 전원이 나가도
* 견디는 것은 아니다. 그래야 할 때는 proven_fs_write_file_durable 이 그것을 청한다. */
proven_u8str_view_t text2 = PROVEN_LIT("replacement\n");
err = proven_fs_write_file_atomic(alloc, path, proven_mem_view_from_u8(text2));
EXAMPLE_REQUIRE(proven_is_ok(err), "the atomic rewrite should succeed");
proven_fs_stat_t st2 = {0};
err = proven_fs_stat(alloc, path, &st2);
EXAMPLE_REQUIRE(proven_is_ok(err), "stat after the atomic rewrite should succeed");
EXAMPLE_REQUIRE(st2.size == text2.size, "the file should now hold the replacement text");
/* rename 은 옛 이름 위에 *새* 아이노드를 씌우므로, 권한을 옮겨 주지 않으면 잃는다.
* 옮겨 준다 - 0600 파일을 다시 써도 0644 로 다시 공개되지 않는다. */
EXAMPLE_REQUIRE(st2.perms == private_perms,
"the atomic rewrite must preserve the target's permissions");
/* --- 뒷정리 -------------------------------------------------------------- */
err = proven_fs_remove(alloc, path);
EXAMPLE_REQUIRE(proven_is_ok(err), "removing the temp file should succeed");
return EXAMPLE_OK();
}완성 예제: open, read, write, close
테스트 스위트에 의해 컴파일되고 실행됨. read 루프에 주목하라: 파일 끝에서의 읽기는 0바이트 성공이 아니라 PROVEN_ERR_EOF를 반환하므로, 0바이트만 검사하는 루프는 작성자가 기대한 방식으로 결코 종료되지 않는다.
/*
* 열고-읽고-쓰고-닫는 길. 파일 통째 API(ex_05_fs_wholefile)로는 모자랄 때 쓴다.
* 흘려 보내고 있거나, 버퍼를 여러분이 소유하고 싶을 때다.
*
* 여기서 반드시 맞춰야 할 하나: 읽기나 쓰기 한 번은 청한 바이트 수 *만큼까지*를 옮기지,
* 정확히 그만큼을 옮기지 않는다. 짧은 읽기 한 번을 파일 끝으로 여기는 것이 파일의 꼬리를
* 조용히 잃는 고전적인 방법이다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
proven_u8str_view_t path = PROVEN_LIT("proven_example_stream.tmp");
proven_u8str_view_t text = PROVEN_LIT("streamed bytes, read back in chunks\n");
/* --- 쓰기 --------------------------------------------------------------- */
/* CREATE 는 없으면 만들고, TRUNC 는 있으면 비운다. 할당자는 플랫폼 호출을 위해 경로를
* 변환하는 데만 쓰인다. */
proven_result_file_t out = proven_fs_open(alloc, path,
PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
EXAMPLE_REQUIRE(proven_is_ok(out.err), "opening the file for writing should succeed");
if (!proven_is_ok(out.err)) return 1;
/* write_all 이 우리 대신 반복해 준다. proven_fs_write 는 한 번만 시도하고 더 적게 쓸
* 수도 있는데, 그것은 부르는 쪽이 뜻한 바가 거의 아니다. */
proven_err_t err = proven_fs_write_all(out.value, proven_mem_view_from_u8(text));
/* close 는 쓰기의 일부이고, 네트워크나 할당량이 걸린 파일 시스템에서는 실패가 드러나는
* *유일한* 자리다. 바이트는 버퍼에 담겼고 write() 는 그렇다고 했으며, 디스크가 마침내
* 아니라고 말하는 자리가 close() 다. 실패 경로에서도 닫을 것 - 손잡이는 어느 쪽이든
* 우리 것이다 - 다만 그 답을 버리지는 말 것. */
proven_err_t cerr = proven_fs_close(out.value);
if (proven_is_ok(err)) err = cerr;
EXAMPLE_REQUIRE(proven_is_ok(err), "writing the whole buffer should succeed");
if (!proven_is_ok(err)) {
(void)proven_fs_remove(alloc, path);
return 1;
}
/* --- 읽기 --------------------------------------------------------------- */
proven_result_file_t in = proven_fs_open(alloc, path, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(in.err), "opening the file for reading should succeed");
if (!proven_is_ok(in.err)) {
(void)proven_fs_remove(alloc, path);
return 1;
}
/* size 는 버퍼 크기를 잡는 힌트이지, 읽기 한 번이 몇 바이트를 건넬지에 대한 약속이
* 아니다 - 그리고 보통 파일이 아닌 것(파이프, 장치, /proc 항목)에서는 0 이다. 아래
* 반복문은 그것에 기대지 않는다. */
proven_result_size_t sz = proven_fs_size(in.value);
EXAMPLE_REQUIRE(proven_is_ok(sz.err), "querying the size of an open file should succeed");
EXAMPLE_REQUIRE(sz.value == text.size, "the file should be as long as what we wrote");
proven_byte_t buf[128];
proven_size_t total = 0;
/* 부분 읽기 반복문. 한 바퀴마다 버퍼에 남은 만큼을 청하고 실제로 온 만큼 나아간다.
* 짧은 읽기는 파일의 끝이 아니라 정상이다. 파일의 끝은 따로 있는 상태 -
* 바이트 0 과 함께 오는 PROVEN_ERR_EOF - 이므로 반복문은 그것으로만 끝난다. 원본이
* 버퍼보다 커져도 반복문은 멈추는데, 그것을 알아채는 일은 부르는 쪽의 몫이다(여기서는
* 일어날 수 없지만, 자라나는 파일이라면 그럴 수 있다). */
for (;;) {
if (total == sizeof buf) break; /* 버퍼가 가득 찼다: 어떻게 할지는 부르는 쪽이 정한다 */
proven_mem_mut_t dest = { .ptr = buf + total, .size = sizeof buf - total };
proven_result_size_t r = proven_fs_read(in.value, dest);
if (r.err == PROVEN_ERR_EOF) break;
if (!proven_is_ok(r.err)) {
(void)proven_fs_close(in.value);
(void)proven_fs_remove(alloc, path);
EXAMPLE_REQUIRE(false, "reading from the open file should not fail");
return 1;
}
total += r.value;
}
(void)proven_fs_close(in.value);
EXAMPLE_REQUIRE(total == text.size, "the loop should have read every byte in the file");
proven_u8str_view_t got = { .ptr = buf, .size = total };
EXAMPLE_REQUIRE(proven_u8str_view_eq(got, text), "the bytes should come back unchanged");
printf("read %zu bytes in chunks: %.*s", (size_t)total, (int)total, (const char *)buf);
/* --- 뒷정리 -------------------------------------------------------------- */
err = proven_fs_remove(alloc, path);
EXAMPLE_REQUIRE(proven_is_ok(err), "removing the temp file should succeed");
return EXAMPLE_OK();
}디렉터리를 한 번에 한 엔트리씩 읽기
proven_fs_list는 당신이 그 중 어느 것을 보기 전에 디렉터리 전체를 읽으며, 모든 이름마다 문자열을 할당한다. 50,000개 엔트리로 측정: 189 ms, +4.2 MB 상주(resident), 50,008회 할당, 마지막 엔트리가 읽힐 때까지 아무것도 보이지 않는다. 이는 설정 디렉터리에는 괜찮고 메일 스풀에는 무용하다.
proven_fs_dir_open / _next / _close는 같은 디렉터리를 한 번에 한 엔트리씩 순회하며 엔트리당 아무것도 할당하지 않는다.
| API | 의도 | 반환 |
proven_fs_dir_open(scratch, path) | 스트리밍 반복자를 연다. scratch는 경로 변환에만 쓰인다. | proven_result_dir_t. |
proven_fs_dir_next(&dir, &entry) | 다음 엔트리. 더 이상 없으면 PROVEN_ERR_EOF. | proven_err_t. |
proven_fs_dir_close(&dir) | 반복자를 해제. | void. |
typedef struct {
proven_u8str_view_t name; /* BORROWED: points into the iterator's own storage,
valid only until the next proven_fs_dir_next */
proven_fs_type_t type; /* FILE / DIR / OTHER - and it follows symlinks */
} proven_fs_dir_entry_t;이렇게 사용하라:
proven_result_dir_t d = proven_fs_dir_open(alloc, PROVEN_LIT("."));
if (proven_is_ok(d.err)) {
proven_fs_dir_t dir = d.value;
proven_fs_dir_entry_t entry;
for (;;) {
proven_err_t e = proven_fs_dir_next(&dir, &entry);
if (e == PROVEN_ERR_EOF) break;
if (!proven_is_ok(e)) break; /* a real error: report it */
proven_println("{}", PROVEN_ARG(entry.name));
}
proven_fs_dir_close(&dir);
}이름은 빌려 쓰는(borrowed)이며, 다음 호출에서 소멸한다. 이것이 전체를 무할당으로 만드는 것—그리고 당신이 그것을 붙들어 두는 순간 dangling 포인터로 만드는 것이다.
잘못됨(Wrong):
proven_u8str_view_t names[100];
int n = 0;
while (proven_is_ok(proven_fs_dir_next(&dir, &entry)))
names[n++] = entry.name; /* wrong: every entry aliases the same storage, and the
next _next overwrites it. All 100 end up equal - to
whatever the last entry happened to be. */올바름(Correct): 보관해야 하는 것들에 대해 바이트를 복사하라(proven_u8str_create_from_view)—또는 proven_fs_list를 사용하라. 그것은 모든 엔트리에 대해 정확히 그렇게 하고 그 대가를 청구한다.
PROVEN_ERR_EOF가 끝이며, 그 밖의 것은 실패다. "OK 아님"에서 멈추는 루프는 권한 에러를 완전한 목록으로 취급한다.
while (proven_is_ok(proven_fs_dir_next(&dir, &entry))) { ... }
/* wrong: an I/O error ends the loop exactly like the end of the directory does,
and you cannot tell whether you saw everything. */트리 순회
proven_fs_dir_*는 디렉터리 하나를 순회한다. proven_fs_walk는 트리를 순회한다—그리고 그것이 하기를 거부하는 것을 정확히 말할 가치가 있다. 그 거부들이 곧 기능이기 때문이다:
| 루프에 빠질 수 없다 | 결코 symlink를 통과해 내려가지 않는다. |
| 탈출할 수 없다 | 같은 규칙: 트리 밖으로 나가는 링크는 보고되되 따라가지 않는다. |
| 거짓말할 수 없다 | 읽을 수 없는 디렉터리는 그 디렉터리를 명명하는 *에러*로 돌아오고, 순회는 계속된다. 읽을 수 없는 하위 트리를 조용히 건너뛰는 트리 순회기는 백업이 파일을 놓치고도 성공을 보고하는 방식이다. |
| 비대해질 수 없다 | 현재 경로의 *레벨*당 하나의 열린 핸들, 그리고 재사용되는 하나의 경로 버퍼. 메모리는 파일 수가 아니라 깊이의 함수다. |
symlink된 디렉터리는 여전히 보고된다—그것은 존재하고, type은 DIR이며, is_symlink는 true다—단지 진입되지 않을 뿐이다. 그것을 숨기는 것은 그 자체로 일종의 거짓말이 될 것이다. 그것을 따라가고 싶다면, 당신에게는 경로가 있다: 그것에 대해 두 번째 순회를 열면, 사이클 문제는 당신의 소유가 된다.
당신이 받는 구조체
typedef struct {
proven_u8str_view_t path; /* the whole path, from the root you passed in.
BORROWED: it points into the walk's one reused
buffer and is valid only until the next call. */
proven_u8str_view_t name; /* the last component of `path`. Same lifetime. */
proven_fs_type_t type; /* FILE / DIR / OTHER - and it FOLLOWS symlinks,
exactly as proven_fs_stat does. */
proven_size_t size; /* bytes, for a regular file; 0 otherwise. */
proven_size_t depth; /* 0 for an entry directly inside the root. */
bool is_symlink; /* reached through a symlink. `type` describes
the TARGET; the walk does not enter it. */
} proven_fs_walk_entry_t;entry.path와 entry.name은 순회의 재사용되는 하나의 버퍼에서 빌린 것이며 다음 호출 전까지만 유효하다. 이 단계보다 오래 살아남게 하려면 복사하라. 그것이 백만 엔트리의 순회가 백만 번이 아니라 한 번의 할당으로 드는 것의 대가다.
잘못됨(Wrong)—디렉터리 반복자가 가진 것과 같은 함정, 같은 이유로:
proven_u8str_view_t found[100];
int n = 0;
while (proven_is_ok(proven_fs_walk_next(&walk, &entry)))
found[n++] = entry.path; /* wrong: every entry aliases ONE buffer. When the loop
ends, all 100 point at whatever the last path was. */두 개의 제한, 둘 다 조용해지는 대신 그렇다고 말한다:
max_depth—얼마나 깊이 내려갈지. 제한 에 있는 디렉터리는 여전히 보고된다(그것은 하나의 엔트리다). 진입되지 않을 뿐이다.PROVEN_FS_WALK_DEPTH_LIMIT(256)—순회 자체의 스택이 결코 넘지 않는 깊이. 그것을 지나친 디렉터리는 그 디렉터리를 명명하며PROVEN_ERR_OUT_OF_BOUNDS로 돌아온다.
테스트 스위트에 의해 컴파일되고 실행됨:
/*
* 나무 훑기.
*
* 재귀 훑기가 틀리는 세 가지, 그리고 이것이 대신 하는 일.
*
* 맴돈다. 조상을 가리키는 심링크는 순환이다. 이 훑기는 심링크를 *지나* 내려가지
* 않는다 - 심링크된 디렉터리도 보고는 되고, 다만 들어가지 않을 뿐이다 -
* 그래서 순환이 불가능하고, 물어본 나무를 벗어나 나머지 파일 시스템으로
* 걸어 나가는 일도 없다.
*
* 거짓말한다. 읽을 수 없는 디렉터리를 건너뛰고는 성공했다고 보고한다. 백업이 하위
* 나무 하나를 통째로 놓치는 방식이 그것이다. 여기서는 오류가
* proven_fs_walk_next 에서 돌아오고, 그 항목이 어느 디렉터리인지 말해
* 주며, 훑기는 다음 형제부터 이어 간다. 어떻게 할지는 여러분이 정한다.
*
* 부푼다. 하나라도 내주기 전에 디렉터리 전체를 기억에 읽어 들이면, 큰 나무를
* 훑는 데 큰 할당이 든다. 이것은 지금 경로의 *층*마다 열린 손잡이 하나와
* 다시 쓰는 경로 버퍼 하나만 쥔다 - 그래서 기억 사용량은 파일이 몇
* 개인가가 아니라 깊이의 함수다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* 훑어 볼 작은 나무: 파일 하나, 디렉터리 하나, 그 안의 파일 하나. */
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_mkdir(alloc, PROVEN_LIT("ex_walk"))), "mkdir");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_mkdir(alloc, PROVEN_LIT("ex_walk/inner"))), "mkdir inner");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_write_file(alloc, PROVEN_LIT("ex_walk/top.txt"),
proven_mem_view_from_u8(PROVEN_LIT("top")))), "write top.txt");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_write_file(alloc, PROVEN_LIT("ex_walk/inner/deep.txt"),
proven_mem_view_from_u8(PROVEN_LIT("deep")))), "write deep.txt");
proven_result_walk_t walk = proven_fs_walk_open(alloc, PROVEN_LIT("ex_walk"),
PROVEN_FS_WALK_UNLIMITED);
EXAMPLE_REQUIRE(proven_is_ok(walk.err), "the walk should open");
proven_size_t files = 0;
proven_size_t dirs = 0;
proven_size_t unreadable = 0;
proven_size_t total_bytes = 0;
for (;;) {
proven_fs_walk_entry_t entry = {0};
proven_err_t err = proven_fs_walk_next(&walk.value, &entry);
if (err == PROVEN_ERR_EOF) break;
if (!proven_is_ok(err)) {
/* 읽을 수 없었던 디렉터리이거나, 훑기의 스택보다 깊은 것이다. 건너뛰는 것이
* 아니라 *보고된다* - `entry.path` 가 어느 것인지 말해 준다 - 그리고 훑기는
* 계속된다. 나무를 복사하는 도구라면 여기서 실패해야 하고, 나무를 보고하는
* 도구라면 세어서 알려야 한다. 하지 말아야 할 것은 없던 일로 하는 것이다. */
unreadable++;
continue;
}
/* `entry.path` 와 `entry.name` 은 빌린 것이다. 훑기가 다시 쓰는 버퍼 하나를
* 가리키고 다음 호출 전까지만 쓸 수 있다. 더 오래 필요하면 복사할 것. */
if (entry.type == PROVEN_FS_TYPE_DIR) {
dirs++;
} else if (entry.type == PROVEN_FS_TYPE_FILE) {
files++;
total_bytes += entry.size;
}
}
proven_fs_walk_close(&walk.value);
EXAMPLE_REQUIRE(files == 2, "two files: top.txt and inner/deep.txt");
EXAMPLE_REQUIRE(dirs == 1, "one directory: inner");
EXAMPLE_REQUIRE(unreadable == 0, "and nothing in this tree is unreadable");
EXAMPLE_REQUIRE(total_bytes == 7, "three bytes plus four");
/* 깊이 제한: max_depth 0 은 뿌리 바로 안에 있는 것을 보고하고 아무 데도 내려가지
* 않는다. 한계에 걸린 디렉터리도 여전히 항목이므로 여전히 보고된다. */
walk = proven_fs_walk_open(alloc, PROVEN_LIT("ex_walk"), 0);
EXAMPLE_REQUIRE(proven_is_ok(walk.err), "the shallow walk should open");
proven_size_t shallow = 0;
for (;;) {
proven_fs_walk_entry_t entry = {0};
proven_err_t err = proven_fs_walk_next(&walk.value, &entry);
if (err == PROVEN_ERR_EOF) break;
if (proven_is_ok(err)) shallow++;
}
proven_fs_walk_close(&walk.value);
EXAMPLE_REQUIRE(shallow == 2, "top.txt and inner - but nothing inside inner");
(void)proven_fs_remove(alloc, PROVEN_LIT("ex_walk/inner/deep.txt"));
(void)proven_fs_remove(alloc, PROVEN_LIT("ex_walk/top.txt"));
(void)proven_fs_remove(alloc, PROVEN_LIT("ex_walk/inner"));
(void)proven_fs_remove(alloc, PROVEN_LIT("ex_walk"));
return EXAMPLE_OK();
}스트림: writer와 reader
포매터의 유일한 sink는 예전에 proven_u8str_t였다. 파일은 proven_file_t였다. 인메모리 스캐너는 view를 읽었고, 파일 스캐너는 또 다른 무언가를 읽었다. 네 개의 타입, 네 개의 함수 계열, 공통 인터페이스 없음—그래서 메모리와 파일 양쪽에서 동작하는 하나의 serialize(sink, value)를 쓸 수 없었고, 파일로 포매팅하는 것 자체가 불가능했으며, 파일을 한 줄씩 읽을 방법이 전혀 없었다.
writer는 바이트 sink다. reader는 바이트 source다. 둘 다 값으로 전달되는 작은 vtable로, 정확히 proven_allocator_t처럼, 같은 이유로 그렇다: 호출자가 바이트가 어디로 가는지 결정하며, 아무것도 숨겨지지 않는다.
| API | 의도 |
proven_writer_from_file(&file) | 열린 파일 위의 언버퍼드 sink. |
proven_writer_from_u8str(&state, &str, alloc) | owned 문자열에 덧붙인다. |
proven_writer_from_buffer(&state) | 고정된 호출자 메모리. 절대로 아무것도 할당하지 않는다. |
proven_writer_buffered(&state, inner, buf) | 작은 쓰기들을 누적한다. 버퍼는 당신이 공급한다. |
proven_fprint(w, fmt, ...) / proven_fprintln | writer로 직접 포매팅. 무할당. |
proven_reader_from_file(&file) / _from_view(&state, view) | 바이트 source. |
proven_reader_buffered(&state, inner, buf) | 버퍼드 source; 라인 읽기에 필수. |
proven_reader_read_line(&state) | 개행 없는 한 줄. |
proven_writer_is_valid(w) / proven_reader_is_valid(r) | 생성자가 성공했는가? 0으로 채워진 핸들은 invalid이고, 모든 생성자는 잘못된 인자에 대해 그런 것을 반환한다—따라서 NULL 검사가 아니라 이것이 그 검사다. |
proven_fwrite_fmt(w, scratch, fmt, ...) | 당신이 크기를 정한 임시 작업용(scratch) 버퍼를 쓰는 proven_fprint. proven_fprint는 512바이트 스택 버퍼를 사용하고 더 긴 줄에 대해 OUT_OF_BOUNDS를 반환한다. 이것이 그런 줄을 포매팅하는 방법이다. |
proven_fmt_to_writer_impl(w, scratch, fmt, args, n) | 위의 두 매크로가 확장되는 함수. 자신만의 가변 인자 래퍼를 만드는 경우에만 직접 호출하라. 매크로는 당신이 인자를 세지 않아도 되도록 존재한다. |
명확히 언급할 가치가 있는 네 가지 규칙. 각각은 이것이 잘못 설계될 수 있었던 방식이기 때문이다:
버퍼링은 당신이 공급하는 메모리를 사용한다.
proven_writer_buffered는proven_arena_create가 그러하듯proven_mem_mut_t를 받는다. 숨겨진 전역 버퍼는 없으며, 이는 곧 당신을 위해 그것을 flush해 줄 소멸자도 없다는 뜻이다—버퍼가 스코프를 벗어나기 전에 당신이 flush해야 한다. 그 대가로, 당신의 로깅 경로는 결코 할당하지 않으며, out-of-memory 상태에서 빠져나오며 로그를 남기는 프로그램이 여전히 로그를 남길 수 있다.가득 찬 sink는 거부한다. 절단하지 않는다. 가득 찬 고정 버퍼는
PROVEN_ERR_OUT_OF_BOUNDS를 반환하고overflowed를 기록한다. 당신의 데이터 끝을 조용히 버리는 sink는 받을 수 없다고 말하는 sink보다 나쁘다.reader의 버퍼에 비해 너무 긴 줄은 에러다, 절단된 줄이 아니다. 온전한 것처럼 돌려받은 절단된 줄은 호출자가 탐지할 방법이 없는 손상이다. 버퍼는 당신 것이다; 예상하는 입력에 맞춰 크기를 정하라.
부분 쓰기는 얼버무릴 실패가 아니라 하나의 사실이다.
write_fn은proven_result_size_t를 반환한다: sink가 얼마나 받았는지, 그리고 무엇이 잘못되었는지. 파이프, 소켓, 또는 차오르는 디스크는 정말로 당신의 6000바이트 중 4096바이트를 받아들이고 나서 실패한다. 그리고 "전부 소비하거나 실패한다"고 말하는 트레이트는 그런 sink를 올바르게 작성하는 것을 그저 불가능하게 만든다.proven_writer_write는 여전히 전부-아니면-전무를 뜻한다(루프를 돈다);proven_writer_write_partial은 당신이 얼마나 진행했는지 봐야 할 때 있다. 버퍼드 writer는 sink가 받지 않은 꼬리만 보관한다—첫 버전은 버퍼 전체를 보관하고 다시 보냈으며, 그래서 실패하는 sink가 받아들인 접두부를 두 번 받았다.한 번 실패한 writer는 실패한 채로 남는다. writer가 바이트를 잃은 뒤에는—sink가 죽은 버퍼드 writer, 오버플로한 고정 버퍼—그것이 만들어내던 스트림에 수신자가 볼 수 없는 구멍이 생기므로, 이후의 모든 쓰기와 flush는 에러를 반환한다. 들어갈 수 있는 더 짧은 청크조차 거부된다: 그것을 쓰면 구멍 뒤에 놓여, 결과가 완전한 출력처럼 보일 것이기 때문이다.
clear()는 없다: 복구 스토리가 있다면 그것은 이 writer가 괜찮은 척하는 것이 아니라 새 sink 위의 새 writer를 수반한다. (이전에는 실패한 쓰기 이후flush가PROVEN_OK를 답했다—버퍼가 비어 있어서 실패할 것이 남지 않았기 때문이다—그리고 "쓰고, 쓰고, 쓰고, flush를 검사한다"는, 거의 모두가 버퍼드 writer를 쓰는 방식이 가득 찬 디스크에서 성공을 보고했다.)
reader의 규칙은 그 거울상이다: 실패한 읽기는 에러이지, 결코 파일의 끝이 아니다. proven_reader_read는 source가 깨질 때 깨끗한 0바이트 EOF가 아니라 PROVEN_ERR_IO를 반환한다— 디스크 에러로 잘려나간 파일과 그냥 끝난 파일은, 둘을 구별할 수 없는 호출자에게는 같은 것이며, 그 중 하나만 안전하게 처리할 수 있기 때문이다.
10,000줄을 stdout으로 내보내며 측정한 비용:
write() 시스템 콜 | malloc() | |
proven_println | 10,000 | 0 (이전엔 10,000) |
| 버퍼드 writer, 호출자 메모리 8 KiB | 24 | 0 |
malloc() 열은 512바이트 스택 버퍼에 들어가는 줄, 즉 일반적인 경우에 대한 값이다. 그보다 긴 줄은 거부되는 대신 그 한 번의 호출에 한해 힙(heap)으로 물러난다.
proven_println은 의도적으로 여전히 줄당 하나의 시스템 콜이다: 그것을 버퍼링하려면 숨겨진 전역 상태가 필요할 것이다. 24를 원하는 호출자는 버퍼드 writer를 만들고 그렇게 하겠다고 말한다.
완성 예제: 하나의 직렬화기, 세 개의 목적지, 그리고 되읽기
테스트 스위트에 의해 컴파일되고 실행됨. render_row가 자신의 바이트가 어디로 가는지 모른다는 점에 주목하라—그것이 바로 요점 전부다.
/*
* 쓰는 쪽과 읽는 쪽. "바이트가 어디로 가는가" 를 위한 인터페이스 하나와 "바이트가
* 어디서 오는가" 를 위한 인터페이스 하나.
*
* 요점은 아래 코드 - render_row - 가 자기가 문자열로 쓰는지, 고정 버퍼로 쓰는지, 파일로
* 쓰는지 모르고 신경도 쓰지 않는다는 것이다. 예전에는 그럴 수 없었다. 형식화기가 받는
* 그릇은 proven_u8str_t 하나뿐이었다.
*/
/* 직렬화기 하나. 목적지가 아니라 그릇을 받는다. */
static proven_err_t render_row(proven_writer_t w, int id, const char *name) {
proven_fmt_result_t r = proven_fprintln(w, "{:>4} | {}", PROVEN_ARG(id), PROVEN_ARG(name));
return r.err;
}
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* --- 같은 코드로, 늘어나는 문자열에 ------------------------------------ */
proven_result_u8str_t s = proven_u8str_create(alloc, 16);
EXAMPLE_REQUIRE(proven_is_ok(s.err), "string create");
proven_writer_u8str_t s_state;
proven_writer_t to_string = proven_writer_from_u8str(&s_state, &s.value, alloc);
EXAMPLE_REQUIRE(proven_is_ok(render_row(to_string, 7, "ada")), "render into a string");
printf("into a string:\n%s", proven_u8str_as_cstr(&s.value));
proven_u8str_destroy(alloc, &s.value);
/* --- 같은 코드로, 여러분이 소유한 기억에: 할당은 0 -------------------- */
proven_byte_t fixed[64];
proven_writer_buf_t b_state = { .buf = { .ptr = fixed, .size = sizeof fixed } };
proven_writer_t to_buffer = proven_writer_from_buffer(&b_state);
EXAMPLE_REQUIRE(proven_is_ok(render_row(to_buffer, 8, "grace")), "render into a buffer");
EXAMPLE_REQUIRE(b_state.len > 0, "the buffer received the row");
/* 가득 찬 버퍼는 *거부한다*. 자르지 않는다. 여러분 자료의 끝을 조용히 버리는 그릇은
* 받을 수 없다고 말하는 그릇보다 나쁘다. */
proven_byte_t tiny[4];
proven_writer_buf_t t_state = { .buf = { .ptr = tiny, .size = sizeof tiny } };
proven_writer_t to_tiny = proven_writer_from_buffer(&t_state);
EXAMPLE_REQUIRE(proven_writer_write_str(to_tiny, PROVEN_LIT("far too long")) == PROVEN_ERR_OUT_OF_BOUNDS,
"a full buffer refuses rather than truncating");
EXAMPLE_REQUIRE(t_state.overflowed, "and it records that it did");
/* --- 같은 코드로, 파일에, 버퍼를 두고 ---------------------------------- */
/*
* 버퍼는 아레나에서와 똑같이 *여러분이* 대는 기억이다. 이 라이브러리에는 숨은 전역
* 상태가 없으므로 종료 시점에 대신 흘려 줄 수 없다 - 그래서 버퍼가 스코프를 벗어나기
* 전에 반드시 flush 해야 한다. 그 대가로 로그 경로에서는 할당이 전혀 일어나지 않는다.
* 여기서 만 줄은 malloc 0 번에 write 시스템 호출 스물 몇 번이지만, proven_println 을
* 만 번 부르면 시스템 호출 10,000 번이다.
*/
proven_u8str_view_t path = PROVEN_LIT("example_stream_rows.txt");
proven_result_file_t f = proven_fs_open(alloc, path,
(proven_fs_mode_t)(PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC));
EXAMPLE_REQUIRE(proven_is_ok(f.err), "open the output file");
proven_file_t file = f.value;
proven_byte_t out_buf[4096];
proven_writer_buffered_t w_state;
proven_writer_t to_file = proven_writer_buffered(&w_state,
proven_writer_from_file(&file),
(proven_mem_mut_t){ .ptr = out_buf, .size = sizeof out_buf });
for (int i = 0; i < 3; ++i) {
EXAMPLE_REQUIRE(proven_is_ok(render_row(to_file, i, "row")), "render into the file");
}
EXAMPLE_REQUIRE(proven_is_ok(proven_writer_flush(to_file)),
"flush: nothing is written until you say so");
/* 그리고 close - 쓰기가 도착하지 못했다고 알려 줄 수 있는 마지막 자리다. */
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(file)), "closing the written file");
/* --- 한 줄씩 되읽기 ---------------------------------------------------- */
/* 파일을 한 줄씩 읽는 것은 예전에는 아예 되지 않았다. 길은 파일 전체를 기억에 올려
* 손으로 자르는 것뿐이었다. */
proven_result_file_t rf = proven_fs_open(alloc, path, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(rf.err), "reopen for reading");
proven_file_t rfile = rf.value;
proven_byte_t in_buf[128];
proven_reader_buffered_t r_state;
(void)proven_reader_buffered(&r_state, proven_reader_from_file(&rfile),
(proven_mem_mut_t){ .ptr = in_buf, .size = sizeof in_buf });
int lines = 0;
for (;;) {
proven_result_u8str_view_t line = proven_reader_read_line(&r_state);
if (line.err == PROVEN_ERR_EOF) break;
EXAMPLE_REQUIRE(proven_is_ok(line.err), "read a line");
/* 그 뷰는 읽는 쪽의 버퍼 *안*을 가리키고, 다음 호출 전까지만 쓸 수 있다. 그보다
* 오래 살아야 한다면 복사할 것. */
printf("line %d: %.*s\n", lines, (int)line.val.size, (const char *)line.val.ptr);
++lines;
}
EXAMPLE_REQUIRE(lines == 3, "three rows in, three lines out");
(void)proven_fs_close(rfile);
(void)proven_fs_remove(alloc, path);
return EXAMPLE_OK();
}당신이 보유하는 구조체
두 핸들은 값으로 전달되며 저렴하다:
typedef struct {
void *ctx;
proven_result_size_t (*write_fn)(void *ctx, proven_mem_view_t chunk);
proven_err_t (*flush_fn)(void *ctx); /* may be NULL: this sink holds nothing back */
} proven_writer_t;
typedef struct {
void *ctx;
proven_result_size_t (*read_fn)(void *ctx, proven_mem_mut_t dest);
} proven_reader_t;write_fn은 그 뒤에 실패하더라도 얼마나 나갔는지 보고한다. 그리고 그것은 사소한 배려가 아니다: 파이프나 가득 찬 디스크로의 쓰기는 정말로 일부 바이트를 내보내고 나서 실패한다. 더 깔끔한 "전부 아니면 전무"라는 거짓말 위에 세워진 버퍼드 writer는 실패 시 버퍼 전체를 보관하고 다음 flush에 다시 보냈다—6000바이트 페이로드가 처음 4096바이트가 중복된 채로 10,096바이트로 도착했다. 데이터를 잃는 것은 나쁘다; 조용히 두 배로 만드는 것은 더 나쁘다. 수신자가 알 수 없기 때문이다.
그 밖의 모든 것은 호출자 소유 상태다— proven_writer_buf_t, proven_writer_u8str_t, proven_writer_buffered_t, proven_reader_view_t, proven_reader_buffered_t. 이들은 아무것도 할당하지 않고, destroy가 없으며, 핸들이 그 안을 가리키고 있는 동안 복사되거나 이동되어서는 안 된다.
주의사항, 그리고 무엇이 잘못되는가
결코 flush되지 않는 버퍼드 writer는 결코 일어나지 않은 출력이다. 숨겨진 상태가 없으므로, 당신을 위해 그것을 flush해 줄 소멸자도 없다—그리고 여기 있는 어떤 것도 atexit 핸들러를 등록하지 않는다. 당신의 프로세스를 소유하는 라이브러리는 당신이 추론할 수 없는 라이브러리이기 때문이다.
잘못됨(Wrong):
proven_writer_buffered_t st;
proven_writer_t w = proven_writer_buffered(&st, inner, buf);
(void)proven_fprintln(w, "the important line");
return; /* wrong: the buffer dies with the frame, and so does the line */올바름(Correct)—버퍼나 내부 sink가 사라지기 전에 flush하라:
proven_byte_t buf[256];
proven_sysio_out_t out;
proven_writer_t w = proven_sysio_stdout_buffered(&out,
(proven_mem_mut_t){ .ptr = buf, .size = sizeof buf });
(void)proven_fprintln(w, "the important line");
(void)proven_writer_flush(w); /* now it has happened */reader가 당신에게 건네는 줄은 그 버퍼 안을 가리킨다. 그것은 다음 호출 전까지만 유효하다. 그것이 백만 줄을 읽는 데 백만 번의 할당이 아니라 하나의 버퍼가 들게 하는 원리다—그리고 당신이 그것을 붙들어 두는 순간 dangling 포인터다.
잘못됨(Wrong):
proven_u8str_view_t lines[100];
for (int i = 0; i < 100; ++i) {
proven_result_u8str_view_t ln = proven_reader_read_line(&st);
lines[i] = ln.val; /* wrong: every entry aliases the SAME buffer, and the
next read_line overwrites what the last one returned */
}올바름(Correct): 다시 호출하기 전에, 보관해야 하는 바이트를 복사하라(proven_u8str_t로, 아레나로, 어디로든).
flush는 durability 장벽이 아니다. proven_writer_flush는 버퍼드 writer의 바이트를 그 뒤의 것으로 밀어 넣는다; 파일의 바이트를 디스크에 올리는 것은 proven_fs_sync다. 이 둘은 서로 다른 연산이며, 한 단어가 정직하게 둘 모두를 뜻할 수는 없었다—그것이 바로 둘 다라고 주장하면서 어느 쪽도 아니었던 옛 proven_sysio_flush가 사라진 이유다.
버퍼보다 긴 줄은 절단이 아니라 거부된다. PROVEN_ERR_OUT_OF_BOUNDS—그리고 reader는 그 줄에서 멈춘 채로 남는다: 재동기화(resync)는 없다. 담을 수 없는 줄은 그 바이트를 어떻게 할지 결정하지 않고서는 건너뛸 수 있는 줄이 아니기 때문이다. 예상하는 입력에 맞춰 버퍼의 크기를 정하라. (버퍼를 정확히 채우는 줄은 괜찮다: 개행이 함께 들어가지 않아도 되고, 개행이 전혀 없는 마지막 줄도 마찬가지다.)
표준 스트림
stream.h에는 writer, reader, 버퍼드 writer, 그리고 라인 reader가 있다. sysio.h에는 stdin, stdout, stderr가 있다. 이 둘이 서로 소개되기 전까지는, 두 가지가 그저 불가능했다—그리고 한 호출은 거짓말이었다.
stdin을 한 줄씩 읽는 방법이 없었다. 프로그램이 stdin으로 하는 가장 흔한 일인데, 선택지는 토큰 스캐너이거나, 결코 끝나지 않을 수도 있는 스트림 전체를 읽는 것이었다. 그 다리(bridge)가 그것을 고친다: 표준 핸들이 호출자 소유 저장소에 주차되므로, 라인 reader에게 가리킬 안정적인 무언가가 생긴다.
proven_byte_t buf[4096];
proven_sysio_lines_t lines;
if (proven_is_ok(proven_sysio_stdin_lines(&lines, (proven_mem_mut_t){ .ptr = buf, .size = sizeof buf }))) {
for (;;) {
proven_result_u8str_view_t line = proven_sysio_read_line(&lines);
if (line.err == PROVEN_ERR_EOF) break;
if (!proven_is_ok(line.err)) break; /* OUT_OF_BOUNDS: a line longer than `buf` */
/* `line.val` points INTO `buf` and is valid only until the next call. */
proven_println("{}", PROVEN_ARG(line.val));
}
}이것은 라인 reader의 속성을 물려받는데, 이는 두 번째 것을 쓰지 않는 것의 요점이다: view는 무할당, "\r\n"이 처리되고, 후행 개행이 없는 마지막 줄도 여전히 반환되며, 당신의 버퍼보다 긴 줄은 PROVEN_ERR_OUT_OF_BOUNDS다—결코 조용히 절단된 줄이 아니다.
포매터를 표준 스트림으로 겨눌 수 없었다. proven_fprintln은 writer를 받는데; stdout은 그것이 아니었다. 이제는 그렇고, 버퍼드 writer일 수도 있다—그래서 천 개의 작은 줄이 천 번이 아니라 한 번의 시스템 콜로 든다.
proven_sysio_stdout_writer(&st) | stdout 위의 언버퍼드 writer. 모든 쓰기가 하나의 write 시스템 콜이다. |
proven_sysio_stderr_writer(&st) | stderr에 대해 같은 것—에러에 대해 당신이 원하는 것이다: 다음 코드 줄이 실행되기 전에 그것이 나간다. |
proven_sysio_stdin_reader(&st) | stdin 위의 reader. |
proven_sysio_stdout_buffered(&out, buf) | 당신이 소유하는 버퍼 위의 버퍼드 writer 뒤에 놓인 stdout. |
proven_sysio_file_buffered(&out, file, buf) | 임의의 열린 파일에 대해 같은 것. |
그리고 이제 flush는 무언가를 뜻한다. proven_sysio_flush는 예전에 존재하지 않는 버퍼를 flush한다고 주장했다: POSIX에서는 no-op, Windows에서는 디스크 sync. 그것은 삭제되었다. 버퍼드 writer의 바이트를 OS로 밀어 넣는 것은 proven_writer_flush이고, OS의 바이트를 디스크로 밀어 넣는 것은 proven_fs_sync다. 이 둘은 서로 다른 연산이며 이제는 그렇다고 말한다.
당신은 버퍼드 writer를 flush해야 한다. 버퍼가 차거나 당신이 flush하기 전까지는 아무것도 터미널에 도달하지 않으며, 이 라이브러리의 어떤 것도 당신 몰래 그것을 하기 위해atexit핸들러를 등록하지 않는다—당신의 프로세스를 소유하는 라이브러리는 당신이 추론할 수 없는 라이브러리이기 때문이다. 결코 flush되지 않는 버퍼드 출력은 결코 일어나지 않은 출력이다. 직접 호출들 (proven_print,proven_println,proven_eprint)은 바로 이 이유로 언버퍼드로 남는다: 그것들이 쓰는 것은 반환되기 전에 이미 나가는 중이다.
당신이 보유하는 구조체
typedef struct { proven_file_t file; } proven_sysio_std_t;
/* Storage for a standard handle, so a writer or reader has something stable to
point at. proven_writer_from_file takes a proven_file_t * and the file must
outlive the writer - so it cannot be a temporary. This is that storage. */
typedef struct {
proven_sysio_std_t std;
proven_writer_buffered_t buffered;
} proven_sysio_out_t; /* a buffered writer over a standard stream or a file */
typedef struct {
proven_sysio_std_t std;
proven_reader_buffered_t buffered;
} proven_sysio_lines_t; /* a line reader over a standard stream or a file */셋 모두 호출자 소유 상태다.
주의사항, 그리고 무엇이 잘못되는가
이 상태 구조체들은 자기 자신을 가리키는 포인터를 담는다. 당신이 돌려받는 writer는 당신이 넘긴 구조체 안의 &st->std.file을 주소로 삼는다. 구조체를 복사하거나 값으로 반환하면, writer는 여전히 원본을 가리킨다—그것은 죽은 프레임일 수 있다.
잘못됨(Wrong)—그리고 한 감사에서 정확히 이것이 heap-use-after-free로 재현되었다:
proven_sysio_out_t out;
proven_writer_t w = proven_sysio_stdout_buffered(&out, buf);
proven_sysio_out_t copy = out; /* wrong: `w` still points into `out` */
/* ... `out` goes out of scope ... */
(void)proven_writer_write_str(w, PROVEN_LIT("boom")); /* writes through dead storage */유일한 예외는 proven_sysio_lines_t다: proven_sysio_read_line은 매 호출마다 그것을 다시 바인딩하므로, 라인 reader는 이동될 수 있다. 그것은 의도된 배려인데, 그것이 상태를 포인터로 받기 때문이다—"재배치 가능"을 뜻하는 형태—그리고 라이브러리는 약속의 형태를 한 함정을 놓아서는 안 되기 때문이다.
0으로 초기화된 proven_file_t는 invalid 핸들이 아니다—POSIX에서 그것은 fd 0, 즉 stdin이다. 라이브러리는 당신이 채워 넣기를 잊은 핸들과 정당하게 fd 0을 가리키는 핸들을 구별할 수 없다.
proven_sysio_out_t out;
proven_file_t f = {0}; /* wrong: this is stdin */
proven_writer_t w = proven_sysio_file_buffered(&out, f, buf); /* writes to fd 0 */버퍼드 stdout과 언버퍼드 stderr는 당신이 쓴 순서대로 뒤섞이지 않는다. 당신이 버퍼링하는 것은 당신의 버퍼에 앉아 있는 동안 stderr는 곧장 나간다. 당신의 출력 뒤에 나타나야 하는 에러를 출력하기 전에 flush하라.
난수, 용도별
단일한 "random"은 없다. 동일해 보이지만 그렇지 않은 두 가지 작업이 있다:
| 당신의 작업 | 사용 | 이유 |
| 키, 토큰, nonce—공격자가 추측해서는 안 되는 모든 것. | proven_random_bytes, 또는 그것으로 시드된 proven_chacha_rng_t. | 오직 암호학적 source만이 추측 불가능하다. |
| 같지만, OS가 없는 타깃에서. | 보드 자체의 엔트로피로 시드된 proven_chacha_rng_t. | ChaCha20은 순수 산술이다; OS가 필요 없다. 그것은 오직 그 시드만큼만 추측 불가능하다. |
| 시뮬레이션, 테스트, 게임, 표본추출. | proven_xoshiro256ss_t. | 빠르고, 재현 가능하다: 같은 시드가 같은 실행을 재생하며, 그것이 실패하는 테스트를 디버깅 가능하게 만든다. |
| 범위 내의 수, 셔플, [0,1)의 float. | proven_rng_below, proven_rng_range, proven_rng_f64, proven_rng_shuffle—어떤 source 위에서든. | % n은 편향되어 있고, 모두가 그럼에도 그것을 쓴다. 이것들은 그렇지 않다. |
두 요구사항은 정면으로 대립한다. 재현 가능이란 예측 가능을 뜻하고, 예측 가능은 정확히 토큰이 그래서는 안 되는 것이다: proven_xoshiro256ss_t의 몇 개 출력이 그 전체 상태를, 따라서 그것이 앞으로 낳을 모든 수를 드러낸다. 그것은 재생해야 하는 시뮬레이션에는 기능이고 세션 토큰에는 재앙이다—그래서 둘은 혼동될 수 없는 이름을 지니며, 선택은 무언가가 어떻게 시드되었는가에 묻히는 대신 호출 지점에서 가시적이다.
트레이트는 실패하지 않는다(infallible). proven_rng_t는 무작위 바이트의 source이며, valid한 것에서 뽑는 것은 실패할 수 없다. 그것은 단순화가 아니라, 실패가 어디로 갔는지다. 운영체제에 엔트로피를 요청하는 것은 실패할 수 있으므로, 그 실패는 정확히 한 곳—시딩—에 국한되며, 당신은 그것을 시작 시점에 한 번 검사한다. 하류의 모든 draw는 전역적(total)이다.
proven_random_bytes(buf, len) | OS CSPRNG. 실패 시 false를 반환한다; 그러면 buf를 사용하지 말라. len == 0은 성공적인 no-op이다. |
proven_random_u64() | OS로부터의 강한 한 워드, 실패 시 0. |
proven_chacha_rng_seed_from_entropy(&g) | 엔트로피 source로부터 암호학적 생성기를 시드. 이것이 실패할 수 있는 호출이다. |
proven_random_set_source(fn, ctx) | 엔트로피 source를 설치. 호스티드 타깃에는 OS가 이미 설치되어 있다; 베어메탈 타깃은 보드의 TRNG를 설치한다. |
proven_chacha_rng_seed(&g, seed32) | 당신이 공급하는 32바이트로부터 시드—보드의 하드웨어 엔트로피 source. 결코 시계로부터는 아니다. |
proven_xoshiro256ss_seed(&g, seed) | 재현 가능 생성기를 시드. 시드 0도 괜찮다: SplitMix64를 통해 확장된다. |
엔트로피는 어디서 오는가
위의 모든 것은 순수 산술이다—생성기들, 헬퍼들, 그리고 proven_random_bytes 자체까지. 플랫폼에 따라 다른 것은 그 뒤의 엔트로피 source인데, 그것이 프로그램이 스스로 계산할 수 없는 유일한 것이기 때문이다.
- 호스티드: OS CSPRNG가 당신을 위해 설치되어 있다—Linux의
getrandom, BSD와 macOS의getentropy, Windows의BCryptGenRandom, 그리고 이들 중 어느 것도 없는 곳의/dev/urandom. 당신은 아무것도 호출하지 않는다. - 베어메탈: 당신이 하나를 설치하기 전까지는 source가 없다. 보드는 실제 엔트로피를 가지고 있다—온칩 TRNG, 링 오실레이터, ADC의 노이즈 플로어—그리고 라이브러리는 그것이 어디 있는지 알 수 없다.
/* On a board: hand the library its hardware entropy, once, at startup.
* (A listing, not a fragment: it defines a function, and `hardware_rng_read` is
* whatever your SoC calls its entropy register.) */
static bool board_trng(void *ctx, void *buf, proven_size_t len) {
(void)ctx;
/* read the SoC's entropy register into buf; return false if it is not ready */
return hardware_rng_read(buf, len);
}
proven_random_set_source(board_trng, NULL);
/* From here everything above works unchanged - including the one call that turns a few
* hundred bytes of hardware entropy into an endless cryptographic stream. */
proven_chacha_rng_t g;
if (!proven_chacha_rng_seed_from_entropy(&g)) {
/* the TRNG was not ready. The generator is INERT - it yields zeros and an invalid
* trait - so ignoring this does not get you plausible-looking bytes. */
}설치된 source가 없으면 proven_random_bytes는 false를 반환한다. 그것은 시계로 시드된 PRNG로 폴백하지 않는다. 그것은 성공처럼 보이지만 아무것도 보고하지 않는 보안 구멍이기 때문이다—거부는 호출자가 처리할 수 있는 하나의 사실이다.
의도적으로 내장된 RDRAND / RNDR 백엔드는 없다. 호스티드 타깃에서는 OS가 이미 CPU의 명령을 자신의 풀에 섞어 넣으므로, 그것을 직접 호출하는 것은 아무것도 얻지 못하면서 그 섞임을 잃게 하고; 유일한 source로 쓰이는 원시 하드웨어 명령은 사람들이 10년간 논쟁해 온 바로 그 방식이다. 원한다면, 그것은 이 훅 뒤로 네 줄이다—그러면 그 선택은 가시적으로 당신 것이다.
당신이 보유하는 구조체
셋 모두 호출자 소유 상태다: 아무것도 할당하지 않고, 파괴할 것이 없으며, 하나를 복사하면 그 시퀀스를 복제한다.
typedef struct { const proven_rng_vtable_t *vt; void *ctx; } proven_rng_t;
/* The trait: two pointers, held by value. `ctx` points at one of the generators
below, which must outlive it. Drawing from a VALID one cannot fail - that is
the whole design: the failure lives in seeding, not in drawing. */
typedef struct { proven_u64 s[4]; } proven_xoshiro256ss_t;
/* 256 bits of state. Reproducible, and NOT secret-grade. */
typedef struct {
proven_u32 state[16]; /* the ChaCha state: constants, key, counter, nonce */
proven_byte_t block[64]; /* the keystream block currently being handed out */
proven_size_t used; /* how much of it is spent */
proven_u32 seeded; /* set only by seeding. A zero-initialised struct is
the shape of "never seeded", and must stay inert. */
} proven_chacha_rng_t;레퍼런스
| API | 의도 | 반환 |
proven_random_bytes(buf, len) | 엔트로피 source로부터 채움(기본은 OS). 실패할 수 있는 유일한 호출. | bool. false이면 buf는 미지정이며 사용해서는 안 된다. len == 0은 성공한다. |
proven_random_u64() | 같은 source로부터의 강한 한 워드. | proven_u64, 실패 시 0—그것도 valid한 draw이므로, 둘을 구별해야 할 때는 proven_random_bytes를 사용하라. |
proven_random_set_source(fn, ctx) | 엔트로피 source를 설치. 호스티드 타깃에는 불필요; 이것이 보드가 자신의 TRNG를 넘겨주는 방법이다. | void. |
proven_xoshiro256ss_seed(&g, seed) | 재현 가능 생성기를 시드. 어떤 시드든 괜찮다—0조차; SplitMix64를 통해 확장된다. | void. |
proven_xoshiro256ss_next(&g) | 다음 워드. 핫 패스: 트레이트를 통해서가 아니라 직접 호출하라. | proven_u64. |
proven_xoshiro256ss_rng(&g) | 헬퍼를 위해 proven_rng_t로 본다. | proven_rng_t. |
proven_chacha_rng_seed(&g, seed32) | 당신이 공급하는 32바이트의 실제 엔트로피로부터 암호학적 생성기를 시드. | void. |
proven_chacha_rng_seed_from_entropy(&g) | 설치된 source로부터 시드. 이것을 검사하라. | bool. false이면 생성기는 INERT 상태로 남는다—0을 낳고 invalid한 트레이트를 낳는다. |
proven_chacha_rng_next/_fill | draw. 일단 시드되면 실패할 수 없다. | proven_u64 / void. |
proven_chacha_rng(&g) | proven_rng_t로 본다. | proven_rng_t—생성기가 성공적으로 시드된 적이 없으면 invalid. |
proven_rng_u64(rng) / proven_rng_fill(rng, buf, len) | 어느 생성기로부터든 트레이트를 통해 draw. | proven_u64 / void. invalid source에는 0 / no-op. |
proven_rng_below(rng, bound) | [0, bound)에서 균일, 무편향. | proven_u64; bound == 0일 때 0. |
proven_rng_range(rng, lo, hi) | [lo, hi]에서 균일, 포함적. 전체 INT64_MIN..INT64_MAX 범위도 오버플로하지 않는다. | proven_i64; hi < lo이면 lo. |
proven_rng_f64(rng) | [0, 1)에서 균일. 53비트; 결코 1.0을 반환하지 않는다. | double. |
proven_rng_shuffle(rng, base, count, elem_size) | 무편향 Fisher-Yates 순열, 제자리(in place). | void. |
주의사항, 그리고 무엇이 잘못되는가
proven_xoshiro256ss_t로 결코 비밀을 생성하지 말라. 그것은 예측 가능하기 때문에 빠르다: 그 출력 몇 개가 256비트 상태 전체를, 그리고 그 상태로부터 그것이 앞으로 낳을 모든 수를 드러낸다. 두 생성기는 정확히 이 이유로 혼동될 수 없는 이름을 지닌다.
잘못됨(Wrong)—공격자가 몇 개를 지켜본 뒤 계산할 수 있는 세션 토큰:
proven_xoshiro256ss_t g;
proven_xoshiro256ss_seed(&g, 12345);
proven_u64 session_token = proven_xoshiro256ss_next(&g); /* wrong: predictable */암호학적 생성기를 결코 시계, 카운터, 또는 시리얼 번호로 시드하지 말라. ChaCha20은 정확히 그 시드만큼만 추측 불가능하다. 시계에서 유도된 시드는 완벽하게 무작위처럼 보이지만 그렇지 않은 스트림을 낳는다—그것은 명백한 실패보다 나쁘다. 아무것도 그것을 보고하지 않기 때문이다.
proven_byte_t seed[32] = { 0 };
memcpy(seed, &now_ns, sizeof now_ns); /* wrong: ~20 bits of real entropy, and guessable */
proven_chacha_rng_seed(&g, seed);시딩을 검사하라. 그것은 여기서 실패할 수 있는 유일한 것이며, 바로 그래서 그것을 무시하는 것이 솔깃하다. 무시하면 생성기는 inert 상태가 되어 당신에게 0을 건넨다—설계상 그럴듯한 값이 아니라 가시적으로 죽은 값이다.
잘못됨(Wrong):
proven_chacha_rng_t g;
proven_chacha_rng_seed_from_entropy(&g); /* wrong: the bool was the point */
proven_chacha_rng_fill(&g, key, 32); /* key is now 32 zero bytes */올바름(Correct):
proven_chacha_rng_t g;
if (!proven_chacha_rng_seed_from_entropy(&g)) {
/* No entropy. There is nothing safe to do here except refuse to continue. */
} else {
proven_byte_t key[32];
proven_chacha_rng_fill(&g, key, sizeof key); /* cannot fail: it is seeded */
}% n은 편향되어 있고, 모두가 그럼에도 그것을 쓴다. n이 2^64를 나누어떨어지게 하지 않는 한 낮은 값들이 더 자주 나온다—어쩌다 하는 점검에서는 보이지 않지만, 셔플이나 표본추출에서는 실재한다.
proven_u64 die = proven_rng_u64(rng) % 6 + 1; /* wrong: 1 and 2 are slightly likelier */올바름(Correct): proven_rng_below(rng, 6) + 1.
시드된 생성기를 복사하지 말라—그 스트림을 복제하려는 의도가 아니라면. 하나에서 복사된 두 개의 "독립적인" 생성기는 동일한 출력을 낳는다—그것은 시뮬레이션 재생에는 기능이고 두 개의 토큰을 발급하는 데는 재앙이다.
테스트 스위트에 의해 컴파일되고 실행됨:
/*
* 쓰임새로 나눈 난수. "난수" 하나는 없다. 똑같이 생겼지만 같지 않은 할 일이 둘 있고,
* 잘못 고르는 것이 위험의 전부다.
*
* 키, 토큰, 논스 - 공격자가 알아맞히면 안 되는 것 - 에는 *암호용* 난수원이 필요하다.
* 시뮬레이션, 시험, 게임에는 *재현 가능한* 것이 필요하다. 다시 돌려 볼 수 없는 실패한
* 실행은 디버깅할 수 없는 실패한 실행이기 때문이다. 두 요구는 정면으로 부딪친다.
* 재현 가능하다는 것은 예측 가능하다는 뜻이고, 예측 가능한 것이야말로 토큰이 되어서는
* 안 되는 것이다. 그래서 라이브러리는 둘에 다른 이름을 주고, 선택은 씨앗을 어떻게
* 뿌렸는지에 묻히는 대신 여기 호출 자리에서 눈에 보인다.
*/
int main(void) {
/* ---- 할 일 1: 비밀. OS 의 CSPRNG - 그리고 난수가 실패할 수 있는 유일한 자리. ---- */
proven_byte_t key[32];
EXAMPLE_REQUIRE(proven_random_bytes(key, sizeof key),
"the OS must give us strong bytes on a hosted platform");
/* ---- 할 일 2: 암호용 바이트를 아주 많이, 또는 OS 가 없는 보드에서 조금이라도.
* ChaCha20 은 순수한 산술이다. 진짜 엔트로피로 한 번 씨를 뿌리면 그 뒤로는 운영체제가
* 필요 없다 - 뽑을 때마다 시스템 호출을 하지 않고, 베어메탈에서도 돈다.
* 씨 뿌리기가 실패할 수 있는 *유일한* 걸음이므로, 확인해야 할 것도 그것 하나다. ---- */
proven_chacha_rng_t crypto;
EXAMPLE_REQUIRE(proven_chacha_rng_seed_from_entropy(&crypto), "seed the CSPRNG from the OS, once");
proven_byte_t token[16];
proven_chacha_rng_fill(&crypto, token, sizeof token); /* 실패할 수 없다: 이미 씨가 뿌려져 있다 */
/* ---- 할 일 3: *재현되는* 실행. xoshiro256** 은 빠르고 씨앗에서 똑같이 다시
* 재생된다. 그것이 실패한 시뮬레이션을 디버깅할 수 있게 만든다. 비밀급이 *아니다* -
* 출력 몇 개면 상태 전체가 드러난다. 토큰을 만들라고 건네는 일은 절대 없어야 한다. ---- */
proven_xoshiro256ss_t sim;
proven_xoshiro256ss_seed(&sim, 12345);
proven_xoshiro256ss_t replay;
proven_xoshiro256ss_seed(&replay, 12345);
EXAMPLE_REQUIRE(proven_xoshiro256ss_next(&sim) == proven_xoshiro256ss_next(&replay),
"the same seed replays the same run - that is the whole point");
/* ---- 도우미들은 proven_rng_t 특성을 통해 *어떤* 난수원 위에서도 돈다. ---- */
proven_rng_t rng = proven_xoshiro256ss_rng(&sim);
/* 범위 안의 수. `rng_u64() % 6` 은 모두가 쓰는 것이고, 상한이 2^64 를 나누어떨어지게
* 하지 않는 한 *치우쳐* 있다 - 작은 값이 더 자주 나온다. 이것은 그렇지 않다. */
for (int i = 0; i < 100; ++i) {
proven_u64 die = proven_rng_below(rng, 6) + 1;
EXAMPLE_REQUIRE(die >= 1 && die <= 6, "a die roll is 1..6, uniformly");
}
proven_i64 temperature = proven_rng_range(rng, -40, 85);
EXAMPLE_REQUIRE(temperature >= -40 && temperature <= 85, "an inclusive range, both ends");
double p = proven_rng_f64(rng);
EXAMPLE_REQUIRE(p >= 0.0 && p < 1.0, "a double in [0, 1) - never 1.0");
/* 치우치지 않은 섞기: 위의 치우치지 않은 인덱스 위에서 도는 피셔-예이츠. 이 반복문의
* `% n` 판은 어떤 순서를 눈에 띄게 더 좋아한다. */
int deck[10];
for (int i = 0; i < 10; ++i) deck[i] = i;
proven_rng_shuffle(rng, deck, 10, sizeof deck[0]);
int sum = 0;
for (int i = 0; i < 10; ++i) sum += deck[i];
EXAMPLE_REQUIRE(sum == 45, "a shuffle is a permutation: every card is still there, once");
/* 암호용 생성기도 같은 특성을 만족하므로, 고르기만 하면 되는 것이 아니라 알아맞힐 수
* 없어야 할 때 같은 도우미를 그대로 쓸 수 있다. */
proven_rng_t secure = proven_chacha_rng(&crypto);
proven_u64 unguessable_index = proven_rng_below(secure, 1000);
EXAMPLE_REQUIRE(unguessable_index < 1000, "the helpers do not care which source they draw from");
(void)token;
(void)key;
return EXAMPLE_OK();
}실전 예제: 정전이 나도 파일이 깨지지 않게 교체하기
파일을 제자리에서 덮어쓰는 방식에는, 시험에서는 결코 보이지 않고 운영에서는 언젠가 반드시 보이는 실패 방식이 있다. 쓰는 도중에 전원이 나가고, 남은 파일은 예전 것도 새 것도 아니게 된다. 해법은 오래된 네 단계 절차이며, 네 단계 모두가 제 몫을 한다.
- 새 내용을 진짜 파일 옆의 임시 파일에 쓴다.
proven_fs_sync()— 새 파일의 바이트가 운영체제 캐시가 아니라 저장 장치에 도달한다.proven_fs_rename()— 이름이 한 번의 나눌 수 없는 단계로 새 파일로 넘어간다. 읽는 쪽은 옛 파일 전체나 새 파일 전체를 보며, 섞인 것은 결코 보지 않는다.proven_fs_sync_dir()— 이름 바꾸기 자체가 저장 장치에 도달한다.
2단계를 건너뛰면 내용이 도착한 적 없는 파일을 이름이 가리킬 수 있다. 4단계를 건너뛰면 내용은 안전한데 그것을 가리키는 이름이 안전하지 않다.
같은 예제가 함께 쓰이는 레코드 단위 호출들도 다룬다.
| 호출 | 무엇을 위한 것인가 |
proven_fs_pread / proven_fs_pwrite | 절대 오프셋에서 읽고 쓰되 파일 위치를 옮기지 않는다. 하나의 핸들을 여러 스레드가 함께 써도 안전한 이유다. |
proven_fs_seek / proven_fs_tell | 위치를 옮기고, 지금 어디인지 묻는다. 끝에서 음수 오프셋으로 seek하면 길이를 몰라도 마지막 레코드를 찾는다. |
proven_fs_truncate | 길이를 직접 정한다. 한 번의 호출로 파일 시스템이 숫자 하나를 고치며, 대안은 남길 부분을 복사하는 것이다. |
proven_fs_lock | 권고(advisory) 잠금이다. 마찬가지로 잠금을 요청하는 다른 프로세스만 막고, 아예 요청하지 않는 프로그램에는 아무 영향이 없다. |
proven_fs_copy | 바이트를 복제해 독립된 두 번째 파일을 만든다. |
proven_fs_link | 같은 파일에 대한 두 번째 이름(하드 링크). 원본이라는 것이 없고, 마지막 이름이 사라질 때까지 데이터가 산다. 같은 파일 시스템 안에서만 된다. |
proven_fs_symlink | 경로를 담은 작은 파일(심볼릭 링크). 파일 시스템을 건너뛸 수 있고, 아무것도 가리키지 않을 수도 있다. |
proven_fs_is_absolute | 이 경로가 루트에서 시작하는가? 규칙이 플랫폼마다 달라서 함수인 것이다. |
proven_fs_rmdir | 빈 디렉터리를 지운다. 비어 있지 않으면 거절하므로, 재귀 삭제는 명시적인 결정으로 남는다. |
/*
* 정전이 파일을 반쯤 쓰인 채로 남겨 두지 못하게 하며 파일을 고치기.
*
* 조리법은 오래되었고, 그 모든 걸음이 하중을 받는다.
*
* 1. 새 내용을 진짜 파일 옆의 *임시* 파일에 쓴다,
* 2. proven_fs_sync - 이제 새 파일의 바이트가 장치에 있다,
* 3. proven_fs_rename - 이름이 한 걸음에 새 파일로 넘어간다. 읽는 쪽은 옛 파일 전체나
* 새 파일 전체를 보지, 그 사이를 보지 않는다,
* 4. proven_fs_sync_dir - 이제 그 이름 바꾸기 자체가 장치에 있다.
*
* 2를 건너뛰면 내용이 도착한 적 없는 파일을 이름이 공표할 수 있다. 4를 건너뛰면 내용은
* 안전한데 그 이름이 안전하지 않을 수 있다. 둘 다 시험에서는 드러나지 않는다. 둘 다
* 운영에서, 한 번, 드러난다.
*
* 이 프로그램은 레코드 단위 호출들 - seek/tell, pread/pwrite, truncate - 과, 이 모든
* 일을 두 벌의 프로그램이 동시에 하지 못하게 막는 권고 잠금도 함께 보인다.
*/
typedef struct {
proven_u32 id;
proven_u32 score;
} record_t;
static proven_err_t write_records(proven_file_t f, const record_t *recs, proven_size_t n) {
proven_mem_view_t view = { .ptr = (const proven_byte_t *)recs, .size = n * sizeof recs[0] };
return proven_fs_write_all(f, view);
}
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
proven_u8str_view_t live = PROVEN_LIT("proven_example_durable.dat");
proven_u8str_view_t temp = PROVEN_LIT("proven_example_durable.dat.new");
proven_u8str_view_t here = PROVEN_LIT(".");
/* is_absolute 는 경로를 잇거나 기준 디렉터리에 대고 풀기 전에 물어 둘 값어치가 있는
* 물음에 답한다 - 이 경로가 이미 뿌리에서 시작하는가? 규칙은 플랫폼마다 다르다 -
* 여기서는 앞의 '/', 윈도에서는 드라이브 문자나 UNC 접두사 - 그리고 바로 그 때문에
* 이것은 '/' 와의 비교가 아니라 호출이다. */
EXAMPLE_REQUIRE(!proven_fs_is_absolute(live), "the working paths in this example are relative");
EXAMPLE_REQUIRE(proven_fs_is_absolute(PROVEN_LIT("/etc/hosts")), "a leading slash is absolute on POSIX");
static const record_t initial[] = {
{ .id = 1, .score = 10 }, { .id = 2, .score = 20 },
{ .id = 3, .score = 30 }, { .id = 4, .score = 40 },
};
/* --- 평범한 첫 쓰기 ---------------------------------------------------- */
proven_result_file_t f = proven_fs_open(alloc, live, PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
EXAMPLE_REQUIRE(proven_is_ok(f.err), "creating the data file must succeed");
if (!proven_is_ok(f.err)) return 1;
proven_err_t err = write_records(f.value, initial, 4);
EXAMPLE_REQUIRE(proven_is_ok(err), "writing the initial records must succeed");
err = proven_fs_close(f.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "closing after a write must succeed");
/* --- 두 벌이 서로 끼어들지 못하게 하는 권고 잠금 ----------------------- */
proven_result_file_t rw = proven_fs_open(alloc, live, PROVEN_FS_READ | PROVEN_FS_WRITE);
EXAMPLE_REQUIRE(proven_is_ok(rw.err), "reopening for update must succeed");
if (!proven_is_ok(rw.err)) return 1;
/* *배타* 잠금은 같은 것을 청하는 다른 모든 프로세스를 밖에 세운다. "권고" 는 말
* 그대로다. 협조하는 프로그램을 막을 뿐, 잠금을 아예 청하지 않는 프로그램에는 아무
* 영향이 없다. `wait = false` 는 막지 않고 곧바로 돌아온다 - 달리 할 일이 있을 때
* 옳은 선택이고, 잠금을 쥔 쪽이 여러분을 기다리고 있을 수 있을 때는 유일하게 안전한
* 선택이다. */
err = proven_fs_lock(rw.value, PROVEN_FS_LOCK_EXCLUSIVE, false);
EXAMPLE_REQUIRE(proven_is_ok(err), "taking the exclusive lock must succeed when nobody holds it");
/* --- 레코드 하나를, 위치를 지정해 읽고 쓰기 --------------------------- */
/* pread 는 절대 위치에서 읽고 파일 위치를 옮기지 *않는다*. 그래서 손잡이 하나를
* 나눠 쓰는 두 스레드에서도 안전하다 - 그들이 다툴 공유 커서가 없다. */
record_t third = {0};
proven_mem_mut_t into = { .ptr = (proven_byte_t *)&third, .size = sizeof third };
proven_result_size_t got = proven_fs_pread(rw.value, into, 2 * sizeof(record_t));
EXAMPLE_REQUIRE(proven_is_ok(got.err) && got.value == sizeof third, "reading record 2 must succeed");
EXAMPLE_REQUIRE(third.id == 3 && third.score == 30, "and yield the record that was written there");
/* tell 은 위치를 알려 준다. pread 뒤에도 그것은 움직이지 않았다. */
proven_result_u64_t pos = proven_fs_tell(rw.value);
EXAMPLE_REQUIRE(proven_is_ok(pos.err) && pos.val == 0, "pread must not move the file position");
/* pwrite 는 그 레코드를 제자리에서 고친다. 이번에도 커서는 건드리지 않는다. */
third.score = 99;
proven_mem_view_t out_view = { .ptr = (const proven_byte_t *)&third, .size = sizeof third };
proven_result_size_t put = proven_fs_pwrite(rw.value, out_view, 2 * sizeof(record_t));
EXAMPLE_REQUIRE(proven_is_ok(put.err) && put.value == sizeof third, "writing record 2 back must succeed");
/* seek 은 커서를 옮기는 쪽이고, 도착한 위치를 돌려준다. 음수 오프셋으로 *끝*에서
* seek 하는 것이 파일 길이를 먼저 알지 않고 마지막 레코드를 찾는 방법이다. */
proven_result_u64_t last = proven_fs_seek(rw.value, -(proven_i64)sizeof(record_t), PROVEN_FS_SEEK_END);
EXAMPLE_REQUIRE(proven_is_ok(last.err), "seeking to the last record must succeed");
EXAMPLE_REQUIRE(last.val == 3 * sizeof(record_t), "which is three records in");
pos = proven_fs_tell(rw.value);
EXAMPLE_REQUIRE(proven_is_ok(pos.err) && pos.val == last.val, "tell agrees with the seek result");
/* truncate 는 길이를 곧장 정한다. 마지막 레코드를 버리는 것이 호출 하나에 O(1) 이다.
* 옛날 방식 - 전부 읽고 남길 부분을 다시 쓰기 - 은 파일 시스템이 수 하나를 고쳐서
* 하는 일에 O(n) 복사를 치르는 것이었다. */
err = proven_fs_truncate(rw.value, 3 * sizeof(record_t));
EXAMPLE_REQUIRE(proven_is_ok(err), "truncating to three records must succeed");
proven_result_size_t size = proven_fs_size(rw.value);
EXAMPLE_REQUIRE(proven_is_ok(size.err) && size.value == 3 * sizeof(record_t),
"the file is now exactly three records long");
/* 잠금을 명시적으로 푼다. 손잡이를 닫아도 풀리지만, 그렇다고 적어 두면 임계 구역이
* 코드에 눈에 보인 채로 남는다. */
err = proven_fs_lock(rw.value, PROVEN_FS_LOCK_UNLOCK, false);
EXAMPLE_REQUIRE(proven_is_ok(err), "releasing the lock must succeed");
err = proven_fs_close(rw.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "closing the update handle must succeed");
/* --- 견디는 바꿔치기 --------------------------------------------------- */
static const record_t replacement[] = {
{ .id = 1, .score = 11 }, { .id = 2, .score = 22 },
};
/* 1. 새 내용을 옛 파일 옆에 쓴다. CREATE_NEW 는 임시 이름이 이미 있으면 거부하고,
* 그것이 죽어 버린 실행이 남긴 찌꺼기를 조용히 재사용하지 않고 알아채는 방법이다. */
proven_result_file_t tmp = proven_fs_open(alloc, temp,
PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
EXAMPLE_REQUIRE(proven_is_ok(tmp.err), "creating the temporary file must succeed");
if (!proven_is_ok(tmp.err)) return 1;
err = write_records(tmp.value, replacement, 2);
EXAMPLE_REQUIRE(proven_is_ok(err), "writing the new contents must succeed");
/* 2. 그 바이트를 저장 장치까지 끝까지 밀어 준다. 값이 비싸고, 비싸야 마땅하다.
* 정전 뒤에도 자료가 있다는 보장을 사는 것이고, 그 값은 장치까지 실제로 다녀오는
* 일이다. */
err = proven_fs_sync(tmp.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "syncing the new file's data must succeed");
err = proven_fs_close(tmp.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "closing the temporary file must succeed");
/* 3. 이름을 넘긴다. 한 디렉터리 안의 rename 은 원자적이다. 읽는 쪽은 옛 파일이나 새
* 파일을 보지, 반쯤 쓰인 것을 보지 않는다. */
err = proven_fs_rename(alloc, temp, live);
EXAMPLE_REQUIRE(proven_is_ok(err), "renaming the temporary file over the live one must succeed");
/* 4. 그 이름 바꾸기 자체를 견디게 만든다. 디렉터리가 장치에 닿기 전까지는, 새 내용이
* 안전하지 않을 수도 있는 이름 아래에 안전하게 있는 것이다. */
err = proven_fs_sync_dir(alloc, here);
EXAMPLE_REQUIRE(proven_is_ok(err), "syncing the directory must succeed");
proven_result_file_t check = proven_fs_open(alloc, live, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(check.err), "the live path must now open");
size = proven_fs_size(check.value);
EXAMPLE_REQUIRE(proven_is_ok(size.err) && size.value == 2 * sizeof(record_t),
"and hold exactly the replacement records");
err = proven_fs_close(check.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "closing the verification handle must succeed");
/* --- 복사, 그리고 두 가지 링크 ---------------------------------------- */
/* copy 는 바이트를 복제한다. 이제부터는 서로 독립인 파일 둘이다. 할당자는 복사가
* 자료를 옮겨 가는 임시 버퍼를 위한 것이다. */
proven_u8str_view_t backup = PROVEN_LIT("proven_example_durable.bak");
err = proven_fs_copy(alloc, live, backup);
EXAMPLE_REQUIRE(proven_is_ok(err), "copying the file must succeed");
/* *하드* 링크는 같은 파일의 두 번째 이름이다. 원본이라는 것이 없다. 자료는 마지막
* 이름이 지워질 때까지 산다. 두 이름은 같은 파일 시스템에 있어야 한다. 이름과 그
* 자료가 둘에 걸칠 수는 없기 때문이다. */
proven_u8str_view_t hard = PROVEN_LIT("proven_example_durable.hard");
err = proven_fs_link(alloc, live, hard);
EXAMPLE_REQUIRE(proven_is_ok(err), "creating a hard link must succeed");
proven_fs_stat_t st_live = {0}, st_hard = {0};
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_stat(alloc, live, &st_live)), "stat of the live name");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_stat(alloc, hard, &st_hard)), "stat of the hard link");
EXAMPLE_REQUIRE(st_live.ino == st_hard.ino && st_live.dev == st_hard.dev,
"both names refer to the same file, which is what a hard link means");
/* *심볼릭* 링크는 경로를 담은 작은 파일이다. 다른 파일 시스템의 무언가를 가리킬 수도
* 있고, 아무것도 아닌 것을 가리킬 수도 있다 - 그때 따라가기는 실패하는데, 하드 링크는
* 결코 그럴 수 없다. */
proven_u8str_view_t soft = PROVEN_LIT("proven_example_durable.link");
err = proven_fs_symlink(alloc, live, soft);
EXAMPLE_REQUIRE(proven_is_ok(err), "creating a symbolic link must succeed");
proven_fs_stat_t st_soft = {0};
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_stat(alloc, soft, &st_soft)),
"stat follows the symbolic link to its target");
EXAMPLE_REQUIRE(st_soft.size == st_live.size, "so it reports the target's size");
/* --- 디렉터리, 그리고 뒷정리 ------------------------------------------ */
proven_u8str_view_t dir = PROVEN_LIT("proven_example_durable_dir");
err = proven_fs_mkdir(alloc, dir);
EXAMPLE_REQUIRE(proven_is_ok(err), "creating a directory must succeed");
/* rmdir 은 *빈* 디렉터리만 지운다. 그 거부가 기능이다. 재귀 삭제는 부르는 쪽이
* 명시적으로 내려야 하는 결정이지, 잘못 들어온 경로 인자 하나가 일으킬 수 있는 일이
* 아니다. */
proven_u8str_view_t inside = PROVEN_LIT("proven_example_durable_dir/file.txt");
proven_result_file_t child = proven_fs_open(alloc, inside, PROVEN_FS_WRITE | PROVEN_FS_CREATE);
EXAMPLE_REQUIRE(proven_is_ok(child.err), "creating a file inside it must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(child.value)), "closing it must succeed");
err = proven_fs_rmdir(alloc, dir);
EXAMPLE_REQUIRE(err != PROVEN_OK, "removing a non-empty directory must be refused");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_remove(alloc, inside)), "removing the file must succeed");
err = proven_fs_rmdir(alloc, dir);
EXAMPLE_REQUIRE(proven_is_ok(err), "and then the empty directory can be removed");
printf("durable replace complete: %zu byte(s) live\n", (size_t)size.value);
(void)proven_fs_remove(alloc, soft);
(void)proven_fs_remove(alloc, hard);
(void)proven_fs_remove(alloc, backup);
(void)proven_fs_remove(alloc, live);
return EXAMPLE_OK();
}반례 — 이 절차가 대신하려는 제자리 덮어쓰기:
proven_result_file_t f = proven_fs_open(alloc, live, PROVEN_FS_WRITE | PROVEN_FS_TRUNC);
proven_err_t e = proven_fs_write_all(f.value, new_contents); /* wrong */자르기와 마지막 바이트 쓰기 사이에서 디스크 위의 파일은 불완전하다. 그 사이의 크래시는 갱신을 잃는 것이 아니라 이미 있던 데이터를 잃는다.
반례 — sync 없이 이름부터 바꾸는 경우:
proven_err_t e = proven_fs_close(tmp.value); /* no proven_fs_sync */
e = proven_fs_rename(alloc, temp, live); /* wrong */파일을 닫는 것은 그 바이트를 장치에 올려놓는 일이 아니다. 이름 바꾸기는 지속되는데 그것이 가리키는 내용은 그렇지 않을 수 있다.
반례 — 잠금이 모두를 막는다고 가정하는 경우:
proven_err_t e = proven_fs_lock(f.value, PROVEN_FS_LOCK_EXCLUSIVE, true);
/* another program writes the file without ever calling proven_fs_lock */권고 잠금은 그것을 모두가 쓰기로 한 프로그램들 사이의 약속이다. 접근 권한 통제가 아니다.
실전 예제: reader와 writer, 그리고 표준 스트림
writer는 "바이트가 가는 곳", reader는 "바이트가 오는 곳"이다. 각각은 포인터 둘이다. 작은 함수 표 하나와, 그 함수들이 다루는 상태 하나. 이 구조의 값어치는 전부 여기서 나온다. writer를 상대로 쓴 코드는 그 바이트가 파일로 가는지, 문자열로 가는지, 터미널로 가는지, 시험용 버퍼로 가는지 알지 못하고 — reader를 상대로 쓴 코드는 디스크의 파일 대신 메모리의 문자열을 상대로 시험할 수 있다.
예제는 그 조각들을 한데 놓는다.
proven_reader_from_view()는 이미 손에 있는 바이트 위에 reader를 만든다. 파서를 파일 없이 시험하는 방법이다.proven_reader_read()는 버퍼 크기까지 채우고 실제로 얻은 양을 알려 준다. 짧은 읽기는 정상이며, 입력의 끝은 0바이트 성공이 아니라PROVEN_ERR_EOF다.proven_sysio_stdout_writer()와proven_sysio_stderr_writer()는 버퍼링 없는 표준 스트림이고,proven_sysio_stdin_reader()는 표준 입력이다.proven_writer_write()는 "전부, 아니면 에러"를 뜻하고,proven_writer_write_partial()은 옮길 수 있는 만큼 옮기고 그 개수를 알려 준다 — 재시도나 배압(back-pressure)을 직접 다루는 호출자를 위한 것이다.proven_reader_is_valid()와proven_writer_is_valid()는 만들어진 적 없는 핸들을 첫 읽기·쓰기가 아니라 경계에서 잡는다.proven_sysio_file_buffered()는 열린 파일을, 여러분이 준 버퍼 위의 버퍼드 writer로 감싼다. 버퍼링의 메모리 비용이 여러분이 고른 숫자로 남는다는 뜻이다. 반드시 flush해야 한다. 여기서는 아무것도 종료 시에 대신 flush해 주지 않는다.proven_sysio_lines_open()은 같은 방식의 호출자 제공 버퍼로 그 파일을 한 줄씩 읽는다. 예상되는 가장 긴 줄에 맞춰 크기를 잡는다. 더 긴 줄은 조용히 잘리는 대신PROVEN_ERR_OUT_OF_BOUNDS가 된다.proven_scan_fmt_from_file()(내부적으로proven_sysio_scan_chunk_impl()을 부른다)은 입력이 자유 서식이 아니라 정해진 모양일 때 파일 핸들에서 타입 있는 값을 바로 뽑는다.
/*
* 쓰는 쪽은 "바이트가 가는 어딘가" 이고 읽는 쪽은 "바이트가 오는 어딘가" 이며, 각각
* 포인터 둘이다. 작은 함수 표 하나와 그 함수들이 다룰 상태 하나. 그것이 전부다. 이
* 얼개의 값어치는, 쓰는 쪽에 기대어 쓴 코드가 그 바이트가 파일로 가는지 문자열로 가는지
* 터미널로 가는지 시험용 버퍼로 가는지 알지도 못하고 신경 쓰지도 않는다는 것이다 -
* 그리고 읽는 쪽에 기대어 쓴 코드는 디스크의 파일 대신 기억 속 문자열에 대고 시험할 수
* 있다.
*
* 이 프로그램은 둘 다, 그리고 거기 매달린 표준 스트림과 파일 배관을 보인다.
*
* - 뷰(기억 속 문자열) 위의 읽는 쪽. 파일 시스템을 건드리지 않고 파서를 시험하는
* 방법이다,
* - 버퍼 없는 stdout 과 stderr 쓰는 쪽, 그리고 "이걸 다 써라" 와 "쓸 수 있는 만큼
* 써라" 의 차이,
* - 파일 위의 버퍼 둔 쓰는 쪽. 작은 쓰기 여럿을 큰 쓰기 몇으로 바꿔 준다,
* - 그 같은 파일 위의 줄 단위 읽는 쪽,
* - 파일 손잡이에서 곧장 형식화된 값 읽기.
*
* 사람들이 반나절을 잃게 만드는 주의 하나: 아래의 상태 구조체들은 선언된 자리에 그대로
* 있어야 한다. 쓰는 쪽은 자기 상태 구조체 *안*을 가리키는 포인터를 쥐므로, 구조체를
* 복사하면 사본은 죽은 것이 되고 주소는 원본을 가리킨 채로 남는다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* --- 1. 이미 가진 바이트 위의 읽는 쪽 --------------------------------- */
/* 아래 파서는 이것이 문자열이라는 것을 모른다. 파일이든 파이프든 소켓이든 똑같이
* 돌 것이다 - 그래서 시험이 값싼 쪽을 쓸 수 있다. */
proven_reader_view_t src_state;
proven_reader_t src = proven_reader_from_view(&src_state, PROVEN_LIT("id=41\nid=42\n"));
/* is_valid 는 그 읽는 쪽이 실제로 만들어졌는지 묻는다 - 0 으로 초기화된 손잡이는
* 읽는 쪽이 아니고, 그것을 첫 읽기가 아니라 경계에서 말해 주는 검사가 이것이다. */
EXAMPLE_REQUIRE(proven_reader_is_valid(src), "a reader made from a view must be usable");
proven_reader_t never_made = {0};
EXAMPLE_REQUIRE(!proven_reader_is_valid(never_made), "a zero-initialised reader handle is not");
/* read 는 dest.size 바이트까지 채우고 실제로 얼마나 받았는지 알려 준다. 짧은 읽기는
* 정상이다 - 파일의 끝이 아니고, 그것을 끝으로 여기는 것이 입력의 꼬리를 잃는
* 고전적인 방법이다. 파일의 끝에는 제 코드가 따로 있다, PROVEN_ERR_EOF. */
proven_byte_t chunk[5];
proven_mem_mut_t into = { .ptr = chunk, .size = sizeof chunk };
proven_result_size_t got = proven_reader_read(src, into);
EXAMPLE_REQUIRE(proven_is_ok(got.err), "the first read must succeed");
EXAMPLE_REQUIRE(got.value == 5, "and fill the buffer from the view");
proven_size_t total = got.value;
for (;;) {
got = proven_reader_read(src, into);
if (got.err == PROVEN_ERR_EOF) {
break;
}
EXAMPLE_REQUIRE(proven_is_ok(got.err), "reads before end of file must succeed");
total += got.value;
}
EXAMPLE_REQUIRE(total == 12, "the loop must consume the whole view, however it was chunked");
/* --- 2. 표준 스트림 ---------------------------------------------------- */
/* 상태 구조체가 쓰는 쪽이 가리키는 손잡이를 쥐고 있으므로, 쓰는 쪽보다 오래 살아야
* 한다. 둘을 나란히 선언하는 습관이 그것을 지켜 준다. */
proven_sysio_std_t out_state;
proven_writer_t out = proven_sysio_stdout_writer(&out_state);
EXAMPLE_REQUIRE(proven_writer_is_valid(out), "the stdout writer must be usable");
/* write 는 "전부, 아니면 오류" 라는 뜻이다. 안에서 반복한다. 시스템 수준의 쓰기 한
* 번은 청한 것보다 적은 바이트를 옮길 수 있기 때문이다. */
proven_err_t err = proven_writer_write(out, proven_mem_view_from_u8(PROVEN_LIT("stream example: start\n")));
EXAMPLE_REQUIRE(proven_is_ok(err), "writing a whole line to stdout must succeed");
/* write_partial 은 정직한 저수준 쌍둥이다. 옮길 수 있는 만큼 옮기고 그 개수를 알려
* 준다. 재시도나 역압을 여러분이 다룰 때 쓰고, 그냥 바이트를 내보내고 싶으면 write 를
* 쓸 것. */
proven_result_size_t part = proven_writer_write_partial(out, proven_mem_view_from_u8(PROVEN_LIT("partial write\n")));
EXAMPLE_REQUIRE(proven_is_ok(part.err), "a partial write to a terminal or pipe must succeed");
EXAMPLE_REQUIRE(part.value > 0, "and report how many bytes it moved");
/* stderr 에 버퍼를 두지 않은 것은 일부러다. 진단은 다음 줄의 코드가 돌기 전에 나가야
* 하고, 그 다음 줄이 바로 죽는 줄일 때 그것이 필요하다. */
proven_sysio_std_t err_state;
proven_writer_t diag = proven_sysio_stderr_writer(&err_state);
EXAMPLE_REQUIRE(proven_writer_is_valid(diag), "the stderr writer must be usable");
err = proven_writer_write(diag, proven_mem_view_from_u8(PROVEN_LIT("stream example: diagnostics go here\n")));
EXAMPLE_REQUIRE(proven_is_ok(err), "writing to stderr must succeed");
/* stdin 위의 읽는 쪽도 같은 방식으로 만든다. 이 예제는 거기서 읽지 않는다 - 시험
* 실행에는 타이핑하는 사람이 없다 - 다만 만드는 모양을 보이고, 필터 프로그램이 돌
* 손잡이가 이것이다. */
proven_sysio_std_t in_state;
proven_reader_t stdin_reader = proven_sysio_stdin_reader(&in_state);
EXAMPLE_REQUIRE(proven_reader_is_valid(stdin_reader), "the stdin reader must be usable");
/* --- 3. 파일로 나가는 버퍼 둔 출력 ------------------------------------ */
proven_u8str_view_t path = PROVEN_LIT("proven_example_streams.txt");
proven_result_file_t f = proven_fs_open(alloc, path, PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
EXAMPLE_REQUIRE(proven_is_ok(f.err), "creating the output file must succeed");
if (!proven_is_ok(f.err)) return 1;
/* 버퍼는 여러분의 것이다. 라이브러리가 등 뒤에서 하나 할당하지 않으므로, 버퍼링의
* 기억 비용은 여러분이 고른, 눈에 보이는 수다. 256바이트 버퍼를 지나는 예순 줄은
* 예순 번이 아니라 몇 번의 쓰기다. */
proven_byte_t outbuf[256];
proven_sysio_out_t file_out;
proven_writer_t w = proven_sysio_file_buffered(&file_out, f.value,
(proven_mem_mut_t){ .ptr = outbuf, .size = sizeof outbuf });
EXAMPLE_REQUIRE(proven_writer_is_valid(w), "the buffered file writer must be usable");
for (int i = 0; i < 20; ++i) {
proven_fmt_result_t line_out = proven_fprintln(w, "reading {} = {}",
proven_arg_i32(i), proven_arg_i32(i * i));
EXAMPLE_REQUIRE(proven_is_ok(line_out.err), "writing a formatted line must succeed");
}
/* 흘려 보내지 않은 버퍼 출력은 일어나지 않은 출력이다. 여기 어느 것도 여러분 대신
* 종료 시점에 흘려 주지 않는다. */
err = proven_writer_flush(w);
EXAMPLE_REQUIRE(proven_is_ok(err), "the flush is what actually writes the file");
err = proven_fs_close(f.value);
EXAMPLE_REQUIRE(proven_is_ok(err), "closing the file must succeed");
/* --- 4. 한 줄씩 되읽기 ------------------------------------------------ */
proven_result_file_t rf = proven_fs_open(alloc, path, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(rf.err), "reopening for reading must succeed");
if (!proven_is_ok(rf.err)) return 1;
/* 버퍼는 예상되는 가장 긴 *줄*에 맞춰 잡을 것. 더 긴 줄은 OUT_OF_BOUNDS 로 보고된다 -
* 조용히 반으로 잘리는 일은 없다. 그 자르기가 나쁜 레코드 하나를 그럴듯한 레코드
* 둘로 만드는 실패다. */
proven_byte_t linebuf[128];
proven_sysio_lines_t lines;
err = proven_sysio_lines_open(&lines, rf.value, (proven_mem_mut_t){ .ptr = linebuf, .size = sizeof linebuf });
EXAMPLE_REQUIRE(proven_is_ok(err), "opening a line reader over the file must succeed");
proven_size_t count = 0;
for (;;) {
proven_result_u8str_view_t line = proven_sysio_read_line(&lines);
if (line.err == PROVEN_ERR_EOF) {
break;
}
EXAMPLE_REQUIRE(proven_is_ok(line.err), "reading a line must succeed until end of file");
/* 그 뷰는 linebuf 안을 가리키고 다음 호출 전까지만 쓸 수 있다. 그래서 백만 줄이
* 백만 번의 할당이 아니라 버퍼 하나만큼의 값이 든다 - 그리고 남겨 둘 줄은 복사해야
* 하는 이유이기도 하다. */
if (count == 0) {
EXAMPLE_REQUIRE(proven_u8str_view_eq(line.val, PROVEN_LIT("reading 0 = 0")),
"the first line reads back exactly as it was written");
}
++count;
}
EXAMPLE_REQUIRE(count == 20, "every line written must be read back");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(rf.value)), "closing the read handle must succeed");
/* --- 5. 파일에서 줄이 아니라 값을 읽기 -------------------------------- */
/* 입력이 자유로운 글이 아니라 정해진 모양일 때는, 곧장 파싱하는 편이 자르고 변환하는
* 반복문을 손으로 쓰는 수고를 던다. 이것은 파일 손잡이에서 한 토막을 읽어 그 안에서
* 수 둘을 뽑아낸다. */
proven_result_file_t sf = proven_fs_open(alloc, path, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(sf.err), "opening the file for scanning must succeed");
proven_i32 index = -1, square = -1;
err = proven_scan_fmt_from_file(sf.value, "reading {} = {}",
proven_scan_arg_i32(&index), proven_scan_arg_i32(&square));
EXAMPLE_REQUIRE(proven_is_ok(err), "scanning the first record must succeed");
EXAMPLE_REQUIRE(index == 0 && square == 0, "and produce the values that were written");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(sf.value)), "closing the scan handle must succeed");
printf("streams: %zu byte(s) read from the view, %zu line(s) round-tripped\n",
(size_t)total, (size_t)count);
(void)proven_fs_remove(alloc, path);
return EXAMPLE_OK();
}반례 — writer를 만든 뒤 상태 구조체를 복사하는 경우:
proven_sysio_out_t state;
proven_writer_t w = proven_sysio_file_buffered(&state, file, buf);
proven_sysio_out_t moved = state; /* wrong: w still points into `state` */writer는 그 구조체 안쪽을 가리키는 포인터를 들고 있다. writer가 사는 동안 상태는 선언한 자리에 둔다.
반례 — flush를 잊는 경우:
proven_fmt_result_t r = proven_fprintln(w, "done");
return 0; /* wrong: the buffer never reached the file */실전 예제: 파일을 메모리에 매핑하고, 그 변경을 지속시키기
메모리 매핑된 파일은 프로세서가 메모리처럼 읽는 파일이다. 운영체제가 그 페이지들을 어떤 주소에 나타나게 해 주고, 읽기는 read 호출 없이 일어난다. 큰 파일에 무작위로 접근할 때, 특히 여러 프로세스가 함께 볼 때 어울린다. 스트리밍에는 어울리지 않는다. 거기서는 버퍼드 reader가 더 단순하고, 파일 크기를 주소 공간에 묶지도 않는다.
쓰기가 살아남는지를 정하는 구분은 이것이다.
| 매핑 | 쓰기가 가는 곳 | proven_mmap_sync() |
PROVEN_MMAP_SHARED | 파일로 간다 | 저장 장치까지 밀어 넣는다 |
PROVEN_MMAP_PRIVATE | 사적인 사본으로 간다(기록 시 복사, copy-on-write) — 이 프로세스에만 | 되돌려 쓸 것이 없으므로 PROVEN_ERR_UNSUPPORTED를 돌려준다 |
그 거절은 일부러 그런 것이다. 공유 매핑이라고 믿었던 호출자가, 데이터가 없어진 뒤가 아니라 sync 자리에서 그 사실을 알게 된다.
예제는 달력 형식화로 끝난다. 기록하는 레코드에 날짜가 붙기 때문이다. UTF-8 형태는 proven_time_u8_fmt(), UTF-16 형태는 proven_time_u16_fmt()이며, 후자가 옳은 상황은 딱 하나 — 와이드 문자열을 받는 시스템 호출에 그 텍스트를 바로 넘길 때다.
#include <string.h>
/*
* 기억에 사상된 파일은 프로세서가 기억처럼 읽는 파일이다. 운영체제가 그 쪽들을 어떤
* 주소에 나타나게 해 주고, 읽기는 read 호출 없이 일어난다. 한 모양의 문제에는 옳은
* 도구다 - 여러 프로세스가 함께 보는 큰 파일에 무작위로 접근하는 일 - 그리고 흘려 읽기에는
* 틀린 도구다. 거기서는 버퍼를 둔 읽기 쪽이 더 단순하고, 파일 크기를 여러분의 주소 공간에
* 묶지도 않는다.
*
* 틀리기 쉬운 자리는 지속성이고, 이 예제가 있는 이유가 그것이다.
*
* PROVEN_MMAP_SHARED - 쓰기가 파일로 간다. proven_mmap_sync 가 그것을 저장 장치까지
* 밀어 준다.
* PROVEN_MMAP_PRIVATE - 쓰기가 복사-후-쓰기다. 이 프로세스 안에만 있고 다른 어디에도
* 없다. 되쓸 것이 없으므로, sync 를 청하면 조용히 아무 일도 하지
* 않는 대신 그렇다고 말한다.
*
* 이 예제는 달력 형식화기도 나란히 보인다. 여기서 쓰는 레코드가 시각을 싣기 때문이고,
* 윈도 API 를 위해 쓰는 시각이야말로 UTF-16 형식화기가 옳은 호출인 유일한 자리다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
proven_u8str_view_t path = PROVEN_LIT("proven_example_mmap.dat");
/* 사상은 파일을 늘릴 수 없다. 그러니 파일은 사상하기 전에 사상하려는 크기여야 한다. */
static const char initial[] = "record 0: pending \n";
proven_result_file_t create = proven_fs_open(alloc, path,
PROVEN_FS_WRITE | PROVEN_FS_CREATE | PROVEN_FS_TRUNC);
EXAMPLE_REQUIRE(proven_is_ok(create.err), "creating the backing file must succeed");
if (!proven_is_ok(create.err)) return 1;
proven_mem_view_t seed = { .ptr = (const proven_byte_t *)initial, .size = sizeof initial - 1 };
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_write_all(create.value, seed)), "writing the record must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(create.value)), "closing it must succeed");
/* --- 공유 사상: 쓰기가 파일에 닿는다 ---------------------------------- */
proven_result_file_t f = proven_fs_open(alloc, path, PROVEN_FS_READ | PROVEN_FS_WRITE);
EXAMPLE_REQUIRE(proven_is_ok(f.err), "opening the file for mapping must succeed");
if (!proven_is_ok(f.err)) return 1;
/* size 0 은 "파일 끝까지" 라는 뜻이다. */
proven_result_mmap_t m = proven_mmap_create(f.value, 0, 0,
PROVEN_MMAP_READ | PROVEN_MMAP_WRITE,
PROVEN_MMAP_SHARED);
EXAMPLE_REQUIRE(proven_is_ok(m.err), "mapping the file must succeed");
if (!proven_is_ok(m.err)) {
(void)proven_fs_close(f.value);
return 1;
}
proven_mmap_t map = m.value;
/* 읽기는 그냥 기억을 읽는 것이다 - 호출도 복사도 없다. as_view 는 사상 전체를 바이트
* 뷰로 돌려주므로, 보통의 뷰 도우미들이 그대로 먹는다. */
proven_u8str_view_t contents = proven_mmap_as_view(map);
EXAMPLE_REQUIRE(contents.size == sizeof initial - 1, "the mapping covers the whole file");
EXAMPLE_REQUIRE(proven_u8str_view_starts_with(contents, PROVEN_LIT("record 0:")),
"and shows the bytes that were written");
/* 쓰기는 기억에 쓰는 것이다. 상태 필드를 일부러 폭 고정으로 두었다. 사상은 파일을
* 길게 만들 수 없으므로, 제자리 편집은 이미 있는 자리에 들어가야 한다. */
proven_size_t at = proven_u8str_view_find(contents, 0, PROVEN_LIT("pending"));
EXAMPLE_REQUIRE(at != PROVEN_SIZE_MAX, "the status field must be found");
memcpy((proven_byte_t *)map.ptr + at, "done ", 7);
/* sync 가 지속성의 걸음이다. 그것 없이는 바뀐 내용이 페이지 캐시에 있고, 거기서는 이
* 프로그램이 끝나는 것은 견디지만 기계가 전원을 잃는 것은 견디지 못한다. */
EXAMPLE_REQUIRE(proven_is_ok(proven_mmap_sync(&map)), "syncing a shared mapping must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_mmap_destroy(&map)), "unmapping must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(f.value)), "closing the mapped file must succeed");
/* 편집이 이 프로세스의 기억에만이 아니라 파일에 들어가 있다. */
proven_result_u8str_t back = proven_fs_read_all_u8str(alloc, path);
EXAMPLE_REQUIRE(proven_is_ok(back.err), "reading the file back must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_find(proven_u8str_as_view(&back.value), 0, PROVEN_LIT("done")) != PROVEN_SIZE_MAX,
"the mapped write reached the file");
proven_u8str_destroy(alloc, &back.value);
/* --- 사적 사상: 쓰기가 아무 데도 가지 않는다 -------------------------- */
proven_result_file_t pf = proven_fs_open(alloc, path, PROVEN_FS_READ);
EXAMPLE_REQUIRE(proven_is_ok(pf.err), "reopening for a private mapping must succeed");
proven_result_mmap_t pm = proven_mmap_create(pf.value, 0, 0, PROVEN_MMAP_READ, PROVEN_MMAP_PRIVATE);
EXAMPLE_REQUIRE(proven_is_ok(pm.err), "a private read mapping must succeed");
proven_mmap_t priv = pm.value;
/* 사적 사상에 sync 를 청하면 받아 놓고 무시하는 대신 거부된다. 그 거부가 쓸모 있는
* 동작이다. 사상이 공유라고 믿고 있던 쪽이 자료가 사라진 뒤가 아니라 여기서 알게 된다. */
EXAMPLE_REQUIRE(proven_mmap_sync(&priv) == PROVEN_ERR_UNSUPPORTED,
"a private mapping has nothing to write back, and says so");
EXAMPLE_REQUIRE(proven_is_ok(proven_mmap_destroy(&priv)), "unmapping the private mapping must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_fs_close(pf.value)), "closing it must succeed");
/* --- 시각을, 두 문자열 타입 모두로 ------------------------------------ */
proven_datetime_t now = proven_time_now_datetime();
proven_result_u8str_t stamp = proven_u8str_create(alloc, 64);
EXAMPLE_REQUIRE(proven_is_ok(stamp.err), "creating the timestamp string must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_time_u8_fmt(alloc, &stamp.value, now, &proven_time_locale_en,
"{year}-{month:0>2}-{day:0>2}")),
"formatting the date as UTF-8 must succeed");
EXAMPLE_REQUIRE(proven_u8str_as_view(&stamp.value).size == 10, "a YYYY-MM-DD date is ten characters");
/* 같은 호출의 UTF-16 꼴이다. 그것이 옳은 유일한 자리를 위한 것 - 와이드 문자열을 받는
* 시스템 호출에 글을 곧장 건네는 자리다. */
proven_result_u16str_t wide = proven_u16str_create(alloc, 64);
EXAMPLE_REQUIRE(proven_is_ok(wide.err), "creating the wide timestamp string must succeed");
EXAMPLE_REQUIRE(proven_is_ok(proven_time_u16_fmt(alloc, &wide.value, now, &proven_time_locale_en,
"{year}-{month:0>2}-{day:0>2}")),
"formatting the same date as UTF-16 must succeed");
EXAMPLE_REQUIRE(proven_u16str_len(&wide.value) == 10, "ten code units, one per character here");
EXAMPLE_REQUIRE(proven_u16str_as_ptr(&wide.value)[4] == (proven_u16)'-',
"and the same layout as the UTF-8 form");
printf("mapped record updated; stamped %s\n", proven_u8str_as_cstr(&stamp.value));
proven_u16str_destroy(alloc, &wide.value);
proven_u8str_destroy(alloc, &stamp.value);
(void)proven_fs_remove(alloc, path);
return EXAMPLE_OK();
}반례 — 매핑이 파일을 늘려 줄 것이라고 기대하는 경우:
proven_result_mmap_t m = proven_mmap_create(f.value, 0, 1 << 20, ...); /* file is 22 bytes */
memcpy((char *)m.value.ptr + 4096, data, n); /* wrong */파일 길이를 먼저 정하고 — proven_fs_truncate()가 한 번에 해 준다 — 존재하는 만큼을 매핑한다.
실전 예제: 난수는 어디에서 오는가
위의 예제는 어떤 생성기를 고를지를 다룬다. 이것은 그 아래 계층, 곧 바이트의 출처와, 어느 출처를 받았는지 신경 쓰지 않는 코드를 쓰는 법이다.
proven_rng_t가 그 인터페이스다. 이것을 받는 함수는 운영체제 생성기로도, ChaCha20으로도, xoshiro로도, 시험용으로 여러분이 만든 가짜로도 고치지 않고 동작한다.proven_rng_is_valid()는 출처가 실제로 만들어졌는지 말해 준다. 만들어지지 않은 것에서 뽑으면 숫자를 지어내는 대신 0을 돌려주므로, 뽑을 때마다 믿는 대신 경계에서 한 번 확인한다.proven_rng_u64()는 64비트 낱말 하나를,proven_rng_fill()은 버퍼 전체를 한 번에 채운다.- 고정된 씨앗(seed)은 시험을 재현 가능하게 만든다.
proven_chacha_rng_seed()는 씨앗 바이트를 직접 받으므로proven_chacha_rng_next()는 알려진 수열을 걷는다. "일주일에 한 번 실패한다"를 다시 돌려볼 수 있는 실패로 바꾸는 것이 바로 이것이다. 운영에서는 씨앗이 진짜 엔트로피에서 와야 한다. 그것을 아는 사람은 이후의 모든 바이트를 알기 때문이다. proven_random_u64()는 엔트로피 출처에서 강한 낱말 하나를 바로 뽑는다. 시동 시의 해시 키나 식별자 같은 일회성에는 알맞고, 반복문 안에서는 그르다. 호출마다 운영체제까지 다녀오기 때문이다.proven_random_set_source()는 엔트로피 출처 자체를 설치한다. 호스티드(hosted) 프로그램에는 운영체제의 것이 이미 있으니 그대로 두면 되고, 베어메탈 프로그램에는 없으니 그 보드의 하드웨어 출처가 여기로 들어간다.
#include <string.h>
/*
* 다른 예제는 어느 생성기를 고를지를 보인다. 이것은 그 밑의 층에 대한 것이다. 난수가
* *어디서* 오는가, 그리고 그것을 신경 쓰지 않는 코드를 어떻게 쓰는가.
*
* proven_rng_t 는 난수 바이트의 원천을 포인터 한 쌍으로 나타낸 것이다 - 작은 함수
* 표와 그 함수들이 다룰 생성기 상태. proven_rng_t 를 받는 코드는 OS 생성기에서도,
* ChaCha20 에서도, xoshiro 에서도, 시험용으로 여러분이 지어낸 가짜에서도 한 줄도
* 고치지 않고 돈다.
*
* proven_random_set_source 는 *그보다* 아래 층이다. 생성기가 씨를 받는 날 엔트로피가
* 어디서 오는가. 호스트가 있는 프로그램에는 이미 하나가 있고 - 운영체제의 것 - 그대로
* 두어야 한다. 베어메탈 프로그램에는 없고, 그 기계의 하드웨어 원천을 다는 자리가
* 이 고리다.
*
* 고정된 씨앗 부분은 보기보다 중요하다. *알려진* 씨앗으로 씨를 뿌린 암호용 생성기는
* 알려진 수열을 내놓고, 그것이 난수가 끼는 시험을 "일주일에 한 번 실패" 가 아니라
* 재현 가능한 것으로 만든다.
*/
/* 전혀 무작위가 아닌 "엔트로피" 원천이다. 그냥 센다. 이런 것이 실제 프로그램에 들어갈
* 자리는 없고 - 장의 반례를 볼 것 - 다만 이 고리가 어떻게 도는지 보이기에, 그리고 돌
* 때마다 같은 바이트를 내야 하는 시험에 딱 맞는 모양이다. */
static bool counting_entropy(void *ctx, void *buf, proven_size_t len) {
proven_u8 *next = (proven_u8 *)ctx;
proven_u8 *out = (proven_u8 *)buf;
for (proven_size_t i = 0; i < len; ++i) {
out[i] = (*next)++;
}
return true;
}
/* 특성에 기대어 쓴 함수. 자기가 어느 생성기를 받았는지 끝내 알지 못한다. */
static proven_u64 roll_total(proven_rng_t rng, int rolls) {
proven_u64 sum = 0;
for (int i = 0; i < rolls; ++i) {
sum += proven_rng_below(rng, 6) + 1;
}
return sum;
}
int main(void) {
/* --- 1. 쓰기 전에 확인할 수 있는 원천 --------------------------------- */
proven_rng_t nothing = {0};
EXAMPLE_REQUIRE(!proven_rng_is_valid(nothing), "a zero-initialised source is not a generator");
/* 잘못된 원천에서 뽑아도 죽지 않고 수를 지어내지도 않는다. 0 을 돌려준다. 정의된,
* 심심한 답이다 - 그런데 0 의 연속은 난수가 아니므로, 뽑을 때마다 믿는 대신 받을 때
* 한 번 원천을 확인할 것. */
EXAMPLE_REQUIRE(proven_rng_u64(nothing) == 0, "an invalid source yields 0, not a fabricated value");
/* --- 2. *알려진* 씨앗에서 나온 암호용 생성기 --------------------------- */
/* proven_chacha_rng_seed 는 씨앗 바이트를 곧장 받으므로 수열이 재현된다. 시험에서는
* 그것이 원하는 바이고 운영에서는 결코 아니다. 씨앗을 알아낸 사람은 그 생성기가
* 내놓을 모든 바이트를 안다. */
proven_byte_t seed[PROVEN_CHACHA_SEED_SIZE];
memset(seed, 0xA5, sizeof seed);
proven_chacha_rng_t a, b;
proven_chacha_rng_seed(&a, seed);
proven_chacha_rng_seed(&b, seed);
/* next 는 한 번에 64비트 낱말 하나를 돌려준다. 같은 씨앗을 받은 생성기 둘은 같은
* 수열을 걷는다 - 시험이 기대는 성질이 그것이다. */
proven_u64 first = proven_chacha_rng_next(&a);
EXAMPLE_REQUIRE(first == proven_chacha_rng_next(&b), "the same seed replays the same sequence");
EXAMPLE_REQUIRE(proven_chacha_rng_next(&a) == proven_chacha_rng_next(&b), "and keeps replaying it");
/* --- 3. 특성을 통해 쓰기 ---------------------------------------------- */
proven_rng_t rng = proven_chacha_rng(&a);
EXAMPLE_REQUIRE(proven_rng_is_valid(rng), "a seeded generator makes a valid source");
proven_u64 word = proven_rng_u64(rng);
(void)word; /* 어떤 64비트 값이든 옳은 답이다. 단언할 것이 없다 */
/* fill 은 무더기로 하는 꼴이다. 나머지를 스스로 처리해야 하는 64비트 낱말 반복문
* 대신, 버퍼 하나를 한 번의 호출로 채운다. */
proven_byte_t nonce[12] = {0};
proven_rng_fill(rng, nonce, sizeof nonce);
bool all_zero = true;
for (proven_size_t i = 0; i < sizeof nonce; ++i) {
if (nonce[i] != 0) all_zero = false;
}
EXAMPLE_REQUIRE(!all_zero, "filling from a seeded generator must produce something");
/* 같은 함수를, 서로 다른 생성기 둘이 굴린다. 이 특성이 존재하는 이유는 이것 하나다. */
proven_chacha_rng_t c;
proven_chacha_rng_seed(&c, seed);
proven_u64 crypto_total = roll_total(proven_chacha_rng(&c), 50);
proven_xoshiro256ss_t fast;
proven_xoshiro256ss_seed(&fast, 7);
proven_u64 fast_total = roll_total(proven_xoshiro256ss_rng(&fast), 50);
EXAMPLE_REQUIRE(crypto_total >= 50 && crypto_total <= 300, "50 dice must total between 50 and 300");
EXAMPLE_REQUIRE(fast_total >= 50 && fast_total <= 300, "whichever generator produced them");
/* --- 4. 생성기를 쥐지 않고 강한 낱말 하나 ----------------------------- */
/* proven_random_u64 는 엔트로피 원천에서 곧장 뽑는다. 한 번뿐인 일에는 편하고 -
* 시작할 때 잡는 표의 해시 키, 요청 번호 - 반복문에는 틀린 도구다. 호출마다 운영체제로
* 다녀오는 값이 들기 때문이다. 무더기로 뽑을 때는 생성기에 한 번 씨를 뿌리고 거기서
* 뽑을 것. */
proven_u64 one_off = proven_random_u64();
proven_u64 another = proven_random_u64();
EXAMPLE_REQUIRE(one_off != another || one_off != 0,
"two draws from the OS source are essentially never the same value");
/* --- 5. 엔트로피 원천 달기 -------------------------------------------- */
/* 호스트가 있는 대상에서는 운영체제의 원천이 이미 달려 있고 그대로 두어야 한다. 이
* 고리는 베어메탈을 위한 것이다. 거기서는 그 보드의 엔트로피가 어느 하드웨어
* 레지스터에 사는지 라이브러리가 알 길이 없다. 여기서는 일부러 가짜 원천을 달았다.
* 오직 그 장치를 보이고 갈아 끼우기가 먹혔음을 증명하기 위해서다 - 진짜는 진짜
* 하드웨어 엔트로피여야 한다. */
proven_u8 counter = 0;
proven_random_set_source(counting_entropy, &counter);
proven_byte_t drawn[4] = {0};
EXAMPLE_REQUIRE(proven_random_bytes(drawn, sizeof drawn), "the installed source must answer");
EXAMPLE_REQUIRE(drawn[0] == 0 && drawn[1] == 1 && drawn[2] == 2 && drawn[3] == 3,
"and it is the source we installed that answered");
/* 플랫폼 기본값을 되돌려 놓는다. 시험용 원천을 그대로 둔 채로 두는 것이, 프로그램이
* 운영에서 예측 가능한 키를 만들어 내게 되는 방법이다. */
proven_random_set_source(NULL, NULL);
proven_byte_t real[8] = {0};
EXAMPLE_REQUIRE(proven_random_bytes(real, sizeof real), "the OS source is back and working");
printf("random: first word %llu, dice totals %llu and %llu\n",
(unsigned long long)first, (unsigned long long)crypto_total, (unsigned long long)fast_total);
return EXAMPLE_OK();
}반례 — 무작위처럼 보이기만 하는 것을 설치하는 경우:
static bool clock_entropy(void *ctx, void *buf, proven_size_t len) {
proven_time_t t = proven_time_now(); /* wrong: predictable */
memcpy(buf, &t, len < sizeof t ? len : sizeof t);
return true;
}
proven_random_set_source(clock_entropy, NULL);시계, 일련번호, 초기화되지 않은 버퍼, 의사 난수 생성기는 모두 얼핏 보아 넘어가지만 추측할 수 있는 것을 만든다. 보드에 진짜 엔트로피가 없다면 아무것도 설치하지 않는 것으로 그 사실을 말한다. 거절은 호출자가 대응할 수 있는 사실이고, 조용한 예측 가능성은 그렇지 않다.
반례 — 시험용 출처를 설치한 채 두는 경우:
proven_random_set_source(counting_entropy, &counter);
/* ... the rest of the program, now generating predictable keys ... */시험이 끝나는 즉시 proven_random_set_source(NULL, NULL)로 플랫폼 기본값을 되돌린다.