mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-16 05:00:59 +08:00
* feat: support multi-instance library mode via runtime symbol localization - Add the abi.localize_runtime profile field: the archive build combines the program object with exactly the runtime/vendor members it reaches and demotes every external definition except the profile-declared symbols to a local symbol (darwin: one ld -r pass with -exported_symbols_list; linux: ld -r then objcopy --keep-global-symbols) - N archives built under pairwise-distinct prefixes now link into one process with no symbol collisions and no shared mutable runtime state: each instance owns a private copy of the allocator, collector, result arena, and panic sink, so sinks register per instance and a trap poisons only the instance it fired in - Document the embedder contract beside the profile spec: one thread per instance (an instance is never entered from two threads), and values cross instances only through the embedder's own byte/record marshalling - Refuse cross-target localized builds with SC3002 (the step runs the host toolchain's ld/objcopy over host-format objects); absent or false keeps the classic single-archive artifact byte-for-byte - Add the two-instance acceptance probe (two archives, two embedder threads, independent collects, a trap delivered to its own sink exactly once while the other instance keeps answering) plus symbol-exactness, profile-shape, and target-posture suites, and schedule the artifact contracts on the gate host * fix: tighten runtime localization validation * fix: harden localized library publication
199 lines
6.4 KiB
C
199 lines
6.4 KiB
C
/* Multi-instance acceptance probe: TWO runtime-localized library archives
|
|
* (prefixes ma_ / mb_) linked into ONE process, each instance driven from
|
|
* its own dedicated embedder thread — the documented contract: one thread
|
|
* per instance, an instance never entered from two threads. Checks:
|
|
* - both instances init, run their export families, and run their
|
|
* collect entries independently;
|
|
* - a deliberate range trap in instance A reaches ONLY A's registered
|
|
* sink, exactly once, as the structured message (SC4014, symbol
|
|
* ma_boom) with A's registration ctx and a nonzero fault address;
|
|
* - instance B keeps answering: its worker loops through allocation-
|
|
* heavy calls across A's trap window and still answers afterwards,
|
|
* with exact values throughout;
|
|
* - B's sink never fires.
|
|
* Workers record results into per-thread state; main prints everything
|
|
* after both joins, so stdout is deterministic under any interleaving. */
|
|
#include <pthread.h>
|
|
#include <setjmp.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
extern void ma_init(void);
|
|
extern void ma_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
|
extern void ma_collect(void);
|
|
extern double ma_bump(double x);
|
|
extern double ma_calls_seen(void);
|
|
extern double ma_boom(double i);
|
|
|
|
extern void mb_init(void);
|
|
extern void mb_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
|
extern void mb_collect(void);
|
|
extern double mb_sum_to(double n);
|
|
extern double mb_add(double x);
|
|
|
|
/* Probe stages: 0 start; 1 A initialized; 2 B initialized (both workers'
|
|
* concurrent loops run); 3 both fixed loops finished (A traps, B keeps
|
|
* looping); 4 A's trap delivered (B answers once more and exits). */
|
|
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
|
|
static int stage = 0;
|
|
static int fixed_loops_done = 0;
|
|
|
|
static void stage_set(int s) {
|
|
pthread_mutex_lock(&mu);
|
|
if (stage < s) stage = s;
|
|
pthread_cond_broadcast(&cv);
|
|
pthread_mutex_unlock(&mu);
|
|
}
|
|
|
|
static int stage_get(void) {
|
|
pthread_mutex_lock(&mu);
|
|
int s = stage;
|
|
pthread_mutex_unlock(&mu);
|
|
return s;
|
|
}
|
|
|
|
static void stage_wait(int s) {
|
|
pthread_mutex_lock(&mu);
|
|
while (stage < s) pthread_cond_wait(&cv, &mu);
|
|
pthread_mutex_unlock(&mu);
|
|
}
|
|
|
|
static void fixed_loop_done(void) {
|
|
pthread_mutex_lock(&mu);
|
|
fixed_loops_done++;
|
|
if (fixed_loops_done == 2 && stage < 3) {
|
|
stage = 3;
|
|
pthread_cond_broadcast(&cv);
|
|
}
|
|
pthread_mutex_unlock(&mu);
|
|
}
|
|
|
|
/* Per-instance sink records. Both instances register the SAME sink
|
|
* function with instance-specific ctx: delivery routing is observable as
|
|
* which record advances. */
|
|
typedef struct {
|
|
int calls;
|
|
int ctx_ok;
|
|
int fields;
|
|
int text_printable;
|
|
int addr_nonzero;
|
|
char code[32];
|
|
char symbol[64];
|
|
} SinkRec;
|
|
|
|
static SinkRec rec_a, rec_b;
|
|
static jmp_buf trap_jmp_a;
|
|
|
|
static void copy_field(char *dst, size_t cap, const uint8_t *p, size_t len) {
|
|
if (len >= cap) len = cap - 1;
|
|
memcpy(dst, p, len);
|
|
dst[len] = 0;
|
|
}
|
|
|
|
static void record_structured(SinkRec *r, const uint8_t *msg, size_t len) {
|
|
if (len == 0 || msg[0] != 0x01) return; /* fields stays 0: not structured */
|
|
r->text_printable = len > 1 && msg[1] >= 0x20;
|
|
const uint8_t *p = msg + 1, *end = msg + len;
|
|
int i = 0;
|
|
for (;;) {
|
|
const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p));
|
|
const uint8_t *stop = sep != NULL ? sep : end;
|
|
if (i == 1) copy_field(r->code, sizeof r->code, p, (size_t)(stop - p));
|
|
if (i == 2) copy_field(r->symbol, sizeof r->symbol, p, (size_t)(stop - p));
|
|
i++;
|
|
if (sep == NULL) break;
|
|
p = sep + 1;
|
|
}
|
|
r->fields = i;
|
|
}
|
|
|
|
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
|
|
SinkRec *r = ctx;
|
|
r->calls++;
|
|
r->addr_nonzero = addr != 0;
|
|
record_structured(r, msg, len);
|
|
if (r == &rec_a) {
|
|
r->ctx_ok = 1;
|
|
longjmp(trap_jmp_a, 1); /* the conforming survival pattern */
|
|
}
|
|
/* B's sink must never fire; returning would abort, and main would never
|
|
* print (the probe's failure shows as missing output). */
|
|
}
|
|
|
|
/* Instance A's dedicated thread: state-advancing calls, collects, then the
|
|
* deliberate trap. */
|
|
static double a_last_bump, a_calls;
|
|
static int a_trap_fell_through = 0;
|
|
|
|
static void *worker_a(void *arg) {
|
|
(void)arg;
|
|
ma_set_panic_sink(sink, &rec_a);
|
|
ma_init(); /* prints "multi-a ready" */
|
|
stage_set(1);
|
|
stage_wait(2);
|
|
double last = 0;
|
|
for (int i = 0; i < 200; i++) {
|
|
last = ma_bump(1);
|
|
if ((i + 1) % 50 == 0) ma_collect();
|
|
}
|
|
a_last_bump = last;
|
|
a_calls = ma_calls_seen();
|
|
fixed_loop_done();
|
|
stage_wait(3);
|
|
if (setjmp(trap_jmp_a) == 0) {
|
|
ma_boom(9); /* xs[9] of a length-3 array: the runtime's range trap */
|
|
a_trap_fell_through = 1;
|
|
}
|
|
stage_set(4);
|
|
return NULL;
|
|
}
|
|
|
|
/* Instance B's dedicated thread: allocation-heavy calls that keep running
|
|
* across A's trap window, then post-trap answers. */
|
|
static int b_sums_ok = 1, b_adds_ok = 1, b_reached_200 = 0, b_post_ok = 0;
|
|
|
|
static void *worker_b(void *arg) {
|
|
(void)arg;
|
|
mb_set_panic_sink(sink, &rec_b);
|
|
stage_wait(1);
|
|
mb_init(); /* prints "multi-b ready" */
|
|
stage_set(2);
|
|
double total = 0;
|
|
long iters = 0;
|
|
for (;;) {
|
|
if (mb_sum_to(100) != 5050.0) b_sums_ok = 0;
|
|
total = mb_add(1);
|
|
iters++;
|
|
if (total != (double)iters) b_adds_ok = 0;
|
|
if (iters % 25 == 0) mb_collect();
|
|
if (iters == 200) fixed_loop_done();
|
|
if (iters >= 200 && stage_get() >= 4) break;
|
|
}
|
|
b_reached_200 = iters >= 200;
|
|
/* A's trap has been delivered; B answers again. */
|
|
double post_sum = mb_sum_to(10);
|
|
double post_add = mb_add(5);
|
|
mb_collect();
|
|
b_post_ok = post_sum == 55.0 && post_add == total + 5.0;
|
|
return NULL;
|
|
}
|
|
|
|
int main(void) {
|
|
pthread_t ta, tb;
|
|
pthread_create(&ta, NULL, worker_a, NULL);
|
|
pthread_create(&tb, NULL, worker_b, NULL);
|
|
pthread_join(ta, NULL);
|
|
pthread_join(tb, NULL);
|
|
printf("a: bump(1) x200 -> %.0f, calls_seen %.0f, trap fell through %d\n",
|
|
a_last_bump, a_calls, a_trap_fell_through);
|
|
printf("a sink: calls=%d ctx_ok=%d fields=%d code=[%s] symbol=[%s] text_printable=%d addr_nonzero=%d\n",
|
|
rec_a.calls, rec_a.ctx_ok, rec_a.fields, rec_a.code, rec_a.symbol,
|
|
rec_a.text_printable, rec_a.addr_nonzero);
|
|
printf("b: concurrent sums_ok=%d adds_ok=%d reached_200=%d\n", b_sums_ok, b_adds_ok, b_reached_200);
|
|
printf("b: post-trap answers ok=%d\n", b_post_ok);
|
|
printf("b sink: calls=%d\n", rec_b.calls);
|
|
return 0;
|
|
}
|