From 8afd8609ac4c6fc5c2c2d148563bceb3acf66e38 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Jul 2026 09:39:50 -0700 Subject: [PATCH] [WebAssembly] Default export tables with `--cooperative-threading` This commit is a change to `wasm-ld`'s behavior when the `--cooperative-threading` flag is passed to the linker. The change here is to by default work as if `--export-table` was passed as well. This is required conventionally on this target because the table is where function pointers are read from in the component model `thread.new-indirect` intrinsic. If the table is not exported then there's no way to turn the core module into a component so it's effectively required. This behavior only applies to when the table isn't otherwise imported, for example in shared libraries. The other motivation behind this change is that it'll avoid the need to manually specify `-Wl,--export-table` when compiling for the `wasm32-wasip3` target. This additionally avoids the need for the Clang driver to figure out if flags like `--import-table` were otherwise passed. Basically it seemed best to put this in `wasm-ld` itself to avoid as little juggling of pieces as necessary. cc WebAssembly/wasi-libc#808 --- lld/test/wasm/cooperative-threading.s | 34 ++++++++++++++++++++++++++- lld/wasm/Driver.cpp | 8 +++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lld/test/wasm/cooperative-threading.s b/lld/test/wasm/cooperative-threading.s index 8b0f7eb1c256f0..a4afb01dc22648 100644 --- a/lld/test/wasm/cooperative-threading.s +++ b/lld/test/wasm/cooperative-threading.s @@ -2,7 +2,7 @@ # thread-context globals (__init_stack_pointer, __init_tls_base, etc.) and # works without --shared-memory and atomics. -# RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s +# RUN: llvm-mc -mattr=+call-indirect-overlong -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s # RUN: wasm-ld --cooperative-threading -no-gc-sections -o %t.wasm %t.o # RUN: obj2yaml %t.wasm | FileCheck %s # RUN: llvm-objdump -d --no-print-imm-hex --no-show-raw-insn %t.wasm | FileCheck %s --check-prefix=DIS @@ -11,12 +11,22 @@ # RUN: not wasm-ld --cooperative-threading --shared-memory %t.o -o %t2.wasm 2>&1 | FileCheck %s --check-prefix=INCOMPAT # INCOMPAT: --cooperative-threading is incompatible with --shared-memory +.globl __indirect_function_table +.tabletype __indirect_function_table, funcref + .globl __wasm_get_tls_base __wasm_get_tls_base: .functype __wasm_get_tls_base () -> (i32) i32.const 0 end_function +.globl do_call_indirect +do_call_indirect: + .functype do_call_indirect () -> () + i32.const 1 + call_indirect __indirect_function_table, () -> () + end_function + .globl _start _start: .functype _start () -> (i32) @@ -66,12 +76,23 @@ foo: .int8 7 .ascii "atomics" +# CHECK: - Type: TABLE +# CHECK-NEXT: Tables: +# CHECK-NEXT: - Index: 0 +# CHECK-NEXT: ElemType: FUNCREF + # Memory must NOT be marked as shared. # CHECK: - Type: MEMORY # CHECK-NEXT: Memories: # CHECK-NEXT: - Minimum: 0x2 # CHECK-NOT: Shared +# The function table is exported by default. +# CHECK: - Type: EXPORT +# CHECK: - Name: __indirect_function_table +# CHECK-NEXT: Kind: TABLE +# CHECK-NEXT: Index: 0 + # Only TLS needs a passive data segment; .data stays active and .bss gets no # segment at all since memory is only instantiated once and starts zeroed. # CHECK: - Type: DATACOUNT @@ -118,3 +139,14 @@ foo: # DIS-NEXT: i32.load 0 # DIS-NEXT: i32.add # DIS-NEXT: end + +# When the table is imported instead there is no need to also export it. +# RUN: wasm-ld --cooperative-threading --import-table -no-gc-sections -o %t3.wasm %t.o +# RUN: obj2yaml %t3.wasm | FileCheck %s --check-prefix=IMPORT-TABLE + +# When the table is imported instead there is no need to also export it. +# IMPORT-TABLE: - Type: IMPORT +# IMPORT-TABLE: - Module: env +# IMPORT-TABLE-NEXT: Field: __indirect_function_table +# IMPORT-TABLE-NEXT: Kind: TABLE +# IMPORT-TABLE-NOT: Kind: TABLE diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp index 9a2e3a82a9279f..c213d7ca0b0f39 100644 --- a/lld/wasm/Driver.cpp +++ b/lld/wasm/Driver.cpp @@ -759,6 +759,14 @@ static void setConfigs() { if (ctx.arg.sharedMemory) error("--cooperative-threading is incompatible with --shared-memory"); ctx.arg.libcallThreadContext = true; + + // Cooperative threading requires the table is either imported or exported + // or otherwise there's no way for embedders to read spawned functions from + // the table. If we've gotten this far and the table isn't otherwise + // imported (e.g in `isPic` mode) then export the table instead to ensure + // that it's visible to the outside world. + if (!ctx.arg.importTable) + ctx.arg.exportTable = true; } }