Skip to content

[WebAssembly] Cooperative threading for WASIP3 - #200855

Merged
sbc100 merged 30 commits into
llvm:mainfrom
TartanLlama:sy/wasip3-coop
Jun 23, 2026
Merged

[WebAssembly] Cooperative threading for WASIP3#200855
sbc100 merged 30 commits into
llvm:mainfrom
TartanLlama:sy/wasip3-coop

Conversation

@TartanLlama

Copy link
Copy Markdown
Contributor

This PR builds on the changes to allow libcall thread context from https://github.com/llvm/llvm-project/pull/175800/changes and adds the necessary changes to support cooperative multithreading in the WASIP3 target:

  • Not marking memory as shared
  • Allowing thread local accesses without atomics
  • Only using passive segments for TLS segments

The linker changes are supported by a new flag called --cooperative-multithreading. We talked about having two flags, one for the --libcall-thread-context part and one for the cooperative multithreading part. For now, I've simply replaced the --libcall-thread-context flag with the --cooperative-multithreading one and kept the internal configuration intact for simplicity.

@llvmorg-github-actions llvmorg-github-actions Bot added lld backend:WebAssembly clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' lld:wasm labels Jun 1, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-lld-wasm

@llvm/pr-subscribers-lld

Author: Sy Brand (TartanLlama)

Changes

This PR builds on the changes to allow libcall thread context from https://github.com/llvm/llvm-project/pull/175800/changes and adds the necessary changes to support cooperative multithreading in the WASIP3 target:

  • Not marking memory as shared
  • Allowing thread local accesses without atomics
  • Only using passive segments for TLS segments

The linker changes are supported by a new flag called --cooperative-multithreading. We talked about having two flags, one for the --libcall-thread-context part and one for the cooperative multithreading part. For now, I've simply replaced the --libcall-thread-context flag with the --cooperative-multithreading one and kept the internal configuration intact for simplicity.


Patch is 22.26 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200855.diff

16 Files Affected:

  • (modified) clang/lib/Driver/ToolChains/WebAssembly.cpp (+10-5)
  • (modified) clang/test/Driver/wasm-toolchain.c (+7)
  • (added) lld/test/wasm/cooperative-multithreading.s (+85)
  • (modified) lld/test/wasm/stack-pointer-abi.s (+1-1)
  • (modified) lld/test/wasm/thread-context-abi-mismatch.s (+2-3)
  • (modified) lld/test/wasm/tls-libcall.s (+1-1)
  • (modified) lld/wasm/Config.h (+5)
  • (modified) lld/wasm/Driver.cpp (+11-3)
  • (modified) lld/wasm/Options.td (+2-2)
  • (modified) lld/wasm/Relocations.cpp (+1-1)
  • (modified) lld/wasm/SyntheticSections.cpp (+7-6)
  • (modified) lld/wasm/Writer.cpp (+19-12)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp (+5-2)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h (+4)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp (+9-2)
  • (added) llvm/test/CodeGen/WebAssembly/cooperative-strip-tls.ll (+20)
diff --git a/clang/lib/Driver/ToolChains/WebAssembly.cpp b/clang/lib/Driver/ToolChains/WebAssembly.cpp
index 4c1cd937e81aa..d8f23175eb58b 100644
--- a/clang/lib/Driver/ToolChains/WebAssembly.cpp
+++ b/clang/lib/Driver/ToolChains/WebAssembly.cpp
@@ -88,11 +88,16 @@ static bool WantsPthread(const llvm::Triple &Triple, const ArgList &Args) {
   return WantsPthread;
 }
 
-static bool WantsLibcallThreadContext(const llvm::Triple &Triple,
-                                      const ArgList &Args) {
+static bool WantsCooperativeMultithreading(const llvm::Triple &Triple,
+                                           const ArgList &Args) {
   return Triple.getOS() == llvm::Triple::WASIp3;
 }
 
+static bool WantsSharedMemory(const llvm::Triple &Triple, const ArgList &Args) {
+  return WantsPthread(Triple, Args) &&
+         !WantsCooperativeMultithreading(Triple, Args);
+}
+
 void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
                                 const InputInfo &Output,
                                 const InputInfoList &Inputs,
@@ -174,10 +179,10 @@ void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
 
   AddLinkerInputs(ToolChain, Inputs, Args, CmdArgs, JA);
 
-  if (WantsLibcallThreadContext(ToolChain.getTriple(), Args))
-    CmdArgs.push_back("--libcall-thread-context");
+  if (WantsCooperativeMultithreading(ToolChain.getTriple(), Args))
+    CmdArgs.push_back("--cooperative-multithreading");
 
-  if (WantsPthread(ToolChain.getTriple(), Args))
+  if (WantsSharedMemory(ToolChain.getTriple(), Args))
     CmdArgs.push_back("--shared-memory");
 
   if (!Args.hasArg(options::OPT_nostdlib, options::OPT_nodefaultlibs)) {
diff --git a/clang/test/Driver/wasm-toolchain.c b/clang/test/Driver/wasm-toolchain.c
index 29a94aeec77a9..40d75da3166d9 100644
--- a/clang/test/Driver/wasm-toolchain.c
+++ b/clang/test/Driver/wasm-toolchain.c
@@ -303,3 +303,10 @@
 // RUN:   | FileCheck -check-prefix=LINK_WALI_BASIC %s
 // LINK_WALI_BASIC: "-cc1" {{.*}} "-o" "[[temp:[^"]*]]"
 // LINK_WALI_BASIC: wasm-ld{{.*}}" "-L/foo/lib/wasm32-linux-muslwali" "crt1.o" "[[temp]]" "-lc" "{{.*[/\\]}}libclang_rt.builtins.a" "-o" "a.out"
+
+// Test that `wasm32-wasip3` passes `--cooperative-multithreading` to the linker.
+
+// RUN: %clang -### --target=wasm32-wasip3 -fuse-ld=lld %s --sysroot /foo 2>&1 \
+// RUN:   | FileCheck -check-prefix=LINK_WASIP3_COOP %s
+// LINK_WASIP3_COOP: wasm-ld{{.*}}" {{.*}} "--cooperative-multithreading"
+// LINK_WASIP3_COOP-NOT: "--libcall-thread-context"
diff --git a/lld/test/wasm/cooperative-multithreading.s b/lld/test/wasm/cooperative-multithreading.s
new file mode 100644
index 0000000000000..5adfaa99e40a0
--- /dev/null
+++ b/lld/test/wasm/cooperative-multithreading.s
@@ -0,0 +1,85 @@
+# Test that --cooperative-multithreading uses the libcall ABI naming for
+# 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: wasm-ld --cooperative-multithreading -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
+
+# Test that --cooperative-multithreading and --shared-memory are mutually exclusive.
+# RUN: not wasm-ld --cooperative-multithreading --shared-memory %t.o -o %t2.wasm 2>&1 | FileCheck %s --check-prefix=INCOMPAT
+# INCOMPAT: --cooperative-multithreading is incompatible with --shared-memory
+
+.globl         __wasm_get_tls_base
+__wasm_get_tls_base:
+    .functype   __wasm_get_tls_base () -> (i32)
+    i32.const 0
+    end_function
+
+.globl _start
+_start:
+  .functype _start () -> (i32)
+  call __wasm_get_tls_base
+  i32.const tls1@TLSREL
+  i32.add
+  i32.load 0
+  call __wasm_get_tls_base
+  i32.const tls2@TLSREL
+  i32.add
+  i32.load 0
+  i32.add
+  end_function
+
+.section  .tdata.tls1,"",@
+.globl  tls1
+tls1:
+  .int32  1
+  .size tls1, 4
+
+.section  .tdata.tls2,"",@
+.globl  tls2
+tls2:
+  .int32  2
+  .size tls2, 4
+
+.section  .custom_section.target_features,"",@
+  .int8 2
+  .int8 43
+  .int8 11
+  .ascii  "bulk-memory"
+  .int8 43
+  .int8 7
+  .ascii  "atomics"
+
+# Memory must NOT be marked as shared.
+# CHECK:      - Type:            MEMORY
+# CHECK-NEXT:   Memories:
+# CHECK-NEXT:     - Minimum:         0x2
+# CHECK-NOT:       Shared:          false
+
+# Globals should use the libcall ABI naming, not the global ABI.
+# CHECK:      GlobalNames:
+# CHECK-NEXT:      - Index:           0
+# CHECK-NEXT:        Name:            __init_stack_pointer
+# CHECK-NEXT:      - Index:           1
+# CHECK-NEXT:        Name:            __init_tls_base
+# CHECK-NEXT:      - Index:           2
+# CHECK-NEXT:        Name:            __tls_size
+# CHECK-NEXT:      - Index:           3
+# CHECK-NEXT:        Name:            __tls_align
+
+# DIS-LABEL: <__wasm_init_memory>:
+
+# DIS-LABEL: <_start>:
+# DIS-EMPTY:
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       4
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       end
diff --git a/lld/test/wasm/stack-pointer-abi.s b/lld/test/wasm/stack-pointer-abi.s
index 869f972710991..fbae0475bcba2 100644
--- a/lld/test/wasm/stack-pointer-abi.s
+++ b/lld/test/wasm/stack-pointer-abi.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --no-gc-sections -o %t.libcall.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --no-gc-sections -o %t.libcall.wasm %t.o
 # RUN: obj2yaml %t.libcall.wasm | FileCheck %s --check-prefix=LIBCALL
 # RUN: wasm-ld --no-gc-sections -o %t.global.wasm %t.o
 # RUN: obj2yaml %t.global.wasm | FileCheck %s --check-prefix=GLOBAL
diff --git a/lld/test/wasm/thread-context-abi-mismatch.s b/lld/test/wasm/thread-context-abi-mismatch.s
index 069534cbe5762..a817ca4407aab 100644
--- a/lld/test/wasm/thread-context-abi-mismatch.s
+++ b/lld/test/wasm/thread-context-abi-mismatch.s
@@ -3,10 +3,9 @@
 # as an indication that the global thread context ABI is being used.
 
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: not wasm-ld --libcall-thread-context %t.o -o %t.wasm 2>&1 | FileCheck %s
-
-# CHECK: object file uses globals for thread context, but --libcall-thread-context was specified
+# RUN: not wasm-ld --cooperative-multithreading %t.o -o %t.wasm 2>&1 | FileCheck %s
 
+# CHECK: object file uses globals for thread context, but --cooperative-multithreading was specified
 .globl _start
 _start:
   .functype _start () -> ()
diff --git a/lld/test/wasm/tls-libcall.s b/lld/test/wasm/tls-libcall.s
index df8b8f8be0207..a0a7f37379bac 100644
--- a/lld/test/wasm/tls-libcall.s
+++ b/lld/test/wasm/tls-libcall.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --shared-memory -no-gc-sections -o %t.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --shared-memory -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
 
diff --git a/lld/wasm/Config.h b/lld/wasm/Config.h
index 71a378a412e9e..d4789b88203eb 100644
--- a/lld/wasm/Config.h
+++ b/lld/wasm/Config.h
@@ -65,6 +65,7 @@ struct Config {
   bool growableTable;
   bool gcSections;
   llvm::StringSet<> keepSections;
+  bool cooperativeMultithreading;
   bool libcallThreadContext;
   std::optional<std::pair<llvm::StringRef, llvm::StringRef>> memoryImport;
   std::optional<llvm::StringRef> memoryExport;
@@ -134,6 +135,10 @@ struct Config {
   std::optional<std::vector<std::string>> features;
   std::optional<std::vector<std::string>> extraFeatures;
   llvm::SmallVector<uint8_t, 0> buildIdVector;
+
+  bool isMultithreaded() const {
+    return sharedMemory || cooperativeMultithreading;
+  }
 };
 
 // The Ctx object hold all other (non-configuration) global state.
diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp
index fe1e2eec95037..b2723220b5afb 100644
--- a/lld/wasm/Driver.cpp
+++ b/lld/wasm/Driver.cpp
@@ -561,7 +561,9 @@ static void readConfigs(opt::InputArgList &args) {
   ctx.arg.soName = args.getLastArgValue(OPT_soname);
   ctx.arg.importTable = args.hasArg(OPT_import_table);
   ctx.arg.importUndefined = args.hasArg(OPT_import_undefined);
-  ctx.arg.libcallThreadContext = args.hasArg(OPT_libcall_thread_context);
+  ctx.arg.cooperativeMultithreading =
+      args.hasArg(OPT_cooperative_multithreading);
+  ;
   ctx.arg.ltoo = args::getInteger(args, OPT_lto_O, 2);
   if (ctx.arg.ltoo > 3)
     error("invalid optimization level for LTO: " + Twine(ctx.arg.ltoo));
@@ -755,6 +757,12 @@ static void setConfigs() {
   if (!ctx.arg.memoryExport.has_value() && !ctx.arg.memoryImport.has_value()) {
     ctx.arg.memoryExport = memoryName;
   }
+  if (ctx.arg.cooperativeMultithreading) {
+    if (ctx.arg.sharedMemory)
+      error(
+          "--cooperative-multithreading is incompatible with --shared-memory");
+    ctx.arg.libcallThreadContext = true;
+  }
 }
 
 // Some command line options or some combinations of them are not allowed.
@@ -964,7 +972,7 @@ static void createSyntheticSymbols() {
         createGlobalVariable(stack_pointer_name, !ctx.arg.libcallThreadContext);
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     // TLS symbols are all hidden/dso-local
     auto tls_base_name =
         ctx.arg.libcallThreadContext ? "__init_tls_base" : "__tls_base";
@@ -1028,7 +1036,7 @@ static void createOptionalSymbols() {
   //
   // __tls_size and __tls_align are not needed in this case since they are only
   // needed for __wasm_init_tls (which we do not create in this case).
-  if (!ctx.arg.sharedMemory)
+  if (!ctx.sym.tlsBase)
     ctx.sym.tlsBase = createOptionalGlobal("__tls_base", false);
 }
 
diff --git a/lld/wasm/Options.td b/lld/wasm/Options.td
index 144eee33061e1..6d18a0400ef97 100644
--- a/lld/wasm/Options.td
+++ b/lld/wasm/Options.td
@@ -238,8 +238,8 @@ def page_size: JJ<"page-size=">,
 def initial_memory: JJ<"initial-memory=">,
   HelpText<"Initial size of the linear memory">;
 
-def libcall_thread_context: FF<"libcall-thread-context">,
-  HelpText<"Use library calls for thread context access instead of globals.">;
+def cooperative_multithreading: FF<"cooperative-multithreading">,
+  HelpText<"Enable cooperative multithreading.">;
   
 def max_memory: JJ<"max-memory=">,
   HelpText<"Maximum size of the linear memory">;
diff --git a/lld/wasm/Relocations.cpp b/lld/wasm/Relocations.cpp
index a1840abe88b3a..cb597fdeffcf3 100644
--- a/lld/wasm/Relocations.cpp
+++ b/lld/wasm/Relocations.cpp
@@ -125,7 +125,7 @@ void scanRelocations(InputChunk *chunk) {
       // In single-threaded builds TLS is lowered away and TLS data can be
       // merged with normal data and allowing TLS relocation in non-TLS
       // segments.
-      if (ctx.arg.sharedMemory) {
+      if (ctx.arg.isMultithreaded()) {
         if (!sym->isTLS()) {
           error(toString(file) + ": relocation " +
                 relocTypeToString(reloc.Type) +
diff --git a/lld/wasm/SyntheticSections.cpp b/lld/wasm/SyntheticSections.cpp
index d1a01c7ec3f9d..050f61c7f5c56 100644
--- a/lld/wasm/SyntheticSections.cpp
+++ b/lld/wasm/SyntheticSections.cpp
@@ -57,7 +57,7 @@ void writeGetTLSBase(const Ctx &ctx, raw_ostream &os) {
     writeU8(os, WASM_OPCODE_CALL, "call");
     writeUleb128(os, ctx.sym.getTLSBase->getFunctionIndex(), "function index");
   } else {
-    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_SET");
+    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_GET");
     writeUleb128(os, ctx.sym.tlsBase->getGlobalIndex(), "__tls_base");
   }
 }
@@ -532,7 +532,7 @@ void GlobalSection::writeBody() {
         mutable_ = true;
       // With multi-threading any TLS globals must be mutable since they get
       // set during `__wasm_apply_global_tls_relocs`
-      if (ctx.arg.sharedMemory && sym->isTLS())
+      if (ctx.arg.isMultithreaded() && sym->isTLS())
         mutable_ = true;
     }
     WasmGlobalType type{itype, mutable_};
@@ -569,10 +569,11 @@ void GlobalSection::writeBody() {
     } else {
       WasmInitExpr initExpr;
       if (auto *d = dyn_cast<DefinedData>(sym))
-        // In the sharedMemory case TLS globals are set during
-        // `__wasm_apply_global_tls_relocs`, but in the non-shared case
+        // In the multithreaded case, TLS globals are set during
+        // `__wasm_apply_global_tls_relocs`, but in the single-threaded case
         // we know the absolute value at link time.
-        initExpr = intConst(d->getVA(/*absolute=*/!ctx.arg.sharedMemory), is64);
+        initExpr =
+            intConst(d->getVA(/*absolute=*/!ctx.arg.isMultithreaded()), is64);
       else if (auto *f = dyn_cast<FunctionSymbol>(sym))
         initExpr = intConst(f->isStub ? 0 : f->getTableIndex(), is64);
       else {
@@ -680,7 +681,7 @@ bool DataCountSection::isNeeded() const {
   // instructions are not yet supported in input files.  However, in the case
   // of shared memory, lld itself will generate these instructions as part of
   // `__wasm_init_memory`. See Writer::createInitMemoryFunction.
-  return numSegments && ctx.arg.sharedMemory;
+  return numSegments && ctx.arg.isMultithreaded();
 }
 
 void LinkingSection::writeBody() {
diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp
index 688bb829e1c42..d90ca859f3479 100644
--- a/lld/wasm/Writer.cpp
+++ b/lld/wasm/Writer.cpp
@@ -425,7 +425,7 @@ void Writer::layoutMemory() {
   // Even in the absense of any actual TLS data, this symbol can still be
   // referenced (for example by __builtin_thread_pointer, which should not
   // return NULL).
-  if (!ctx.arg.sharedMemory && ctx.sym.tlsBase) {
+  if (!ctx.arg.isMultithreaded() && ctx.sym.tlsBase) {
     auto *tlsBase = cast<DefinedGlobal>(ctx.sym.tlsBase);
     setGlobalPtr(tlsBase, fixedTLSBase);
   }
@@ -653,7 +653,7 @@ void Writer::populateTargetFeatures() {
                  sym->importModule && sym->importModule == "env";
         }))
       error(fileName + ": object file uses globals for thread context, "
-                       "but --libcall-thread-context was specified");
+                       "but --cooperative-threading was specified");
   }
 
   if (inferFeatures)
@@ -1057,7 +1057,15 @@ static StringRef getOutputDataSegmentName(const InputChunk &seg) {
 OutputSegment *Writer::createOutputSegment(StringRef name) {
   LLVM_DEBUG(dbgs() << "new segment: " << name << "\n");
   OutputSegment *s = make<OutputSegment>(name);
-  if (ctx.arg.sharedMemory)
+  // In the shared memory case, all data segments must be passive since they
+  // will be initialized once by the main thread and then shared with other
+  // threads. In the non-shared memory case, we use passive segments only for
+  // TLS segments, so that they can be reused, and for .bss segments, which
+  // don't need to be included in the binary at all.
+  bool needsPassiveInit =
+      ctx.arg.sharedMemory || (ctx.arg.cooperativeMultithreading &&
+                               (s->isTLS() || s->name.starts_with(".bss")));
+  if (needsPassiveInit)
     s->initFlags = WASM_DATA_SEGMENT_IS_PASSIVE;
   if (!ctx.arg.relocatable && name.starts_with(".bss"))
     s->isBss = true;
@@ -1116,7 +1124,7 @@ void Writer::combineOutputSegments() {
   // This restriction does not apply when the extended const extension is
   // available: https://github.com/WebAssembly/extended-const
   assert(!ctx.arg.extendedConst);
-  assert(ctx.isPic && !ctx.arg.sharedMemory);
+  assert(ctx.isPic && !ctx.arg.isMultithreaded());
   if (segments.size() <= 1)
     return;
   OutputSegment *combined = make<OutputSegment>(".data");
@@ -1198,7 +1206,7 @@ void Writer::createSyntheticInitFunctions() {
     }
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     if (out.globalSec->needsTLSRelocations()) {
       ctx.sym.applyGlobalTLSRelocs = symtab->addSyntheticFunction(
           "__wasm_apply_global_tls_relocs", WASM_SYMBOL_VISIBILITY_HIDDEN,
@@ -1378,7 +1386,7 @@ void Writer::createInitMemoryFunction() {
         // When we initialize the TLS segment we also set the TLS base.
         // This allows the runtime to use this static copy of the TLS data
         // for the first/main thread.
-        if (ctx.arg.sharedMemory && s->isTLS()) {
+        if (ctx.arg.isMultithreaded() && s->isTLS()) {
           if (ctx.isPic) {
             // Cache the result of the addionion in local 0
             writeU8(os, WASM_OPCODE_LOCAL_TEE, "local.tee");
@@ -1449,7 +1457,7 @@ void Writer::createInitMemoryFunction() {
       if (needsPassiveInitialization(s) && !s->isBss) {
         // The TLS region should not be dropped since its is needed
         // during the initialization of each thread (__wasm_init_tls).
-        if (ctx.arg.sharedMemory && s->isTLS())
+        if (ctx.arg.isMultithreaded() && s->isTLS())
           continue;
         // data.drop instruction
         writeU8(os, WASM_OPCODE_MISC_PREFIX, "bulk-memory prefix");
@@ -1502,7 +1510,7 @@ void Writer::createApplyDataRelocationsFunction() {
     writeUleb128(os, 0, "num locals");
     bool generated = false;
     for (const OutputSegment *seg : segments)
-      if (!ctx.arg.sharedMemory || !seg->isTLS())
+      if (!ctx.arg.isMultithreaded() || !seg->isTLS())
         for (const InputChunk *inSeg : seg->inputSegments)
           generated |= inSeg->generateRelocationCode(os);
 
@@ -1658,7 +1666,6 @@ void Writer::createInitTLSFunction() {
     if (tlsSeg) {
       writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get");
       writeUleb128(os, 0, "local index");
-
       writeSetTLSBase(ctx, os);
 
       // FIXME(wvo): this local needs to be I64 in wasm64, or we need an extend
@@ -1791,9 +1798,9 @@ void Writer::run() {
   // `__memory_base` import.  Unless we support the extended const expression we
   // can't do addition inside the constant expression, so we much combine the
   // segments into a single one that can live at `__memory_base`.
-  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.sharedMemory) {
-    // In shared memory mode all data segments are passive and initialized
-    // via __wasm_init_memory.
+  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.isMultithreaded()) {
+    // In multithreaded modes (shared or cooperative), data segments may be
+    // passive and must not be combined into a single active segment.
     log("-- combineOutputSegments");
     combineOutputSegments();
   }
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
index 6326b7d76db82..9dea29fb0205d 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
@@ -40,9 +40,12 @@ WebAssemblySubtarget::initializeSubtargetDependencies(StringRef CPU,
 
   ParseSubtargetFeatures(CPU, /*TuneCPU*/ CPU, FS);
 
-  // WASIP3 implies using the libcall thread context.
-  if (TargetTriple.getOS() == Triple::WASIp3)
+  // WASIP3 uses cooperative multithreading, which implies using libcall
+  // thread context.
+  if (TargetTriple.getOS() == Triple::WASIp3) {
+    HasCooperativeMultithreading = true;
     HasLibcallThreadContext = true;
+  }
 
   FeatureBitset Bits = getFeatureBits();
 
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
index 5c6f4cb5b36ff..f637ce59ebfce 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
@@ -52,6 +52,7 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool HasExtendedConst = false;
   bool HasFP16 = false;
   bool HasGC = false;
+  bool HasCooperativeMultithreading = false;
   bool HasLibcallThreadContext = false;
   bool HasMultiMemory = false;
   bool HasMultivalue = false;
@@ -117,6 +118,9 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool hasExtendedConst() const { return HasExtendedConst; }
   bool hasFP16() const { return HasFP16; }
   bool hasGC() const { return HasGC; }
+  bool hasCooperativeMultithreading() const {
+    return HasCooperativeMultithre...
[truncated]

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-clang-driver

Author: Sy Brand (TartanLlama)

Changes

This PR builds on the changes to allow libcall thread context from https://github.com/llvm/llvm-project/pull/175800/changes and adds the necessary changes to support cooperative multithreading in the WASIP3 target:

  • Not marking memory as shared
  • Allowing thread local accesses without atomics
  • Only using passive segments for TLS segments

The linker changes are supported by a new flag called --cooperative-multithreading. We talked about having two flags, one for the --libcall-thread-context part and one for the cooperative multithreading part. For now, I've simply replaced the --libcall-thread-context flag with the --cooperative-multithreading one and kept the internal configuration intact for simplicity.


Patch is 22.26 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200855.diff

16 Files Affected:

  • (modified) clang/lib/Driver/ToolChains/WebAssembly.cpp (+10-5)
  • (modified) clang/test/Driver/wasm-toolchain.c (+7)
  • (added) lld/test/wasm/cooperative-multithreading.s (+85)
  • (modified) lld/test/wasm/stack-pointer-abi.s (+1-1)
  • (modified) lld/test/wasm/thread-context-abi-mismatch.s (+2-3)
  • (modified) lld/test/wasm/tls-libcall.s (+1-1)
  • (modified) lld/wasm/Config.h (+5)
  • (modified) lld/wasm/Driver.cpp (+11-3)
  • (modified) lld/wasm/Options.td (+2-2)
  • (modified) lld/wasm/Relocations.cpp (+1-1)
  • (modified) lld/wasm/SyntheticSections.cpp (+7-6)
  • (modified) lld/wasm/Writer.cpp (+19-12)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp (+5-2)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h (+4)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp (+9-2)
  • (added) llvm/test/CodeGen/WebAssembly/cooperative-strip-tls.ll (+20)
diff --git a/clang/lib/Driver/ToolChains/WebAssembly.cpp b/clang/lib/Driver/ToolChains/WebAssembly.cpp
index 4c1cd937e81aa..d8f23175eb58b 100644
--- a/clang/lib/Driver/ToolChains/WebAssembly.cpp
+++ b/clang/lib/Driver/ToolChains/WebAssembly.cpp
@@ -88,11 +88,16 @@ static bool WantsPthread(const llvm::Triple &Triple, const ArgList &Args) {
   return WantsPthread;
 }
 
-static bool WantsLibcallThreadContext(const llvm::Triple &Triple,
-                                      const ArgList &Args) {
+static bool WantsCooperativeMultithreading(const llvm::Triple &Triple,
+                                           const ArgList &Args) {
   return Triple.getOS() == llvm::Triple::WASIp3;
 }
 
+static bool WantsSharedMemory(const llvm::Triple &Triple, const ArgList &Args) {
+  return WantsPthread(Triple, Args) &&
+         !WantsCooperativeMultithreading(Triple, Args);
+}
+
 void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
                                 const InputInfo &Output,
                                 const InputInfoList &Inputs,
@@ -174,10 +179,10 @@ void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
 
   AddLinkerInputs(ToolChain, Inputs, Args, CmdArgs, JA);
 
-  if (WantsLibcallThreadContext(ToolChain.getTriple(), Args))
-    CmdArgs.push_back("--libcall-thread-context");
+  if (WantsCooperativeMultithreading(ToolChain.getTriple(), Args))
+    CmdArgs.push_back("--cooperative-multithreading");
 
-  if (WantsPthread(ToolChain.getTriple(), Args))
+  if (WantsSharedMemory(ToolChain.getTriple(), Args))
     CmdArgs.push_back("--shared-memory");
 
   if (!Args.hasArg(options::OPT_nostdlib, options::OPT_nodefaultlibs)) {
diff --git a/clang/test/Driver/wasm-toolchain.c b/clang/test/Driver/wasm-toolchain.c
index 29a94aeec77a9..40d75da3166d9 100644
--- a/clang/test/Driver/wasm-toolchain.c
+++ b/clang/test/Driver/wasm-toolchain.c
@@ -303,3 +303,10 @@
 // RUN:   | FileCheck -check-prefix=LINK_WALI_BASIC %s
 // LINK_WALI_BASIC: "-cc1" {{.*}} "-o" "[[temp:[^"]*]]"
 // LINK_WALI_BASIC: wasm-ld{{.*}}" "-L/foo/lib/wasm32-linux-muslwali" "crt1.o" "[[temp]]" "-lc" "{{.*[/\\]}}libclang_rt.builtins.a" "-o" "a.out"
+
+// Test that `wasm32-wasip3` passes `--cooperative-multithreading` to the linker.
+
+// RUN: %clang -### --target=wasm32-wasip3 -fuse-ld=lld %s --sysroot /foo 2>&1 \
+// RUN:   | FileCheck -check-prefix=LINK_WASIP3_COOP %s
+// LINK_WASIP3_COOP: wasm-ld{{.*}}" {{.*}} "--cooperative-multithreading"
+// LINK_WASIP3_COOP-NOT: "--libcall-thread-context"
diff --git a/lld/test/wasm/cooperative-multithreading.s b/lld/test/wasm/cooperative-multithreading.s
new file mode 100644
index 0000000000000..5adfaa99e40a0
--- /dev/null
+++ b/lld/test/wasm/cooperative-multithreading.s
@@ -0,0 +1,85 @@
+# Test that --cooperative-multithreading uses the libcall ABI naming for
+# 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: wasm-ld --cooperative-multithreading -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
+
+# Test that --cooperative-multithreading and --shared-memory are mutually exclusive.
+# RUN: not wasm-ld --cooperative-multithreading --shared-memory %t.o -o %t2.wasm 2>&1 | FileCheck %s --check-prefix=INCOMPAT
+# INCOMPAT: --cooperative-multithreading is incompatible with --shared-memory
+
+.globl         __wasm_get_tls_base
+__wasm_get_tls_base:
+    .functype   __wasm_get_tls_base () -> (i32)
+    i32.const 0
+    end_function
+
+.globl _start
+_start:
+  .functype _start () -> (i32)
+  call __wasm_get_tls_base
+  i32.const tls1@TLSREL
+  i32.add
+  i32.load 0
+  call __wasm_get_tls_base
+  i32.const tls2@TLSREL
+  i32.add
+  i32.load 0
+  i32.add
+  end_function
+
+.section  .tdata.tls1,"",@
+.globl  tls1
+tls1:
+  .int32  1
+  .size tls1, 4
+
+.section  .tdata.tls2,"",@
+.globl  tls2
+tls2:
+  .int32  2
+  .size tls2, 4
+
+.section  .custom_section.target_features,"",@
+  .int8 2
+  .int8 43
+  .int8 11
+  .ascii  "bulk-memory"
+  .int8 43
+  .int8 7
+  .ascii  "atomics"
+
+# Memory must NOT be marked as shared.
+# CHECK:      - Type:            MEMORY
+# CHECK-NEXT:   Memories:
+# CHECK-NEXT:     - Minimum:         0x2
+# CHECK-NOT:       Shared:          false
+
+# Globals should use the libcall ABI naming, not the global ABI.
+# CHECK:      GlobalNames:
+# CHECK-NEXT:      - Index:           0
+# CHECK-NEXT:        Name:            __init_stack_pointer
+# CHECK-NEXT:      - Index:           1
+# CHECK-NEXT:        Name:            __init_tls_base
+# CHECK-NEXT:      - Index:           2
+# CHECK-NEXT:        Name:            __tls_size
+# CHECK-NEXT:      - Index:           3
+# CHECK-NEXT:        Name:            __tls_align
+
+# DIS-LABEL: <__wasm_init_memory>:
+
+# DIS-LABEL: <_start>:
+# DIS-EMPTY:
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       4
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       end
diff --git a/lld/test/wasm/stack-pointer-abi.s b/lld/test/wasm/stack-pointer-abi.s
index 869f972710991..fbae0475bcba2 100644
--- a/lld/test/wasm/stack-pointer-abi.s
+++ b/lld/test/wasm/stack-pointer-abi.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --no-gc-sections -o %t.libcall.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --no-gc-sections -o %t.libcall.wasm %t.o
 # RUN: obj2yaml %t.libcall.wasm | FileCheck %s --check-prefix=LIBCALL
 # RUN: wasm-ld --no-gc-sections -o %t.global.wasm %t.o
 # RUN: obj2yaml %t.global.wasm | FileCheck %s --check-prefix=GLOBAL
diff --git a/lld/test/wasm/thread-context-abi-mismatch.s b/lld/test/wasm/thread-context-abi-mismatch.s
index 069534cbe5762..a817ca4407aab 100644
--- a/lld/test/wasm/thread-context-abi-mismatch.s
+++ b/lld/test/wasm/thread-context-abi-mismatch.s
@@ -3,10 +3,9 @@
 # as an indication that the global thread context ABI is being used.
 
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: not wasm-ld --libcall-thread-context %t.o -o %t.wasm 2>&1 | FileCheck %s
-
-# CHECK: object file uses globals for thread context, but --libcall-thread-context was specified
+# RUN: not wasm-ld --cooperative-multithreading %t.o -o %t.wasm 2>&1 | FileCheck %s
 
+# CHECK: object file uses globals for thread context, but --cooperative-multithreading was specified
 .globl _start
 _start:
   .functype _start () -> ()
diff --git a/lld/test/wasm/tls-libcall.s b/lld/test/wasm/tls-libcall.s
index df8b8f8be0207..a0a7f37379bac 100644
--- a/lld/test/wasm/tls-libcall.s
+++ b/lld/test/wasm/tls-libcall.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --shared-memory -no-gc-sections -o %t.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --shared-memory -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
 
diff --git a/lld/wasm/Config.h b/lld/wasm/Config.h
index 71a378a412e9e..d4789b88203eb 100644
--- a/lld/wasm/Config.h
+++ b/lld/wasm/Config.h
@@ -65,6 +65,7 @@ struct Config {
   bool growableTable;
   bool gcSections;
   llvm::StringSet<> keepSections;
+  bool cooperativeMultithreading;
   bool libcallThreadContext;
   std::optional<std::pair<llvm::StringRef, llvm::StringRef>> memoryImport;
   std::optional<llvm::StringRef> memoryExport;
@@ -134,6 +135,10 @@ struct Config {
   std::optional<std::vector<std::string>> features;
   std::optional<std::vector<std::string>> extraFeatures;
   llvm::SmallVector<uint8_t, 0> buildIdVector;
+
+  bool isMultithreaded() const {
+    return sharedMemory || cooperativeMultithreading;
+  }
 };
 
 // The Ctx object hold all other (non-configuration) global state.
diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp
index fe1e2eec95037..b2723220b5afb 100644
--- a/lld/wasm/Driver.cpp
+++ b/lld/wasm/Driver.cpp
@@ -561,7 +561,9 @@ static void readConfigs(opt::InputArgList &args) {
   ctx.arg.soName = args.getLastArgValue(OPT_soname);
   ctx.arg.importTable = args.hasArg(OPT_import_table);
   ctx.arg.importUndefined = args.hasArg(OPT_import_undefined);
-  ctx.arg.libcallThreadContext = args.hasArg(OPT_libcall_thread_context);
+  ctx.arg.cooperativeMultithreading =
+      args.hasArg(OPT_cooperative_multithreading);
+  ;
   ctx.arg.ltoo = args::getInteger(args, OPT_lto_O, 2);
   if (ctx.arg.ltoo > 3)
     error("invalid optimization level for LTO: " + Twine(ctx.arg.ltoo));
@@ -755,6 +757,12 @@ static void setConfigs() {
   if (!ctx.arg.memoryExport.has_value() && !ctx.arg.memoryImport.has_value()) {
     ctx.arg.memoryExport = memoryName;
   }
+  if (ctx.arg.cooperativeMultithreading) {
+    if (ctx.arg.sharedMemory)
+      error(
+          "--cooperative-multithreading is incompatible with --shared-memory");
+    ctx.arg.libcallThreadContext = true;
+  }
 }
 
 // Some command line options or some combinations of them are not allowed.
@@ -964,7 +972,7 @@ static void createSyntheticSymbols() {
         createGlobalVariable(stack_pointer_name, !ctx.arg.libcallThreadContext);
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     // TLS symbols are all hidden/dso-local
     auto tls_base_name =
         ctx.arg.libcallThreadContext ? "__init_tls_base" : "__tls_base";
@@ -1028,7 +1036,7 @@ static void createOptionalSymbols() {
   //
   // __tls_size and __tls_align are not needed in this case since they are only
   // needed for __wasm_init_tls (which we do not create in this case).
-  if (!ctx.arg.sharedMemory)
+  if (!ctx.sym.tlsBase)
     ctx.sym.tlsBase = createOptionalGlobal("__tls_base", false);
 }
 
diff --git a/lld/wasm/Options.td b/lld/wasm/Options.td
index 144eee33061e1..6d18a0400ef97 100644
--- a/lld/wasm/Options.td
+++ b/lld/wasm/Options.td
@@ -238,8 +238,8 @@ def page_size: JJ<"page-size=">,
 def initial_memory: JJ<"initial-memory=">,
   HelpText<"Initial size of the linear memory">;
 
-def libcall_thread_context: FF<"libcall-thread-context">,
-  HelpText<"Use library calls for thread context access instead of globals.">;
+def cooperative_multithreading: FF<"cooperative-multithreading">,
+  HelpText<"Enable cooperative multithreading.">;
   
 def max_memory: JJ<"max-memory=">,
   HelpText<"Maximum size of the linear memory">;
diff --git a/lld/wasm/Relocations.cpp b/lld/wasm/Relocations.cpp
index a1840abe88b3a..cb597fdeffcf3 100644
--- a/lld/wasm/Relocations.cpp
+++ b/lld/wasm/Relocations.cpp
@@ -125,7 +125,7 @@ void scanRelocations(InputChunk *chunk) {
       // In single-threaded builds TLS is lowered away and TLS data can be
       // merged with normal data and allowing TLS relocation in non-TLS
       // segments.
-      if (ctx.arg.sharedMemory) {
+      if (ctx.arg.isMultithreaded()) {
         if (!sym->isTLS()) {
           error(toString(file) + ": relocation " +
                 relocTypeToString(reloc.Type) +
diff --git a/lld/wasm/SyntheticSections.cpp b/lld/wasm/SyntheticSections.cpp
index d1a01c7ec3f9d..050f61c7f5c56 100644
--- a/lld/wasm/SyntheticSections.cpp
+++ b/lld/wasm/SyntheticSections.cpp
@@ -57,7 +57,7 @@ void writeGetTLSBase(const Ctx &ctx, raw_ostream &os) {
     writeU8(os, WASM_OPCODE_CALL, "call");
     writeUleb128(os, ctx.sym.getTLSBase->getFunctionIndex(), "function index");
   } else {
-    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_SET");
+    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_GET");
     writeUleb128(os, ctx.sym.tlsBase->getGlobalIndex(), "__tls_base");
   }
 }
@@ -532,7 +532,7 @@ void GlobalSection::writeBody() {
         mutable_ = true;
       // With multi-threading any TLS globals must be mutable since they get
       // set during `__wasm_apply_global_tls_relocs`
-      if (ctx.arg.sharedMemory && sym->isTLS())
+      if (ctx.arg.isMultithreaded() && sym->isTLS())
         mutable_ = true;
     }
     WasmGlobalType type{itype, mutable_};
@@ -569,10 +569,11 @@ void GlobalSection::writeBody() {
     } else {
       WasmInitExpr initExpr;
       if (auto *d = dyn_cast<DefinedData>(sym))
-        // In the sharedMemory case TLS globals are set during
-        // `__wasm_apply_global_tls_relocs`, but in the non-shared case
+        // In the multithreaded case, TLS globals are set during
+        // `__wasm_apply_global_tls_relocs`, but in the single-threaded case
         // we know the absolute value at link time.
-        initExpr = intConst(d->getVA(/*absolute=*/!ctx.arg.sharedMemory), is64);
+        initExpr =
+            intConst(d->getVA(/*absolute=*/!ctx.arg.isMultithreaded()), is64);
       else if (auto *f = dyn_cast<FunctionSymbol>(sym))
         initExpr = intConst(f->isStub ? 0 : f->getTableIndex(), is64);
       else {
@@ -680,7 +681,7 @@ bool DataCountSection::isNeeded() const {
   // instructions are not yet supported in input files.  However, in the case
   // of shared memory, lld itself will generate these instructions as part of
   // `__wasm_init_memory`. See Writer::createInitMemoryFunction.
-  return numSegments && ctx.arg.sharedMemory;
+  return numSegments && ctx.arg.isMultithreaded();
 }
 
 void LinkingSection::writeBody() {
diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp
index 688bb829e1c42..d90ca859f3479 100644
--- a/lld/wasm/Writer.cpp
+++ b/lld/wasm/Writer.cpp
@@ -425,7 +425,7 @@ void Writer::layoutMemory() {
   // Even in the absense of any actual TLS data, this symbol can still be
   // referenced (for example by __builtin_thread_pointer, which should not
   // return NULL).
-  if (!ctx.arg.sharedMemory && ctx.sym.tlsBase) {
+  if (!ctx.arg.isMultithreaded() && ctx.sym.tlsBase) {
     auto *tlsBase = cast<DefinedGlobal>(ctx.sym.tlsBase);
     setGlobalPtr(tlsBase, fixedTLSBase);
   }
@@ -653,7 +653,7 @@ void Writer::populateTargetFeatures() {
                  sym->importModule && sym->importModule == "env";
         }))
       error(fileName + ": object file uses globals for thread context, "
-                       "but --libcall-thread-context was specified");
+                       "but --cooperative-threading was specified");
   }
 
   if (inferFeatures)
@@ -1057,7 +1057,15 @@ static StringRef getOutputDataSegmentName(const InputChunk &seg) {
 OutputSegment *Writer::createOutputSegment(StringRef name) {
   LLVM_DEBUG(dbgs() << "new segment: " << name << "\n");
   OutputSegment *s = make<OutputSegment>(name);
-  if (ctx.arg.sharedMemory)
+  // In the shared memory case, all data segments must be passive since they
+  // will be initialized once by the main thread and then shared with other
+  // threads. In the non-shared memory case, we use passive segments only for
+  // TLS segments, so that they can be reused, and for .bss segments, which
+  // don't need to be included in the binary at all.
+  bool needsPassiveInit =
+      ctx.arg.sharedMemory || (ctx.arg.cooperativeMultithreading &&
+                               (s->isTLS() || s->name.starts_with(".bss")));
+  if (needsPassiveInit)
     s->initFlags = WASM_DATA_SEGMENT_IS_PASSIVE;
   if (!ctx.arg.relocatable && name.starts_with(".bss"))
     s->isBss = true;
@@ -1116,7 +1124,7 @@ void Writer::combineOutputSegments() {
   // This restriction does not apply when the extended const extension is
   // available: https://github.com/WebAssembly/extended-const
   assert(!ctx.arg.extendedConst);
-  assert(ctx.isPic && !ctx.arg.sharedMemory);
+  assert(ctx.isPic && !ctx.arg.isMultithreaded());
   if (segments.size() <= 1)
     return;
   OutputSegment *combined = make<OutputSegment>(".data");
@@ -1198,7 +1206,7 @@ void Writer::createSyntheticInitFunctions() {
     }
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     if (out.globalSec->needsTLSRelocations()) {
       ctx.sym.applyGlobalTLSRelocs = symtab->addSyntheticFunction(
           "__wasm_apply_global_tls_relocs", WASM_SYMBOL_VISIBILITY_HIDDEN,
@@ -1378,7 +1386,7 @@ void Writer::createInitMemoryFunction() {
         // When we initialize the TLS segment we also set the TLS base.
         // This allows the runtime to use this static copy of the TLS data
         // for the first/main thread.
-        if (ctx.arg.sharedMemory && s->isTLS()) {
+        if (ctx.arg.isMultithreaded() && s->isTLS()) {
           if (ctx.isPic) {
             // Cache the result of the addionion in local 0
             writeU8(os, WASM_OPCODE_LOCAL_TEE, "local.tee");
@@ -1449,7 +1457,7 @@ void Writer::createInitMemoryFunction() {
       if (needsPassiveInitialization(s) && !s->isBss) {
         // The TLS region should not be dropped since its is needed
         // during the initialization of each thread (__wasm_init_tls).
-        if (ctx.arg.sharedMemory && s->isTLS())
+        if (ctx.arg.isMultithreaded() && s->isTLS())
           continue;
         // data.drop instruction
         writeU8(os, WASM_OPCODE_MISC_PREFIX, "bulk-memory prefix");
@@ -1502,7 +1510,7 @@ void Writer::createApplyDataRelocationsFunction() {
     writeUleb128(os, 0, "num locals");
     bool generated = false;
     for (const OutputSegment *seg : segments)
-      if (!ctx.arg.sharedMemory || !seg->isTLS())
+      if (!ctx.arg.isMultithreaded() || !seg->isTLS())
         for (const InputChunk *inSeg : seg->inputSegments)
           generated |= inSeg->generateRelocationCode(os);
 
@@ -1658,7 +1666,6 @@ void Writer::createInitTLSFunction() {
     if (tlsSeg) {
       writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get");
       writeUleb128(os, 0, "local index");
-
       writeSetTLSBase(ctx, os);
 
       // FIXME(wvo): this local needs to be I64 in wasm64, or we need an extend
@@ -1791,9 +1798,9 @@ void Writer::run() {
   // `__memory_base` import.  Unless we support the extended const expression we
   // can't do addition inside the constant expression, so we much combine the
   // segments into a single one that can live at `__memory_base`.
-  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.sharedMemory) {
-    // In shared memory mode all data segments are passive and initialized
-    // via __wasm_init_memory.
+  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.isMultithreaded()) {
+    // In multithreaded modes (shared or cooperative), data segments may be
+    // passive and must not be combined into a single active segment.
     log("-- combineOutputSegments");
     combineOutputSegments();
   }
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
index 6326b7d76db82..9dea29fb0205d 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
@@ -40,9 +40,12 @@ WebAssemblySubtarget::initializeSubtargetDependencies(StringRef CPU,
 
   ParseSubtargetFeatures(CPU, /*TuneCPU*/ CPU, FS);
 
-  // WASIP3 implies using the libcall thread context.
-  if (TargetTriple.getOS() == Triple::WASIp3)
+  // WASIP3 uses cooperative multithreading, which implies using libcall
+  // thread context.
+  if (TargetTriple.getOS() == Triple::WASIp3) {
+    HasCooperativeMultithreading = true;
     HasLibcallThreadContext = true;
+  }
 
   FeatureBitset Bits = getFeatureBits();
 
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
index 5c6f4cb5b36ff..f637ce59ebfce 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
@@ -52,6 +52,7 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool HasExtendedConst = false;
   bool HasFP16 = false;
   bool HasGC = false;
+  bool HasCooperativeMultithreading = false;
   bool HasLibcallThreadContext = false;
   bool HasMultiMemory = false;
   bool HasMultivalue = false;
@@ -117,6 +118,9 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool hasExtendedConst() const { return HasExtendedConst; }
   bool hasFP16() const { return HasFP16; }
   bool hasGC() const { return HasGC; }
+  bool hasCooperativeMultithreading() const {
+    return HasCooperativeMultithre...
[truncated]

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-backend-webassembly

Author: Sy Brand (TartanLlama)

Changes

This PR builds on the changes to allow libcall thread context from https://github.com/llvm/llvm-project/pull/175800/changes and adds the necessary changes to support cooperative multithreading in the WASIP3 target:

  • Not marking memory as shared
  • Allowing thread local accesses without atomics
  • Only using passive segments for TLS segments

The linker changes are supported by a new flag called --cooperative-multithreading. We talked about having two flags, one for the --libcall-thread-context part and one for the cooperative multithreading part. For now, I've simply replaced the --libcall-thread-context flag with the --cooperative-multithreading one and kept the internal configuration intact for simplicity.


Patch is 22.26 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200855.diff

16 Files Affected:

  • (modified) clang/lib/Driver/ToolChains/WebAssembly.cpp (+10-5)
  • (modified) clang/test/Driver/wasm-toolchain.c (+7)
  • (added) lld/test/wasm/cooperative-multithreading.s (+85)
  • (modified) lld/test/wasm/stack-pointer-abi.s (+1-1)
  • (modified) lld/test/wasm/thread-context-abi-mismatch.s (+2-3)
  • (modified) lld/test/wasm/tls-libcall.s (+1-1)
  • (modified) lld/wasm/Config.h (+5)
  • (modified) lld/wasm/Driver.cpp (+11-3)
  • (modified) lld/wasm/Options.td (+2-2)
  • (modified) lld/wasm/Relocations.cpp (+1-1)
  • (modified) lld/wasm/SyntheticSections.cpp (+7-6)
  • (modified) lld/wasm/Writer.cpp (+19-12)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp (+5-2)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h (+4)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp (+9-2)
  • (added) llvm/test/CodeGen/WebAssembly/cooperative-strip-tls.ll (+20)
diff --git a/clang/lib/Driver/ToolChains/WebAssembly.cpp b/clang/lib/Driver/ToolChains/WebAssembly.cpp
index 4c1cd937e81aa..d8f23175eb58b 100644
--- a/clang/lib/Driver/ToolChains/WebAssembly.cpp
+++ b/clang/lib/Driver/ToolChains/WebAssembly.cpp
@@ -88,11 +88,16 @@ static bool WantsPthread(const llvm::Triple &Triple, const ArgList &Args) {
   return WantsPthread;
 }
 
-static bool WantsLibcallThreadContext(const llvm::Triple &Triple,
-                                      const ArgList &Args) {
+static bool WantsCooperativeMultithreading(const llvm::Triple &Triple,
+                                           const ArgList &Args) {
   return Triple.getOS() == llvm::Triple::WASIp3;
 }
 
+static bool WantsSharedMemory(const llvm::Triple &Triple, const ArgList &Args) {
+  return WantsPthread(Triple, Args) &&
+         !WantsCooperativeMultithreading(Triple, Args);
+}
+
 void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
                                 const InputInfo &Output,
                                 const InputInfoList &Inputs,
@@ -174,10 +179,10 @@ void wasm::Linker::ConstructJob(Compilation &C, const JobAction &JA,
 
   AddLinkerInputs(ToolChain, Inputs, Args, CmdArgs, JA);
 
-  if (WantsLibcallThreadContext(ToolChain.getTriple(), Args))
-    CmdArgs.push_back("--libcall-thread-context");
+  if (WantsCooperativeMultithreading(ToolChain.getTriple(), Args))
+    CmdArgs.push_back("--cooperative-multithreading");
 
-  if (WantsPthread(ToolChain.getTriple(), Args))
+  if (WantsSharedMemory(ToolChain.getTriple(), Args))
     CmdArgs.push_back("--shared-memory");
 
   if (!Args.hasArg(options::OPT_nostdlib, options::OPT_nodefaultlibs)) {
diff --git a/clang/test/Driver/wasm-toolchain.c b/clang/test/Driver/wasm-toolchain.c
index 29a94aeec77a9..40d75da3166d9 100644
--- a/clang/test/Driver/wasm-toolchain.c
+++ b/clang/test/Driver/wasm-toolchain.c
@@ -303,3 +303,10 @@
 // RUN:   | FileCheck -check-prefix=LINK_WALI_BASIC %s
 // LINK_WALI_BASIC: "-cc1" {{.*}} "-o" "[[temp:[^"]*]]"
 // LINK_WALI_BASIC: wasm-ld{{.*}}" "-L/foo/lib/wasm32-linux-muslwali" "crt1.o" "[[temp]]" "-lc" "{{.*[/\\]}}libclang_rt.builtins.a" "-o" "a.out"
+
+// Test that `wasm32-wasip3` passes `--cooperative-multithreading` to the linker.
+
+// RUN: %clang -### --target=wasm32-wasip3 -fuse-ld=lld %s --sysroot /foo 2>&1 \
+// RUN:   | FileCheck -check-prefix=LINK_WASIP3_COOP %s
+// LINK_WASIP3_COOP: wasm-ld{{.*}}" {{.*}} "--cooperative-multithreading"
+// LINK_WASIP3_COOP-NOT: "--libcall-thread-context"
diff --git a/lld/test/wasm/cooperative-multithreading.s b/lld/test/wasm/cooperative-multithreading.s
new file mode 100644
index 0000000000000..5adfaa99e40a0
--- /dev/null
+++ b/lld/test/wasm/cooperative-multithreading.s
@@ -0,0 +1,85 @@
+# Test that --cooperative-multithreading uses the libcall ABI naming for
+# 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: wasm-ld --cooperative-multithreading -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
+
+# Test that --cooperative-multithreading and --shared-memory are mutually exclusive.
+# RUN: not wasm-ld --cooperative-multithreading --shared-memory %t.o -o %t2.wasm 2>&1 | FileCheck %s --check-prefix=INCOMPAT
+# INCOMPAT: --cooperative-multithreading is incompatible with --shared-memory
+
+.globl         __wasm_get_tls_base
+__wasm_get_tls_base:
+    .functype   __wasm_get_tls_base () -> (i32)
+    i32.const 0
+    end_function
+
+.globl _start
+_start:
+  .functype _start () -> (i32)
+  call __wasm_get_tls_base
+  i32.const tls1@TLSREL
+  i32.add
+  i32.load 0
+  call __wasm_get_tls_base
+  i32.const tls2@TLSREL
+  i32.add
+  i32.load 0
+  i32.add
+  end_function
+
+.section  .tdata.tls1,"",@
+.globl  tls1
+tls1:
+  .int32  1
+  .size tls1, 4
+
+.section  .tdata.tls2,"",@
+.globl  tls2
+tls2:
+  .int32  2
+  .size tls2, 4
+
+.section  .custom_section.target_features,"",@
+  .int8 2
+  .int8 43
+  .int8 11
+  .ascii  "bulk-memory"
+  .int8 43
+  .int8 7
+  .ascii  "atomics"
+
+# Memory must NOT be marked as shared.
+# CHECK:      - Type:            MEMORY
+# CHECK-NEXT:   Memories:
+# CHECK-NEXT:     - Minimum:         0x2
+# CHECK-NOT:       Shared:          false
+
+# Globals should use the libcall ABI naming, not the global ABI.
+# CHECK:      GlobalNames:
+# CHECK-NEXT:      - Index:           0
+# CHECK-NEXT:        Name:            __init_stack_pointer
+# CHECK-NEXT:      - Index:           1
+# CHECK-NEXT:        Name:            __init_tls_base
+# CHECK-NEXT:      - Index:           2
+# CHECK-NEXT:        Name:            __tls_size
+# CHECK-NEXT:      - Index:           3
+# CHECK-NEXT:        Name:            __tls_align
+
+# DIS-LABEL: <__wasm_init_memory>:
+
+# DIS-LABEL: <_start>:
+# DIS-EMPTY:
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       call    {{[0-9]+}}
+# DIS-NEXT:       i32.const       4
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       i32.load        0
+# DIS-NEXT:       i32.add
+# DIS-NEXT:       end
diff --git a/lld/test/wasm/stack-pointer-abi.s b/lld/test/wasm/stack-pointer-abi.s
index 869f972710991..fbae0475bcba2 100644
--- a/lld/test/wasm/stack-pointer-abi.s
+++ b/lld/test/wasm/stack-pointer-abi.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --no-gc-sections -o %t.libcall.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --no-gc-sections -o %t.libcall.wasm %t.o
 # RUN: obj2yaml %t.libcall.wasm | FileCheck %s --check-prefix=LIBCALL
 # RUN: wasm-ld --no-gc-sections -o %t.global.wasm %t.o
 # RUN: obj2yaml %t.global.wasm | FileCheck %s --check-prefix=GLOBAL
diff --git a/lld/test/wasm/thread-context-abi-mismatch.s b/lld/test/wasm/thread-context-abi-mismatch.s
index 069534cbe5762..a817ca4407aab 100644
--- a/lld/test/wasm/thread-context-abi-mismatch.s
+++ b/lld/test/wasm/thread-context-abi-mismatch.s
@@ -3,10 +3,9 @@
 # as an indication that the global thread context ABI is being used.
 
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: not wasm-ld --libcall-thread-context %t.o -o %t.wasm 2>&1 | FileCheck %s
-
-# CHECK: object file uses globals for thread context, but --libcall-thread-context was specified
+# RUN: not wasm-ld --cooperative-multithreading %t.o -o %t.wasm 2>&1 | FileCheck %s
 
+# CHECK: object file uses globals for thread context, but --cooperative-multithreading was specified
 .globl _start
 _start:
   .functype _start () -> ()
diff --git a/lld/test/wasm/tls-libcall.s b/lld/test/wasm/tls-libcall.s
index df8b8f8be0207..a0a7f37379bac 100644
--- a/lld/test/wasm/tls-libcall.s
+++ b/lld/test/wasm/tls-libcall.s
@@ -1,5 +1,5 @@
 # RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
-# RUN: wasm-ld --libcall-thread-context --shared-memory -no-gc-sections -o %t.wasm %t.o
+# RUN: wasm-ld --cooperative-threading --shared-memory -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
 
diff --git a/lld/wasm/Config.h b/lld/wasm/Config.h
index 71a378a412e9e..d4789b88203eb 100644
--- a/lld/wasm/Config.h
+++ b/lld/wasm/Config.h
@@ -65,6 +65,7 @@ struct Config {
   bool growableTable;
   bool gcSections;
   llvm::StringSet<> keepSections;
+  bool cooperativeMultithreading;
   bool libcallThreadContext;
   std::optional<std::pair<llvm::StringRef, llvm::StringRef>> memoryImport;
   std::optional<llvm::StringRef> memoryExport;
@@ -134,6 +135,10 @@ struct Config {
   std::optional<std::vector<std::string>> features;
   std::optional<std::vector<std::string>> extraFeatures;
   llvm::SmallVector<uint8_t, 0> buildIdVector;
+
+  bool isMultithreaded() const {
+    return sharedMemory || cooperativeMultithreading;
+  }
 };
 
 // The Ctx object hold all other (non-configuration) global state.
diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp
index fe1e2eec95037..b2723220b5afb 100644
--- a/lld/wasm/Driver.cpp
+++ b/lld/wasm/Driver.cpp
@@ -561,7 +561,9 @@ static void readConfigs(opt::InputArgList &args) {
   ctx.arg.soName = args.getLastArgValue(OPT_soname);
   ctx.arg.importTable = args.hasArg(OPT_import_table);
   ctx.arg.importUndefined = args.hasArg(OPT_import_undefined);
-  ctx.arg.libcallThreadContext = args.hasArg(OPT_libcall_thread_context);
+  ctx.arg.cooperativeMultithreading =
+      args.hasArg(OPT_cooperative_multithreading);
+  ;
   ctx.arg.ltoo = args::getInteger(args, OPT_lto_O, 2);
   if (ctx.arg.ltoo > 3)
     error("invalid optimization level for LTO: " + Twine(ctx.arg.ltoo));
@@ -755,6 +757,12 @@ static void setConfigs() {
   if (!ctx.arg.memoryExport.has_value() && !ctx.arg.memoryImport.has_value()) {
     ctx.arg.memoryExport = memoryName;
   }
+  if (ctx.arg.cooperativeMultithreading) {
+    if (ctx.arg.sharedMemory)
+      error(
+          "--cooperative-multithreading is incompatible with --shared-memory");
+    ctx.arg.libcallThreadContext = true;
+  }
 }
 
 // Some command line options or some combinations of them are not allowed.
@@ -964,7 +972,7 @@ static void createSyntheticSymbols() {
         createGlobalVariable(stack_pointer_name, !ctx.arg.libcallThreadContext);
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     // TLS symbols are all hidden/dso-local
     auto tls_base_name =
         ctx.arg.libcallThreadContext ? "__init_tls_base" : "__tls_base";
@@ -1028,7 +1036,7 @@ static void createOptionalSymbols() {
   //
   // __tls_size and __tls_align are not needed in this case since they are only
   // needed for __wasm_init_tls (which we do not create in this case).
-  if (!ctx.arg.sharedMemory)
+  if (!ctx.sym.tlsBase)
     ctx.sym.tlsBase = createOptionalGlobal("__tls_base", false);
 }
 
diff --git a/lld/wasm/Options.td b/lld/wasm/Options.td
index 144eee33061e1..6d18a0400ef97 100644
--- a/lld/wasm/Options.td
+++ b/lld/wasm/Options.td
@@ -238,8 +238,8 @@ def page_size: JJ<"page-size=">,
 def initial_memory: JJ<"initial-memory=">,
   HelpText<"Initial size of the linear memory">;
 
-def libcall_thread_context: FF<"libcall-thread-context">,
-  HelpText<"Use library calls for thread context access instead of globals.">;
+def cooperative_multithreading: FF<"cooperative-multithreading">,
+  HelpText<"Enable cooperative multithreading.">;
   
 def max_memory: JJ<"max-memory=">,
   HelpText<"Maximum size of the linear memory">;
diff --git a/lld/wasm/Relocations.cpp b/lld/wasm/Relocations.cpp
index a1840abe88b3a..cb597fdeffcf3 100644
--- a/lld/wasm/Relocations.cpp
+++ b/lld/wasm/Relocations.cpp
@@ -125,7 +125,7 @@ void scanRelocations(InputChunk *chunk) {
       // In single-threaded builds TLS is lowered away and TLS data can be
       // merged with normal data and allowing TLS relocation in non-TLS
       // segments.
-      if (ctx.arg.sharedMemory) {
+      if (ctx.arg.isMultithreaded()) {
         if (!sym->isTLS()) {
           error(toString(file) + ": relocation " +
                 relocTypeToString(reloc.Type) +
diff --git a/lld/wasm/SyntheticSections.cpp b/lld/wasm/SyntheticSections.cpp
index d1a01c7ec3f9d..050f61c7f5c56 100644
--- a/lld/wasm/SyntheticSections.cpp
+++ b/lld/wasm/SyntheticSections.cpp
@@ -57,7 +57,7 @@ void writeGetTLSBase(const Ctx &ctx, raw_ostream &os) {
     writeU8(os, WASM_OPCODE_CALL, "call");
     writeUleb128(os, ctx.sym.getTLSBase->getFunctionIndex(), "function index");
   } else {
-    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_SET");
+    writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_GET");
     writeUleb128(os, ctx.sym.tlsBase->getGlobalIndex(), "__tls_base");
   }
 }
@@ -532,7 +532,7 @@ void GlobalSection::writeBody() {
         mutable_ = true;
       // With multi-threading any TLS globals must be mutable since they get
       // set during `__wasm_apply_global_tls_relocs`
-      if (ctx.arg.sharedMemory && sym->isTLS())
+      if (ctx.arg.isMultithreaded() && sym->isTLS())
         mutable_ = true;
     }
     WasmGlobalType type{itype, mutable_};
@@ -569,10 +569,11 @@ void GlobalSection::writeBody() {
     } else {
       WasmInitExpr initExpr;
       if (auto *d = dyn_cast<DefinedData>(sym))
-        // In the sharedMemory case TLS globals are set during
-        // `__wasm_apply_global_tls_relocs`, but in the non-shared case
+        // In the multithreaded case, TLS globals are set during
+        // `__wasm_apply_global_tls_relocs`, but in the single-threaded case
         // we know the absolute value at link time.
-        initExpr = intConst(d->getVA(/*absolute=*/!ctx.arg.sharedMemory), is64);
+        initExpr =
+            intConst(d->getVA(/*absolute=*/!ctx.arg.isMultithreaded()), is64);
       else if (auto *f = dyn_cast<FunctionSymbol>(sym))
         initExpr = intConst(f->isStub ? 0 : f->getTableIndex(), is64);
       else {
@@ -680,7 +681,7 @@ bool DataCountSection::isNeeded() const {
   // instructions are not yet supported in input files.  However, in the case
   // of shared memory, lld itself will generate these instructions as part of
   // `__wasm_init_memory`. See Writer::createInitMemoryFunction.
-  return numSegments && ctx.arg.sharedMemory;
+  return numSegments && ctx.arg.isMultithreaded();
 }
 
 void LinkingSection::writeBody() {
diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp
index 688bb829e1c42..d90ca859f3479 100644
--- a/lld/wasm/Writer.cpp
+++ b/lld/wasm/Writer.cpp
@@ -425,7 +425,7 @@ void Writer::layoutMemory() {
   // Even in the absense of any actual TLS data, this symbol can still be
   // referenced (for example by __builtin_thread_pointer, which should not
   // return NULL).
-  if (!ctx.arg.sharedMemory && ctx.sym.tlsBase) {
+  if (!ctx.arg.isMultithreaded() && ctx.sym.tlsBase) {
     auto *tlsBase = cast<DefinedGlobal>(ctx.sym.tlsBase);
     setGlobalPtr(tlsBase, fixedTLSBase);
   }
@@ -653,7 +653,7 @@ void Writer::populateTargetFeatures() {
                  sym->importModule && sym->importModule == "env";
         }))
       error(fileName + ": object file uses globals for thread context, "
-                       "but --libcall-thread-context was specified");
+                       "but --cooperative-threading was specified");
   }
 
   if (inferFeatures)
@@ -1057,7 +1057,15 @@ static StringRef getOutputDataSegmentName(const InputChunk &seg) {
 OutputSegment *Writer::createOutputSegment(StringRef name) {
   LLVM_DEBUG(dbgs() << "new segment: " << name << "\n");
   OutputSegment *s = make<OutputSegment>(name);
-  if (ctx.arg.sharedMemory)
+  // In the shared memory case, all data segments must be passive since they
+  // will be initialized once by the main thread and then shared with other
+  // threads. In the non-shared memory case, we use passive segments only for
+  // TLS segments, so that they can be reused, and for .bss segments, which
+  // don't need to be included in the binary at all.
+  bool needsPassiveInit =
+      ctx.arg.sharedMemory || (ctx.arg.cooperativeMultithreading &&
+                               (s->isTLS() || s->name.starts_with(".bss")));
+  if (needsPassiveInit)
     s->initFlags = WASM_DATA_SEGMENT_IS_PASSIVE;
   if (!ctx.arg.relocatable && name.starts_with(".bss"))
     s->isBss = true;
@@ -1116,7 +1124,7 @@ void Writer::combineOutputSegments() {
   // This restriction does not apply when the extended const extension is
   // available: https://github.com/WebAssembly/extended-const
   assert(!ctx.arg.extendedConst);
-  assert(ctx.isPic && !ctx.arg.sharedMemory);
+  assert(ctx.isPic && !ctx.arg.isMultithreaded());
   if (segments.size() <= 1)
     return;
   OutputSegment *combined = make<OutputSegment>(".data");
@@ -1198,7 +1206,7 @@ void Writer::createSyntheticInitFunctions() {
     }
   }
 
-  if (ctx.arg.sharedMemory) {
+  if (ctx.arg.isMultithreaded()) {
     if (out.globalSec->needsTLSRelocations()) {
       ctx.sym.applyGlobalTLSRelocs = symtab->addSyntheticFunction(
           "__wasm_apply_global_tls_relocs", WASM_SYMBOL_VISIBILITY_HIDDEN,
@@ -1378,7 +1386,7 @@ void Writer::createInitMemoryFunction() {
         // When we initialize the TLS segment we also set the TLS base.
         // This allows the runtime to use this static copy of the TLS data
         // for the first/main thread.
-        if (ctx.arg.sharedMemory && s->isTLS()) {
+        if (ctx.arg.isMultithreaded() && s->isTLS()) {
           if (ctx.isPic) {
             // Cache the result of the addionion in local 0
             writeU8(os, WASM_OPCODE_LOCAL_TEE, "local.tee");
@@ -1449,7 +1457,7 @@ void Writer::createInitMemoryFunction() {
       if (needsPassiveInitialization(s) && !s->isBss) {
         // The TLS region should not be dropped since its is needed
         // during the initialization of each thread (__wasm_init_tls).
-        if (ctx.arg.sharedMemory && s->isTLS())
+        if (ctx.arg.isMultithreaded() && s->isTLS())
           continue;
         // data.drop instruction
         writeU8(os, WASM_OPCODE_MISC_PREFIX, "bulk-memory prefix");
@@ -1502,7 +1510,7 @@ void Writer::createApplyDataRelocationsFunction() {
     writeUleb128(os, 0, "num locals");
     bool generated = false;
     for (const OutputSegment *seg : segments)
-      if (!ctx.arg.sharedMemory || !seg->isTLS())
+      if (!ctx.arg.isMultithreaded() || !seg->isTLS())
         for (const InputChunk *inSeg : seg->inputSegments)
           generated |= inSeg->generateRelocationCode(os);
 
@@ -1658,7 +1666,6 @@ void Writer::createInitTLSFunction() {
     if (tlsSeg) {
       writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get");
       writeUleb128(os, 0, "local index");
-
       writeSetTLSBase(ctx, os);
 
       // FIXME(wvo): this local needs to be I64 in wasm64, or we need an extend
@@ -1791,9 +1798,9 @@ void Writer::run() {
   // `__memory_base` import.  Unless we support the extended const expression we
   // can't do addition inside the constant expression, so we much combine the
   // segments into a single one that can live at `__memory_base`.
-  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.sharedMemory) {
-    // In shared memory mode all data segments are passive and initialized
-    // via __wasm_init_memory.
+  if (ctx.isPic && !ctx.arg.extendedConst && !ctx.arg.isMultithreaded()) {
+    // In multithreaded modes (shared or cooperative), data segments may be
+    // passive and must not be combined into a single active segment.
     log("-- combineOutputSegments");
     combineOutputSegments();
   }
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
index 6326b7d76db82..9dea29fb0205d 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.cpp
@@ -40,9 +40,12 @@ WebAssemblySubtarget::initializeSubtargetDependencies(StringRef CPU,
 
   ParseSubtargetFeatures(CPU, /*TuneCPU*/ CPU, FS);
 
-  // WASIP3 implies using the libcall thread context.
-  if (TargetTriple.getOS() == Triple::WASIp3)
+  // WASIP3 uses cooperative multithreading, which implies using libcall
+  // thread context.
+  if (TargetTriple.getOS() == Triple::WASIp3) {
+    HasCooperativeMultithreading = true;
     HasLibcallThreadContext = true;
+  }
 
   FeatureBitset Bits = getFeatureBits();
 
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
index 5c6f4cb5b36ff..f637ce59ebfce 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
+++ b/llvm/lib/Target/WebAssembly/WebAssemblySubtarget.h
@@ -52,6 +52,7 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool HasExtendedConst = false;
   bool HasFP16 = false;
   bool HasGC = false;
+  bool HasCooperativeMultithreading = false;
   bool HasLibcallThreadContext = false;
   bool HasMultiMemory = false;
   bool HasMultivalue = false;
@@ -117,6 +118,9 @@ class WebAssemblySubtarget final : public WebAssemblyGenSubtargetInfo {
   bool hasExtendedConst() const { return HasExtendedConst; }
   bool hasFP16() const { return HasFP16; }
   bool hasGC() const { return HasGC; }
+  bool hasCooperativeMultithreading() const {
+    return HasCooperativeMultithre...
[truncated]

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 206098 tests passed
  • 6713 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 137740 tests passed
  • 4819 tests skipped

✅ The build succeeded and all tests passed.

Comment thread lld/wasm/Driver.cpp Outdated
writeUleb128(os, ctx.sym.getTLSBase->getFunctionIndex(), "function index");
} else {
writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_SET");
writeU8(os, WASM_OPCODE_GLOBAL_GET, "GLOBAL_GET");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a typo from the previous commit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed

Comment thread lld/wasm/Writer.cpp Outdated
Comment thread lld/wasm/Writer.cpp Outdated
// will be initialized once by the main thread and then shared with other
// threads. In the non-shared memory case, we use passive segments only for
// TLS segments, so that they can be reused, and for .bss segments, which
// don't need to be included in the binary at all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the .bss part of the this comment. If they are not needed in the binary at all why would need passive init?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the comment

Comment thread llvm/test/CodeGen/WebAssembly/cooperative-strip-tls.ll Outdated
sbc100 added a commit to sbc100/llvm-project that referenced this pull request Jun 1, 2026
sbc100 added a commit that referenced this pull request Jun 1, 2026
Comment thread clang/lib/Driver/ToolChains/WebAssembly.cpp Outdated
Comment thread lld/wasm/Driver.cpp Outdated
// For non-shared memory programs we still need to define __tls_base since we
// For non-multithreaded programs we still need to define __tls_base since we
// allow object files built with TLS to be linked into single threaded
// programs, and such object files can contain references to this symbol.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence has both non-multithreaded and single threaded which reads a little odd to me.

Could we rephrase? Maybe shorten this sentence somehow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified the comment

if (!CooperativeThreading)
StrippedTLS = stripThreadLocals(M);
}
if (!Features[WebAssembly::FeatureBulkMemory]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the the removal of the else there change the behaviour in the case we have atomics enabled but not explicitly bulk memory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference in behaviour is in the CooperativeThreading case, which now does not strip thread locals in the !atomics case, but still does in the !bulk_memory case. The latter was the reason for removing the else.

Comment thread lld/test/wasm/cooperative-threading.s Outdated
Comment thread clang/test/Driver/wasm-toolchain.c Outdated
Comment thread lld/test/wasm/cooperative-threading.s Outdated
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@TartanLlama

Copy link
Copy Markdown
Contributor Author

@sbc100 looks all green now, anything else needed?

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm % nits

Comment thread lld/wasm/Driver.cpp Outdated
StrippedTLS = stripThreadLocals(M);
}
if (!Features[WebAssembly::FeatureBulkMemory]) {
StrippedTLS |= stripThreadLocals(M);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we risk calling stripThreadLocals twice now? Is there must cost to that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I forgot |= doesn't short-circuit, I've changed the condition to ensure it's called called twice

Comment thread lld/test/wasm/cooperative-threading.s Outdated
@sbc100
sbc100 merged commit b74154c into llvm:main Jun 23, 2026
11 checks passed
beamandala pushed a commit to beamandala/llvm-project that referenced this pull request Jun 24, 2026
This PR builds on the changes to allow libcall thread context from
https://github.com/llvm/llvm-project/pull/175800/changes and adds the
necessary changes to support cooperative multithreading in the WASIP3
target:

- Not marking memory as shared 
- Allowing thread local accesses without atomics
- Only using passive segments for TLS segments

The linker changes are supported by a new flag called
`--cooperative-multithreading`. We talked about having two flags, one
for the `--libcall-thread-context` part and one for the cooperative
multithreading part. For now, I've simply replaced the
`--libcall-thread-context` flag with the `--cooperative-multithreading`
one and kept the internal configuration intact for simplicity.
alexcrichton added a commit to WebAssembly/wasi-sdk that referenced this pull request Jul 1, 2026
This is a work-in-progress to get things enabled. The main bulk of the
change here is updating LLVM itself to tip-of-tree as of yesterday
morning. This removes two `*.patch` files because they've landed, and
adds another `*.patch` file to work around a new issue that's cropped
up. I've also included a `*.patch` for
llvm/llvm-project#200855, too.

This isn't ready yet to land as it's still failing to build/link/etc,
and I'll comment some more below.
sbc100 pushed a commit that referenced this pull request Aug 12, 2026
…#206831)

With `--gc-sections` (the default), `wasm-ld` garbage-collects functions
that are only reachable through `__wasm_get_tls_base` /
`__wasm_set_tls_base` in the cooperative-threading (libcall
thread-context) configuration. This produces a linked module that is
invalid or behaves incorrectly: the relocation inside
`__wasm_set_tls_base` is left dangling / mis-resolved, so callers trap
at runtime (e.g. `validation error: ... values remaining on stack at end
of block`, or a call to an unrelated function).

In cooperative-threading mode (`--cooperative-threading`, added in
#200855), per-task thread context is accessed through libcalls rather
than wasm globals. `wasm-ld` synthesizes `__wasm_init_tls` /
`__wasm_init_memory`, which invoke `__wasm_get_tls_base` and
`__wasm_set_tls_base` via **raw `call` instructions that carry no
relocations**. To keep those accessors in the output, the linker marks
them live with `Symbol::markLive()`.

## Error and Repro

```bash
Error: failed to compile: wasm[0]::function[16]::__wasm_set_tls_base

Caused by:
    0: WebAssembly translation error
    1: Invalid input WebAssembly code at offset 778: type mismatch: values remaining on stack at end of block
```

import was dropped and __wasm_set_tls_base was rewritten from

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_set_1)   ;; correct
```

to

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_get_0)   ;; wrong: get_0 is () -> i32
```

Minimal linker-only repro (no runtime needed) is:
```bash
wasm-ld --cooperative-threading tls.o libc.a   # context-set-1 dropped, set_tls_base calls get_0
wasm-ld --cooperative-threading --no-gc-sections tls.o libc.a   # correct
```

## Root cause

`Symbol::markLive()` sets the live flag (and the chunk's live bit) but
does not push the defining chunk onto the mark queue. The mark phase
only follows relocations of chunks that were enqueued via
`MarkLive::enqueue()`. As a result the accessors' own relocations are
never traversed.

This is the same situation already handled for constructors reached
through the relocation-less `__wasm_call_ctors`, which `MarkLive::run()`
enqueues explicitly.

## Fix

In `MarkLive::run()`, enqueue the defining chunks of
`__wasm_get_tls_base` and `__wasm_set_tls_base` before `mark()`, so
their relocations are followed.

```cpp
for (Symbol *sym : {static_cast<Symbol *>(ctx.sym.getTLSBase),
                    static_cast<Symbol *>(ctx.sym.setTLSBase)})
  if (sym)
    if (InputChunk *c = sym->getChunk())
      enqueue(c);
```

The symbols are only set in the libcall-thread-context configuration and
are null otherwise, so the loop is a no-op for all other builds.

## Testing

Adds `lld/test/wasm/cooperative-threading-gc.s`, which links with
`--cooperative-threading --gc-sections` and checks (via `obj2yaml`) that
the import called by `__wasm_set_tls_base` survives GC. Without this
change the test drops the import and rewrites `__wasm_set_tls_base` to
call an unrelated function; with it the import is retained and the call
is correct.

```sh
ninja check-lld-wasm
```

## Note

Human-in-the-loop with claude opus 4.8. I iterated with this patch and
got to a working component running with a wasi-sdk fork.
zhangweize9-cyber pushed a commit to zhangweize9-cyber/llvm-project that referenced this pull request Aug 16, 2026
…llvm#206831)

With `--gc-sections` (the default), `wasm-ld` garbage-collects functions
that are only reachable through `__wasm_get_tls_base` /
`__wasm_set_tls_base` in the cooperative-threading (libcall
thread-context) configuration. This produces a linked module that is
invalid or behaves incorrectly: the relocation inside
`__wasm_set_tls_base` is left dangling / mis-resolved, so callers trap
at runtime (e.g. `validation error: ... values remaining on stack at end
of block`, or a call to an unrelated function).

In cooperative-threading mode (`--cooperative-threading`, added in
llvm#200855), per-task thread context is accessed through libcalls rather
than wasm globals. `wasm-ld` synthesizes `__wasm_init_tls` /
`__wasm_init_memory`, which invoke `__wasm_get_tls_base` and
`__wasm_set_tls_base` via **raw `call` instructions that carry no
relocations**. To keep those accessors in the output, the linker marks
them live with `Symbol::markLive()`.

## Error and Repro

```bash
Error: failed to compile: wasm[0]::function[16]::__wasm_set_tls_base

Caused by:
    0: WebAssembly translation error
    1: Invalid input WebAssembly code at offset 778: type mismatch: values remaining on stack at end of block
```

import was dropped and __wasm_set_tls_base was rewritten from

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_set_1)   ;; correct
```

to

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_get_0)   ;; wrong: get_0 is () -> i32
```

Minimal linker-only repro (no runtime needed) is:
```bash
wasm-ld --cooperative-threading tls.o libc.a   # context-set-1 dropped, set_tls_base calls get_0
wasm-ld --cooperative-threading --no-gc-sections tls.o libc.a   # correct
```

## Root cause

`Symbol::markLive()` sets the live flag (and the chunk's live bit) but
does not push the defining chunk onto the mark queue. The mark phase
only follows relocations of chunks that were enqueued via
`MarkLive::enqueue()`. As a result the accessors' own relocations are
never traversed.

This is the same situation already handled for constructors reached
through the relocation-less `__wasm_call_ctors`, which `MarkLive::run()`
enqueues explicitly.

## Fix

In `MarkLive::run()`, enqueue the defining chunks of
`__wasm_get_tls_base` and `__wasm_set_tls_base` before `mark()`, so
their relocations are followed.

```cpp
for (Symbol *sym : {static_cast<Symbol *>(ctx.sym.getTLSBase),
                    static_cast<Symbol *>(ctx.sym.setTLSBase)})
  if (sym)
    if (InputChunk *c = sym->getChunk())
      enqueue(c);
```

The symbols are only set in the libcall-thread-context configuration and
are null otherwise, so the loop is a no-op for all other builds.

## Testing

Adds `lld/test/wasm/cooperative-threading-gc.s`, which links with
`--cooperative-threading --gc-sections` and checks (via `obj2yaml`) that
the import called by `__wasm_set_tls_base` survives GC. Without this
change the test drops the import and rewrites `__wasm_set_tls_base` to
call an unrelated function; with it the import is retained and the call
is correct.

```sh
ninja check-lld-wasm
```

## Note

Human-in-the-loop with claude opus 4.8. I iterated with this patch and
got to a working component running with a wasi-sdk fork.
carlobertolli pushed a commit to carlobertolli/llvm-project that referenced this pull request Aug 18, 2026
…llvm#206831)

With `--gc-sections` (the default), `wasm-ld` garbage-collects functions
that are only reachable through `__wasm_get_tls_base` /
`__wasm_set_tls_base` in the cooperative-threading (libcall
thread-context) configuration. This produces a linked module that is
invalid or behaves incorrectly: the relocation inside
`__wasm_set_tls_base` is left dangling / mis-resolved, so callers trap
at runtime (e.g. `validation error: ... values remaining on stack at end
of block`, or a call to an unrelated function).

In cooperative-threading mode (`--cooperative-threading`, added in
llvm#200855), per-task thread context is accessed through libcalls rather
than wasm globals. `wasm-ld` synthesizes `__wasm_init_tls` /
`__wasm_init_memory`, which invoke `__wasm_get_tls_base` and
`__wasm_set_tls_base` via **raw `call` instructions that carry no
relocations**. To keep those accessors in the output, the linker marks
them live with `Symbol::markLive()`.

## Error and Repro

```bash
Error: failed to compile: wasm[0]::function[16]::__wasm_set_tls_base

Caused by:
    0: WebAssembly translation error
    1: Invalid input WebAssembly code at offset 778: type mismatch: values remaining on stack at end of block
```

import was dropped and __wasm_set_tls_base was rewritten from

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_set_1)   ;; correct
```

to

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_get_0)   ;; wrong: get_0 is () -> i32
```

Minimal linker-only repro (no runtime needed) is:
```bash
wasm-ld --cooperative-threading tls.o libc.a   # context-set-1 dropped, set_tls_base calls get_0
wasm-ld --cooperative-threading --no-gc-sections tls.o libc.a   # correct
```

## Root cause

`Symbol::markLive()` sets the live flag (and the chunk's live bit) but
does not push the defining chunk onto the mark queue. The mark phase
only follows relocations of chunks that were enqueued via
`MarkLive::enqueue()`. As a result the accessors' own relocations are
never traversed.

This is the same situation already handled for constructors reached
through the relocation-less `__wasm_call_ctors`, which `MarkLive::run()`
enqueues explicitly.

## Fix

In `MarkLive::run()`, enqueue the defining chunks of
`__wasm_get_tls_base` and `__wasm_set_tls_base` before `mark()`, so
their relocations are followed.

```cpp
for (Symbol *sym : {static_cast<Symbol *>(ctx.sym.getTLSBase),
                    static_cast<Symbol *>(ctx.sym.setTLSBase)})
  if (sym)
    if (InputChunk *c = sym->getChunk())
      enqueue(c);
```

The symbols are only set in the libcall-thread-context configuration and
are null otherwise, so the loop is a no-op for all other builds.

## Testing

Adds `lld/test/wasm/cooperative-threading-gc.s`, which links with
`--cooperative-threading --gc-sections` and checks (via `obj2yaml`) that
the import called by `__wasm_set_tls_base` survives GC. Without this
change the test drops the import and rewrites `__wasm_set_tls_base` to
call an unrelated function; with it the import is retained and the call
is correct.

```sh
ninja check-lld-wasm
```

## Note

Human-in-the-loop with claude opus 4.8. I iterated with this patch and
got to a working component running with a wasi-sdk fork.
dyung pushed a commit that referenced this pull request Aug 20, 2026
…#206831)

With `--gc-sections` (the default), `wasm-ld` garbage-collects functions
that are only reachable through `__wasm_get_tls_base` /
`__wasm_set_tls_base` in the cooperative-threading (libcall
thread-context) configuration. This produces a linked module that is
invalid or behaves incorrectly: the relocation inside
`__wasm_set_tls_base` is left dangling / mis-resolved, so callers trap
at runtime (e.g. `validation error: ... values remaining on stack at end
of block`, or a call to an unrelated function).

In cooperative-threading mode (`--cooperative-threading`, added in
#200855), per-task thread context is accessed through libcalls rather
than wasm globals. `wasm-ld` synthesizes `__wasm_init_tls` /
`__wasm_init_memory`, which invoke `__wasm_get_tls_base` and
`__wasm_set_tls_base` via **raw `call` instructions that carry no
relocations**. To keep those accessors in the output, the linker marks
them live with `Symbol::markLive()`.

## Error and Repro

```bash
Error: failed to compile: wasm[0]::function[16]::__wasm_set_tls_base

Caused by:
    0: WebAssembly translation error
    1: Invalid input WebAssembly code at offset 778: type mismatch: values remaining on stack at end of block
```

import was dropped and __wasm_set_tls_base was rewritten from

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_set_1)   ;; correct
```

to

```wat
(func $__wasm_set_tls_base (param i32)
  local.get 0
  call $__wasm_component_model_builtin_context_get_0)   ;; wrong: get_0 is () -> i32
```

Minimal linker-only repro (no runtime needed) is:
```bash
wasm-ld --cooperative-threading tls.o libc.a   # context-set-1 dropped, set_tls_base calls get_0
wasm-ld --cooperative-threading --no-gc-sections tls.o libc.a   # correct
```

## Root cause

`Symbol::markLive()` sets the live flag (and the chunk's live bit) but
does not push the defining chunk onto the mark queue. The mark phase
only follows relocations of chunks that were enqueued via
`MarkLive::enqueue()`. As a result the accessors' own relocations are
never traversed.

This is the same situation already handled for constructors reached
through the relocation-less `__wasm_call_ctors`, which `MarkLive::run()`
enqueues explicitly.

## Fix

In `MarkLive::run()`, enqueue the defining chunks of
`__wasm_get_tls_base` and `__wasm_set_tls_base` before `mark()`, so
their relocations are followed.

```cpp
for (Symbol *sym : {static_cast<Symbol *>(ctx.sym.getTLSBase),
                    static_cast<Symbol *>(ctx.sym.setTLSBase)})
  if (sym)
    if (InputChunk *c = sym->getChunk())
      enqueue(c);
```

The symbols are only set in the libcall-thread-context configuration and
are null otherwise, so the loop is a no-op for all other builds.

## Testing

Adds `lld/test/wasm/cooperative-threading-gc.s`, which links with
`--cooperative-threading --gc-sections` and checks (via `obj2yaml`) that
the import called by `__wasm_set_tls_base` survives GC. Without this
change the test drops the import and rewrites `__wasm_set_tls_base` to
call an unrelated function; with it the import is retained and the call
is correct.

```sh
ninja check-lld-wasm
```

## Note

Human-in-the-loop with claude opus 4.8. I iterated with this patch and
got to a working component running with a wasi-sdk fork.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend:WebAssembly clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' lld:wasm lld

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants