14 Embedding in a C program
Builds on: The language of chapters 1 to 13. Knowledge of C is assumed.
Questions this chapter answers
- How does a C program open Pinax and call a script?
- Who decides what a script can do outside itself?
- How are scripts that are not trusted run?
14.1 Open, register, load, call
A host has four jobs:
- give a block of memory and open a state with
pinax_open; everything of the state is inside that block; - register the host words scripts may use with
pinax_register, with signatures written as in Pinax; - load the script with
pinax_load; when it is refused, the reason is in pinax_error_text; - find a word, with its signature, using
pinax_find, and call it with 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;
}
Output
$ ./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
Q&A
Q. How big must the block be?
A. Opening takes about 150 KB with the defaults. Most of it is the main run’s region (64 KB) and the integer bank (32 KB), which config.region_bytes and config.int_slots can shrink. While a load runs, what is left of the block is the compiler’s working memory: about 20 KB for a small source, about 1 KB per word, and 64 KB more when there is a constant pattern. When the load ends only the program stays and the working memory is free again. The 256 KB of the examples is plenty for a short script; for a long one, after PINAX_ERR_NO_ROOM, open again with at least the size pinax_needed gives. That size is a lower bound, so it may fall short once more.
Arguments and results pass in arrays by type: integers in args->i[] and args->ri[], strings in args->s[] and args->rs[]. When a host word returns something other than PINAX_OK, a word whose signature has or fail fails (the script takes it with or_else), and a word without it becomes a fault.
Q&A
Q. Can a script delete files or reach the network?
A. Not unless the host registered words that do. The standard modules do not reach outside; everything outside is a host word. Calling C functions with extern reaches only the functions the host’s resolver allows.
14.2 Scripts that are not trusted
config.budget is the number of instructions run at a time. When it is used up the run stops and returns PINAX_SUSPENDED, and the host continues it with pinax_resume or gives up. While a run is stopped, the state takes no other run. To give up, call pinax_abort: it ends the run as a fault would, closing the handles the run opened. What the script stored in globals until then stays.
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;
}
Output
$ ./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
This is what the processor guarantees. Whatever the source — wrong, or written to do harm — and whatever bytecode passes the checks of pinax_load_bytecode, a script cannot read or write outside the state’s block and the memory the host gave it. With a budget other than 0, one run always comes back within a bounded number of instructions. A wrong script is refused at load or stops with a fault; it does not bring the host down. The bytes of an image, a share file and memory bound with pinax_bind, however, are data the host trusts; the processor checks only their shape.
Common mistake: A new pinax_run replaces a stopped run
It does not. A new run is refused with
PINAX_ERR_STATE. Either continue the stopped run to its end or give it up with
pinax_abort. When even the globals must go back to how they were before the run, save an image before running and open the state again from it (standard 19.9).
14.3 On several threads
One state runs on one thread at a time. To use several cores, open several states. The host loop of the host library (pinax_loop.h) runs states on a set number of threads, and channels carry messages between them. The loop is opened with a configuration, as a state is: the number of threads, how many states it holds at once, and a block of memory that holds the loop and its channels; two size functions say how large. Without a block it takes one piece of the heap of that size. The host hands a state and a word to the loop with pinax_loop_start and hears that it ended through the function it gave there. The host names a channel and registers it as a module; the script writes use orders and then orders.send, orders.receive and orders.close. A task that puts into a full channel or takes from an empty one waits, and its thread runs other states meanwhile.
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;
}
Output
$ ./14-threads
apple x3
pear x1
plum x12
sent 3, served 3
A message is bytes. A row of records is packed with bytes.pack and the receiver unpacks it with bytes.unpack (chapter 11), so both sides declare the same record. When a closed channel is empty, receive fails with not_found, and the receiver reads that failure as the end.
Q&A
Q. Can two states use the same global?
A. Not one that changes. When several states change the same value, the race cannot be caught at load. What changed is passed on in the messages of channels. When every run waits for a channel and nothing is left to send, the loop ends those runs with PINAX_ERR_DEADLOCK (standard 17.4).
Large data that is only read (a dictionary, a rate table, a word list and its index) is not loaded into every state; one copy is read by all. One state loads the data, and the states that read it write shared in front of the same declarations. When the host lends them the data state’s storage with pinax_share, they read the same bytes without a copy. Code that changes a shared global is refused at load, and the state that lent its storage neither runs nor loads from then on. Since the bytes do not change, several threads read them together without locks.
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;
}
Output
$ ./14-shared
first knows 1 of 2, second knows 3 of 3
An index is given after its table, since the index’s slots point at the table’s rows.
Programs that run as several processes share a file instead. The program that makes the data writes it with pinax_share_save; each process maps it with pinax_map_file and gives its states the globals with pinax_share_file. The operating system keeps one copy of the file’s pages for all of them (standard 19.7).
Common mistake: A shared list cannot be passed to a word that adds it up
It can, as long as the word only reads that parameter. The processor sees whether a word changes a collection parameter, itself or through the words it passes it to, and refuses only a shared collection given to such a word (standard 8.13).
14.4 Serving many requests
A server takes many requests at once, and most of their time goes in waiting: for a database, a file, another server. A host word that waits inside its C function holds the thread and the whole state. So a host word that waits is registered with PINAX_WORD_AWAITABLE. It keeps a token with pinax_call_pending, starts the operation and returns PINAX_PENDING. The task that called it waits, and the other tasks of the state go on. When no task can go on, pinax_run or pinax_resume returns PINAX_WAITING. The host’s own event loop finishes the operations with pinax_complete and calls pinax_resume again.
Each request is served by a task of its own (chapter 13). The block of spawn sees no locals, so the id of the request goes to the task through a queue. The task ends with its request, and the bytes it made in its region go with it. When every task of the group is taken, spawn fails with full, and the script answers “busy” at once. How many requests a state serves at once is the host’s to know, so the script may write the count host (tasks requests host) and the host gives it with pinax_capacity(state, "requests", 64) before the load (standard 8 (4a), 19.4 (1d)). pnx gives it with --capacity requests=64 (standard 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;
}
Output
$ ./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
To give up on one request (a timeout, a client that went away), the host completes its token with a failure, here PINAX_ERR_CANCELLED. The word fails in the task that waits, or_else takes the failure, and the other requests are not touched. pinax_abort would end the whole run, and every request of the state with it.
A fault in one request must not end the others. Declare the group of the request tasks apart (chapter 13): a task that faults then ends alone. The host must still answer that request, so the task names itself with the request’s id (id task_tag, standard 14.5) as soon as it has it. The function the host gave pinax_on_task_fault is called at every such fault with that tag, here for the third request, whose id runs past the array served. Several faults in one run are told one by one (standard 19.10).
Q&A
Q. What if the word that waits has no or fail?
A. Then a failure from the host can only become a fault, and a fault ends the whole run: every request of the state, unless the group is apart. A word the host may give up on is declared or fail, as fetch is above (standard 19.8).
A task gives way only where it waits or yields. A request that computes for a long time holds the other requests of its state back until it waits or ends. The budget stops the whole state, not one task. Long work goes to a state of its own, or gives way in its loop with yield. To stop a request that runs too long, give its group a turn limit, the fourth size of tasks requests 64 32 8192 1000000 apart. A task that runs past it without yielding or waiting faults with cancelled, ends alone, and is answered from the fault callback (standard 14.6).
Q&A
Q. Why not a clone of the state for every request?
A. It works, and it keeps the requests apart: nothing one request stores is seen by another. But a clone copies the whole state, its fixed parts and its program, for every request, and it must be closed again. A task costs only its own region and frames, as declared with tasks (standard 14 (2)). The tasks of one state share its globals, so what belongs to one request stays in its locals. A host that clones takes pinax_clone_with(source, block, PINAX_CLONE_KEEP_HOSTS, &clone): the clone keeps the source’s host words and exported code, and runs without them being given again (standard 19.9 (2b)).
14.5 Faster: the code as C
When a program spends its time in integer loops, array work or many small words, its code can be compiled with the host’s C compiler. pnx export-c prog.pnx > prog_native.c writes it, without running any of it; the host compiles that file next to its own (it includes pinax_native.h, which ./nob dist writes beside pinax.h), and calls pinax_native_attach(state, &pnx_native) before or after loading the same program. Nothing else changes: results, faults, the budget, waiting and tasks are as before (standard 19.11).
What the state loads afterwards, a session’s lines or a module loaded late, runs interpreted, and the exported part stays exported: a later word that calls a word of the program goes into the compiled code for that call. Only a redefine of a word of the exported program ends it; the state then runs everything interpreted.
Q&A
Q. How much faster is it?
A. Integer loops run two to seven times as fast, the checks the loader proved cannot fire being left out. Work in the library’s string functions, and calls into the host, change little.
Common mistake: Exported code is a separate program that no longer needs the state
It is not. It works on the state as the interpreter does, and the interpreter itself still does some instructions for it. Compile it with the same build of Pinax that exported it: another build’s hash differs, and the table is then never used.
14.6 In a web page and in node
./nob wasm builds the core as the WebAssembly module build/wasm/pinax.wasm and puts the JavaScript module pinax.mjs beside it. It needs clang and wasm-ld and nothing else; no C library is used. createPinax(bytes, { write }) of pinax.mjs opens the module, and run(source, name) of the object it gives loads the source into a fresh state and runs the code outside definitions. It gives { ok: true } on success and { ok: false, error } otherwise. write(stream, text) receives what io.print and io.println print on stream 1, and io.eprint, load errors and a fault’s line and locals on stream 2, in the same form as pnx.
In this edition the WebAssembly host registers one module, io; io.read_line always fails with not_found. There is no file, clock, random, env or extern. The modules of the core (str, json, csv, rex and the rest) and tasks run as everywhere.
Common mistake: On WebAssembly, tasks run on several cores
They do not. Tasks run one after the other inside one state, everywhere (chapter 13). The WebAssembly build uses the same core, and the same script prints the same thing.
14.7 More
Handles (giving a script a C object to hold), binding (host memory as globals), host words that finish later, images and clones, and extern are in clauses 18 and 19 of the standard, with examples.
Summary
- Give a block and open, register words, load, find and call (standard 19.2–19.5).
- A failing host word is a failure with
or fail, a fault without (19.3). - What a script can do outside itself is only what the host registered (13.3, 18).
- While a run stopped by the budget remains, new runs are refused; go back to an image (19.5, 19.9).
- For several cores, open several states and run them on the host loop; connect them with the messages of channels (17.4).
- Data that is only read is declared
shared and read as one copy through pinax_share (8.13, 19.7). - Several processes map one file written with
pinax_share_save (pinax_map_file) and read it together (19.7). - A host word that waits is registered awaitable and finished later with
pinax_complete; meanwhile the other tasks go on (19.8). - Each request is a task of its own. One request is given up by completing its token with a failure, the whole run by
pinax_abort (14, 19.5, 19.8). - A group declared
apart loses only the task that faults; the host is told of each with the task’s tag (14.4 (3a), 14.5, 19.10). A turn limit stops a task that never yields (14.6). - Exported code runs a program’s code compiled as C, with the same behaviour (19.11).