17 표준 호스트 모듈

(1) 이 절의 모듈은 처리기가 함께 내는 호스트 낱말이다. 언어의 일부가 아니다 — 호스트가 등록할 때만 있다(13.3). pnx 는 여섯 모두를 등록한다. 라이브러리를 끼워 쓰는 호스트는 pinax_host_register(state,
"io")
처럼 모듈 하나씩 고른다(라이브러리 libpinax_host). 등록하지 않은 모듈을 use 하면 거절된다(13.1).

(2) 호스트 낱말도 다른 낱말과 같은 규칙을 따른다: 서명은 적재 때 검사되고, or fail 이 있는 낱말은 지켜야 하며, owned 값은 정확히 한 번 넘긴다.

(3) 운영체제가 거절한 일은 실패(or fail)다. 없는 파일과 입력의 끝은 err.not_found, 바이트가 모양에 맞지 않으면 err.format, 자리가 모자라면 err.full, 그 밖의 거절(권한, 끊긴 장치)은 err.failed 다. or fail 이 없는 낱말(print, close)은 운영체제의 거절을 알리지 않는다.

17.1 io — 표준 입출력

(1)

낱말서명뜻
print println(s str)표준 출력에 쓴다. println 은 줄바꿈(LF)을 붙인다
eprint(s str)표준 오류에 쓴다
read_line(gives line str or fail)표준 입력의 한 줄. 줄 끝은 뗀다. 입력이 끝나면 err.not_found
read_line_into(buf buffer or fail)한 줄을 버퍼에 붙인다

바이트는 고치지 않고 쓴다. 모든 운영체제에서 같은 바이트가 나간다.

17-io.pnx

use io
use str
word demo ()
  "no new line, " io.print
  "then one" io.println
  loop
    io.read_line or_else (e) break end to line         rem fails at the end of the input
    cat ("read: ", line, " (", line str.len, " bytes)") io.println
  end
end
demo

출력

$ pnx 17-io.pnx
no new line, then one
read: first line (10 bytes)
read: second line (11 bytes)

17.2 file — 파일

(1) 핸들 타입 file 은 열린 파일이다. 여는 낱말이 owned file 을 주고, close 가 받는다(12.4).

낱말서명뜻
open_read open_write(path str gives owned file or fail)읽기로, 쓰기로(새로 만들거나 비우고) 연다
write(s str, f file or fail)바이트를 쓴다
read_line(f file gives line str or fail)한 줄. 끝이면 err.not_found
read(f file, buf buffer gives n int or fail)버퍼에 읽어 붙이고 읽은 바이트 수를 준다
read_line_into(f file, buf buffer or fail)한 줄을 버퍼에 붙인다
read_all(path str gives body str or fail)파일 전체를 구역에 읽는다
read_all_into(path str, buf buffer or fail)파일 전체를 버퍼에 붙인다
close(f owned file)닫는다

없는 파일은 err.not_found 다.

17-file.pnx

use io
use file
buffer buf 64
word demo (or fail)
  "data.txt" file.open_write or_fail to out
  "alpha\nbeta\n" out file.write or_else (e) out file.close e fail end
  out file.close
  "data.txt" file.open_read or_fail to in
  loop
    in file.read_line or_else (e) break end to line
    cat ("line: ", line) io.println
  end
  in file.close
  "data.txt" buf file.read_all_into or_fail
  cat ("all: ", buf contents) io.print
end
demo or_else (e) cat ("file error: ", e err_text) io.println end
"missing.txt" file.read_all or_else (e) cat ("missing.txt: ", e err_text) end io.println

출력

$ pnx 17-file.pnx
line: alpha
line: beta
all: alpha
beta
missing.txt: not_found

17.3 clock random env

(1)

낱말서명뜻
clock.now_ms(gives int)지금 시각, 1970-01-01 UTC 부터의 밀리초
clock.monotonic_ns(gives int)거꾸로 가지 않는 시계의 나노초
clock.sleep(ms int)멈춰 기다린다. 그동안 상태는 다른 일을 하지 않는다
random.seed(n int)수열을 정한다. 같은 씨앗은 같은 수열을 낸다
random.random(gives int)64 비트 난수(암호용이 아님)
random.random_below(n int gives int)0 부터 n−1 까지
env.arg_count(gives int)인자의 수. 0 번은 프로그램 파일
env.arg(i int gives s str or fail)인자 i
env.getenv(name str gives s str or fail)환경 변수. 없으면 err.not_found

17-env.pnx

use io
use env
cat ("arguments: ", env.arg_count) io.println
0 env.arg_count range do (i int) i env.arg or_else (e) "?" end io.println end each
"PINAX_SURELY_UNSET" env.getenv or_else (e) cat ("not set: ", e err_text) end io.println

출력

$ pnx 17-env.pnx alpha beta
arguments: 3
17-env.pnx
alpha
beta
not set: not_found

17-random-clock.pnx

use io
use random
use clock
word demo ()
  42 random.seed                                       rem the same seed, the same numbers
  6 random.random_below to a
  a 0 ge a 6 lt and if "a die in 0..5" io.println end
  clock.monotonic_ns to t0
  clock.monotonic_ns to t1
  t1 t0 ge if "the monotonic clock does not go back" io.println end
end
demo

출력

$ pnx 17-random-clock.pnx
a die in 0..5
the monotonic clock does not go back

17.4 채널과 호스트 루프

(1) 채널(channel)은 바이트 메시지를 정한 수까지 담는 줄이다. 호스트가 채널에 이름을 짓고 상태에 그 이름의 모듈로 등록한다(pinax_channel_register, 라이브러리 libpinax_host). 같은 채널을 등록한 상태들이 메시지를 주고받는다. 상태끼리 함께 쓰는 메모리는 없다: 메시지는 들어갈 때와 나올 때 복사된다.

낱말서명뜻
send(msg str or fail)메시지를 넣는다. 채널이 차 있으면 기다린다
receive(gives msg str or fail)가장 먼저 들어온 메시지를 꺼낸다. 비어 있으면 기다린다
close()더는 넣지 못하게 한다

(2) send 는 메시지가 채널이 정한 길이보다 길면 err.full, 채널이 닫힌 뒤에는 err.cancelled 로 실패한다. receive 는 닫히고 빈 채널에서 err.not_found 로 실패한다. 닫힌 채널에 남은 메시지는 그대로 꺼낸다.

(3) 기다리는 것은 부른 작업뿐이다(14.2). 같은 상태의 다른 작업은 계속 돈다. 기다릴 수 있는 것은 호스트 루프(pinax_loop_start)가 돌리는 실행뿐이다. 다른 실행에서 기다려야 하는 send 나 receive 는 결함 PINAX_ERR_STATE 로 실행을 끝낸다.

(4) 호스트 루프(host loop)는 상태들을 정한 수의 스레드에서 돌린다. pinax_loop_open 은 설정을 받는다: 스레드의 수, 한 번에 쥐는 실행의 수, 호스트 일의 결과가 차지할 수 있는 크기, 그리고 상태가 받듯 메모리 블록 하나. 루프의 객체는 그 안의 고정 칸이고(pinax_loop_size 바이트), 채널은 열릴 때 같은 블록에서 자기 칸을 잘라 간다 (pinax_channel_size 바이트). 실행이 도는 동안 할당은 없다. 블록이 없으면 루프가 그 크기의 조각 하나를 proven 의 힙에서 받는다. 한 상태는 한 번에 한 스레드에서만 돈다. 예산(19.5)이 다한 실행은 줄의 뒤로 가고, 기다리는 실행은 스레드를 내놓는다. 호스트가 pinax_loop_wait 로 기다리는 동안 남은 실행이 모두 채널을 기다리고 끝나지 않은 호스트 일이 없으면, 루프는 그 실행을 모두 PINAX_ERR_DEADLOCK 으로 끝낸다.

17-channel.c

#include "pinax.h"
#include "pinax_loop.h"
#include <stdio.h>
#include <string.h>

/* One state sends the numbers 1 to 100 through the channel `jobs`; two others take them and
   add them up. The loop runs the three on two threads. */
static const char producer[] =
    "use jobs\nuse str\n"
    "word produce (n int gives r int)\n"
    "  1 to i\n"
    "  loop\n"
    "    i n gt until\n"
    "    i str.to_str jobs.send or_else (e) -1 return end\n"
    "    i 1 add to i\n"
    "  end\n"
    "  jobs.close\n"
    "  n\n"
    "end\n";
static const char consumer[] =
    "use jobs\nuse str\n"
    "word consume (n int gives r int)\n"
    "  0 to total\n"
    "  loop\n"
    "    jobs.receive or_else (e) \"\" end to m      rem not_found: closed and empty\n"
    "    m str.is_empty until\n"
    "    m str.to_int or_else (e) 0 end total add to total\n"
    "  end\n"
    "  total\n"
    "end\n";

typedef struct worker {
    unsigned char block[1 << 20];
    pinax_state *state;
    pinax_word word;
    int64_t in, out;
    pinax_args args;
    pinax_err outcome;
} worker;
static worker w[3];

static void done(void *user, pinax_state *state, pinax_err outcome) {
    (void)state;
    ((worker *)user)->outcome = outcome;
}

static int start(pinax_loop *loop, pinax_channel *jobs, worker *x, const char *source, const char *word) {
    pinax_config config = {.struct_size = sizeof config, .budget = 1000};
    if (pinax_open((pinax_mem){x->block, sizeof x->block}, &config, &x->state) != PINAX_OK ||
        pinax_channel_register(x->state, jobs) != PINAX_OK ||
        pinax_load(x->state, "main", (pinax_str){(const unsigned char *)source, strlen(source)}, PINAX_LOAD_CODE) ||
        pinax_find(x->state, word, "(n int gives r int)", 0, &x->word) != PINAX_OK)
        return 1;
    x->args = (pinax_args){.i = &x->in, .ri = &x->out};
    return pinax_loop_start(loop, x->state, x->word, &x->args, done, x) != PINAX_OK;
}

int main(void) {
    // the loop and its channel live in one block the host sizes with the two size functions
    pinax_loop_config loop_config = {.struct_size = sizeof loop_config, .threads = 2, .states = 3};
    static unsigned char loop_block[1 << 16];
    if (pinax_loop_size(&loop_config) + pinax_channel_size(&loop_config, 4, 20) > sizeof loop_block)
        return 1;
    loop_config.block = (pinax_mem){loop_block, sizeof loop_block};
    pinax_loop *loop;
    pinax_channel *jobs;
    if (pinax_loop_open(&loop_config, &loop) != PINAX_OK || pinax_channel_open(loop, "jobs", 4, 20, &jobs) != PINAX_OK)
        return 1;
    w[0].in = 100;
    if (start(loop, jobs, &w[1], consumer, "consume") || start(loop, jobs, &w[2], consumer, "consume") ||
        start(loop, jobs, &w[0], producer, "produce"))
        return 1;
    pinax_loop_wait(loop);
    printf("sent %lld, received %lld\n", (long long)w[0].out, (long long)(w[1].out + w[2].out));

    /* the channel is closed and empty now: one more receiver ends at once */
    w[1].in = 0;
    if (pinax_loop_start(loop, w[1].state, w[1].word, &w[1].args, done, &w[1]) != PINAX_OK)
        return 1;
    pinax_loop_wait(loop);
    printf("after close: %lld\n", (long long)w[1].out);

    /* a receiver on a channel nobody sends to: every run left waits, so the loop ends it */
    pinax_channel *idle;
    pinax_close(w[2].state);
    if (pinax_channel_open(loop, "jobs", 1, 20, &idle) != PINAX_OK || start(loop, idle, &w[2], consumer, "consume"))
        return 1;
    pinax_loop_wait(loop);
    printf("nobody sends: %s\n", w[2].outcome == PINAX_ERR_DEADLOCK ? "deadlock" : "?");
    pinax_loop_close(loop);
    for (int k = 0; k < 3; ++k)
        pinax_close(w[k].state);
    return 0;
}

출력

$ ./17-channel
sent 100, received 5050
after close: 0
nobody sends: deadlock

17.5 sh — 명령

(1) sh 는 시스템 셸(/bin/sh -c, Windows 에서는 cmd.exe /c)로 명령을 돌린다. pnx 는 이를 등록한다. 라이브러리를 넣는 호스트는 명령을 돌려도 되는 스크립트에만 등록한다(pinax_host_register(state, "sh"). pinax_host_register_all 은 등록하지 않는다). WebAssembly 호스트에는 sh 가 없다.

낱말서명뜻
sh.run(cmd str gives status int or fail)cmd 를 돌린다. 그 출력은 프로그램이 쓴 것 뒤에, 프로그램의 출력이 가는 곳으로 간다. 상태는 명령의 것이다: 0 이 아닌 상태는 실패가 아니다. 셸을 띄우지 못할 때만 실패한다
sh.output(cmd str gives out str or fail)cmd 를 돌리고, 그것이 표준 출력에 쓴 것을 쓴 그대로(줄바꿈과 함께) 영역에 준다
sh.output_into(cmd str, buf buffer gives status int or fail)같은 것을 버퍼에 덧붙인다. 들어가지 않으면 err.full(명령은 끝까지 읽는다)
sh.open_read(cmd str gives owned file or fail)명령의 표준 출력에서 오는 관을 file(17.2)로 준다: file.read_line, file.read, file.close
sh.open_write(cmd str gives owned file or fail)명령의 표준 입력으로 가는 관: file.write, file.close
sh.status(gives int)마지막으로 끝난 명령의 상태: run, output, output_into, 또는 관의 file.close(명령이 끝나기를 기다린다)
sh.quote(s str gives str)s 를 시스템 셸의 낱말 하나로 만든다. 자료로 명령을 만들 때 쓴다: /bin/sh 에는 작은따옴표로, cmd.exe 에는 큰따옴표로 싼다(거기서 % 와 ! 는 뜻을 지킨다)
sh.cd(path str or fail)프로세스의 작업 디렉터리를 바꾼다. 못 하면 err.not_found
sh.cwd(gives path str or fail)작업 디렉터리
sh.setenv(name str, value str or fail)뒤따르는 명령(과 env.getenv)이 볼 환경 변수를 정한다
sh.exit(status int)붙들린 것을 내보낸 뒤 그 상태(0 부터 255, 밖의 값은 잘린다)로 프로그램을 끝낸다. 실행은 거기서 멈추고, pnx repl 에서는 세션이 끝난다

17-sh.pnx

use io
use sh
word main (or fail)
  "echo one; echo two" sh.output or_fail io.print
  cat ("status ", "exit 4" sh.run or_fail) io.println
  cat ("echo ", "it's here" sh.quote) sh.output or_fail io.print
end
main or_else (e) cat ("failed: ", e err_text) io.println end

출력

$ pnx 17-sh.pnx
one
two
status 4
it's here

(2) 상태는 명령의 종료 코드다. 신호로 끝난 명령은 128 에 신호의 번호를 더한 것이다. 찾지 못한 명령은 실패가 아니라 상태다(/bin/sh 에서 127). 0 바이트를 담은 명령은 err.argument 다. 읽기를 멈춘 명령으로 가는 관에 쓰면 실패한다 (err.failed). sh.status 와 sh.exit 는 상태를 돌리는 스레드의 것이다.