From c6849bde6cfbe51837227789840ecf2b05791070 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Mon, 13 Jul 2026 02:43:03 -0400 Subject: [PATCH 1/2] sdk: preserve static linker argument order Keep parser classifications for SDK mode and thread inference, but forward non-SDK arguments in their original sequence so objects, -l libraries, archives, and linker group controls retain normal static-link semantics. Add unit and real wasm-ld coverage for mixed input ordering and linker controls. --- docs/sdk-guide.md | 5 +++ sdk/src/bin/cc.ts | 10 ++--- sdk/src/lib/flags.ts | 11 ++++- sdk/test/cc.test.ts | 13 ++++++ sdk/test/flags.test.ts | 19 +++++++++ sdk/test/integration.test.ts | 79 ++++++++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 6 deletions(-) diff --git a/docs/sdk-guide.md b/docs/sdk-guide.md index a18448a68d..f17eaac35f 100644 --- a/docs/sdk-guide.md +++ b/docs/sdk-guide.md @@ -127,6 +127,11 @@ wasm32posix-ar rcs libfoo.a lib_a.o lib_b.o wasm32posix-cc main.c -L. -lfoo -o program.wasm ``` +The compiler wrapper preserves the caller's linker-input order. Objects, +explicit archives, `-l` libraries, and linker group controls reach Clang in +the same relative sequence supplied by the build system, as required by +normal static archive resolution. + ### With dynamic loading (dlopen) ```bash diff --git a/sdk/src/bin/cc.ts b/sdk/src/bin/cc.ts index 247df83eb7..1b65abde23 100755 --- a/sdk/src/bin/cc.ts +++ b/sdk/src/bin/cc.ts @@ -43,11 +43,11 @@ export function buildClangArgs(userArgs: string[], toolchain: Toolchain, arch: W if (parsed.preprocessOnly) args.push('-E'); if (parsed.assemblyOnly) args.push('-S'); if (parsed.outputFile) args.push('-o', parsed.outputFile); - args.push(...parsed.otherArgs); - - args.push(...parsed.sourceFiles); - args.push(...parsed.objectFiles); - args.push(...parsed.archiveFiles); + // Static link semantics depend on the caller's exact ordering of objects, + // archives, -l flags, and linker group controls. Parsed classifications are + // for SDK decisions only; forwarding must never rebuild the command in + // type-based buckets. + args.push(...parsed.forwardedArgs); // -fPIC is consumed by parseArgs (so the linker can see `parsed.pic`), // but it must also reach clang at compile time so the resulting object diff --git a/sdk/src/lib/flags.ts b/sdk/src/lib/flags.ts index 9b2876d155..444ae5784e 100644 --- a/sdk/src/lib/flags.ts +++ b/sdk/src/lib/flags.ts @@ -131,6 +131,8 @@ export interface ParsedArgs { objectFiles: string[]; archiveFiles: string[]; otherArgs: string[]; + /** Arguments forwarded to clang, in the exact order supplied by the caller. */ + forwardedArgs: string[]; } const SOURCE_EXTS = new Set(['.c', '.cc', '.cpp', '.cxx', '.m', '.mm', '.i', '.ii']); @@ -162,6 +164,7 @@ export function parseArgs(args: string[]): ParsedArgs { objectFiles: [], archiveFiles: [], otherArgs: [], + forwardedArgs: [], }; for (let i = 0; i < args.length; i++) { @@ -199,8 +202,12 @@ export function parseArgs(args: string[]): ParsedArgs { } else if (FLAGS_WITH_VALUE.has(arg)) { // Flag that takes the next arg as its value — keep both as otherArgs result.otherArgs.push(arg); + result.forwardedArgs.push(arg); i++; - if (i < args.length) result.otherArgs.push(args[i]); + if (i < args.length) { + result.otherArgs.push(args[i]); + result.forwardedArgs.push(args[i]); + } } else if (!arg.startsWith('-')) { const ext = arg.substring(arg.lastIndexOf('.')); if (SOURCE_EXTS.has(ext)) { @@ -212,8 +219,10 @@ export function parseArgs(args: string[]): ParsedArgs { } else { result.otherArgs.push(arg); } + result.forwardedArgs.push(arg); } else { result.otherArgs.push(arg); + result.forwardedArgs.push(arg); } } diff --git a/sdk/test/cc.test.ts b/sdk/test/cc.test.ts index 1e3897b4bf..0ae07c0e89 100644 --- a/sdk/test/cc.test.ts +++ b/sdk/test/cc.test.ts @@ -47,6 +47,19 @@ describe('buildClangArgs', () => { expect(args.join(' ')).toContain('channel_syscall.c'); }); + it('preserves user linker input order across argument categories', () => { + const userLinkArgs = [ + 'main.o', + '-Wl,--start-group', + '-lfoo', + 'libbar.a', + '-Wl,--end-group', + ]; + const args = buildClangArgs([...userLinkArgs, '-o', 'out.wasm'], toolchain); + const forwarded = args.slice(args.indexOf('main.o'), args.indexOf('-Wl,--end-group') + 1); + expect(forwarded).toEqual(userLinkArgs); + }); + it('preprocess-only: no link flags', () => { const args = buildClangArgs(['-E', 'foo.c'], toolchain); expect(args).not.toContain('-Wl,--entry=_start'); diff --git a/sdk/test/flags.test.ts b/sdk/test/flags.test.ts index ad3165cb4b..acaedbefc1 100644 --- a/sdk/test/flags.test.ts +++ b/sdk/test/flags.test.ts @@ -106,6 +106,25 @@ describe('parseArgs', () => { expect(parsed.archiveFiles).toEqual(['libbar.a']); }); + it('retains the original order of forwarded linker inputs and controls', () => { + const parsed = parseArgs([ + 'main.o', + '-Wl,--start-group', + '-lfoo', + 'libbar.a', + '-Wl,--end-group', + '-o', + 'out.wasm', + ]); + expect(parsed.forwardedArgs).toEqual([ + 'main.o', + '-Wl,--start-group', + '-lfoo', + 'libbar.a', + '-Wl,--end-group', + ]); + }); + it('handles -ofilename (no space) syntax', () => { const parsed = parseArgs(['-c', 'foo.c', '-ofoo.o']); expect(parsed.outputFile).toBe('foo.o'); diff --git a/sdk/test/integration.test.ts b/sdk/test/integration.test.ts index 416b446c3d..1a1280aa41 100644 --- a/sdk/test/integration.test.ts +++ b/sdk/test/integration.test.ts @@ -172,4 +172,83 @@ describe('integration: compile C program', () => { try { unlinkSync(srcFile); } catch {} try { unlinkSync(objFile); } catch {} }, 30_000); + + it('keeps direct helper objects ahead of dependent static libraries', async () => { + const toolchain = await resolveToolchain(); + mkdirSync(TMP_DIR, { recursive: true }); + + const mainSource = join(TMP_DIR, 'link-order-main.c'); + const mainObject = join(TMP_DIR, 'link-order-main.o'); + const directHelperSource = join(TMP_DIR, 'link-order-direct-helper.c'); + const directHelperObject = join(TMP_DIR, 'link-order-direct-helper.o'); + const apiSource = join(TMP_DIR, 'link-order-api.c'); + const apiObject = join(TMP_DIR, 'link-order-api.o'); + const archiveHelperSource = join(TMP_DIR, 'link-order-archive-helper.c'); + const archiveHelperObject = join(TMP_DIR, 'link-order-archive-helper.o'); + const providerArchive = join(TMP_DIR, 'liblink-order-provider.a'); + const output = join(TMP_DIR, 'link-order.wasm'); + const paths = [ + mainSource, + mainObject, + directHelperSource, + directHelperObject, + apiSource, + apiObject, + archiveHelperSource, + archiveHelperObject, + providerArchive, + output, + ]; + + writeFileSync(mainSource, ` + extern int link_order_api(void); + int main(void) { return link_order_api(); } + `); + writeFileSync(directHelperSource, ` + int link_order_helper(void) { return 42; } + `); + writeFileSync(apiSource, ` + extern int link_order_helper(void); + int link_order_api(void) { return link_order_helper() == 42 ? 0 : 1; } + `); + writeFileSync(archiveHelperSource, ` + int link_order_helper(void) { return 7; } + `); + + try { + for (const [source, object] of [ + [mainSource, mainObject], + [directHelperSource, directHelperObject], + [apiSource, apiObject], + [archiveHelperSource, archiveHelperObject], + ]) { + const compile = await run(toolchain.cc, buildClangArgs(['-c', source, '-o', object], toolchain)); + expect(compile.exitCode, compile.stderr).toBe(0); + } + const archive = await run(toolchain.ar, ['rcs', providerArchive, apiObject, archiveHelperObject]); + expect(archive.exitCode, archive.stderr).toBe(0); + + const linkArgs = [ + mainObject, + directHelperObject, + '-L', + TMP_DIR, + '-llink-order-provider', + '-o', + output, + ]; + await prepareExecutableLinker(linkArgs, toolchain); + const link = await run(toolchain.cc, buildClangArgs(linkArgs, toolchain)); + expect(link.exitCode, link.stderr).toBe(0); + + const module = new WebAssembly.Module(readFileSync(output)); + const imports = WebAssembly.Module.imports(module).map((entry) => entry.name); + expect(imports).not.toContain('link_order_api'); + expect(imports).not.toContain('link_order_helper'); + } finally { + for (const path of paths) { + try { unlinkSync(path); } catch {} + } + } + }, 30_000); }); From a27676e3b397996417397c0428a46ea12bf54ecc Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Mon, 13 Jul 2026 22:42:25 -0400 Subject: [PATCH 2/2] sdk: preserve pthread compiler semantics Forward -pthread to Clang so configure probes receive the standard _REENTRANT definition while keeping -lpthread as the existing musl compatibility no-op. Keep the source and in-guest SDK drivers aligned and cover the Autoconf compile-and-link shape. --- docs/sdk-guide.md | 11 ++++++-- sdk/kandelo/bin/wasm32posix-cc | 6 ++++- sdk/src/lib/flags.ts | 2 +- sdk/test/cc.test.ts | 18 ++++++++++--- sdk/test/flags.test.ts | 4 +-- sdk/test/integration.test.ts | 47 ++++++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/docs/sdk-guide.md b/docs/sdk-guide.md index f17eaac35f..d371dccbb0 100644 --- a/docs/sdk-guide.md +++ b/docs/sdk-guide.md @@ -230,7 +230,7 @@ The count is a resource limit, not a static memory slab reservation. The host dy ### Flags silently ignored These flags are common in build systems but irrelevant for Wasm: -- `-pthread`, `-lpthread` (threads are host-managed) +- `-lpthread` (pthread symbols are provided by musl's `libc.a`) - `-fPIE`, `-pie` (no position-independent executables in Wasm) - `-lrt`, `-lresolv`, `-lm`, `-lcrypt`, `-lutil` (all in musl libc.a) - `-rdynamic`, `-Wl,-Bsymbolic` @@ -238,6 +238,11 @@ These flags are common in build systems but irrelevant for Wasm: ## Autoconf Projects +The compiler drivers preserve `-pthread` so Clang supplies its standard +thread-aware compilation semantics, including defining `_REENTRANT`. The +separate `-lpthread` link flag remains an accepted no-op because Kandelo's musl +provides pthread symbols through `libc.a`. + Use `wasm32posix-configure` to run `./configure` with the correct cross-compilation settings: ```bash @@ -373,7 +378,9 @@ See the [Porting Guide](porting-guide.md) for preparing browser-facing package i ## Tips -- **Don't add `-pthread`**: Thread creation is host-managed via `clone()`. The SDK silently ignores `-pthread`. +- **Use `-pthread` when the build requires it**: The SDK forwards it to Clang + for standard compiler semantics such as `_REENTRANT`; pthread symbols still + come from musl's `libc.a`. - **Use `-O2` or `-Os`**: Unoptimized Wasm is significantly slower and larger. - **Check build script examples**: `packages/registry/` contains complete build scripts for 12 real-world libraries including autoconf, CMake, and plain Makefile projects. - **For fork support**: Run `scripts/run-wasm-fork-instrument.sh` as the final diff --git a/sdk/kandelo/bin/wasm32posix-cc b/sdk/kandelo/bin/wasm32posix-cc index 6727457329..fb503aa1f6 100755 --- a/sdk/kandelo/bin/wasm32posix-cc +++ b/sdk/kandelo/bin/wasm32posix-cc @@ -90,7 +90,11 @@ while [[ "$#" -gt 0 ]]; do --kandelo-thread-slots=*|--wasm-posix-thread-slots=*) thread_slots_decl="$(parse_thread_slots_decl "${arg#*=}")" ;; - -pthread|-lpthread|-rdynamic) + -pthread) + raw_threads_or_dynamic=1 + filtered+=("$arg") + ;; + -lpthread|-rdynamic) raw_threads_or_dynamic=1 ;; -fPIE|-pie|-lrt|-lresolv|-lm|-lcrypt|-lutil|-Wl,-Bsymbolic) diff --git a/sdk/src/lib/flags.ts b/sdk/src/lib/flags.ts index 444ae5784e..2f7e584210 100644 --- a/sdk/src/lib/flags.ts +++ b/sdk/src/lib/flags.ts @@ -62,7 +62,7 @@ export const SHARED_LINK_FLAGS: string[] = [ ]; const IGNORED_EXACT = new Set([ - '-pthread', '-lpthread', + '-lpthread', '-fPIE', '-pie', '-lrt', '-lresolv', '-lm', '-lcrypt', '-lutil', '-rdynamic', '-Wl,-Bsymbolic', diff --git a/sdk/test/cc.test.ts b/sdk/test/cc.test.ts index 0ae07c0e89..d54fd310d4 100644 --- a/sdk/test/cc.test.ts +++ b/sdk/test/cc.test.ts @@ -65,9 +65,10 @@ describe('buildClangArgs', () => { expect(args).not.toContain('-Wl,--entry=_start'); }); - it('filters ignored flags', () => { - const args = buildClangArgs(['-c', '-pthread', '-fPIC', 'foo.c'], toolchain); - expect(args).not.toContain('-pthread'); + it('preserves -pthread compiler semantics while filtering -lpthread', () => { + const args = buildClangArgs(['-c', '-pthread', '-lpthread', '-fPIC', 'foo.c'], toolchain); + expect(args).toContain('-pthread'); + expect(args).not.toContain('-lpthread'); expect(args).toContain('-fPIC'); }); @@ -114,6 +115,17 @@ describe('buildClangArgs', () => { expect(script).not.toContain('WASM_LD="$(find_tool wasm-ld'); }); + it('preserves -pthread in the packaged SDK compiler path', () => { + const script = readFileSync( + join(import.meta.dirname, '../kandelo/bin/wasm32posix-cc'), + 'utf8', + ); + + expect(script).toContain(`-pthread) + raw_threads_or_dynamic=1 + filtered+=("$arg")`); + }); + it('preserves stack-after-data layout with LLD 22 and newer', () => { const args = buildClangArgs( ['foo.c', '-o', 'foo.wasm'], diff --git a/sdk/test/flags.test.ts b/sdk/test/flags.test.ts index acaedbefc1..578ecd1762 100644 --- a/sdk/test/flags.test.ts +++ b/sdk/test/flags.test.ts @@ -17,9 +17,9 @@ describe('filterArgs', () => { expect(result.warnings).toEqual([]); }); - it('silently removes ignored flags', () => { + it('preserves -pthread while silently removing target no-ops', () => { const result = filterArgs(['-O2', '-pthread', '-fPIE', '-pie', 'main.c']); - expect(result.filtered).toEqual(['-O2', 'main.c']); + expect(result.filtered).toEqual(['-O2', '-pthread', 'main.c']); expect(result.warnings).toEqual([]); }); diff --git a/sdk/test/integration.test.ts b/sdk/test/integration.test.ts index 1a1280aa41..c09dd9b671 100644 --- a/sdk/test/integration.test.ts +++ b/sdk/test/integration.test.ts @@ -251,4 +251,51 @@ describe('integration: compile C program', () => { } } }, 30_000); + + it('preserves Autoconf pthread compiler semantics', async () => { + const toolchain = await resolveToolchain(); + mkdirSync(TMP_DIR, { recursive: true }); + + const srcFile = join(TMP_DIR, 'autoconf-pthread.c'); + const outFile = join(TMP_DIR, 'autoconf-pthread.wasm'); + writeFileSync(srcFile, ` + #include + + #ifndef _REENTRANT + #error "-pthread must define _REENTRANT" + #endif + + static void *start(void *arg) { + return arg; + } + + int main(void) { + pthread_attr_t attr; + pthread_t thread; + void *result = 0; + + if (pthread_attr_init(&attr) != 0) return 1; + if (pthread_create(&thread, &attr, start, 0) != 0) return 2; + if (pthread_join(thread, &result) != 0) return 3; + return pthread_attr_destroy(&attr); + } + `); + + try { + const userArgs = ['-pthread', srcFile, '-lpthread', '-o', outFile]; + await prepareExecutableLinker(userArgs, toolchain); + const args = buildClangArgs(userArgs, toolchain); + const result = await run(toolchain.cc, args); + if (result.exitCode !== 0) { + console.error('clang stderr:', result.stderr); + } + expect(args).toContain('-pthread'); + expect(args).not.toContain('-lpthread'); + expect(result.exitCode).toBe(0); + expect(existsSync(outFile)).toBe(true); + } finally { + try { unlinkSync(srcFile); } catch {} + try { unlinkSync(outFile); } catch {} + } + }, 30_000); });