[llvm] [WIP][Support] Add LLVMToolSession for in-process tool invocation (PR #221996)

Anutosh Bhat via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 8 06:17:51 PDT 2026


https://github.com/anutosh491 created https://github.com/llvm/llvm-project/pull/221996

Please read [RFC](https://discourse.llvm.org/t/rfc-embeddable-llvm-tool-drivers-for-long-lived-hosts/91754) & [RFC section](https://discourse.llvm.org/t/rfc-embeddable-llvm-tool-drivers-for-long-lived-hosts/91754#p-367966-why-initllvm-ownership-matters-7)

This patch introduces `LLVMToolSession`, which lets a long-lived host own LLVM process initialization and invoke registered LLVM tools without constructing and destroying `InitLLVM` for every call.

The existing `llvm` multicall driver is used as the first consumer. It already combines several callable tool entry points in one executable, making it a small and concrete place to establish the session model without changing the individual tools.

An `LLVMToolSession`:

- owns one `InitLLVM` instance for the lifetime of the host;
- owns the registry of callable tools;
- creates a `ToolContext` for each invocation;
- allows a tool to find or invoke another tool through that same session.

This is intended to support hosts such as [WasmBolt](https://github.com/anutosh491/WasmBolt) (try [here](https://anutosh21.github.io/WasmBolt/)), where multiple LLVM tools run sequentially—and sometimes call one another—inside one long-lived WebAssembly process.

The patch deliberately does not yet:

- change Clang, LLD, `llc`, or `opt`;
- address process-global `llvm::cl` state;
- remove `InitLLVM` from standalone tool wrappers.

Those should remain separate follow-up changes so each part can be reviewed independently.

I have added tests for 

- a real `llvm-cxxfilt` invocation through `LLVMToolSession`;
- repeated and nested tool invocations within one session;
- unknown-tool handling;
- the existing `llvm-driver` tests, including multicall and symlink invocation.

>From e6b6e8565c0ae8399af3e9df69d4a5e822e617ca Mon Sep 17 00:00:00 2001
From: anutosh491 <andersonbhat491 at gmail.com>
Date: Tue, 8 Sep 2026 18:42:12 +0530
Subject: [PATCH] [Support] Add LLVMToolSession for in-process tool invocation

---
 llvm/include/llvm/Support/LLVMDriver.h        |  67 +++++++++-
 llvm/lib/Support/CMakeLists.txt               |   1 +
 llvm/lib/Support/LLVMToolSession.cpp          | 119 ++++++++++++++++++
 .../tools/llvm-driver/session-dispatch.test   |   7 ++
 llvm/tools/llvm-driver/llvm-driver.cpp        |  66 +++-------
 llvm/unittests/Support/CMakeLists.txt         |   1 +
 .../Support/LLVMToolSession/CMakeLists.txt    |  20 +++
 .../LLVMToolSession/LLVMToolSessionTest.cpp   |  65 ++++++++++
 8 files changed, 298 insertions(+), 48 deletions(-)
 create mode 100644 llvm/lib/Support/LLVMToolSession.cpp
 create mode 100644 llvm/test/tools/llvm-driver/session-dispatch.test
 create mode 100644 llvm/unittests/Support/LLVMToolSession/CMakeLists.txt
 create mode 100644 llvm/unittests/Support/LLVMToolSession/LLVMToolSessionTest.cpp

diff --git a/llvm/include/llvm/Support/LLVMDriver.h b/llvm/include/llvm/Support/LLVMDriver.h
index 0b2e265d50b42..48122bfaceb7c 100644
--- a/llvm/include/llvm/Support/LLVMDriver.h
+++ b/llvm/include/llvm/Support/LLVMDriver.h
@@ -9,9 +9,35 @@
 #ifndef LLVM_SUPPORT_LLVMDRIVER_H
 #define LLVM_SUPPORT_LLVMDRIVER_H
 
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/ErrorOr.h"
+
+#include <memory>
+
 namespace llvm {
 
-struct ToolContext {
+class LLVMToolSession;
+class ToolContext;
+
+using ToolMainFn = int (*)(int, char **, const ToolContext &);
+
+/// An LLVM command-line tool that can be invoked without creating a process.
+struct CallableTool {
+  StringRef Name;
+  ToolMainFn Main;
+
+  explicit operator bool() const { return Main != nullptr; }
+};
+
+/// Describes how a tool was invoked and provides access to its host session.
+class ToolContext {
+  LLVMToolSession *Session = nullptr;
+
+  friend class LLVMToolSession;
+
+public:
   const char *Path;
   const char *PrependArg;
   // PrependArg will be added unconditionally by the llvm-driver, but
@@ -20,6 +46,45 @@ struct ToolContext {
   // point to the llvm-driver executable, where PrependArg will be needed to
   // invoke the correct tool.
   bool NeedsPrependArg;
+
+  ToolContext(const char *Path, const char *PrependArg, bool NeedsPrependArg)
+      : Path(Path), PrependArg(PrependArg), NeedsPrependArg(NeedsPrependArg) {}
+
+  /// Finds a tool registered with the session that owns this context.
+  LLVM_ABI ErrorOr<CallableTool> getCallableTool(StringRef Name) const;
+
+  /// Invokes another tool registered with the same host session.
+  LLVM_ABI int callTool(ArrayRef<const char *> Args) const;
+};
+
+/// Owns LLVM process initialization and an in-process tool registry.
+///
+/// A long-lived host constructs one session and uses it for every embedded
+/// tool invocation. The individual tools borrow a ToolContext and therefore do
+/// not initialize or shut down LLVM themselves.
+class LLVM_ABI LLVMToolSession {
+public:
+  LLVMToolSession(int &Argc, char **&Argv, ArrayRef<CallableTool> Tools,
+                  bool InstallPipeSignalExitHandler = true,
+                  bool NeedsPOSIXUtilitySignalHandling = false);
+  ~LLVMToolSession();
+
+  LLVMToolSession(const LLVMToolSession &) = delete;
+  LLVMToolSession &operator=(const LLVMToolSession &) = delete;
+
+  /// Invokes the tool named by Args[0]. Args may instead contain a
+  /// process-style argv beginning with the session executable or an LLVM
+  /// multicall name.
+  int callTool(ArrayRef<const char *> Args);
+
+private:
+  struct Impl;
+  std::unique_ptr<Impl> PImpl;
+
+  ErrorOr<CallableTool> findTool(StringRef Name) const;
+  ToolContext makeContext(StringRef InvokedName);
+
+  friend class ToolContext;
 };
 
 } // namespace llvm
diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt
index c85c784993e20..a834fef252d70 100644
--- a/llvm/lib/Support/CMakeLists.txt
+++ b/llvm/lib/Support/CMakeLists.txt
@@ -222,6 +222,7 @@ add_llvm_component_library(LLVMSupport
   LineIterator.cpp
   Locale.cpp
   LockFileManager.cpp
+  LLVMToolSession.cpp
   ManagedStatic.cpp
   MathExtras.cpp
   MemAlloc.cpp
diff --git a/llvm/lib/Support/LLVMToolSession.cpp b/llvm/lib/Support/LLVMToolSession.cpp
new file mode 100644
index 0000000000000..94f596a9d8903
--- /dev/null
+++ b/llvm/lib/Support/LLVMToolSession.cpp
@@ -0,0 +1,119 @@
+//===-- LLVMToolSession.cpp ----------------------------------------------===//
+//
+// 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/Support/LLVMDriver.h"
+
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/Path.h"
+
+#include <string>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+using namespace llvm;
+
+namespace {
+
+bool matchesToolName(StringRef RegisteredName, StringRef InvokedName) {
+  StringRef Stem = sys::path::stem(InvokedName);
+  auto Matches = [RegisteredName](StringRef Candidate) {
+    size_t Position = Candidate.rfind_insensitive(RegisteredName);
+    return Position != StringRef::npos &&
+           (Position + RegisteredName.size() == Candidate.size() ||
+            !llvm::isAlnum(Candidate[Position + RegisteredName.size()]));
+  };
+  return Matches(Stem) || Matches(sys::path::filename(InvokedName));
+}
+
+bool isMulticallName(StringRef Name) { return matchesToolName("llvm", Name); }
+
+} // namespace
+
+struct LLVMToolSession::Impl {
+  InitLLVM Initialization;
+  std::string ExecutablePath;
+  std::vector<std::pair<std::string, ToolMainFn>> Tools;
+
+  Impl(int &Argc, char **&Argv, ArrayRef<CallableTool> RegisteredTools,
+       bool InstallPipeSignalExitHandler, bool NeedsPOSIXUtilitySignalHandling)
+      : Initialization(Argc, Argv, InstallPipeSignalExitHandler,
+                       NeedsPOSIXUtilitySignalHandling),
+        ExecutablePath(Argv[0]) {
+    Tools.reserve(RegisteredTools.size());
+    for (const CallableTool &Tool : RegisteredTools)
+      Tools.emplace_back(Tool.Name.str(), Tool.Main);
+  }
+};
+
+LLVMToolSession::LLVMToolSession(int &Argc, char **&Argv,
+                                 ArrayRef<CallableTool> Tools,
+                                 bool InstallPipeSignalExitHandler,
+                                 bool NeedsPOSIXUtilitySignalHandling)
+    : PImpl(std::make_unique<Impl>(Argc, Argv, Tools,
+                                   InstallPipeSignalExitHandler,
+                                   NeedsPOSIXUtilitySignalHandling)) {}
+
+LLVMToolSession::~LLVMToolSession() = default;
+
+ErrorOr<CallableTool> LLVMToolSession::findTool(StringRef Name) const {
+  for (const auto &[RegisteredName, Main] : PImpl->Tools)
+    if (matchesToolName(RegisteredName, Name))
+      return CallableTool{RegisteredName, Main};
+  return make_error_code(std::errc::no_such_file_or_directory);
+}
+
+ToolContext LLVMToolSession::makeContext(StringRef InvokedName) {
+  bool NeedsPrependArg = !matchesToolName(InvokedName, PImpl->ExecutablePath);
+  ToolContext Context(PImpl->ExecutablePath.c_str(), InvokedName.data(),
+                      NeedsPrependArg);
+  Context.Session = this;
+  return Context;
+}
+
+int LLVMToolSession::callTool(ArrayRef<const char *> Args) {
+  if (Args.empty())
+    return -1;
+
+  StringRef InvokedName = Args.front();
+  ErrorOr<CallableTool> Tool = findTool(InvokedName);
+  if (!Tool) {
+    if (InvokedName != PImpl->ExecutablePath && !isMulticallName(InvokedName))
+      return -1;
+    Args = Args.drop_front();
+    if (Args.empty())
+      return -1;
+    InvokedName = Args.front();
+    Tool = findTool(InvokedName);
+  }
+
+  if (!Tool)
+    return -1;
+
+  ToolContext Context = makeContext(InvokedName);
+  SmallVector<char *, 16> MutableArgs;
+  MutableArgs.reserve(Args.size() + 1);
+  for (const char *Arg : Args)
+    MutableArgs.push_back(const_cast<char *>(Arg));
+  MutableArgs.push_back(nullptr);
+  return Tool->Main(Args.size(), MutableArgs.data(), Context);
+}
+
+ErrorOr<CallableTool> ToolContext::getCallableTool(StringRef Name) const {
+  if (!Session)
+    return make_error_code(std::errc::operation_not_permitted);
+  return Session->findTool(Name);
+}
+
+int ToolContext::callTool(ArrayRef<const char *> Args) const {
+  if (!Session)
+    return -1;
+  return Session->callTool(Args);
+}
diff --git a/llvm/test/tools/llvm-driver/session-dispatch.test b/llvm/test/tools/llvm-driver/session-dispatch.test
new file mode 100644
index 0000000000000..6ac93be6ce431
--- /dev/null
+++ b/llvm/test/tools/llvm-driver/session-dispatch.test
@@ -0,0 +1,7 @@
+# REQUIRES: llvm-driver
+
+## Exercise a real LLVM tool entry point through LLVMToolSession rather than
+## only testing the registry with synthetic callbacks.
+# RUN: %llvm cxxfilt _Z3foov | FileCheck %s
+
+# CHECK: foo()
diff --git a/llvm/tools/llvm-driver/llvm-driver.cpp b/llvm/tools/llvm-driver/llvm-driver.cpp
index 14ce162faee46..865d6d432d473 100644
--- a/llvm/tools/llvm-driver/llvm-driver.cpp
+++ b/llvm/tools/llvm-driver/llvm-driver.cpp
@@ -6,14 +6,11 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
-#include "llvm/Support/CommandLine.h"
-#include "llvm/Support/ErrorHandling.h"
-#include "llvm/Support/InitLLVM.h"
 #include "llvm/Support/LLVMDriver.h"
 #include "llvm/Support/Path.h"
-#include "llvm/Support/WithColor.h"
+#include "llvm/Support/raw_ostream.h"
 
 using namespace llvm;
 
@@ -36,51 +33,26 @@ static void printHelpMessage() {
                << "OPTIONS:\n\n  --help - Display this message\n";
 }
 
-static int findTool(int Argc, char **Argv, const char *Argv0) {
-  if (!Argc) {
-    printHelpMessage();
-    return 1;
-  }
+int main(int Argc, char **Argv) {
+  const CallableTool Tools[] = {
+#define LLVM_DRIVER_TOOL(tool, entry) {tool, entry##_main},
+#include "LLVMDriverTools.def"
+  };
 
-  StringRef ToolName = Argv[0];
+  LLVMToolSession Session(Argc, Argv, Tools);
 
-  if (ToolName == "--help") {
+  StringRef Stem = sys::path::stem(Argv[0]);
+  if (Stem.equals_insensitive("llvm") &&
+      (Argc == 1 || (Argc == 2 && StringRef(Argv[1]) == "--help"))) {
     printHelpMessage();
-    return 0;
+    return Argc == 1 ? 1 : 0;
   }
 
-  StringRef Stem = sys::path::stem(ToolName);
-  auto Is = [=](StringRef Tool) {
-    auto IsImpl = [=](StringRef Stem) {
-      auto I = Stem.rfind_insensitive(Tool);
-      return I != StringRef::npos && (I + Tool.size() == Stem.size() ||
-                                      !llvm::isAlnum(Stem[I + Tool.size()]));
-    };
-    for (StringRef S : {Stem, sys::path::filename(ToolName)})
-      if (IsImpl(S))
-        return true;
-    return false;
-  };
-
-  auto MakeDriverArgs = [=]() -> llvm::ToolContext {
-    if (ToolName != Argv0)
-      return {Argv0, ToolName.data(), true};
-    return {Argv0, sys::path::filename(Argv0).data(), false};
-  };
-
-#define LLVM_DRIVER_TOOL(tool, entry)                                          \
-  if (Is(tool))                                                                \
-    return entry##_main(Argc, Argv, MakeDriverArgs());
-#include "LLVMDriverTools.def"
-
-  if (Is("llvm") || Argv0 == Argv[0])
-    return findTool(Argc - 1, Argv + 1, Argv0);
-
-  printHelpMessage();
-  return 1;
-}
-
-int main(int Argc, char **Argv) {
-  llvm::InitLLVM X(Argc, Argv);
-  return findTool(Argc, Argv, Argv[0]);
+  SmallVector<const char *, 16> Args(Argv, Argv + Argc);
+  int Result = Session.callTool(Args);
+  if (Result == -1) {
+    printHelpMessage();
+    return 1;
+  }
+  return Result;
 }
diff --git a/llvm/unittests/Support/CMakeLists.txt b/llvm/unittests/Support/CMakeLists.txt
index cc88a6c5670ca..f9732ff4e0e4b 100644
--- a/llvm/unittests/Support/CMakeLists.txt
+++ b/llvm/unittests/Support/CMakeLists.txt
@@ -162,3 +162,4 @@ if(NOT LLVM_INTEGRATED_CRT_ALLOC)
 endif()
 
 add_subdirectory(CommandLineInit)
+add_subdirectory(LLVMToolSession)
diff --git a/llvm/unittests/Support/LLVMToolSession/CMakeLists.txt b/llvm/unittests/Support/LLVMToolSession/CMakeLists.txt
new file mode 100644
index 0000000000000..2d343295fb191
--- /dev/null
+++ b/llvm/unittests/Support/LLVMToolSession/CMakeLists.txt
@@ -0,0 +1,20 @@
+set(test_name LLVMToolSessionTests)
+set(test_suite UnitTests)
+
+# This test supplies its own main() so a single LLVMToolSession can own
+# InitLLVM for the complete test process.
+if (NOT LLVM_BUILD_TESTS)
+  set(EXCLUDE_FROM_ALL ON)
+endif()
+
+list(APPEND LLVM_LINK_COMPONENTS Support)
+
+add_llvm_executable(${test_name}
+  IGNORE_EXTERNALIZE_DEBUGINFO NO_INSTALL_RPATH
+  LLVMToolSessionTest.cpp)
+
+target_link_libraries(${test_name} PRIVATE llvm_gtest)
+add_dependencies(${test_suite} ${test_name})
+
+set(outdir ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR})
+set_output_directory(${test_name} BINARY_DIR ${outdir} LIBRARY_DIR ${outdir})
diff --git a/llvm/unittests/Support/LLVMToolSession/LLVMToolSessionTest.cpp b/llvm/unittests/Support/LLVMToolSession/LLVMToolSessionTest.cpp
new file mode 100644
index 0000000000000..a8a12f3a7e4d1
--- /dev/null
+++ b/llvm/unittests/Support/LLVMToolSession/LLVMToolSessionTest.cpp
@@ -0,0 +1,65 @@
+//===- LLVMToolSessionTest.cpp -------------------------------------------===//
+//
+// 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/Support/LLVMDriver.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+
+using namespace llvm;
+
+namespace {
+
+std::unique_ptr<LLVMToolSession> Session;
+unsigned CompilerCalls;
+unsigned LinkerCalls;
+
+int linkerMain(int Argc, char **Argv, const ToolContext &Context) {
+  ++LinkerCalls;
+  EXPECT_EQ(Argc, 3);
+  EXPECT_STREQ(Argv[0], "wasm-ld");
+  EXPECT_TRUE(Context.getCallableTool("clang"));
+  return 0;
+}
+
+int compilerMain(int Argc, char **Argv, const ToolContext &Context) {
+  ++CompilerCalls;
+  EXPECT_EQ(Argc, 2);
+  EXPECT_STREQ(Argv[0], "clang");
+  const char *LinkArgs[] = {"wasm-ld", Argv[1], "out.wasm"};
+  return Context.callTool(LinkArgs);
+}
+
+TEST(LLVMToolSessionTest, SupportsSequentialNestedToolCalls) {
+  const char *First[] = {"clang", "first.cpp"};
+  const char *Second[] = {"clang", "second.cpp"};
+
+  EXPECT_EQ(Session->callTool(First), 0);
+  EXPECT_EQ(Session->callTool(Second), 0);
+  EXPECT_EQ(CompilerCalls, 2u);
+  EXPECT_EQ(LinkerCalls, 2u);
+}
+
+TEST(LLVMToolSessionTest, ReportsUnknownTools) {
+  const char *Args[] = {"not-an-llvm-tool"};
+  EXPECT_EQ(Session->callTool(Args), -1);
+}
+
+} // namespace
+
+int main(int Argc, char **Argv) {
+  const CallableTool Tools[] = {
+      {"clang", compilerMain},
+      {"wasm-ld", linkerMain},
+  };
+  Session = std::make_unique<LLVMToolSession>(Argc, Argv, Tools);
+  testing::InitGoogleTest(&Argc, Argv);
+  int Result = RUN_ALL_TESTS();
+  Session.reset();
+  return Result;
+}



More information about the llvm-commits mailing list