diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index c5ea9e26e..2aba7406b 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -266,8 +266,10 @@ SCR_TL size_t scr_cyc_live = 0; SCR_TL size_t scr_cyc_old_freed = 0; #endif -/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type). */ -ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES]; +/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type). + * Explicitly initialized so library localization can demote it: a tentative + * definition is a COMMON symbol on Mach-O, and COMMON symbols stay global. */ +ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES] = {0}; static uint32_t scr_cyc_ntypes = 1; /* Assign `type` its id on its first allocation; any thread may get there diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 71016cfdc..3010721f8 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -96,8 +96,10 @@ typedef struct ScrSidx { /* Keep short-string cursor traffic out of the sparse cache. A short-lived * one-byte receiver can be far more common than a large indexed one; sharing * the round-robin slots would otherwise rebuild a warm index every few calls. + * Explicitly initialized so library localization can demote it: a tentative + * definition is a COMMON symbol on Mach-O, and COMMON symbols stay global. */ -SCR_TL ScrStrScan scr_str_scan; +SCR_TL ScrStrScan scr_str_scan = {0}; static SCR_TL ScrSidx scr_sidx_sparse_tab[SCR_SIDX_N]; static SCR_TL ScrSidx scr_sidx_cursor_tab[SCR_SIDX_N]; static SCR_TL unsigned scr_sidx_sparse_clock; @@ -936,7 +938,8 @@ static inline uint32_t scr_utf8_decode(const char *p, size_t *adv) { /* ── WTF-8 lone surrogates (scr_runtime.h) ────────────────────────── */ -bool scr_wtf8_lone; +/* Explicitly initialized for library localization (see scr_str_scan). */ +bool scr_wtf8_lone = false; void scr_wtf8_note(void) { if (!scr_wtf8_any()) __atomic_store_n(&scr_wtf8_lone, true, __ATOMIC_RELAXED); diff --git a/packages/runtime/test/common-symbols.test.ts b/packages/runtime/test/common-symbols.test.ts new file mode 100644 index 000000000..1244f258d --- /dev/null +++ b/packages/runtime/test/common-symbols.test.ts @@ -0,0 +1,58 @@ +import { execFile } from "node:child_process"; +import { mkdir, readdir, readFile } from "node:fs/promises"; +import { availableParallelism } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; +const srcDir = join(testDir, "../src"); +const vendorDir = join(testDir, "../vendor"); +const buildDir = join(testDir, "build", "common-symbols"); + +// Library localization (abi.localize_runtime) keeps COMMON symbols global, +// so a runtime global written as a tentative definition (`T x;` at file +// scope with no initializer) is exported from every localized library +// archive on toolchains that emit tentative definitions as COMMON symbols, +// such as the macOS runtime build. Linux clang defaults to -fno-common, where +// the same declaration is an ordinary definition, so the library-multi +// symbol checks only catch it on macOS. This test compiles every runtime +// source with -fcommon, in the plain and worker configurations, and expects +// no COMMON symbols: every external runtime global needs an initializer. +test("runtime globals are not tentative definitions", async () => { + await mkdir(buildDir, { recursive: true }); + const sources = (await readdir(srcDir)) + .filter((name) => name.endsWith(".c") && !name.endsWith(".test.c")) + .sort(); + const jobs = sources.flatMap((name) => + [[], ["-DSCR_WORKERS"]].map((defines) => ({ name, defines })), + ); + const common: string[] = []; + let next = 0; + const worker = async (): Promise => { + while (next < jobs.length) { + const { name, defines } = jobs[next++]!; + const output = join(buildDir, `${name}${defines.join("")}.ll`); + await execFileAsync("clang", [ + "-std=c11", + "-O0", + "-fcommon", + "-S", + "-emit-llvm", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + ...defines, + `-I${join(vendorDir, "quickjs-ng")}`, + `-I${join(vendorDir, "mbedtls", "include")}`, + "-o", + output, + join(srcDir, name), + ]); + for (const match of (await readFile(output, "utf8")).matchAll(/^@(\S+) = common /gm)) { + common.push(`${name}${defines.length > 0 ? ` (${defines.join(" ")})` : ""}: ${match[1]}`); + } + } + }; + await Promise.all(Array.from({ length: Math.min(8, availableParallelism()) }, worker)); + expect(common.sort()).toEqual([]); +});