mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-11 02:03:59 +08:00
177 lines
6.3 KiB
C
177 lines
6.3 KiB
C
/* Smoke test for a libnode pack, written the way the scriptc runtime loads
|
|
* one: a plain C program that never includes a Node or V8 header, dlopens
|
|
* the pack's bridge, checks its ABI and Node version, starts Node (one
|
|
* environment from the built-in startup snapshot), evaluates expressions
|
|
* synchronously and asynchronously through the bridge's C ABI, and tears
|
|
* Node down.
|
|
*
|
|
* smoke <pack directory> (or SCRIPTC_LIBNODE=<pack directory> smoke)
|
|
*
|
|
* stdout is deterministic and compared by scripts/smoke.mjs; timings and
|
|
* memory go to stderr. */
|
|
#include "scr_node_bridge.h"
|
|
|
|
#include <dlfcn.h>
|
|
#include <poll.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/resource.h>
|
|
#include <time.h>
|
|
|
|
#if defined(__APPLE__)
|
|
#define BRIDGE_NAME "libscr_node_bridge.dylib"
|
|
#else
|
|
#define BRIDGE_NAME "libscr_node_bridge.so"
|
|
#endif
|
|
|
|
/* The bootstrap runs as the environment's main script. It hands the bridge
|
|
* a dispatcher that evaluates `name` as an expression and answers the
|
|
* bridge's outcome strings: "R<json>" when it returns, "T<json>" when it
|
|
* throws; an asynchronous call settles through the host. */
|
|
static const char kBootstrap[] =
|
|
"const host = globalThis.__scriptcNodeIslandHost;\n"
|
|
"globalThis.require = require;\n"
|
|
"const outcome = (ok, value) => (ok ? 'R' : 'T') + JSON.stringify(\n"
|
|
" ok ? value : { error: { name: value?.name, message: String(value?.message ?? value) } });\n"
|
|
"globalThis.__scriptcNodeIsland = {\n"
|
|
" call(id, sync, file, specifier, name, args) {\n"
|
|
" let value;\n"
|
|
" try { value = (0, eval)(name); } catch (error) { return outcome(false, error); }\n"
|
|
" if (sync) return outcome(true, value);\n"
|
|
" Promise.resolve(value).then((v) => host.settle(id, outcome(true, v)),\n"
|
|
" (e) => host.settle(id, outcome(false, e)));\n"
|
|
" },\n"
|
|
"};\n";
|
|
|
|
static double now_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
static long max_rss_kb(void) {
|
|
struct rusage usage;
|
|
getrusage(RUSAGE_SELF, &usage);
|
|
#if defined(__APPLE__)
|
|
return usage.ru_maxrss / 1024;
|
|
#else
|
|
return usage.ru_maxrss;
|
|
#endif
|
|
}
|
|
|
|
/* Whether a library whose path contains `needle` is mapped (Linux only). */
|
|
static int mapped(const char *needle) {
|
|
FILE *maps = fopen("/proc/self/maps", "r");
|
|
if (maps == NULL) return -1;
|
|
char line[4096];
|
|
int found = 0;
|
|
while (!found && fgets(line, sizeof line, maps) != NULL) found = strstr(line, needle) != NULL;
|
|
fclose(maps);
|
|
return found;
|
|
}
|
|
|
|
static char *settled = NULL;
|
|
static size_t settled_len = 0;
|
|
|
|
static void host_settle(uint32_t id, const char *outcome, size_t len) {
|
|
(void)id;
|
|
settled = malloc(len + 1);
|
|
memcpy(settled, outcome, len);
|
|
settled[len] = '\0';
|
|
settled_len = len;
|
|
}
|
|
|
|
static void host_exit(int code) {
|
|
fflush(stdout);
|
|
exit(code);
|
|
}
|
|
|
|
static const ScrNodeBridgeHost kHost = {host_settle, host_exit};
|
|
|
|
static const ScrNodeBridge *bridge;
|
|
|
|
static void eval_sync(const char *expression) {
|
|
size_t len = 0;
|
|
char *outcome = bridge->call(0, 1, "", "", expression, "[]", 2, &len);
|
|
printf("%s => %s\n", expression, outcome);
|
|
bridge->release(outcome);
|
|
}
|
|
|
|
static void eval_async(const char *expression) {
|
|
settled = NULL;
|
|
size_t len = 0;
|
|
char *immediate = bridge->call(1, 0, "", "", expression, "[]", 2, &len);
|
|
if (immediate != NULL) bridge->release(immediate);
|
|
/* Drive Node's loop until the promise settles, as the runtime's loop does
|
|
* before NI-3 hosts its queues on libuv. */
|
|
while (settled == NULL && bridge->alive()) {
|
|
struct pollfd fd = {bridge->backend_fd(), POLLIN, 0};
|
|
poll(&fd, 1, bridge->backend_timeout());
|
|
bridge->pump();
|
|
}
|
|
if (settled == NULL) bridge->pump();
|
|
printf("await %s => %s\n", expression, settled != NULL ? settled : "(never settled)");
|
|
free(settled);
|
|
settled = NULL;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *dir = argc > 1 ? argv[1] : getenv("SCRIPTC_LIBNODE");
|
|
if (dir == NULL || dir[0] == '\0') {
|
|
fprintf(stderr, "usage: smoke <pack directory> (or set SCRIPTC_LIBNODE)\n");
|
|
return 2;
|
|
}
|
|
char path[4096];
|
|
snprintf(path, sizeof path, "%s/%s", dir, BRIDGE_NAME);
|
|
int before = mapped("/libnode.");
|
|
double t0 = now_ms();
|
|
void *library = dlopen(path, RTLD_NOW | RTLD_GLOBAL);
|
|
if (library == NULL) {
|
|
fprintf(stderr, "smoke: %s\n", dlerror());
|
|
return 1;
|
|
}
|
|
double t1 = now_ms();
|
|
const ScrNodeBridge *(*entry)(void) =
|
|
(const ScrNodeBridge *(*)(void))dlsym(library, SCR_NODE_BRIDGE_ENTRY);
|
|
if (entry == NULL) {
|
|
fprintf(stderr, "smoke: %s has no %s\n", path, SCR_NODE_BRIDGE_ENTRY);
|
|
return 1;
|
|
}
|
|
bridge = entry();
|
|
if (bridge->abi != SCR_NODE_BRIDGE_ABI) {
|
|
fprintf(stderr, "smoke: bridge ABI %u, expected %u\n", bridge->abi, SCR_NODE_BRIDGE_ABI);
|
|
return 1;
|
|
}
|
|
printf("bridge ABI %u, Node %s, NODE_MODULE_VERSION %u\n", bridge->abi, bridge->node_version,
|
|
bridge->module_version);
|
|
printf("libnode mapped before dlopen: %s\n", before == 1 ? "yes" : "no");
|
|
char *error = bridge->start(&kHost, kBootstrap, argv[0], 0);
|
|
if (error != NULL) {
|
|
fprintf(stderr, "smoke: %s\n", error);
|
|
return 1;
|
|
}
|
|
double t2 = now_ms();
|
|
eval_sync("1 + 2");
|
|
double t3 = now_ms();
|
|
eval_sync("process.versions.node");
|
|
eval_sync("process.config.variables.node_use_node_snapshot");
|
|
eval_sync("process.config.variables.icu_small");
|
|
eval_sync("new Intl.NumberFormat('de-DE').format(1234567.891)");
|
|
eval_sync("new Intl.DateTimeFormat('ja-JP', { timeZone: 'UTC', dateStyle: 'full' }).format(0)");
|
|
eval_sync("require('node:crypto').createHash('sha256').update('scriptc').digest('hex')");
|
|
eval_sync("require('node:zlib').gunzipSync(require('node:zlib').gzipSync('round trip')).toString()");
|
|
eval_sync("[typeof require('node:fs').readFileSync, require('node:path').join('a', 'b')]");
|
|
eval_sync("null.property");
|
|
eval_async("new Promise((resolve) => setTimeout(() => resolve('timer fired'), 5))");
|
|
eval_async("require('node:fs/promises').readFile(process.execPath).then((b) => b.length > 0)");
|
|
eval_async("Promise.reject(new RangeError('rejected'))");
|
|
fprintf(stderr,
|
|
"smoke: dlopen %.1f ms, start %.1f ms, first result %.2f ms, dlopen to first result "
|
|
"%.1f ms, max RSS %ld KB\n",
|
|
t1 - t0, t2 - t1, t3 - t2, t3 - t0, max_rss_kb());
|
|
bridge->stop();
|
|
printf("ok\n");
|
|
return 0;
|
|
}
|