32 표준 라이브러리의 지도
먼저 알아야 할 것
use <이름> . 으로 들여온다돌아보기
21장에서 use 가 찾는 것은 파일 이름인가, 모듈 이름인가? 표준 라이브러리에서 이 차이가 왜 걸린다고 했는가?
답. 파일 안의 module 선언이다. 표준 라이브러리에는 파일 이름과 모듈 이름이 다른 곳이 몇 있어서(lib/alloc.low 는 allocs, lib/str.low 는 strings), 파일 이름으로 들여오려다 헤맨다. 이 장의 첫 표가 그 목록이다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 언어에 넣으면 편할 기능을 왜 라이브러리로 두는가?
32.1 세 층 — 언어, 잎, 라이브러리#
| 층 | 누가 만드나 | 자격 |
|---|---|---|
| 언어 | 명세 | 라이브러리로 표현할 수 없거나, 표현하면 비용이 숨는다 |
| 잎(leaf) | 처리기 | 운영체제·하드웨어에 닿는다. Lowent 로 쓸 수 없다 |
| 라이브러리 | 누구나 | 나머지 전부. Lowent 로 쓴다 |
표 32.1 — 세 층
어떤 조각이 잎인지를 가르는 물음은 하나다 — “이것을 Lowent 로 쓸 수 있는가.” 쓸 수 있으면 라이브러리다. 파일을 열어 정수 핸들을 받는 것은 커널에 닿으므로 잎이다. 그 핸들을 잊을 수 없는 소유 값으로 감싸는 것은 Lowent 로 쓸 수 있으므로 라이브러리다. 파일을 통째로 읽어 주는 편의 op 도 라이브러리다. 편의는 잎의 자격이 되지 않는다.
표준 라이브러리는 특권이 없다. 저자가 쓴 코드와 같은 규칙 — 계약, 효과, 권한 — 을 받는다. strings.starts_with 의 소스는 이렇다.
export fn starts_with input s str . input prefix str . output bool . do
guard le (len prefix) (len s) . else return false .
return eq_str (subslice s 0 (len prefix)) prefix .
end이 책에서 배운 것만으로 읽힌다. 라이브러리만 쓸 수 있는 숨은 통로가 있으면 그 통로를 지나는 순간 프로그램의 보장이 끊기기 때문이다.
문. 언어에 넣으면 편할 기능을 왜 라이브러리로 두는가?
답. 언어에 낱말이나 내장 연산을 더하면 모든 프로그램이 그것을 외워야 하고, 처리기가 그것을 특별하게 다뤄야 한다. 라이브러리로 두면 같은 규칙으로 검사되고, 쓰지 않는 프로그램에는 없는 것이다. 그래서 언어는 표현할 수 없는 것만 받아들인다. 표준 라이브러리의 할당기·그릇·파일 핸들이 모두 이 기준을 통과해 라이브러리에 있다. 편하다는 것은 언어에 들어갈 이유가 되지 않는다.
32.2 무엇이 들어오는가#
라이브러리에 들어오는 것은 있어서가 아니라 증거로 들어온다. 명세의 헌장은 여섯 잣대를 둔다.
| 잣대 | 뜻 |
|---|---|
| 이 언어로 쓸 수 있으면 이 언어로 쓴다 | 잎은 바깥 권위를 쓰거나 이 언어가 표현하지 못할 때뿐이다 |
| 시그니처가 비용과 권한의 장부다 | 효과·권한·저장·할당·소유·완결이 경계에서 보여야 한다 |
| 셈과 권위를 가른다 | 꼴 짓기와 내보내기, 해석과 파일 열기, 난수의 다음 걸음과 엔트로피 얻기는 다른 것이다 |
| 자리는 여러 축으로 가른다 | 권위·저장·실행 등급·프로파일을 한 사다리로 눌러 담지 않는다 |
| 안에 있는 것은 작게 둔다 | 이식되는 바탕과 얇은 호스트 어댑터까지 |
| 모듈마다 증거가 있다 | 단위 시험·거절 시험·대조·실패 시험을 갖춘다 |
표 32.2 — 라이브러리 헌장
모듈은 저마다 성숙도를 적는다 — experimental(적혀 있을 뿐) · incubating(결정 문서가 있다) · standard(설명이 있고 회귀 시험이 이름으로 부른다) · deprecated(무엇으로 갈아탈지 적혀 있다). 사다리의 값은 위 칸이 아니라 아래 칸에 있다. experimental 이라고 적는 것은 “아직 믿지 말라” 를 소스에 적는 일이다. 칸은 품질의 등수가 아니라 약속의 크기다.
명세는 라이브러리의 목록을 적지 않는다. 무엇이 있는지는 소스에서 매번 다시 세어 내는 쪽이 권위이고, 문서가 그것을 베끼면 베낀 순간부터 갈라진다. 실제로 있지도 않은 API 셋이 표에 있다는 이유로 사실로 읽힌 일이 있었다. 이 부의 표도 이 판의 소스를 보고 만든 것이며, 모듈별 자세한 쪽은 부록 E 에 있다.
32.3 층으로 본 지도#
| 층 | 성격 | 대표 모듈 |
|---|---|---|
| L0 순수 계산 | effects none · 호출자의 버퍼 | strings strbuf fmt utf8 utf16 unicode codec regex hash math random sortlib sortgen searchlib hashmap strmap spsc term · 암호 모듈들 |
| L1 저장 | 할당 권한이나 빌린 바이트가 필요하다 | allocs pool shard budget segarena pagecache growvec vecgen mapgen nodelist segview wire flags lifemode |
| L2 호스트 | 권한이 필요하다(cap io·file_system·tty·net·clock) | io outbuf files tty net clock |
표 32.3 — 표준 라이브러리의 층
이 부의 나머지 장은 쓰임새로 묶는다. 글자와 부호화(33장), 그릇과 정렬(34장), 저장소와 핸들 (35장), 입출력과 네트워크와 암호(36장), 터미널(37장)이다.
32.4 파일 이름과 모듈 이름#
use 가 찾는 것은 파일 안의 module 선언이다. 몇 곳은 파일 이름과 다르다.
| 소스 파일 | 모듈 이름 |
|---|---|
lib/alloc.low | allocs |
lib/str.low | strings |
lib/vec.low | vecs |
lib/sort.low | sortlib |
lib/search.low | searchlib |
lib/out.low | outbuf |
lib/file.low | files |
표 32.4 — 파일 이름과 다른 모듈 이름
examples/ch32/names.low
module names .
rem run: is_get [71,69,84,32,47]
rem run: is_get [80,79,83,84]
use strings .
fn is_get input line slice u8 . output bool .
do
return strings.starts_with line "GET " .
end
실행 결과
$ lowentc --run is_get names.low [71,69,84,32,47]
is_get([71,69,84,32,47]) = 1
arg0 (written) = [71,69,84,32,47]
$ lowentc --run is_get names.low [80,79,83,84]
is_get([80,79,83,84]) = 0
arg0 (written) = [80,79,83,84]
use strings . 은 from 없이 표준 모듈 자리에서 풀린다. 들여온 이름은 언제나 strings.starts_with 처럼 모듈로 한정해 부른다. 글롭 들여오기는 없다. 파일 이름으로 들여오면 그런 모듈이 없다.
examples/ch32/wrong_name.low
module wrong_name .
rem expect: W-USE-EXTERNAL
use str .
fn is_get input line slice u8 . output bool .
do
return true .
end
실행 결과
$ lowentc --check wrong_name.low
wrong_name.low:4:0 W-USE-EXTERNAL: this module is not in the compilation unit — there is no module search path, so nothing here can confirm it exists. Pass the file that declares it and it WILL be checked (several .low files link into one unit)
진단이 오류가 아니라 경고인 것은 검색 경로가 없어서 이 번역 단위만 보고는 그 이름이 어딘가에 있는지 없는지 알 수 없기 때문이다 (21장). 그 이름을 실제로 쓰는 순간 거절된다.
32.5 모든 모듈이 따르는 규약#
| 규약 | 뜻 |
|---|---|
| 버퍼는 호출자의 것 | L0 모듈은 할당하지 않는다. 출력 버퍼와 작업 공간을 매개변수로 받는다. 그래서 재진입이 공짜고, VM·네이티브 대조가 그대로 돈다 |
| 실패는 값 | option(못 했다)이나 result(무엇이 실패했는지). 트랩도, 조용한 잘림도, 치환 문자도 없다 |
| 전량 아니면 무 | 자리가 모자라면 한 바이트도 쓰지 않는다. 절반 쓴 버퍼는 조용히 틀린 출력이다 |
| 소유는 갚아야 한다 | 완결이 필요한 것(outbuf 의 남은 바이트, files 의 핸들)은 소유 값이라 잊으면 번역이 거절한다 |
표 32.5 — 표준 라이브러리의 공통 규약
흔한 오해. 표준 라이브러리니까 모든 모듈을 똑같이 믿어도 된다
tlssrv 는 핸드셰이크를 모두 하지만 전송은 아직이고, der 은 공개키만 꺼내는 최소 파서이지 인증서 체계(PKI)가 아니다. 들여오기 전에 그 모듈이 스스로 적은 경계를 읽는 것이 이 라이브러리를 쓰는 법이다.32.6 잎 op 의 목록 — 모듈 아래에 무엇이 있나#
라이브러리 모듈은 결국 처리기가 주는 잎 내장 op 을 부른다. 보통은 모듈을 쓰고 잎을 직접 부르지 않는다 — 모듈이 소유·세 자리 답·전량 아니면 무 같은 규약을 입혀 주기 때문이다. 그래도 어느 모듈이 무엇을 딛는지 알면 모듈 문서를 읽거나 새 모듈을 지을 때 길을 잃지 않는다.
| 잎 op | 하는 일 | 감싸는 모듈 · 권한 |
|---|---|---|
file_open · file_read · file_write · file_seek · file_close | 파일을 열고 읽고 쓰고 자리를 옮기고 닫는다 | files · cap file_system |
dir_make · dir_read · dir_close | 디렉터리를 만들고 항목을 읽고 닫는다 | files · cap file_system |
path_remove · path_rename · link_type | 경로를 지우고 이름을 바꾸고, 심링크를 따라가지 않고 그 자신이 무엇인지 묻는다 | files · cap file_system |
net_listen · net_accept · net_connect · net_port | 듣는 소켓 · 연결 받기 · 주소와 포트로 연결 걸기 · 포트 묻기 | net · cap net |
net_resolve | 이름을 IPv4 주소로 — DNS. 연결과 가른 잎이라, 주소를 이미 가진 프로그램은 이름 해석에 닿지 않는다 | net · cap net |
net_send · net_recv · net_close · net_pair | 보내기 · 받기 · 닫기 · 맞물린 한 쌍 | net · cap net |
reactor_new · r_read · r_write | reactor 를 만든다 · reactor 로 읽는다 · reactor 로 쓴다 | 모듈 없음 — cap io |
env_get | 환경 변수를 읽는다 | 모듈 없음 — cap env |
rng_next | 상태에서 다음 난수(splitmix64) | random — 권한 없음(순수) |
hash_bytes · crc32 | FNV-1a 64 해시 · CRC-32 검사값 | hash — 권한 없음 |
sha256 · sha384 · sha512 | SHA-256 · SHA-384 · SHA-512 — SHA-384 은 SHA-512 의 다른 시작값이고 앞 48 바이트다 | hash·hmac·tls13 · ed25519 — 권한 없음 |
aes_ctr · ghash | AES-128-CTR 흐름 · GHASH 누산 — 키·카운터·누산기가 모두 16 바이트다. 카운터와 누산기는 제자리에서 고쳐지므로 이어 불러도 이어진다 | gcm — 권한 없음 |
chacha_poly | ChaCha20-Poly1305 한 덩이 — 흐름과 누산을 한 바퀴에. 뜻은 chacha20 을 돌리고 그 암호문을 poly1305 에 먹인 것과 같다. 두 셈이 쓰는 자리가 달라(벡터 · 정수 곱셈기) 섞으면 겹쳐 돈다(실측 1,080 → 1,199 MB/s) | aead — 권한 없음 |
aes_gcm | AES-128-GCM 한 덩이 — 흐름과 누산을 한 바퀴에 돈다. 뜻은 aes_ctr 를 돌리고 그 암호문을 ghash 에 먹인 것과 같다; 다른 것은 빠르기뿐이다(실측 2848 → 3661 MB/s). 봉인만 쓴다 — 복호는 태그를 먼저 증언하고 맞을 때만 풀어야 하므로 두 바퀴로 남는다 | gcm — 권한 없음 |
chacha20 · poly1305 | ChaCha20 흐름 · Poly1305 누산 — 키 32 · 카운터 블록 16(앞 4 가 블록 번호, 리틀엔디언)이고 카운터는 제자리에서 올라간다. poly1305 은 상태(h 다섯 · r 다섯)를 누산하고, 열여섯의 배수가 아니면 꼬리를 0 으로 채운다. 폭은 빌드가 정한다(lowentc --hw sse2 · avx2) | chacha · poly · aead — 권한 없음 |
aes_round · aes_round_last | AES 한 라운드 — 상태 16 바이트를 제자리에서. 기계에 그 명령이 있으면 처리기가 그것으로 내리고(lowentc --hw aes), 없으면 같은 답을 내는 표 셈으로 내린다 | aes — 권한 없음 |
str_from_cstr | C 문자열(NUL 로 끝남)을 훑어 str 로 만든다 | C 경계 — VM 은 E-VM-CSTR 로 못 한다고 말한다 |
표 32.6 — 잎 내장 op 과 그것을 감싸는 모듈
계산 잎은 call_builtin 뒤에서만 선다. call_builtin sha256 msg out 처럼 이름이 그 자리에만 살고 전역 어휘가 되지 않는다 — 한 번 쓰는 낱말로 모두가 외울 것을 늘리지 않으려는 규율이고, pipe 안의 단계 이름과 cast u8 x 의 타입 자리가 이미 같은 방식이다. 운영체제에 닿는 잎(file_open · net_send …)은 그대로 부른다 — 첫 피연산자가 권한이라 이미 특별함이 보인다.
잎의 자격은 “Lowent 로 쓸 수 있는가” 하나다. rng_next 와 해시 셋이 순수 계산인데도 잎인 것은, 알고리즘을 처리기가 고정해 VM 과 네이티브가 비트까지 같은 답을 내게 하려는 것이다. 권한이 필요한 잎은 모두 그 권한을 첫 피연산자로 받는다(16장).
32.7 흔한 실수#
반례. option 으로 돌아오는 쓰기를 묻지 않고 이어 쓴다
examples/ch32/mistake_chainnone.low
module mistake_chainnone .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem trap: greet [0,0,0,0,0,0]
use fmt .
proc greet input buf mut slice u8 . output u64 . effects none .
do
rem ✘ 앞의 쓰기가 성공했다고 믿고 `some_value` 로 곧장 이어 쓴다
let p u64 be some_value (fmt.put_str buf 0 "hello") .
let q u64 be some_value (fmt.put_byte buf p 32) .
return some_value (fmt.put_u64 buf q 42) .
end
실행 결과
$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0]
== ir diagnostics (1) ==
0:0 E-VM-NONE: some_value of none (panic)
fmt 의 쓰기 op 은 다음 쓸 자리를 option u64 로 준다. 열두 칸 버퍼에서는 셋 다 성공해 8 을 내지만, 여섯 칸에서는 put_u64 가 자리가 모자라 none 을 주고 some_value 가 멈춘다. L0 모듈의 실패는 트랩이 아니라 값이므로, 받는 쪽이 걸음마다 묻는다.
examples/ch32/chainnone_fixed.low
module chainnone_fixed .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem run: greet [0,0,0,0,0,0]
use fmt .
proc greet input buf mut slice u8 . output u64 . effects none .
do
rem 걸음마다 묻는다 --- 모자라면 0 을 돌려 "쓰지 못했다" 를 알린다
let p option u64 be fmt.put_str buf 0 "hello" .
guard is_some p . else return 0 .
let q option u64 be fmt.put_byte buf (some_value p) 32 .
guard is_some q . else return 0 .
let r option u64 be fmt.put_u64 buf (some_value q) 42 .
guard is_some r . else return 0 .
return some_value r .
end
실행 결과
$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0]
greet([104,101,108,108,111,32]) = 0
arg0 (written) = [104,101,108,108,111,32]
고친 판은 여섯 칸 버퍼에서 0 을 돌려준다. 버퍼에 hello 가 남아 있는 것에 주목한다. “전량 아니면 무” 는 op 한 번의 약속이지 여러 번 부른 묶음 전체의 약속이 아니다. 묶음 전체를 되돌려야 하면 성공한 자리(p·q)까지를 유효한 길이로 삼는다.
흔한 오해. 버퍼가 모자라면 들어가는 만큼은 쓴다
examples/ch32/all_or_nothing.low
module all_or_nothing .
rem run: try_put [0,0,0,0]
use fmt .
proc try_put input buf mut slice u8 . output u64 . effects none .
do
rem 여섯 바이트를 네 칸에 --- 앞 네 바이트만 쓰는 대신 하나도 쓰지 않는다
let p option u64 be fmt.put_str buf 0 "Lowent" .
guard is_some p . else return 99 .
return some_value p .
end
실행 결과
$ lowentc --run try_put all_or_nothing.low [0,0,0,0]
try_put([0,0,0,0]) = 99
arg0 (written) = [0,0,0,0]
C 의 snprintf 는 들어가는 만큼 쓰고 잘린다. 표준 라이브러리의 L0 모듈은 전량 아니면 무다. 여섯 바이트를 네 칸에 쓰려 하면 한 바이트도 쓰지 않고 none 을 주며, 결과의 버퍼는 [0,0,0,0] 그대로다. 절반 쓴 버퍼는 조용히 틀린 출력이 되고, 부르는 쪽은 “none 이면 버퍼는 그대로” 를 믿고 다른 길로 이어 쓸 수 있다.
32.8 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
use strings . | 표준 모듈을 들여온다 — 이름은 파일 안의 module 선언 | lib/str.low 의 모듈은 strings |
strings.starts_with line "GET " | 들여온 이름은 모듈로 한정한다 | 글롭 들여오기가 없다 |
fmt.put_str buf pos s → option u64 | 호출자의 버퍼에 쓰고 다음 자리를 준다 | L0 은 할당하지 않는다 — 재진입이 공짜 |
none(자리가 모자람) | 한 바이트도 쓰지 않았다 | 전량 아니면 무 — op 한 번의 약속 |
result t e | 무엇이 실패했는지 말하는 실패 | 트랩도 조용한 잘림도 없다 |
owned 핸들 · 남은 바이트 | 갚아야 하는 소유 | 잊으면 번역이 거절한다 |
experimental · incubating · standard · deprecated | 모듈이 스스로 적는 성숙도 | 칸은 등수가 아니라 약속의 크기 |
표 32.7 — 표준 라이브러리를 쓰는 모양 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리