[llvm] [Instrumentor] Runtime bitcode linking controls (PR #216333)

Vincent Arcila via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 14 08:15:34 PDT 2026


https://github.com/jandrovins updated https://github.com/llvm/llvm-project/pull/216333

>From 0fd94ee68387a62370a69e56c8bee44165c18662 Mon Sep 17 00:00:00 2001
From: "Vincent A. Arcila Larrea" <arcilalarrea1 at llnl.gov>
Date: Mon, 27 Jul 2026 16:39:39 -0700
Subject: [PATCH 1/4] [Instrumentor] Add .json option for keeping symbols
 visible after linking

---
 .../llvm/Transforms/IPO/Instrumentor.h        | 28 +++++++++++++++
 llvm/lib/Transforms/IPO/Instrumentor.cpp      | 20 +++++++++--
 .../Transforms/IPO/InstrumentorConfigFile.cpp | 35 +++++++++++++++++++
 .../Instrumentor/default_config.json          |  6 ++--
 4 files changed, 85 insertions(+), 4 deletions(-)

diff --git a/llvm/include/llvm/Transforms/IPO/Instrumentor.h b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
index 5da53ac0ada15..6fbc620a7c4a9 100644
--- a/llvm/include/llvm/Transforms/IPO/Instrumentor.h
+++ b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
@@ -13,10 +13,12 @@
 #ifndef LLVM_TRANSFORMS_IPO_INSTRUMENTOR_H
 #define LLVM_TRANSFORMS_IPO_INSTRUMENTOR_H
 
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/EnumeratedArray.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
 #include "llvm/ADT/MapVector.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringMap.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -273,6 +275,7 @@ struct BaseConfigurationOption {
   enum KindTy {
     STRING,
     BOOLEAN,
+    STRING_LIST,
   };
 
   /// Create a boolean option with \p Name name, \p Description description and
@@ -287,6 +290,13 @@ struct BaseConfigurationOption {
   createStringOption(InstrumentationConfig &IC, StringRef Name,
                      StringRef Description, StringRef DefaultValue);
 
+  /// Create a string-list option with \p Name name, \p Description
+  /// description and \p DefaultValue as string-list default value.
+  LLVM_ABI static std::unique_ptr<BaseConfigurationOption>
+  createStringListOption(InstrumentationConfig &IC, StringRef Name,
+                         StringRef Description,
+                         ArrayRef<StringRef> DefaultValue);
+
   /// Helper union that holds any possible option type.
   union ValueTy {
     bool Bool;
@@ -317,12 +327,26 @@ struct BaseConfigurationOption {
   }
   ///}
 
+  /// Set and get of the string-list value. Only valid if it is a string-list
+  /// option.
+  ///{
+  void setStringList(ArrayRef<StringRef> Values) {
+    assert(Kind == STRING_LIST && "Not a string list!");
+    StringList.assign(Values.begin(), Values.end());
+  }
+  ArrayRef<StringRef> getStringList() const {
+    assert(Kind == STRING_LIST && "Not a string list!");
+    return StringList;
+  }
+  ///}
+
   /// The information of the option.
   ///{
   StringRef Name;
   StringRef Description;
   KindTy Kind;
   ValueTy Value = {0};
+  SmallVector<StringRef> StringList;
   ///}
 
   /// Construct a base configuration option.
@@ -374,6 +398,9 @@ struct LLVM_ABI InstrumentationConfig {
         *this, "runtime_bitcode", "Link runtime bitcode", "");
     InlineRuntimeEagerly = BaseConfigurationOption::createBoolOption(
         *this, "inline_runtime", "Inline runtime function calls eagerly", true);
+    RuntimeExportSymbols = BaseConfigurationOption::createStringListOption(
+      *this, "runtime_export_symbols",
+      "Runtime symbols that remain externally visible after linking.", {});
     populate(IIRB);
   }
 
@@ -447,6 +474,7 @@ struct LLVM_ABI InstrumentationConfig {
   std::unique_ptr<BaseConfigurationOption> GPUEnabled;
   std::unique_ptr<BaseConfigurationOption> RuntimeBitcode;
   std::unique_ptr<BaseConfigurationOption> InlineRuntimeEagerly;
+  std::unique_ptr<BaseConfigurationOption> RuntimeExportSymbols;
 
   /// The map registered instrumentation opportunities. The map is indexed by
   /// the instrumentation location kind and then by the opportunity name. Notice
diff --git a/llvm/lib/Transforms/IPO/Instrumentor.cpp b/llvm/lib/Transforms/IPO/Instrumentor.cpp
index bfa22199ef9f6..4bd1af2312af8 100644
--- a/llvm/lib/Transforms/IPO/Instrumentor.cpp
+++ b/llvm/lib/Transforms/IPO/Instrumentor.cpp
@@ -262,8 +262,13 @@ void InstrumentorImpl::linkRuntime() {
   }
 
   auto InternalizeCallback = [&](Module &M, const StringSet<> &GVS) {
-    internalizeModule(M, [&GVS](const GlobalValue &GV) {
-      return !GV.hasName() || !GVS.count(GV.getName());
+    StringSet<> RuntimeExports;
+    for (StringRef Name : IConf.RuntimeExportSymbols->getStringList())
+      RuntimeExports.insert(Name);
+
+    internalizeModule(M, [&GVS, &RuntimeExports](const GlobalValue &GV) {
+      return !GV.hasName() || !GVS.count(GV.getName()) ||
+             RuntimeExports.count(GV.getName());
     });
   };
 
@@ -629,6 +634,17 @@ BaseConfigurationOption::createStringOption(InstrumentationConfig &IConf,
   return BCO;
 }
 
+std::unique_ptr<BaseConfigurationOption>
+BaseConfigurationOption::createStringListOption(
+    InstrumentationConfig &IConf, StringRef Name, StringRef Description,
+    ArrayRef<StringRef> DefaultValue) {
+  auto BCO = std::make_unique<BaseConfigurationOption>(Name, Description,
+                                                        STRING_LIST);
+  BCO->setStringList(DefaultValue);
+  IConf.addBaseChoice(BCO.get());
+  return BCO;
+}
+
 void InstrumentationConfig::populate(InstrumentorIRBuilderTy &IIRB) {
   /// List of all instrumentation opportunities.
   BasePointerIO::populate(*this, IIRB);
diff --git a/llvm/lib/Transforms/IPO/InstrumentorConfigFile.cpp b/llvm/lib/Transforms/IPO/InstrumentorConfigFile.cpp
index a2600b0551b65..083eca05b00f4 100644
--- a/llvm/lib/Transforms/IPO/InstrumentorConfigFile.cpp
+++ b/llvm/lib/Transforms/IPO/InstrumentorConfigFile.cpp
@@ -69,6 +69,12 @@ void writeConfigToJSON(InstrumentationConfig &IConf, StringRef OutputFile,
     case BaseConfigurationOption::BOOLEAN:
       J.attribute(BaseCO->Name, BaseCO->getBool());
       break;
+    case BaseConfigurationOption::STRING_LIST:
+      J.attributeArray(BaseCO->Name, [&] {
+        for (StringRef Value : BaseCO->getStringList())
+          J.value(Value);
+      });
+      break;
     }
     if (!BaseCO->Description.empty())
       J.attribute(std::string(BaseCO->Name) + ".description",
@@ -195,6 +201,35 @@ bool readConfigFromJSON(InstrumentationConfig &IConf, StringRef InputFile,
                   DS_Warning));
             }
             break;
+          case BaseConfigurationOption::STRING_LIST: {
+            auto *Values = ObjIt.second.getAsArray();
+            if (!Values) {
+              Ctx.diagnose(DiagnosticInfoInstrumentation(
+                  Twine("configuration key '") + StringRef(ObjIt.first) +
+                      Twine("' expects an array of strings, value ignored"),
+                  DS_Warning));
+              break;
+            }
+
+            SmallVector<StringRef> Strings;
+            bool AllStrings = true;
+            for (const json::Value &Value : *Values) {
+              auto String = Value.getAsString();
+              if (!String) {
+                AllStrings = false;
+                break;
+              }
+              Strings.push_back(IConf.SS.save(*String));
+            }
+            if (AllStrings)
+              BO->setStringList(Strings);
+            else
+              Ctx.diagnose(DiagnosticInfoInstrumentation(
+                  Twine("configuration key '") + StringRef(ObjIt.first) +
+                      Twine("' expects an array of strings, value ignored"),
+                  DS_Warning));
+            break;
+          }
           }
         } else if (!StringRef(ObjIt.first).ends_with(".description")) {
           std::string Diag = "configuration key '" + ObjIt.first.str() +
diff --git a/llvm/test/Instrumentation/Instrumentor/default_config.json b/llvm/test/Instrumentation/Instrumentor/default_config.json
index fc712537e5e22..06057833ed8a6 100644
--- a/llvm/test/Instrumentation/Instrumentor/default_config.json
+++ b/llvm/test/Instrumentation/Instrumentor/default_config.json
@@ -17,7 +17,9 @@
     "runtime_bitcode": "",
     "runtime_bitcode.description": "Link runtime bitcode",
     "inline_runtime": true,
-    "inline_runtime.description": "Inline runtime function calls eagerly"
+    "inline_runtime.description": "Inline runtime function calls eagerly",
+    "runtime_export_symbols": [],
+    "runtime_export_symbols.description": "Runtime symbols that remain externally visible after linking."
   },
   "module_pre": {
     "module": {
@@ -446,4 +448,4 @@
       "id.description": "A unique ID associated with the given instrumentor call"
     }
   }
-}
\ No newline at end of file
+}

>From 314f775d7f726d01da9b25721739afe26c6d60c1 Mon Sep 17 00:00:00 2001
From: "Vincent A. Arcila Larrea" <arcilalarrea1 at llnl.gov>
Date: Mon, 27 Jul 2026 16:48:28 -0700
Subject: [PATCH 2/4] [Instrumentor] Add runtime exports test

---
 .../Instrumentor/runtime_export_symbols.ll        | 14 ++++++++++++++
 .../runtime_export_symbols_config.json            | 15 +++++++++++++++
 .../runtimes/runtime_export_symbols_rt.ll         | 14 ++++++++++++++
 3 files changed, 43 insertions(+)
 create mode 100644 llvm/test/Instrumentation/Instrumentor/runtime_export_symbols.ll
 create mode 100644 llvm/test/Instrumentation/Instrumentor/runtime_export_symbols_config.json
 create mode 100644 llvm/test/Instrumentation/Instrumentor/runtimes/runtime_export_symbols_rt.ll

diff --git a/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols.ll b/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols.ll
new file mode 100644
index 0000000000000..5e822baa0a7a7
--- /dev/null
+++ b/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols.ll
@@ -0,0 +1,14 @@
+; RUN: llvm-as %S/runtimes/runtime_export_symbols_rt.ll -o runtime_export_symbols_rt.bc
+; RUN: opt < %s -passes=instrumentor -instrumentor-read-config-files=%S/runtime_export_symbols_config.json -S | FileCheck %s
+
+; CHECK: @runtime_export = linkonce_odr global i32 0, comdat, align 4
+; CHECK: @runtime_internal = internal global i32 0, align 4
+
+ at runtime_export = external global i32
+ at runtime_internal = external global i32
+
+define i32 @test(i32 %lhs, i32 %rhs) {
+entry:
+  %result = add i32 %lhs, %rhs
+  ret i32 %result
+}
\ No newline at end of file
diff --git a/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols_config.json b/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols_config.json
new file mode 100644
index 0000000000000..2519dfb3796b3
--- /dev/null
+++ b/llvm/test/Instrumentation/Instrumentor/runtime_export_symbols_config.json
@@ -0,0 +1,15 @@
+{
+  "configuration": {
+    "runtime_prefix": "__runtime_export_",
+    "runtime_bitcode": "runtime_export_symbols_rt.bc",
+    "inline_runtime": false,
+    "runtime_export_symbols": ["runtime_export"]
+  },
+  "instruction_pre": {
+    "numeric": {
+      "enabled": true,
+      "size": true,
+      "id": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/llvm/test/Instrumentation/Instrumentor/runtimes/runtime_export_symbols_rt.ll b/llvm/test/Instrumentation/Instrumentor/runtimes/runtime_export_symbols_rt.ll
new file mode 100644
index 0000000000000..342dd718271b9
--- /dev/null
+++ b/llvm/test/Instrumentation/Instrumentor/runtimes/runtime_export_symbols_rt.ll
@@ -0,0 +1,14 @@
+$runtime_export = comdat any
+$runtime_internal = comdat any
+
+ at runtime_export = linkonce_odr global i32 0, comdat, align 4
+ at runtime_internal = linkonce_odr global i32 0, comdat, align 4
+
+define void @__runtime_export_pre_numeric(i32 %size, i32 %id) {
+entry:
+  %export = load i32, ptr @runtime_export, align 4
+  %internal = load i32, ptr @runtime_internal, align 4
+  %sum = add i32 %export, %internal
+  store i32 %sum, ptr @runtime_internal, align 4
+  ret void
+}
\ No newline at end of file

>From 7d319f01dbd7dab2c3ba7420e55dea99c008f534 Mon Sep 17 00:00:00 2001
From: "Vincent A. Arcila Larrea" <arcilalarrea1 at llnl.gov>
Date: Fri, 14 Aug 2026 07:34:06 -0700
Subject: [PATCH 3/4] [Instrumentor] Format

---
 llvm/include/llvm/Transforms/IPO/Instrumentor.h | 4 ++--
 llvm/lib/Transforms/IPO/Instrumentor.cpp        | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/llvm/include/llvm/Transforms/IPO/Instrumentor.h b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
index 6fbc620a7c4a9..915c3f3e35476 100644
--- a/llvm/include/llvm/Transforms/IPO/Instrumentor.h
+++ b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
@@ -399,8 +399,8 @@ struct LLVM_ABI InstrumentationConfig {
     InlineRuntimeEagerly = BaseConfigurationOption::createBoolOption(
         *this, "inline_runtime", "Inline runtime function calls eagerly", true);
     RuntimeExportSymbols = BaseConfigurationOption::createStringListOption(
-      *this, "runtime_export_symbols",
-      "Runtime symbols that remain externally visible after linking.", {});
+        *this, "runtime_export_symbols",
+        "Runtime symbols that remain externally visible after linking.", {});
     populate(IIRB);
   }
 
diff --git a/llvm/lib/Transforms/IPO/Instrumentor.cpp b/llvm/lib/Transforms/IPO/Instrumentor.cpp
index 4bd1af2312af8..fc5b8a9845bef 100644
--- a/llvm/lib/Transforms/IPO/Instrumentor.cpp
+++ b/llvm/lib/Transforms/IPO/Instrumentor.cpp
@@ -638,8 +638,8 @@ std::unique_ptr<BaseConfigurationOption>
 BaseConfigurationOption::createStringListOption(
     InstrumentationConfig &IConf, StringRef Name, StringRef Description,
     ArrayRef<StringRef> DefaultValue) {
-  auto BCO = std::make_unique<BaseConfigurationOption>(Name, Description,
-                                                        STRING_LIST);
+  auto BCO =
+      std::make_unique<BaseConfigurationOption>(Name, Description, STRING_LIST);
   BCO->setStringList(DefaultValue);
   IConf.addBaseChoice(BCO.get());
   return BCO;

>From 0dfbacd0bc6ce9c244e24d47497507eeb0122b72 Mon Sep 17 00:00:00 2001
From: "Vincent A. Arcila Larrea" <arcilalarrea1 at llnl.gov>
Date: Fri, 14 Aug 2026 07:59:58 -0700
Subject: [PATCH 4/4] [Instrumentor] Add pre-runtime-link extension point

---
 .../llvm/Transforms/IPO/Instrumentor.h        |  7 ++
 llvm/lib/Transforms/IPO/Instrumentor.cpp      |  2 +
 llvm/unittests/Transforms/IPO/CMakeLists.txt  |  1 +
 .../Transforms/IPO/InstrumentorTest.cpp       | 94 +++++++++++++++++++
 4 files changed, 104 insertions(+)
 create mode 100644 llvm/unittests/Transforms/IPO/InstrumentorTest.cpp

diff --git a/llvm/include/llvm/Transforms/IPO/Instrumentor.h b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
index 915c3f3e35476..34a4cea86d220 100644
--- a/llvm/include/llvm/Transforms/IPO/Instrumentor.h
+++ b/llvm/include/llvm/Transforms/IPO/Instrumentor.h
@@ -407,6 +407,13 @@ struct LLVM_ABI InstrumentationConfig {
   /// Populate the instrumentation opportunities.
   virtual void populate(InstrumentorIRBuilderTy &IIRB);
 
+  /// Allow embedded users to extend the module after instrumentation and before
+  /// runtime bitcode linking.
+  virtual bool instrumentBeforeRuntimeLink(Module &,
+                                           InstrumentorIRBuilderTy &) {
+    return false;
+  }
+
   /// Get the runtime prefix for the instrumentation runtime functions.
   StringRef getRTName() const { return RuntimePrefix->getString(); }
 
diff --git a/llvm/lib/Transforms/IPO/Instrumentor.cpp b/llvm/lib/Transforms/IPO/Instrumentor.cpp
index fc5b8a9845bef..44e95c32e1d43 100644
--- a/llvm/lib/Transforms/IPO/Instrumentor.cpp
+++ b/llvm/lib/Transforms/IPO/Instrumentor.cpp
@@ -541,6 +541,8 @@ bool InstrumentorImpl::instrument() {
   for (Function &Fn : M)
     Changed |= instrumentFunction(Fn);
 
+  Changed |= IConf.instrumentBeforeRuntimeLink(M, IIRB);
+
   linkRuntime();
 
   return Changed;
diff --git a/llvm/unittests/Transforms/IPO/CMakeLists.txt b/llvm/unittests/Transforms/IPO/CMakeLists.txt
index 5b45191afc711..a54bf05b047b5 100644
--- a/llvm/unittests/Transforms/IPO/CMakeLists.txt
+++ b/llvm/unittests/Transforms/IPO/CMakeLists.txt
@@ -14,5 +14,6 @@ add_llvm_unittest(IPOTests
   AttributorTest.cpp
   FunctionSpecializationTest.cpp
   ImportIDTableTests.cpp
+  InstrumentorTest.cpp
   MergeFunctionsTest.cpp
   )
diff --git a/llvm/unittests/Transforms/IPO/InstrumentorTest.cpp b/llvm/unittests/Transforms/IPO/InstrumentorTest.cpp
new file mode 100644
index 0000000000000..4f89ada4dbb35
--- /dev/null
+++ b/llvm/unittests/Transforms/IPO/InstrumentorTest.cpp
@@ -0,0 +1,94 @@
+//===- InstrumentorTest.cpp - Unit tests for InstrumentorPass -------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Transforms/IPO/Instrumentor.h"
+
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/AsmParser/Parser.h"
+#include "llvm/IR/Constants.h"
+#include "llvm/IR/GlobalVariable.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/SourceMgr.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace llvm::instrumentor;
+
+namespace {
+
+class PreRuntimeLinkConfig final : public InstrumentationConfig {
+public:
+  explicit PreRuntimeLinkConfig(StringRef RuntimePath)
+      : RuntimePath(RuntimePath) {}
+
+  void populate(InstrumentorIRBuilderTy &) override {
+    RuntimeBitcode->setString(RuntimePath);
+  }
+
+  bool instrumentBeforeRuntimeLink(Module &M,
+                                   InstrumentorIRBuilderTy &) override {
+    LLVMContext &Ctx = M.getContext();
+    Type *Ty = Type::getInt1Ty(Ctx);
+    Constant *SawRuntime =
+        ConstantInt::get(Ty, M.getNamedGlobal("runtime_marker") != nullptr);
+    new GlobalVariable(M, Ty, false, GlobalValue::ExternalLinkage, SawRuntime,
+                       "hook_saw_runtime");
+    return true;
+  }
+
+private:
+  std::string RuntimePath;
+};
+
+std::unique_ptr<Module> parseModule(StringRef IR, LLVMContext &Ctx) {
+  SMDiagnostic Err;
+  std::unique_ptr<Module> M = parseAssemblyString(IR, Err, Ctx);
+  EXPECT_TRUE(M);
+  return M;
+}
+
+TEST(InstrumentorTest, RunsHookBeforeRuntimeLink) {
+  SmallString<128> RuntimePath;
+  int FD;
+  ASSERT_FALSE(sys::fs::createTemporaryFile("instrumentor-runtime", "ll", FD,
+                                            RuntimePath));
+  auto RemoveRuntime = make_scope_exit([&] { sys::fs::remove(RuntimePath); });
+
+  raw_fd_ostream OS(FD, true);
+  OS << "@runtime_marker = global i32 0\n";
+  OS.close();
+
+  LLVMContext Ctx;
+  std::unique_ptr<Module> M = parseModule(R"ir(
+    define void @test() {
+    entry:
+      ret void
+    }
+  )ir",
+                                          Ctx);
+  ASSERT_TRUE(M);
+
+  PreRuntimeLinkConfig Config(RuntimePath);
+  ModuleAnalysisManager MAM;
+  InstrumentorPass Pass(nullptr, &Config);
+  Pass.run(*M, MAM);
+
+  GlobalVariable *HookSawRuntime = M->getNamedGlobal("hook_saw_runtime");
+  ASSERT_NE(HookSawRuntime, nullptr);
+  auto *Initializer = dyn_cast<ConstantInt>(HookSawRuntime->getInitializer());
+  ASSERT_NE(Initializer, nullptr);
+  EXPECT_TRUE(Initializer->isZero());
+  EXPECT_NE(M->getNamedGlobal("runtime_marker"), nullptr);
+}
+
+} // namespace



More information about the llvm-commits mailing list