[llvm] [Windows] Export Registry<T> members and accessors for plugins again (PR #221296)

Ryan VanderMeulen via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 8 17:49:09 PDT 2026


https://github.com/rvandermeulen updated https://github.com/llvm/llvm-project/pull/221296

>From 9abdeeb5cd7b1c38ea5d2787d5c9266d923a483b Mon Sep 17 00:00:00 2001
From: Ryan VanderMeulen <ryanvm at gmail.com>
Date: Tue, 8 Sep 2026 20:48:40 -0400
Subject: [PATCH 1/2] [Windows] Export Registry<T> members and accessors for
 plugins again

Since #185141 reworked llvm::Registry, the registered-node list lives behind
the explicitly specialized llvm::detail::getRegistryLinkListInstance<R>()
accessor defined by LLVM_DEFINE_REGISTRY, and the Head/Tail static data
members that used to be referenced across objects are gone.

extract_symbols.py handled neither: the accessor is a "??$" function template
specialization, which are dropped on the assumption that they can be
instantiated in the importing translation unit, and the Registry<T> members
are only exported when some object references the Registry template, which
nothing does anymore. As a result clang.exe built with
LLVM_EXPORT_SYMBOLS_FOR_PLUGINS no longer exports anything a plugin needs to
register itself, and linking a plugin fails with:

  lld-link: error: undefined symbol: __declspec(dllimport) public: static void
  __cdecl llvm::Registry<class clang::PluginASTAction>::add_node(...)

Keep the accessor specializations, and treat a reference to one as a
reference to the Registry template so that its explicitly instantiated
members are exported, as they were before. Drop the Head/Tail rule from
#163391, which no longer matches anything.

Assisted-by: Claude Code (analysis and drafting; reviewed and tested by the author)
---
 .../extract_symbols/Inputs/lib-itanium.s      | 10 +++++++++
 .../tools/extract_symbols/Inputs/lib-msvc.s   | 12 +++++++++++
 .../extract_symbols/Inputs/user-itanium.s     |  8 +++++++
 .../tools/extract_symbols/Inputs/user-msvc.s  | 10 +++++++++
 llvm/test/tools/extract_symbols/lit.local.cfg | 21 +++++++++++++++++++
 .../extract_symbols/registry-itanium.test     | 12 +++++++++++
 .../extract_symbols/registry-microsoft.test   | 17 +++++++++++++++
 llvm/utils/extract_symbols.py                 | 20 +++++++++++-------
 8 files changed, 102 insertions(+), 8 deletions(-)
 create mode 100644 llvm/test/tools/extract_symbols/Inputs/lib-itanium.s
 create mode 100644 llvm/test/tools/extract_symbols/Inputs/lib-msvc.s
 create mode 100644 llvm/test/tools/extract_symbols/Inputs/user-itanium.s
 create mode 100644 llvm/test/tools/extract_symbols/Inputs/user-msvc.s
 create mode 100644 llvm/test/tools/extract_symbols/lit.local.cfg
 create mode 100644 llvm/test/tools/extract_symbols/registry-itanium.test
 create mode 100644 llvm/test/tools/extract_symbols/registry-microsoft.test

diff --git a/llvm/test/tools/extract_symbols/Inputs/lib-itanium.s b/llvm/test/tools/extract_symbols/Inputs/lib-itanium.s
new file mode 100644
index 0000000000000..bedd90e523de1
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/Inputs/lib-itanium.s
@@ -0,0 +1,10 @@
+	.text
+	.globl	_ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv
+_ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv:
+	retq
+	.globl	_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE8add_nodeEPNS3_4nodeE
+_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE8add_nodeEPNS3_4nodeE:
+	retq
+	.globl	_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE5beginEv
+_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE5beginEv:
+	retq
diff --git a/llvm/test/tools/extract_symbols/Inputs/lib-msvc.s b/llvm/test/tools/extract_symbols/Inputs/lib-msvc.s
new file mode 100644
index 0000000000000..11fd9fa5525fe
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/Inputs/lib-msvc.s
@@ -0,0 +1,12 @@
+# Explicit specialization defined by LLVM_DEFINE_REGISTRY, plus the explicitly instantiated Registry<T> member
+# that only a plugin references (LLVM_INSTANTIATE_REGISTRY's `template class Registry<...>`).
+	.text
+	.globl	"??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ"
+"??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ":
+	retq
+	.globl	"?add_node@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SAXPEAVnode at 12@@Z"
+"?add_node@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SAXPEAVnode at 12@@Z":
+	retq
+	.globl	"?begin@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SA?AViterator at 12@XZ"
+"?begin@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SA?AViterator at 12@XZ":
+	retq
diff --git a/llvm/test/tools/extract_symbols/Inputs/user-itanium.s b/llvm/test/tools/extract_symbols/Inputs/user-itanium.s
new file mode 100644
index 0000000000000..b1cffcdd5e08d
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/Inputs/user-itanium.s
@@ -0,0 +1,8 @@
+	.text
+	.globl	_Z10HasPluginsv
+_Z10HasPluginsv:
+	callq	_ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv
+	retq
+	.weak	_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE5beginEv
+_ZN4llvm8RegistryIN5clang15PluginASTActionEJEE5beginEv:
+	retq
diff --git a/llvm/test/tools/extract_symbols/Inputs/user-msvc.s b/llvm/test/tools/extract_symbols/Inputs/user-msvc.s
new file mode 100644
index 0000000000000..e5cdc14f87161
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/Inputs/user-msvc.s
@@ -0,0 +1,10 @@
+# A TU that iterates the registry: references the accessor (declared-only via LLVM_DECLARE_REGISTRY) and
+# carries its own inline copy of begin(), but never touches add_node.
+	.text
+	.globl	"?HasPlugins@@YA_NXZ"
+"?HasPlugins@@YA_NXZ":
+	callq	"??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ"
+	retq
+	.globl	"?begin@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SA?AViterator at 12@XZ"
+"?begin@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SA?AViterator at 12@XZ":
+	retq
diff --git a/llvm/test/tools/extract_symbols/lit.local.cfg b/llvm/test/tools/extract_symbols/lit.local.cfg
new file mode 100644
index 0000000000000..70cdd4f079418
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/lit.local.cfg
@@ -0,0 +1,21 @@
+import os
+
+import lit.util
+
+config.suffixes = [".test"]
+
+if not "X86" in config.root.targets:
+    config.unsupported = True
+
+config.substitutions.append(
+    (
+        "%extract_symbols",
+        "'%s' %s --nm=%s --readobj=%s"
+        % (
+            config.python_executable,
+            os.path.join(config.llvm_src_root, "utils", "extract_symbols.py"),
+            lit.util.which("llvm-nm", config.llvm_tools_dir),
+            lit.util.which("llvm-readobj", config.llvm_tools_dir),
+        ),
+    )
+)
diff --git a/llvm/test/tools/extract_symbols/registry-itanium.test b/llvm/test/tools/extract_symbols/registry-itanium.test
new file mode 100644
index 0000000000000..d88e75a8dfda6
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/registry-itanium.test
@@ -0,0 +1,12 @@
+## Itanium counterpart of registry-microsoft.test: a reference to the
+## getRegistryLinkListInstance<R>() accessor counts as a reference to Registry,
+## so its explicitly instantiated members that are defined once are exported.
+
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=x86_64-linux-gnu -filetype=obj %S/Inputs/lib-itanium.s -o %t/lib.o
+# RUN: llvm-mc -triple=x86_64-linux-gnu -filetype=obj %S/Inputs/user-itanium.s -o %t/user.o
+# RUN: %extract_symbols --mangling=itanium %t/lib.o %t/user.o \
+# RUN:   | FileCheck %s --implicit-check-not=Registry
+
+# CHECK-DAG: _ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv
+# CHECK-DAG: _ZN4llvm8RegistryIN5clang15PluginASTActionEJEE8add_nodeEPNS3_4nodeE
diff --git a/llvm/test/tools/extract_symbols/registry-microsoft.test b/llvm/test/tools/extract_symbols/registry-microsoft.test
new file mode 100644
index 0000000000000..c530b872c37d9
--- /dev/null
+++ b/llvm/test/tools/extract_symbols/registry-microsoft.test
@@ -0,0 +1,17 @@
+## Registry<T> is only ever used through the getRegistryLinkListInstance<R>()
+## accessor that LLVM_DEFINE_REGISTRY defines once, so the accessor (a ??$
+## specialization) and the explicitly instantiated Registry<T> members that only
+## a plugin references must both be exported, or a plugin cannot link against a
+## static tool built with LLVM_EXPORT_SYMBOLS_FOR_PLUGINS.
+
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=x86_64-pc-windows-msvc -filetype=obj %S/Inputs/lib-msvc.s -o %t/lib.obj
+# RUN: llvm-mc -triple=x86_64-pc-windows-msvc -filetype=obj %S/Inputs/user-msvc.s -o %t/user.obj
+# RUN: %extract_symbols --mangling=microsoft %t/lib.obj %t/user.obj \
+# RUN:   | FileCheck %s --implicit-check-not=Registry
+
+## The accessor specialization and add_node are defined once and referenced
+## only through the accessor, so both are exported. begin() is defined in both
+## objects (the user TU instantiated the inline member itself), so it is not.
+# CHECK-DAG: ??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ
+# CHECK-DAG: ?add_node@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SAXPEAVnode at 12@@Z
diff --git a/llvm/utils/extract_symbols.py b/llvm/utils/extract_symbols.py
index 40a43d3655552..b51531dbca111 100755
--- a/llvm/utils/extract_symbols.py
+++ b/llvm/utils/extract_symbols.py
@@ -106,6 +106,13 @@ def should_keep_microsoft_symbol(symbol, calling_convention_decoration):
         # instantiated locally. Pattern: ??$getAs@<template_arg>@Type at clang@@...
         if symbol.startswith("??$getAs@") and "@Type at clang@@" in symbol:
             return symbol
+        # Keep the Registry<T> storage accessors: explicit specializations that
+        # LLVM_DEFINE_REGISTRY defines once, and that plugins call to register.
+        if (
+            symbol.startswith("??$getRegistryLinkListInstance@")
+            and "@detail at llvm@@" in symbol
+        ):
+            return symbol
         return None
     # Delete lambda object constructors and operator() functions. These start
     # with ??R<lambda_ or ??0<lambda_ and can be discarded because lambdas are
@@ -120,14 +127,6 @@ def should_keep_microsoft_symbol(symbol, calling_convention_decoration):
     # Skip X86GenMnemonicTables functions, they are not exposed from llvm/include/.
     elif re.match(r"\?is[A-Z0-9]*@X86 at llvm", symbol):
         return None
-    # Keep Registry<T>::Head and Registry<T>::Tail static members for plugin support.
-    # Pattern matches: ?Head@?$Registry@<template_args>@llvm@@ or ?Tail@?$Registry at ...
-    elif (
-        "?$Registry@" in symbol
-        and "@llvm@@" in symbol
-        and (symbol.startswith("?Head@") or symbol.startswith("?Tail@"))
-    ):
-        return symbol
     # Skip symbols added by the compiler with -fprofile-generate.
     elif symbol.startswith("__prof"):
         return None
@@ -515,6 +514,11 @@ def parse_tool_path(parser, tool, val):
         template = get_template_name(sym, args.mangling)
         if template:
             template_instantiation_refs.add(template)
+            # Registry<T> is only ever used through its storage accessor.
+            if template.endswith("getRegistryLinkListInstance"):
+                template_instantiation_refs.add(
+                    "Registry" if args.mangling == "microsoft" else "8Registry"
+                )
 
     # Print symbols which both:
     #  * Appear in exactly one input, as symbols defined in multiple

>From 6561162b1d896a0709e38042502559590b121d15 Mon Sep 17 00:00:00 2001
From: Ryan VanderMeulen <ryanvm at gmail.com>
Date: Tue, 8 Sep 2026 20:48:41 -0400
Subject: [PATCH 2/2] Add a lit test for extract_symbols.py's Registry handling

extract_symbols.py had no tests. Cover the case this PR fixes with llvm-mc
inputs shaped like FrontendAction.cpp (the LLVM_DEFINE_REGISTRY accessor
specialization and the explicitly instantiated Registry<T> members) and a
translation unit that only references the accessor, for both manglings. The
previous script exported nothing Registry-related for Microsoft mangling and
dropped the members for Itanium.

Assisted-by: Claude Code (analysis and drafting; reviewed and tested by the author)
---
 llvm/test/tools/extract_symbols/registry-itanium.test   | 5 +++--
 llvm/test/tools/extract_symbols/registry-microsoft.test | 6 ++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/llvm/test/tools/extract_symbols/registry-itanium.test b/llvm/test/tools/extract_symbols/registry-itanium.test
index d88e75a8dfda6..f94e4e4df7947 100644
--- a/llvm/test/tools/extract_symbols/registry-itanium.test
+++ b/llvm/test/tools/extract_symbols/registry-itanium.test
@@ -1,6 +1,7 @@
 ## Itanium counterpart of registry-microsoft.test: a reference to the
 ## getRegistryLinkListInstance<R>() accessor counts as a reference to Registry,
 ## so its explicitly instantiated members that are defined once are exported.
+## Symbols are printed in input order, so the checks are ordered.
 
 # RUN: rm -rf %t && mkdir -p %t
 # RUN: llvm-mc -triple=x86_64-linux-gnu -filetype=obj %S/Inputs/lib-itanium.s -o %t/lib.o
@@ -8,5 +9,5 @@
 # RUN: %extract_symbols --mangling=itanium %t/lib.o %t/user.o \
 # RUN:   | FileCheck %s --implicit-check-not=Registry
 
-# CHECK-DAG: _ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv
-# CHECK-DAG: _ZN4llvm8RegistryIN5clang15PluginASTActionEJEE8add_nodeEPNS3_4nodeE
+# CHECK:      _ZN4llvm6detail27getRegistryLinkListInstanceINS_8RegistryIN5clang15PluginASTActionEJEEEEERNS0_23RegistryLinkListStorageIT_EEv
+# CHECK-NEXT: _ZN4llvm8RegistryIN5clang15PluginASTActionEJEE8add_nodeEPNS3_4nodeE
diff --git a/llvm/test/tools/extract_symbols/registry-microsoft.test b/llvm/test/tools/extract_symbols/registry-microsoft.test
index c530b872c37d9..41ff71c8441f5 100644
--- a/llvm/test/tools/extract_symbols/registry-microsoft.test
+++ b/llvm/test/tools/extract_symbols/registry-microsoft.test
@@ -3,6 +3,8 @@
 ## specialization) and the explicitly instantiated Registry<T> members that only
 ## a plugin references must both be exported, or a plugin cannot link against a
 ## static tool built with LLVM_EXPORT_SYMBOLS_FOR_PLUGINS.
+##
+## Symbols are printed in input order, so the checks are ordered.
 
 # RUN: rm -rf %t && mkdir -p %t
 # RUN: llvm-mc -triple=x86_64-pc-windows-msvc -filetype=obj %S/Inputs/lib-msvc.s -o %t/lib.obj
@@ -13,5 +15,5 @@
 ## The accessor specialization and add_node are defined once and referenced
 ## only through the accessor, so both are exported. begin() is defined in both
 ## objects (the user TU instantiated the inline member itself), so it is not.
-# CHECK-DAG: ??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ
-# CHECK-DAG: ?add_node@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SAXPEAVnode at 12@@Z
+# CHECK:      ??$getRegistryLinkListInstance at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@detail at llvm@@YAAEAU?$RegistryLinkListStorage at V?$Registry at VPluginASTAction@clang@@$$V at llvm@@@01 at XZ
+# CHECK-NEXT: ?add_node@?$Registry at VPluginASTAction@clang@@$$V at llvm@@SAXPEAVnode at 12@@Z



More information about the llvm-commits mailing list