Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/sdk-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -225,14 +230,19 @@ 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`
- `-Wl,-rpath,*`, `-Wl,-soname,*`, `-Wl,--version-script*`

## 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
Expand Down Expand Up @@ -368,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
Expand Down
6 changes: 5 additions & 1 deletion sdk/kandelo/bin/wasm32posix-cc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions sdk/src/bin/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions sdk/src/lib/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -162,6 +164,7 @@ export function parseArgs(args: string[]): ParsedArgs {
objectFiles: [],
archiveFiles: [],
otherArgs: [],
forwardedArgs: [],
};

for (let i = 0; i < args.length; i++) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
}
}

Expand Down
31 changes: 28 additions & 3 deletions sdk/test/cc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,28 @@ 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');
});

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');
});

Expand Down Expand Up @@ -101,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'],
Expand Down
23 changes: 21 additions & 2 deletions sdk/test/flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});

Expand Down Expand Up @@ -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');
Expand Down
126 changes: 126 additions & 0 deletions sdk/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,130 @@ 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);

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 <pthread.h>

#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);
});
Loading