14 C 프로그램에 끼워 쓰기
먼저 알아야 할 것: 1장부터 13장까지의 언어. C 를 안다고 본다.
이 장이 답하는 물음
- C 프로그램은 Pinax 를 어떻게 열고, 스크립트를 어떻게 부르는가?
- 스크립트가 할 수 있는 바깥일은 누가 정하는가?
- 믿지 못하는 스크립트는 어떻게 돌리는가?
14.1 열고, 등록하고, 적재하고, 부른다
호스트의 일은 넷이다.
- 메모리 한 덩이를 주고
pinax_open 으로 상태를 연다. 상태의 모든 것이 그 덩이 안에 있다. - 스크립트가 쓸 호스트 낱말을
pinax_register 로 등록한다. 서명은 Pinax 의 서명 그대로 적는다. - 스크립트를
pinax_load 로 적재한다. 거절하면 까닭이 pinax_error_text 에 있다. pinax_find 로 낱말을 서명과 함께 찾고 pinax_run 으로 부른다.
14-host.c
/* A host that lets users write pricing rules in Pinax. The host gives one word (the stock
level), loads the user's rules, and asks them for a price; a broken rule never takes the
host down. */
#include "pinax.h"
#include <stdio.h>
#include <string.h>
static int64_t stock[3] = {120, 4, 0};
static pinax_err stock_of(pinax_call *call, void *user, const pinax_args *args) {
(void)call; (void)user;
if (args->i[0] < 0 || args->i[0] >= 3)
return PINAX_ERR_NOT_FOUND; /* (item int gives n int or fail) */
args->ri[0] = stock[args->i[0]];
return PINAX_OK;
}
static void report(pinax_state *st, const char *what, pinax_err err) {
pinax_str why = pinax_error_text(st);
printf("%s: error %#x, %.*s\n", what, (unsigned)err, (int)why.len, (const char *)why.ptr);
}
int main(void) {
static unsigned char block[256 * 1024]; /* the state takes about 150 KB, a load its working memory */
pinax_config config = {.struct_size = sizeof config, .region_bytes = 4096, .budget = 10000};
pinax_state *st;
pinax_err err = pinax_open((pinax_mem){block, sizeof block}, &config, &st);
if (err != PINAX_OK) {
printf("block too small: %zu bytes needed\n", pinax_needed(st));
return 1;
}
static const pinax_word_def words[] = {{"stock_of", "(item int gives n int or fail)", stock_of, NULL, 0}};
pinax_register(st, "shop", words, 1);
/* the user's rules: scarce items cost more */
static const char rules[] =
"use shop\n"
"export word price (item int, base int gives p int)\n"
" item shop.stock_of or_else (e) 0 end to left\n"
" left 0 eq if 0 return end\n"
" left 10 lt if base 3 mul 2 div else base end\n"
"end\n";
err = pinax_load(st, "rules", (pinax_str){(const unsigned char *)rules, strlen(rules)}, PINAX_LOAD_CODE);
if (err != PINAX_OK) {
report(st, "rules refused", err);
return 1;
}
pinax_word price;
if (pinax_find(st, "price", "(item int, base int gives p int)", 0, &price) != PINAX_OK) {
report(st, "no price word", PINAX_ERR_NOT_FOUND);
return 1;
}
for (int64_t item = 0; item < 4; ++item) {
int64_t in[2] = {item, 1000}, out = 0;
pinax_args args = {.i = in, .ri = &out};
err = pinax_run(st, price, &args);
while (err == PINAX_SUSPENDED)
err = pinax_resume(st, &args);
if (err != PINAX_OK)
report(st, "price failed", err);
else
printf("item %lld: %lld\n", (long long)item, (long long)out);
}
/* a second set of rules with a mistake is refused; the first stays loaded */
static const char broken[] = "word discount (p int gives int) p \"10\" sub end\n";
err = pinax_load(st, "broken", (pinax_str){(const unsigned char *)broken, strlen(broken)}, PINAX_LOAD_CODE);
if (err != PINAX_OK)
report(st, "broken rules refused", err);
pinax_close(st);
return 0;
}
출력
$ ./14-host
item 0: 1000
item 1: 1500
item 2: 0
item 3: 0
broken rules refused: error 0x203, this word takes two int values, or two of the same declared type
문답
물음. 덩이는 얼마나 커야 하는가?
답. 여는 데 기본값으로 약 150 KB 가 든다. 대부분이 주 실행의 구역(64 KB)과 정수 뱅크(32 KB)이므로 config.region_bytes 와 config.int_slots 로 줄일 수 있다. 적재하는 동안은 덩이의 남은 부분이 컴파일러의 작업 메모리다: 작은 소스에 약 20 KB, 낱말 하나에 약 1 KB 씩, 상수 패턴이 하나라도 있으면 64 KB 가 더 든다. 적재가 끝나면 프로그램만 남고 작업 메모리는 다시 빈다. 예제의 256 KB 는 짧은 스크립트에 넉넉하고, 긴 스크립트는 PINAX_ERR_NO_ROOM 을 받으면 pinax_needed 가 말하는 크기 이상으로 다시 연다. 그 크기는 하한이라 한 번 더 모자랄 수 있다.
인자와 결과는 타입별 배열로 오간다: 정수는 args->i[] 와 args->ri[], 문자열은 args->s[] 와 args->rs[]. 호스트 낱말이 PINAX_OK 가 아닌 값을 돌려주면, 서명에 or fail 이 있는 낱말은 실패하고(스크립트가 or_else 로 받는다), 없는 낱말은 결함이 된다.
문답
물음. 스크립트가 파일을 지우거나 네트워크에 닿을 수 있는가?
답. 호스트가 그런 낱말을 등록하지 않았다면 없다. 표준 모듈은 바깥에 닿지 않고, 바깥일은 모두 호스트 낱말이다. extern 으로 C 함수를 부르는 것도 호스트의 해결 함수가 허락한 함수뿐이다.
14.2 믿지 못하는 스크립트
config.budget 은 한 번에 도는 명령의 수다. 예산이 다하면 실행이 멈춰 PINAX_SUSPENDED 로 돌아오고, 호스트가 pinax_resume 으로 잇거나 그만둔다. 멈춘 실행이 있는 동안 상태는 다른 실행을 받지 않는다. 그만둘 때는 pinax_abort 를 부른다. 결함으로 끝난 것처럼 그 실행이 연 핸들을 닫고 실행을 버린다. 스크립트가 그때까지 전역에 넣은 값은 남는다.
14-guard.c
/* Running code you do not trust. A budget stops a script that never ends. While a run is
suspended the state takes no other run; pinax_abort gives the run up, and the state goes on
with what the script had stored. */
#include "pinax.h"
#include <stdio.h>
#include <string.h>
static unsigned char block[256 * 1024]; /* the state takes about 150 KB, a load its working memory */
int main(void) {
pinax_config config = {.struct_size = sizeof config, .budget = 50000};
pinax_state *st;
if (pinax_open((pinax_mem){block, sizeof block}, &config, &st) != PINAX_OK)
return 1;
static const char source[] =
"global calls int\n"
"word spin (gives int) calls 1 add set calls 0 to n loop n 1 add to n end end\n"
"word quick (gives int) calls 1 add set calls 6 7 mul end\n"
"word count_calls (gives int) calls end\n";
if (pinax_load(st, "main", (pinax_str){(const unsigned char *)source, strlen(source)}, PINAX_LOAD_CODE) != PINAX_OK)
return 1;
pinax_word w;
int64_t r = 0;
pinax_args args = {.ri = &r};
pinax_find(st, "spin", "(gives int)", 0, &w);
pinax_err err = pinax_run(st, w, &args);
int slices = 0;
while (err == PINAX_SUSPENDED && slices < 5) { /* the host decides how long to wait */
++slices;
err = pinax_resume(st, &args);
}
printf("spin after %d slices: %s\n", slices, err == PINAX_SUSPENDED ? "still running" : "ended");
pinax_find(st, "quick", "(gives int)", 0, &w);
printf("another run while it is suspended: %s\n", pinax_run(st, w, &args) == PINAX_ERR_STATE ? "PINAX_ERR_STATE" : "?");
pinax_abort(st); /* give the spinning run up */
err = pinax_run(st, w, &args);
printf("after pinax_abort, quick: %lld\n", (long long)r);
pinax_find(st, "count_calls", "(gives int)", 0, &w);
pinax_run(st, w, &args);
printf("calls stored so far: %lld\n", (long long)r);
pinax_close(st);
return 0;
}
출력
$ ./14-guard
spin after 5 slices: still running
another run while it is suspended: PINAX_ERR_STATE
after pinax_abort, quick: 42
calls stored so far: 2
처리기가 지키는 것은 이렇다. 어떤 소스든 — 틀렸든, 일부러 나쁘게 쓴 것이든 — 그리고 pinax_load_bytecode 의 검사를 통과한 바이트코드든, 스크립트는 상태의 덩이와 호스트가 준 메모리 밖을 읽거나 쓰지 못한다. 예산이 0 이 아니면 실행 한 번은 언제나 정해진 명령 수 안에 돌아온다. 틀린 스크립트는 적재 때 거절되거나 결함으로 멈출 뿐, 호스트를 무너뜨리지 않는다. 다만 상(image), 공유 파일, pinax_bind 로 묶은 메모리의 바이트는 호스트가 믿고 주는 자료다. 처리기는 그 모양만 검사한다.
흔한 오해: 멈춘 실행은 새 pinax_run 이 덮어쓴다
덮어쓰지 않는다. 새 실행은
PINAX_ERR_STATE 로 거절된다. 멈춘 실행을 끝까지 잇거나
pinax_abort 로 버린다. 전역까지 실행 앞으로 되돌려야 한다면 실행 앞에 상을 저장해 두고 그 상에서 다시 연다(표준 19.9).
14.3 여러 스레드에서
상태 하나는 한 번에 한 스레드에서만 돈다. 코어를 여럿 쓰려면 상태를 여럿 연다. 호스트 라이브러리의 호스트 루프 (pinax_loop.h)가 상태들을 정한 수의 스레드에서 돌리고, 채널이 상태 사이로 메시지를 나른다. 루프는 상태처럼 설정으로 연다: 스레드의 수, 한 번에 쥐는 상태의 수, 그리고 루프와 채널이 사는 메모리 블록 하나. 크기는 함수 둘이 알려 준다. 블록이 없으면 그 크기의 조각 하나를 힙에서 받는다. 호스트는 pinax_loop_start 로 상태와 낱말을 루프에 넘기고, 끝나면 넘길 때 준 함수로 듣는다. 채널은 호스트가 이름을 지어 모듈로 등록한다. 스크립트는 use orders 뒤에 orders.send, orders.receive, orders.close 를 쓴다. 차 있는 채널에 넣거나 빈 채널에서 꺼내는 작업은 기다리고, 그동안 스레드는 다른 상태를 돌린다.
14-threads.c
/* Two states on two threads. The shop packs each order into bytes and sends it through the
channel `orders`; the clerk unpacks and prints them. Nothing is shared but the messages. */
#include "pinax.h"
#include "pinax_host.h"
#include "pinax_loop.h"
#include <stdio.h>
#include <string.h>
static const char shop[] =
"use orders\nuse bytes\n"
"record order\n item str(8)\n qty int\nend\n"
"array rows order 1\n"
"buffer wire 32\n"
"word send_order (name str, q int)\n"
" 0 rows idx to r\n"
" name r to order.item q r to order.qty\n"
" wire clear\n"
" r wire bytes.pack or_else (e) end\n"
" wire contents orders.send or_else (e) end\n"
"end\n"
"word open_shop (n int gives r int)\n"
" \"apple\" 3 send_order \"pear\" 1 send_order \"plum\" 12 send_order\n"
" orders.close\n"
" 3\n"
"end\n";
static const char clerk[] =
"use orders\nuse bytes\nuse io\nuse str\n"
"record order\n item str(8)\n qty int\nend\n"
"array rows order 1\n"
"word serve (n int gives r int)\n"
" 0 to served\n"
" 0 rows idx to r\n"
" loop\n"
" orders.receive or_else (e) \"\" end to m\n"
" m str.is_empty until\n"
" m r bytes.unpack or_else (e) \"\" end drop\n"
" cat (r order.item, \" x\", r order.qty) io.println\n"
" served 1 add to served\n"
" end\n"
" served\n"
"end\n";
typedef struct side {
unsigned char block[1 << 20];
pinax_state *state;
pinax_word word;
int64_t in, out;
pinax_args args;
} side;
static side shop_side, clerk_side;
static void done(void *user, pinax_state *state, pinax_err outcome) {
(void)user; (void)state;
if (outcome != PINAX_OK)
printf("a run ended with %#x\n", (unsigned)outcome);
}
static int prepare(side *s, pinax_channel *orders, const char *source, const char *word) {
pinax_config config = {.struct_size = sizeof config, .budget = 10000};
if (pinax_open((pinax_mem){s->block, sizeof s->block}, &config, &s->state) != PINAX_OK ||
pinax_channel_register(s->state, orders) != PINAX_OK || pinax_host_register(s->state, "io") != PINAX_OK ||
pinax_load(s->state, "main", (pinax_str){(const unsigned char *)source, strlen(source)}, PINAX_LOAD_CODE) ||
pinax_find(s->state, word, "(n int gives r int)", 0, &s->word) != PINAX_OK)
return 1;
s->args = (pinax_args){.i = &s->in, .ri = &s->out};
return 0;
}
int main(void) {
pinax_loop_config loop_config = {.struct_size = sizeof loop_config, .threads = 2};
pinax_loop *loop;
pinax_channel *orders;
if (pinax_loop_open(&loop_config, &loop) != PINAX_OK || pinax_channel_open(loop, "orders", 2, 32, &orders) != PINAX_OK ||
prepare(&shop_side, orders, shop, "open_shop") || prepare(&clerk_side, orders, clerk, "serve"))
return 1;
pinax_loop_start(loop, clerk_side.state, clerk_side.word, &clerk_side.args, done, NULL);
pinax_loop_start(loop, shop_side.state, shop_side.word, &shop_side.args, done, NULL);
pinax_loop_wait(loop);
printf("sent %lld, served %lld\n", (long long)shop_side.out, (long long)clerk_side.out);
pinax_loop_close(loop);
pinax_close(shop_side.state);
pinax_close(clerk_side.state);
return 0;
}
출력
$ ./14-threads
apple x3
pear x1
plum x12
sent 3, served 3
메시지는 바이트다. 레코드의 줄은 bytes.pack 으로 싸서 보내고 받는 쪽이 bytes.unpack 으로 푼다(11장). 그래서 두 쪽이 같은 레코드를 선언한다. 닫힌 채널이 비면 receive 가 not_found 로 실패하므로, 받는 쪽은 그 실패를 끝으로 읽는다.
문답
물음. 두 상태가 같은 전역을 함께 쓸 수 있는가?
답. 고치는 전역은 안 된다. 같은 값을 여러 상태가 고치면 경합을 적재 때 잡을 수 없기 때문이다. 고친 것은 채널의 메시지로 건넨다. 기다릴 상대가 없는데 모두 채널을 기다리면 루프가 그 실행들을 PINAX_ERR_DEADLOCK 으로 끝낸다(표준 17.4).
읽기만 하는 큰 자료(사전, 요율표, 낱말 목록과 그 찾아보기)는 상태마다 싣지 않고 한 벌을 함께 읽는다. 한 상태가 자료를 싣고, 읽는 상태들은 같은 선언 앞에 shared 를 붙인다. 호스트가 pinax_share 로 자료 상태의 저장소를 빌려 주면 복사 없이 같은 바이트를 읽는다. 공유 전역을 바꾸는 코드는 적재 때 거절되고, 저장소를 빌려준 상태는 그 뒤로 돌지도 적재하지도 않는다. 바이트가 바뀌지 않으니 여러 스레드가 잠금 없이 함께 읽는다.
14-shared.c
/* A word list loaded once and read by two states on two threads at the same time. The states
declare it `shared`; the host lends them the dictionary state's storage. */
#include "pinax.h"
#include "pinax_loop.h"
#include <stdio.h>
#include <string.h>
static const char dictionary[] =
"record pair\n term str(12)\n meaning str(12)\nend\n"
"list pairs pair 64\n"
"index by_term pairs pair.term\n"
"row pairs (\"sagwa\", \"apple\")\n"
"row pairs (\"bae\", \"pear\")\n"
"row pairs (\"podo\", \"grape\")\n";
static const char translator[] =
"use str\n"
"record pair\n term str(12)\n meaning str(12)\nend\n"
"shared list pairs pair 64\n"
"shared index by_term pairs pair.term\n"
"word known (w str gives int) w by_term lookup or_else (e) 0 return end drop 1 end\n"
"word count_known (n int gives r int)\n"
" n 0 eq if \"sagwa\" known \"kiwi\" known add return end\n"
" \"bae\" known \"podo\" known add \"sagwa\" known add\n"
"end\n";
typedef struct worker {
unsigned char block[1 << 20];
pinax_state *state;
pinax_word word;
int64_t in, out;
pinax_args args;
} worker;
static unsigned char dict_block[1 << 20];
static worker w[2];
static void done(void *user, pinax_state *state, pinax_err outcome) {
(void)user; (void)state;
if (outcome != PINAX_OK)
printf("a run ended with %#x\n", (unsigned)outcome);
}
int main(void) {
pinax_config config = {.struct_size = sizeof config, .budget = 10000};
pinax_state *dict;
if (pinax_open((pinax_mem){dict_block, sizeof dict_block}, &config, &dict) != PINAX_OK ||
pinax_load(dict, "dictionary", (pinax_str){(const unsigned char *)dictionary, strlen(dictionary)},
PINAX_LOAD_DATA) != PINAX_OK)
return 1;
pinax_loop_config loop_config = {.struct_size = sizeof loop_config, .threads = 2};
pinax_loop *loop;
if (pinax_loop_open(&loop_config, &loop) != PINAX_OK)
return 1;
for (int k = 0; k < 2; ++k) {
if (pinax_open((pinax_mem){w[k].block, sizeof w[k].block}, &config, &w[k].state) != PINAX_OK ||
pinax_load(w[k].state, "main", (pinax_str){(const unsigned char *)translator, strlen(translator)},
PINAX_LOAD_CODE) != PINAX_OK ||
pinax_share(w[k].state, "pairs", dict, "pairs") != PINAX_OK || /* the table first */
pinax_share(w[k].state, "by_term", dict, "by_term") != PINAX_OK || /* then its index */
pinax_find(w[k].state, "count_known", "(n int gives r int)", 0, &w[k].word) != PINAX_OK)
return 1;
w[k].in = k;
w[k].args = (pinax_args){.i = &w[k].in, .ri = &w[k].out};
pinax_loop_start(loop, w[k].state, w[k].word, &w[k].args, done, NULL);
}
pinax_loop_wait(loop);
printf("first knows %lld of 2, second knows %lld of 3\n", (long long)w[0].out, (long long)w[1].out);
pinax_loop_close(loop);
for (int k = 0; k < 2; ++k)
pinax_close(w[k].state);
pinax_close(dict); /* last: the others read its storage */
return 0;
}
출력
$ ./14-shared
first knows 1 of 2, second knows 3 of 3
찾아보기는 그 표를 받은 뒤에 받는다. 찾아보기의 칸이 그 표의 줄을 가리키기 때문이다.
여러 프로세스로 도는 프로그램은 파일로 나눈다. 자료를 만드는 프로그램이 pinax_share_save 로 파일을 쓰고, 프로세스마다 pinax_map_file 로 사상해 pinax_share_file 로 상태에 전역을 준다. 파일의 쪽은 운영체제가 한 벌만 두고 모두가 함께 쓴다(표준 19.7).
흔한 오해: 공유 목록은 합계를 내는 낱말에 넘길 수 없다
넘길 수 있다. 그 낱말이 매개변수를 읽기만 하면 된다. 처리기는 낱말이 모음 매개변수를 바꾸는지(직접이든 넘겨 준 낱말에서든) 보고, 바꾸는 낱말에 넘긴 공유 모음만 거절한다(표준 8.13).
14.4 요청을 많이 받을 때
서버는 요청을 한꺼번에 많이 받고, 요청의 시간은 대부분 기다리는 데 든다. 데이터베이스, 파일, 다른 서버를 기다린다. C 함수 안에서 기다리는 호스트 낱말은 스레드와 상태 전체를 붙잡는다. 그래서 기다리는 호스트 낱말은 PINAX_WORD_AWAITABLE 로 등록한다. 그 낱말은 pinax_call_pending 으로 표를 받아 두고, 일을 시작하고, PINAX_PENDING 을 돌려준다. 부른 작업은 기다리고, 상태의 다른 작업은 계속 돈다. 더 돌 작업이 없으면 pinax_run 이나 pinax_resume 이 PINAX_WAITING 을 돌려준다. 호스트의 이벤트 루프가 pinax_complete 로 일을 끝내고 pinax_resume 을 다시 부른다.
요청 하나는 자기 작업 하나가 맡는다(13장). spawn 의 블록은 지역 변수를 보지 못하므로, 요청의 번호는 큐로 작업에 건넨다. 작업은 요청과 함께 끝나고, 작업이 구역에 만든 바이트도 함께 사라진다. 그룹의 작업이 모두 차 있으면 spawn 이 full 로 실패하고, 스크립트는 바로 «바쁨» 으로 답한다. 한 상태가 한 번에 몇 요청을 받는지는 호스트가 아는 것이므로, 스크립트는 그 수를 host 로 쓰고(tasks requests host) 호스트가 적재 전에 pinax_capacity(state,
"requests", 64) 로 준다(표준 8 (4a), 19.4 (1d)). pnx 에서는 --capacity requests=64 로 준다(표준 20 (3a)).
14-requests.c
#include "pinax.h"
#include <stdio.h>
/* Requests as tasks. web.next gives the id of the next request (-1: no more). Each request is
* served by a task of its own, which asks the host for a page with web.fetch. fetch waits: it
* keeps a token and returns PINAX_PENDING, so the other tasks go on. The host answers the first
* fetch, and gives up on the second as a timeout would: a failure, which the task takes. The
* third request faults (the array `served` was sized for ids 0 to 2), and only its task ends: the
* group is `apart`, the task named itself with its id, and the host is told and answers it. */
static const int64_t arriving[] = {1, 2, 3, -1};
static int next_at;
static pinax_err web_next(pinax_call *call, void *user, const pinax_args *args) {
(void)call; (void)user;
args->ri[0] = arriving[next_at < 3 ? next_at++ : 3];
return PINAX_OK;
}
static pinax_token fetching[3];
static int n_fetching;
static pinax_err web_fetch(pinax_call *call, void *user, const pinax_args *args) {
(void)user; (void)args;
fetching[n_fetching++] = pinax_call_pending(call);
return PINAX_PENDING;
}
static pinax_err web_reply(pinax_call *call, void *user, const pinax_args *args) {
(void)call; (void)user;
printf("request %lld: %.*s\n", (long long)args->i[0], (int)args->s[0].len, (const char *)args->s[0].ptr);
return PINAX_OK;
}
static void faulted(void *user, const pinax_state *state, const pinax_task_fault *fault) {
(void)user; (void)state;
printf("request %lld: 500 (%.*s)\n", (long long)fault->tag, (int)fault->where.len, (const char *)fault->where.ptr);
}
int main(void) {
static unsigned char block[1 << 19];
pinax_config config = {.struct_size = sizeof config, .budget = 100000};
pinax_state *st;
if (pinax_open((pinax_mem){block, sizeof block}, &config, &st) != PINAX_OK)
return 1;
static const pinax_word_def words[] = {
{"next", "(gives id int)", web_next, NULL, 0},
{"fetch", "(id int gives page str or fail)", web_fetch, NULL, PINAX_WORD_AWAITABLE},
{"reply", "(id int, body str)", web_reply, NULL, 0},
};
pinax_register(st, "web", words, 3);
pinax_on_task_fault(st, faulted, NULL);
static const char source[] =
"use web\n"
"tasks requests 4 apart\n"
"queue incoming int 4\n"
"array served int 3\n"
"word serve ()\n"
" incoming take or_return to id\n"
" id task_tag\n"
" id web.fetch or_else (e) cat (\"gave up: \", e err_text) end to page\n"
" id served idx 1 add id set served\n"
" id page web.reply\n"
"end\n"
"word start (id int)\n"
" do serve end requests spawn or_else (e) id \"busy\" web.reply return end drop\n"
" id incoming put or_else (e) end\n"
"end\n"
"word main ()\n"
" loop web.next to id id 0 lt until id start end\n"
" requests run_all\n"
"end\n";
if (pinax_load(st, "main", (pinax_str){(const unsigned char *)source, sizeof source - 1}, PINAX_LOAD_CODE) != PINAX_OK) {
pinax_str why = pinax_error_text(st);
fprintf(stderr, "%.*s\n", (int)why.len, (const char *)why.ptr);
return 1;
}
pinax_word w;
pinax_find(st, "main", "()", 0, &w);
pinax_err err = pinax_run(st, w, &(pinax_args){0});
printf("%s: %d requests wait for the host\n", err == PINAX_WAITING ? "PINAX_WAITING" : "?", n_fetching);
pinax_str page = {(const unsigned char *)"page one", 8};
pinax_complete(st, fetching[0], PINAX_OK, &(pinax_args){.rs = &page}); /* the first fetch came back */
pinax_complete(st, fetching[1], PINAX_ERR_CANCELLED, NULL); /* the second took too long */
pinax_complete(st, fetching[2], PINAX_OK, &(pinax_args){.rs = &page});
err = pinax_resume(st, &(pinax_args){0});
printf("%s\n", err == PINAX_OK ? "PINAX_OK" : "?");
pinax_close(st);
return 0;
}
출력
$ ./14-requests
PINAX_WAITING: 3 requests wait for the host
request 1: page one
request 2: gave up: cancelled
request 3: 500 (main:9: range)
PINAX_OK
요청 하나를 포기하려면(시간 초과, 떠난 손님) 호스트가 그 표를 실패로 완료한다. 여기서는 PINAX_ERR_CANCELLED 다. 기다리던 작업에서 낱말이 실패하고, or_else 가 그 실패를 받으며, 다른 요청은 건드리지 않는다. pinax_abort 는 실행 전체를 끝내므로 상태의 요청이 모두 함께 끝난다.
요청 하나의 결함이 다른 요청을 끝내서는 안 된다. 요청 작업의 묶음을 apart 로 선언한다(13장). 그러면 결함 난 작업은 혼자 끝난다. 호스트는 그래도 그 요청에 답해야 하므로, 작업은 요청의 번호를 받자마자 그것으로 자기 이름을 정한다(id task_tag, 표준 14.5). 호스트가 pinax_on_task_fault 에 준 함수가 그런 결함마다 그 꼬리표와 함께 불린다. 여기서는 번호가 배열 served 를 넘는 세 번째 요청이다. 한 실행의 여러 결함은 하나씩 알린다(표준 19.10).
문답
물음. 기다리는 낱말에 or fail 이 없으면?
답. 그러면 호스트의 실패는 결함이 될 수밖에 없고, 결함은 실행 전체를 끝낸다. 묶음이 apart 가 아니면 상태의 요청이 모두 끝난다. 호스트가 포기할 수 있는 낱말은 위의 fetch 처럼 or fail 로 선언한다 (표준 19.8).
작업은 기다리거나 양보하는 자리에서만 차례를 넘긴다. 오래 계산하는 요청은 기다리거나 끝날 때까지 같은 상태의 다른 요청을 붙잡는다. 예산은 작업 하나가 아니라 상태 전체를 멈춘다. 오래 걸리는 일은 따로 연 상태에 맡기거나, 루프 안에서 yield 로 차례를 넘긴다. 너무 오래 도는 요청을 멈추려면 그 묶음에 차례 한도를 준다. tasks requests 64 32 8192 1000000
apart 의 네 번째 크기다. 양보하거나 기다리지 않고 그것을 넘긴 작업은 cancelled 로 결함을 내고 혼자 끝나며, 결함 콜백에서 답을 받는다(표준 14.6).
문답
물음. 요청마다 상태를 복제하면 안 되는가?
답. 된다. 그리고 요청끼리 떨어진다. 한 요청이 저장한 것을 다른 요청이 보지 않는다. 그러나 복제는 요청마다 상태 전체, 곧 고정 부분과 프로그램을 복사하고, 다시 닫아야 한다. 작업은 tasks 로 선언한 자기 구역과 프레임만 든다(표준 14 (2)). 한 상태의 작업들은 전역을 함께 보므로, 요청 하나에 속한 것은 그 지역 변수에 둔다. 복제하는 호스트는 pinax_clone_with(source, block, PINAX_CLONE_KEEP_HOSTS, &clone) 를 쓴다. 복제는 원본의 호스트 낱말과 내보낸 코드를 지니고, 그것을 다시 받지 않고 돈다(표준 19.9 (2b)).
14.5 더 빠르게: 코드를 C 로
프로그램이 정수 루프, 배열 일, 작은 낱말을 많이 부르는 데 시간을 쓴다면, 그 코드를 호스트의 C 컴파일러로 컴파일할 수 있다. pnx export-c prog.pnx > prog_native.c 가 그것을 하나도 돌리지 않고 쓴다. 호스트는 그 파일을 자기 파일 옆에서 컴파일하고 (./nob dist 가 pinax.h 옆에 쓰는 pinax_native.h 를 포함한다), 같은 프로그램을 적재하기 전이나 뒤에 pinax_native_attach(state, &pnx_native) 를 부른다. 다른 것은 바뀌지 않는다. 결과, 결함, 예산, 기다림, 작업이 전과 같다(표준 19.11).
상태가 그 뒤에 적재하는 것, 곧 세션의 줄이나 나중에 적재한 모듈은 해석으로 돌고, 내보낸 부분은 그대로 내보낸 코드로 돈다. 뒤의 낱말이 프로그램의 낱말을 부르면 그 호출은 컴파일한 코드로 들어간다. 내보낸 프로그램의 낱말을 redefine 할 때만 그것이 끝나고, 상태는 그때부터 모두 해석으로 돈다.
문답
물음. 얼마나 빨라지는가?
답. 정수 루프는 두 배에서 일곱 배 빠르다. 적재기가 울릴 수 없다고 증명한 검사는 빼기 때문이기도 하다. 라이브러리의 문자열 함수 안의 일과 호스트 호출은 거의 그대로다.
흔한 오해: 내보낸 코드는 상태가 더는 필요 없는 따로 된 프로그램이다
그렇지 않다. 해석기처럼 상태 위에서 돌고, 어떤 명령은 여전히 해석기가 대신 한다. 내보낸 Pinax 빌드와 같은 빌드로 컴파일한다. 다른 빌드는 해시가 달라 표를 결코 쓰지 않는다.
14.6 웹 페이지와 node 에서
./nob wasm 은 핵을 웹어셈블리 모듈 build/wasm/pinax.wasm 으로 만들고, 그 곁에 JavaScript 모듈 pinax.mjs 를 둔다. 필요한 것은 clang 과 wasm-ld 뿐이다. C 라이브러리는 쓰지 않는다. pinax.mjs 의 createPinax(바이트, { write }) 가 모듈을 열고, 돌려준 객체의 run(소스, 이름) 이 소스를 새 상태에 적재해 정의 밖의 코드를 돌린다. 성공하면 { ok: true }, 아니면 { ok: false, error } 다. write(흐름, 글) 은 io.print 와 io.println 이 찍는 것을 흐름 1 로, io.eprint 와 적재 오류, 결함의 줄과 지역 변수를 흐름 2 로 받는다. 글의 꼴은 pnx 와 같다.
이 판의 웹어셈블리 호스트가 등록하는 모듈은 io 하나다. io.read_line 은 늘 not_found 로 실패한다. file, clock, random, env 와 extern 은 없다. 핵의 모듈(str, json, csv, rex 따위)과 작업은 그대로 돈다.
흔한 오해: 웹어셈블리에서는 작업이 여러 코어에서 돈다
돌지 않는다. 작업은 어디서나 한 상태 안에서 차례로 돈다(13장). 웹어셈블리 판도 같은 코어 하나를 쓰고, 같은 스크립트는 같은 것을 찍는다.
14.7 더 보기
핸들(C 객체를 스크립트에 쥐여 주기), 묶기(호스트 메모리를 전역으로), 나중에 끝나는 호스트 낱말, 상과 복제, extern 은 표준 18절과 19절에 예제와 함께 있다.
정리
- 덩이를 주고 열고, 낱말을 등록하고, 적재하고, 찾아 부른다(표준 19.2–19.5).
- 호스트 낱말의 실패는
or fail 이 있으면 실패, 없으면 결함이다(19.3). - 스크립트가 할 수 있는 바깥일은 호스트가 등록한 것뿐이다(13.3, 18).
- 예산으로 멈춘 실행이 있으면 새 실행은 거절된다. 상으로 되돌린다(19.5, 19.9).
- 여러 코어에는 상태를 여럿 열어 호스트 루프로 돌리고, 상태 사이는 채널의 메시지로 잇는다(17.4).
- 읽기만 하는 자료는
shared 로 선언하고 pinax_share 로 한 벌을 함께 읽는다(8.13, 19.7). - 여러 프로세스는
pinax_share_save 로 쓴 파일을 pinax_map_file 로 사상해 함께 읽는다(19.7). - 기다리는 호스트 낱말은 기다릴 수 있다고 등록하고 나중에
pinax_complete 로 끝낸다. 그동안 다른 작업은 돈다(19.8). - 요청 하나는 자기 작업 하나다. 요청 하나는 그 표를 실패로 완료해서, 실행 전체는
pinax_abort 로 포기한다(14, 19.5, 19.8). apart 로 선언한 묶음은 결함 난 작업만 잃고, 호스트는 결함마다 작업의 꼬리표와 함께 듣는다(14.4 (3a), 14.5, 19.10). 차례 한도는 양보하지 않는 작업을 멈춘다(14.6).- 내보낸 코드는 C 로 컴파일한 프로그램의 코드를 같은 동작으로 돌린다(19.11).