[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