Embedding
slate inside a C program: the interpreter as a static archive with a header, so a program with a
main of its own can run slate programs, call the functions they define, and hand them C functions
to call back.
The archive and the header
One command builds both, from a clone of slate:
sysl build-c . -o libslate.a
It writes libslate.a and, beside it, the header libslate.a.h (--header <path> puts the header
somewhere else). It also prints, on standard error, the
libraries the archive still needs — the ones an @link names, and the ones slate’s packages find
through pkg-config:
sysl: link this against: uv, lmdb, nghttp2, ssl, crypto, sqlite3, m
sysl: and against what the packages require, which pkg-config answers for: pkg-config --libs libbrotlidec libbrotlienc hiredis libuv libwebp lmdb libnghttp2 libcrypto libssl sqlite3 libzstd
Link against exactly those, each @link name as a -l and the pkg-config modules through
pkg-config:
clang main.c -I. libslate.a -luv -llmdb -lnghttp2 -lssl -lcrypto -lsqlite3 -lm $(pkg-config --libs libbrotlidec libbrotlienc hiredis libuv libwebp lmdb libnghttp2 libcrypto libssl sqlite3 libzstd) -o main
The lists above are those of a build with every default feature on; read them off your own build rather than off this page, since a build with fewer features names fewer libraries.
The archive is native objects, so any C toolchain links it: gcc or clang, with GNU ld, lld or the
macOS linker, and nothing added to the line above. A host that links with clang and lld and wants
link-time optimisation across the boundary can ask for the bitcode form with
sysl build-c . --lto thin -o libslate.a (or --lto full); build-c then also prints
sysl: this archive is LLVM bitcode: link it with clang and lld (-fuse-ld=lld), or with a linker that carries LLVM's plugin,
and that link takes -fuse-ld=lld (Apple’s clang links it without).
examples/embed/ in the repository is a whole program built this way, and
scripts/embed-check.sh builds, links and runs it.
The smallest program
#include <stdio.h>
#include <string.h>
#include "libslate.a.h"
int main(void) {
slate_vm *vm = slate_new(0);
const char *program = "print(\"hello\")";
int32_t status = slate_eval(vm, (uint8_t *)program, strlen(program), (uint8_t *)"hello.sl", 8);
if (slate_error_len(vm) > 0)
fprintf(stderr, "%s\n", slate_error(vm));
slate_free(vm);
return status;
}
The program it runs is an ordinary slate program, and prints what it would print from a file:
print("hello")
hello
uint8_t * slate_version(void);
slate_vm * slate_new(uint64_t heap_bytes);
void slate_free(slate_vm * h);
int32_t slate_eval(slate_vm * h, uint8_t * text, uint64_t len, uint8_t * name, uint64_t name_len);
uint8_t * slate_error(slate_vm * h);
uint64_t slate_error_len(slate_vm * h);
slate_versionis whatslate --versionanswers, NUL-terminated, good for the life of the process.slate_newmakes an interpreter with a heap and an event loop of its own.heap_bytesof0takes the sizeslateitself uses. It answers null where 64 handles are already live.slate_freegives everything back — the heap, with every finalizer run, and the loop. A pointer it never handed out, or one already freed, is ignored.slate_evaltakes the program as a pointer and a length, andnameis what a diagnostic quotes as the file. It answers0where the program ran to the end, what it passed toexit, or1where slate refused it or it faulted.
The session
Every slate_eval on one handle runs in one session, as a REPL does, or Lua’s luaL_dostring
on one state: what an earlier call bound at its top level — a function, a val, a class, an import —
a later call can use, and what the later call binds joins it. So a C program that evaluates
double(n) = n * 2
and then, in a second call,
print(double(21))
gets the same answer the two written as one file get:
double(n) = n * 2
print(double(21))
42
- A call slate refuses changes nothing. Its diagnostic is in
slate_error, and the session is as it was before the call. - A call that faults keeps what it bound before the fault.
- Two handles share nothing. A name bound on one is not defined on the other.
slate_erroris the last call’s diagnostic, NUL-terminated, andslate_error_lenits length. It is empty where the call ran, and the pointer is good until the next call on the handle. Diagnostics never go anywhere else.- What a program prints goes to standard output as it prints it, exactly as
slate <file>would — unless the handle has an output function (below). A C program that also prints shouldfflushits own output before each call, or the two come out of order.
Values
A value C holds is a handle: a uint64_t naming a slot in a table the interpreter keeps. The
collector walks that table, so a value stays alive for exactly as long as C holds its handle, across
any number of calls that collect. 0 is never a handle, so C can read it as “none”.
uint64_t slate_int(slate_vm * h, int64_t n);
uint64_t slate_real(slate_vm * h, double x);
uint64_t slate_bool(slate_vm * h, int32_t b);
uint64_t slate_null(slate_vm * h);
uint64_t slate_string(slate_vm * h, uint8_t * p, uint64_t len);
uint64_t slate_array(slate_vm * h);
int32_t slate_push(slate_vm * h, uint64_t array, uint64_t v);
uint64_t slate_object(slate_vm * h);
int32_t slate_set(slate_vm * h, uint64_t object, uint8_t * key, uint64_t key_len, uint64_t v);
void slate_release(slate_vm * h, uint64_t v);
int32_t slate_kind(slate_vm * h, uint64_t v);
int64_t slate_to_int(slate_vm * h, uint64_t v);
double slate_to_real(slate_vm * h, uint64_t v);
int32_t slate_to_bool(slate_vm * h, uint64_t v);
uint8_t * slate_string_ptr(slate_vm * h, uint64_t v);
uint64_t slate_string_len(slate_vm * h, uint64_t v);
uint64_t slate_len(slate_vm * h, uint64_t array);
uint64_t slate_at(slate_vm * h, uint64_t array, uint64_t i);
uint64_t slate_get(slate_vm * h, uint64_t object, uint8_t * key, uint64_t key_len);
uint8_t * slate_show(slate_vm * h, uint64_t v);
uint64_t slate_show_len(slate_vm * h);
- Every handle is yours until
slate_release, including the new onesslate_at,slate_get,slate_globalandslate_callanswer. A released slot is taken by the next value made, so a host that releases what it is done with holds a table no bigger than it ever needed.slate_freegives the whole table back. - A handle belongs to the
slate_vmthat made it. Handed to another, it names whatever that one keeps at the same slot, and nothing can tell the difference — as with a pointer. slate_stringcopies its bytes.slate_string_ptrpoints at the string’s own UTF-8 bytes, not terminated,slate_string_lenof them, good for as long as the handle is held: a slate string never changes and the heap never moves an object. Anything that is not a string answers an empty one.slate_to_inttruncates a real toward zero and answers0for anything else;slate_to_realconverts an integer and answers0.0for anything else;slate_to_boolis slate’s truthiness, so0,""andnullare0.slate_pushandslate_setanswer0, or1where the target is not an array or an object, is frozen, or the value is not a live handle.slate_getanswers0for a key the object does not have.slate_showis the value asprintwrites it, NUL-terminated, in one buffer per handle that the nextslate_showreplaces;slate_show_lenis its length.
slate_kind answers:
| kind | value |
|---|---|
0 | null |
1 | a bool |
2 | an integer |
3 | a real |
4 | a string |
5 | an array |
6 | an object, a class instance included |
7 | a function — a builtin and a host function included |
8 | anything else |
-1 | 0, or a handle already released |
Calling slate from C
uint64_t slate_global(slate_vm * h, uint8_t * name, uint64_t name_len);
int32_t slate_call(slate_vm * h, uint64_t f, uint64_t * argv, uint64_t argc, uint64_t * out);
slate_global answers a handle to what a top-level name of the session holds — something a
slate_eval defined, or a builtin — and 0 where nothing by that name is defined.
slate_call calls f with argc handles from argv, writes a handle to the answer at out, and
answers 0. Where the call faults, or f is not something that can be called, it answers 1,
leaves *out as 0, and slate_error has the diagnostic, naming the file and line the fault
happened at.
uint64_t twice = slate_global(vm, (uint8_t *)"double", 6);
uint64_t args[] = { slate_int(vm, 21) };
uint64_t answer = 0;
if (slate_call(vm, twice, args, 1, &answer) == 0)
printf("%lld\n", (long long)slate_to_int(vm, answer));
slate_release(vm, answer);
slate_release(vm, args[0]);
slate_release(vm, twice);
An async function is run to its answer. In the default loop mode slate_call goes through
every await, then drains the loop as it is drained after a program, so what C reads at out is the
value the function returned and never a promise.
Calling C from slate
int32_t slate_register(slate_vm * h, uint8_t * name, uint64_t name_len, uint64_t (*f)(slate_vm *, uint64_t *, uint64_t, uint8_t *), uint8_t * user);
void slate_on_output(slate_vm * h, void (*f)(uint8_t *, uint64_t, uint8_t *), uint8_t * user);
The header spells the two callbacks as raw function-pointer types. Named, they are
typedef uint64_t (*slate_host_fn)(slate_vm *vm, uint64_t *argv, uint64_t argc, uint8_t *user);
typedef void (*slate_output_fn)(uint8_t *bytes, uint64_t len, uint8_t *user);
slate_register binds name at the session’s top level to a C function, so every later program on
that handle — and no other — can call it like any function. It answers 0, or 1 with slate_error
set where name is not a name a program could write, or f is null.
static uint64_t host_add(slate_vm *vm, uint64_t *argv, uint64_t argc, uint8_t *user) {
int64_t sum = 0;
for (uint64_t i = 0; i < argc; i++)
sum += slate_to_int(vm, argv[i]);
return slate_int(vm, sum);
}
slate_register(vm, (uint8_t *)"host_add", 8, host_add, NULL);
after which a program on vm may write print(host_add(40, 2)).
- It is handed a handle per argument, as many as the call wrote. A C function declares no count and slate’s calls are JavaScript’s, so nothing is refused for being too few or too many. The argument handles are slate’s: read them, and do not release them.
- It answers a handle, or
0fornull. slate takes the handle over, reads the value and releases the slot, so answer one you just made or one of the arguments. A handle already released faults in the calling program, with a sentence naming the host function. - Registering a name again replaces it, as Lua’s
lua_registerdoes; a value a program already took keeps calling the function it was. - It may call back into slate.
slate_callfrom inside a host function runs on the machine already running, so C → slate → C → slate works to any depth.slate_evalthere is refused, a program being a whole run of its own, and so isslate_pump. - A host function prints as
<host name>, andslate_kindcalls it7.useris whatever was handed toslate_register, untouched.
slate_on_output sends every line a program on that handle prints to f, without the newline,
with the user pointer beside it — from slate_eval and from a function slate_call runs alike —
instead of to standard output. The bytes are good for the length of the call. f null puts standard
output back.
The event loop
void slate_set_manual_loop(slate_vm * h, int32_t manual);
int32_t slate_pump(slate_vm * h);
int32_t slate_run_until_idle(slate_vm * h);
int32_t slate_loop_fd(slate_vm * h);
int32_t slate_loop_timeout(slate_vm * h);
Every handle has an event loop of its own, and there are two ways to turn it.
- slate turns it — the default, and right for a script host.
slate_evalandslate_calldrain the loop before they return, so every timer a program armed has fired, every socket has closed and everyawaithas settled by the time C sees the status. - The host turns it — right for a GUI or a server with a main loop of its own. After
slate_set_manual_loop(vm, 1),slate_evalandslate_callrun to the first suspension and return with the rest still pending.slate_evalanswers the status it always does;slate_callanswers the value where it has one, and for anasyncfunction still waiting, the promise itself — a handleslate_kindcalls8.slate_set_manual_loop(vm, 0)puts the default back. The change takes effect at the next call, and what is already pending stays pending.
slate_pump runs one turn of the loop without waiting: the callbacks already due, then what they
resumed. It answers 1 while work is still outstanding, 0 once the loop is idle, and -1 where a
callback faulted or a promise was rejected with nothing awaiting it, with slate_error saying so —
each reported once, the next pump carrying on. It works in either mode. slate_run_until_idle is the
whole drain in one call, as slate_eval would have done it, and answers 0 or -1 the same way.
A host with a poll, kqueue or epoll loop adds slate_loop_fd to it, waits at most
slate_loop_timeout milliseconds, then pumps. The timeout is -1 where only the descriptor can wake
the loop, as with a listening socket and no timer, and 0 where something is due now — and also where
nothing at all is pending, which slate_pump answering 0 tells apart.
slate_set_manual_loop(vm, 1);
slate_eval(vm, (uint8_t *)program, strlen(program), (uint8_t *)"timer.sl", 8);
while (slate_pump(vm) == 1)
usleep(2000);
Here a short sleep stands in for the poll.
- Two handles never share a loop, so pumping one runs nothing of the other’s.
slate_freecloses whatever a manual-mode handle left pending.- A program that
awaits at its own top level is still settled beforeslate_evalreturns, in either mode — the rule that gives an imported file’s exports before its importer reads them.
Limits
- 64 handles may be live in one process.
slate_newanswers null past that; a freed handle’s slot is taken again. - One call at a time on a handle. Each call enters the handle’s interpreter and leaves it, so two threads may each drive a handle of their own, but calls on ONE handle from several threads at once are not allowed — serialise them in C.
- Building the archive needs sysl 0.0.138 or later, the compiler slate 0.1.10 is built with.