[PluginAPI] Experimental support for dynamic compiler plugins (#24892) Load experimental compiler plugins at runtime with `--iree-load-plugin` or `IREE_LOAD_PLUGINS`. Validate plugin metadata and registration, and allow embedded compilers to recover from failed dynamic registrations. Use one automatically generated compatibility ID derived from the plugin API headers. This is not a full ABI fingerprint: plugins need matching IREE and LLVM/MLIR revisions, compatible toolchains, and aligned ABI-affecting build settings. An attempt is made to document these requirements and cover loader failures, but it should be considered experimental. Co-authored-by: Jelle Schühmacher <schuehmacher@roofline.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub> --------- Signed-off-by: Jelle Schühmacher <schuehmacher@roofline.ai> Co-authored-by: Florian Walbroel <florian.walbroel@rwth-aachen.de> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/build_tools/bazel/abi_prefix.bzl b/build_tools/bazel/abi_prefix.bzl index 44d73b1..6d84a81 100644 --- a/build_tools/bazel/abi_prefix.bzl +++ b/build_tools/bazel/abi_prefix.bzl
@@ -6,8 +6,7 @@ """The logical namespace the compiler's llvm/mlir symbols are renamed into.""" -# Any identifier no real namespace uses will do; the 18 is arbitrary and tracks -# nothing. A plugin resolves against these names, so changing it invalidates -# every plugin already built: bump it only to force that. CMake takes the same -# value from IREE_COMPILER_ABI_PREFIX. +# This is a symbol namespace, not a compatibility version; 18 has no version +# meaning. Keep it stable across plugin API changes. CMake uses the same default +# for IREE_COMPILER_ABI_PREFIX and exports the host's value to plugin builds. IREE_COMPILER_ABI_PREFIX = "IREE18"
diff --git a/build_tools/cmake/iree_renamed_link.cmake b/build_tools/cmake/iree_renamed_link.cmake index b5647bc..c993ad0 100644 --- a/build_tools/cmake/iree_renamed_link.cmake +++ b/build_tools/cmake/iree_renamed_link.cmake
@@ -8,8 +8,8 @@ # # Renaming every llvm/mlir C++ symbol lets libIREECompiler share a process with # a foreign LLVM/MLIR. Plugins rename to match so they resolve against it. -# gen_rename_map.py decides the spelling, shared with Bazel so both builds land -# on the same ABI. +# CMake and Bazel share gen_rename_map.py to produce the same symbol names. +# Matching names alone do not establish C++ ABI compatibility. # # Everything becomes a static archive first, because that is what llvm-objcopy # takes. The linked library cannot be renamed instead: rewriting .dynsym breaks @@ -93,7 +93,7 @@ # Splits the transitive link closure into archives to rename (STATIC_LIBS_VAR) # and everything else, passed through untouched (OTHER_VAR). Imported targets -# and C archives are safe to pass through: they define no mangled C++ names. +# are not renamed; callers must ensure they do not need LLVM/MLIR renaming. function(iree_collect_static_link_closure STATIC_LIBS_VAR OTHER_VAR) set(_worklist ${ARGN}) set(_visited "")
diff --git a/build_tools/scripts/BUILD.bazel b/build_tools/scripts/BUILD.bazel index 12890e7..a25500b 100644 --- a/build_tools/scripts/BUILD.bazel +++ b/build_tools/scripts/BUILD.bazel
@@ -4,7 +4,7 @@ # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_test") package( default_visibility = ["//visibility:public"], @@ -14,6 +14,7 @@ exports_files([ "amdgpu_device_binaries.py", "amdgpu_target_map.py", + "gen_plugin_abi_hash.py", "generate_vm_isa.py", ]) @@ -21,3 +22,12 @@ name = "generate_vm_isa", srcs = ["generate_vm_isa.py"], ) + +py_test( + name = "gen_plugin_abi_hash_test", + srcs = [ + "gen_plugin_abi_hash.py", + "gen_plugin_abi_hash_test.py", + ], + main = "gen_plugin_abi_hash_test.py", +)
diff --git a/build_tools/scripts/gen_plugin_abi_hash.py b/build_tools/scripts/gen_plugin_abi_hash.py new file mode 100755 index 0000000..ad0ac48 --- /dev/null +++ b/build_tools/scripts/gen_plugin_abi_hash.py
@@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# Copyright 2026 The IREE Authors +# +# Licensed 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 +"""Emits a header with a hash of the plugin API headers. + +This is the single compatibility ID for the entry-point layout, registration +contract, and selected C++ API headers. There is no manual version to bump. + +Only the listed IREE headers are hashed. This is not a full ABI fingerprint: +plugins must use the compiler's LLVM/MLIR revision and ABI-affecting build +settings. Symbol renaming isolates namespaces; it does not enforce that match. +""" + +import argparse +import hashlib +import pathlib + +_GUARD = "IREE_COMPILER_PLUGINAPI_PLUGINABIHASH_H_" + + +def compute_hash(paths: list[str]) -> str: + digest = hashlib.sha256() + for path in sorted(paths, key=lambda p: pathlib.PurePath(p).name): + # Include names and lengths so moving bytes between headers changes + # the hash even when the concatenated contents would be identical. + for data in ( + pathlib.PurePath(path).name.encode(), + pathlib.Path(path).read_bytes(), + ): + digest.update(len(data).to_bytes(8, "big")) + digest.update(data) + return digest.hexdigest()[:16] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--input", action="append", required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + + out = pathlib.Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text( + "// Generated by build_tools/scripts/gen_plugin_abi_hash.py.\n" + f"#ifndef {_GUARD}\n" + f"#define {_GUARD}\n" + "\n" + "#include <stdint.h>\n" + "\n" + f"#define IREE_COMPILER_PLUGIN_ABI_HASH UINT64_C(0x{compute_hash(args.input)})\n" + "\n" + f"#endif // {_GUARD}\n" + ) + + +if __name__ == "__main__": + main()
diff --git a/build_tools/scripts/gen_plugin_abi_hash_test.py b/build_tools/scripts/gen_plugin_abi_hash_test.py new file mode 100644 index 0000000..1f2f2e8 --- /dev/null +++ b/build_tools/scripts/gen_plugin_abi_hash_test.py
@@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright 2026 The IREE Authors +# +# Licensed 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 +"""Tests for gen_plugin_abi_hash.""" + +import os +import pathlib +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(__file__)) +import gen_plugin_abi_hash + + +class ComputeHashTest(unittest.TestCase): + def setUp(self): + self._dir = tempfile.TemporaryDirectory() + self.root = pathlib.Path(self._dir.name) + self.addCleanup(self._dir.cleanup) + + def _write(self, name: str, text: str) -> str: + path = self.root / name + path.write_text(text) + return str(path) + + def test_content_change_changes_the_hash(self): + before = self._write("Client.h", "class A { virtual void f(); };\n") + first = gen_plugin_abi_hash.compute_hash([before]) + self._write("Client.h", "class A { virtual void f(); virtual void g(); };\n") + self.assertNotEqual(first, gen_plugin_abi_hash.compute_hash([before])) + + def test_argument_order_does_not_matter(self): + a = self._write("Client.h", "a\n") + b = self._write("PluginEntryPoint.h", "b\n") + self.assertEqual( + gen_plugin_abi_hash.compute_hash([a, b]), + gen_plugin_abi_hash.compute_hash([b, a]), + ) + + def test_moving_content_between_headers_changes_the_hash(self): + a = self._write("Client.h", "struct S { int x; };\n") + b = self._write("PluginEntryPoint.h", "") + first = gen_plugin_abi_hash.compute_hash([a, b]) + self._write("Client.h", "") + self._write("PluginEntryPoint.h", "struct S { int x; };\n") + self.assertNotEqual(first, gen_plugin_abi_hash.compute_hash([a, b])) + + def test_hash_is_a_short_hex_string(self): + a = self._write("Client.h", "a\n") + digest = gen_plugin_abi_hash.compute_hash([a]) + self.assertEqual(len(digest), 16) + self.assertRegex(digest, r"^[0-9a-f]{16}$") + + def test_header_boundaries_affect_the_hash(self): + a = self._write("A.h", "B.h") + b = self._write("B.h", "") + first = gen_plugin_abi_hash.compute_hash([a, b]) + self._write("A.h", "") + self._write("B.h", "B.h") + self.assertNotEqual(first, gen_plugin_abi_hash.compute_hash([a, b])) + + def test_build_location_does_not_affect_the_hash(self): + a = self._write("Client.h", "struct S {};\n") + other = self.root / "other-build" + other.mkdir() + b = other / "Client.h" + b.write_bytes(pathlib.Path(a).read_bytes()) + self.assertEqual( + gen_plugin_abi_hash.compute_hash([a]), + gen_plugin_abi_hash.compute_hash([str(b)]), + ) + + +if __name__ == "__main__": + unittest.main()
diff --git a/compiler/lit.cfg.py b/compiler/lit.cfg.py index e0c9133..9df1c11 100644 --- a/compiler/lit.cfg.py +++ b/compiler/lit.cfg.py
@@ -38,6 +38,30 @@ } ) +# Bazel passes test plugin paths relative to the runfiles root, CMake absolute. +# Each becomes a substitution: lit's shell does not expand $VAR. +_test_srcdir = os.environ.get("TEST_SRCDIR", "") + + +def _resolve_test_plugin(key, value): + if os.path.isabs(value): + return value + for root in (_test_srcdir, os.path.join(_test_srcdir, "_main"), os.getcwd()): + candidate = os.path.join(root, value) + if os.path.exists(candidate): + return os.path.abspath(candidate) + lit_config.fatal(f"{key}={value} does not resolve from the runfiles root") + + +for _key in sorted(config.environment, key=len, reverse=True): + if _key.startswith("IREE_TEST_") and _key.endswith("_PLUGIN"): + config.environment[_key] = _resolve_test_plugin(_key, config.environment[_key]) + config.substitutions.append(("%" + _key.lower(), config.environment[_key])) + +# Only set when the build carries the loadable test plugins. +if config.environment.get("IREE_TEST_DEPS_PLUGIN"): + config.available_features.add("iree_dynamic_plugins") + # Use the most preferred temp directory. config.test_exec_root = ( os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR")
diff --git a/compiler/src/iree/compiler/API/Internal/CompilerDriver.cpp b/compiler/src/iree/compiler/API/Internal/CompilerDriver.cpp index a1d8b29..909c1fd 100644 --- a/compiler/src/iree/compiler/API/Internal/CompilerDriver.cpp +++ b/compiler/src/iree/compiler/API/Internal/CompilerDriver.cpp
@@ -420,7 +420,7 @@ mlir::iree_compiler::registerAllDialects(registry); mlir::iree_compiler::registerLLVMIRTranslations(registry); - if (!pluginManager.loadAvailablePlugins()) { + if (!pluginManager.loadAvailablePlugins(/*tolerateDynamicFailures=*/true)) { fprintf(stderr, "Failed to initialize IREE compiler plugins.\n"); abort(); } @@ -1531,9 +1531,18 @@ } llvm::cl::ParseCommandLineOptions(argc, argv, banner); + // Reject load flags that were not handled before global initialization. + if (!mlir::iree_compiler::verifyDynamicPluginFlags(llvm::errs())) { + exit(EXIT_FAILURE); + } } void ireeCompilerGlobalInitialize() { + // No argv here, so only IREE_LOAD_PLUGINS names plugins. Failures are + // logged: a library must not kill its host. + (void)mlir::iree_compiler::initializeDynamicPlugins(/*args=*/{}, + llvm::errs()); + if (globalInit) { globalInit->refCount.fetch_add(1); return;
diff --git a/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp b/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp index 75dc57b..b1c50b2 100644 --- a/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp +++ b/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp
@@ -51,6 +51,13 @@ InitLLVM y(argc, argv); + // Before CLI parsing: the plugins loaded here still have options to add. + if (!mlir::iree_compiler::initializeDynamicPlugins( + llvm::ArrayRef<const char *>(const_cast<const char **>(argv), argc), + llvm::errs())) { + return failure(); + } + // We support a limited form of the PluginManager, allowing it to perform // global initialization and dialect registration. mlir::iree_compiler::PluginManager pluginManager;
diff --git a/compiler/src/iree/compiler/Pipelines/BUILD.bazel b/compiler/src/iree/compiler/Pipelines/BUILD.bazel index 86ba007..34337de 100644 --- a/compiler/src/iree/compiler/Pipelines/BUILD.bazel +++ b/compiler/src/iree/compiler/Pipelines/BUILD.bazel
@@ -12,6 +12,9 @@ licenses = ["notice"], # Apache 2.0 ) +# Hashed into the plugin ABI; see PluginAPI/BUILD.bazel. +exports_files(["Options.h"]) + iree_compiler_cc_library( name = "Options", srcs = ["Options.cpp"],
diff --git a/compiler/src/iree/compiler/PluginAPI/BUILD.bazel b/compiler/src/iree/compiler/PluginAPI/BUILD.bazel index 063f2f4..bb42dac 100644 --- a/compiler/src/iree/compiler/PluginAPI/BUILD.bazel +++ b/compiler/src/iree/compiler/PluginAPI/BUILD.bazel
@@ -5,6 +5,7 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception load("//build_tools/bazel:build_defs.oss.bzl", "iree_compiler_cc_library") +load("//build_tools/bazel:iree_genrule.bzl", "iree_genrule") package( default_visibility = ["//visibility:public"], @@ -12,11 +13,45 @@ licenses = ["notice"], # Apache 2.0 ) +# Generate the single compatibility ID from the entry-point and C++ API headers. +iree_genrule( + name = "PluginABIHash_h__generate", + srcs = [ + "Client.h", + "PluginEntryPoint.h", + "//build_tools/scripts:gen_plugin_abi_hash.py", + "//compiler/src/iree/compiler/Pipelines:Options.h", + "//compiler/src/iree/compiler/Utils:OptionUtils.h", + ], + outs = ["PluginABIHash.h"], + cmd = " ".join([ + "python3 $(rootpath //build_tools/scripts:gen_plugin_abi_hash.py)", + "--input $(rootpath Client.h)", + "--input $(rootpath PluginEntryPoint.h)", + "--input $(rootpath //compiler/src/iree/compiler/Pipelines:Options.h)", + "--input $(rootpath //compiler/src/iree/compiler/Utils:OptionUtils.h)", + "--out $(execpath PluginABIHash.h)", + ]), +) + +iree_compiler_cc_library( + name = "PluginABIHash", + hdrs = ["PluginABIHash.h"], +) + +# Plain C and header-only, so a C plugin can use it. +iree_compiler_cc_library( + name = "PluginEntryPoint", + hdrs = ["PluginEntryPoint.h"], + deps = [":PluginABIHash"], +) + iree_compiler_cc_library( name = "PluginAPI", srcs = ["Client.cpp"], hdrs = ["Client.h"], deps = [ + ":PluginEntryPoint", "//compiler/src/iree/compiler/Pipelines:Options", "//compiler/src/iree/compiler/Utils", "@llvm-project//llvm:Support", @@ -33,6 +68,7 @@ ], deps = [ ":PluginAPI", + ":PluginEntryPoint", "//compiler/src/iree/compiler/PluginAPI/Config:StaticLinkedPlugins", "//compiler/src/iree/compiler/Utils", "@llvm-project//llvm:Support",
diff --git a/compiler/src/iree/compiler/PluginAPI/CMakeLists.txt b/compiler/src/iree/compiler/PluginAPI/CMakeLists.txt index e4e74ec..0de130f 100644 --- a/compiler/src/iree/compiler/PluginAPI/CMakeLists.txt +++ b/compiler/src/iree/compiler/PluginAPI/CMakeLists.txt
@@ -10,6 +10,41 @@ iree_add_all_subdirs() +iree_genrule( + NAME + PluginABIHash_h__generate + SRCS + "${PROJECT_SOURCE_DIR}/build_tools/scripts/gen_plugin_abi_hash.py" + "${PROJECT_SOURCE_DIR}/compiler/src/iree/compiler/Pipelines/Options.h" + "${PROJECT_SOURCE_DIR}/compiler/src/iree/compiler/Utils/OptionUtils.h" + "Client.h" + "PluginEntryPoint.h" + OUTS + PluginABIHash.h + CMD + "python3 $(rootpath //build_tools/scripts:gen_plugin_abi_hash.py) --input $(rootpath Client.h) --input $(rootpath PluginEntryPoint.h) --input $(rootpath //compiler/src/iree/compiler/Pipelines:Options.h) --input $(rootpath //compiler/src/iree/compiler/Utils:OptionUtils.h) --out $(execpath PluginABIHash.h)" +) + +iree_cc_library( + NAME + PluginABIHash + HDRS + "PluginABIHash.h" + DEPS + + PUBLIC +) + +iree_cc_library( + NAME + PluginEntryPoint + HDRS + "PluginEntryPoint.h" + DEPS + ::PluginABIHash + PUBLIC +) + iree_cc_library( NAME PluginAPI @@ -18,6 +53,7 @@ SRCS "Client.cpp" DEPS + ::PluginEntryPoint LLVMSupport iree::compiler::Pipelines::Options iree::compiler::Utils @@ -33,6 +69,7 @@ "PluginManager.cpp" DEPS ::PluginAPI + ::PluginEntryPoint LLVMSupport MLIRIR iree::compiler::PluginAPI::Config::StaticLinkedPlugins
diff --git a/compiler/src/iree/compiler/PluginAPI/PluginEntryPoint.h b/compiler/src/iree/compiler/PluginAPI/PluginEntryPoint.h new file mode 100644 index 0000000..a5f01b4 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/PluginEntryPoint.h
@@ -0,0 +1,98 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +#ifndef IREE_COMPILER_PLUGINAPI_PLUGINENTRYPOINT_H_ +#define IREE_COMPILER_PLUGINAPI_PLUGINENTRYPOINT_H_ + +// Plain C up to the macro, so a C plugin can include it. +#include <stdbool.h> +#include <stdint.h> + +#include "iree/compiler/PluginAPI/PluginABIHash.h" + +// One fixed name, so the loader needs no id before the library is open. +#define IREE_COMPILER_PLUGIN_INFO_SYMBOL_NAME "iree_get_compiler_plugin_info" + +#if defined(_MSC_VER) +#define IREE_COMPILER_PLUGIN_WEAK +#define IREE_COMPILER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define IREE_COMPILER_PLUGIN_WEAK __attribute__((weak)) +// Plugins are normally built with hidden visibility, which hides this from +// dlsym. +#define IREE_COMPILER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +namespace mlir::iree_compiler { +class PluginRegistrar; +} // namespace mlir::iree_compiler +using IreeCompilerPluginRegistrar = mlir::iree_compiler::PluginRegistrar; +extern "C" { +#else +// Opaque to C; only the compiler dereferences it. +typedef void IreeCompilerPluginRegistrar; +#endif + +// Experimental plugin entry point; no stable API or ABI is promised. +// +// The generated header hash is the only compatibility ID. Keep it first and +// fixed-width: the loader checks it before reading the rest of the structure. +// Changes to this header automatically change the ID. +// +// The strings are owned by the plugin and live for the process. +typedef struct IreeCompilerPluginInfo { + // Hash of the selected IREE plugin API headers, not a full ABI fingerprint. + // LLVM/MLIR revisions and ABI-affecting build settings must also match. + uint64_t abiHash; + // Unique ID advertised by this library. Use the same ID when registering + // its session with PluginRegistrar and selecting it with --iree-plugin. + const char *pluginId; + // Free-form plugin release string; not checked for compatibility. + const char *pluginVersion; + // Registers sessions and returns true on success. The registrar is borrowed + // only for this call; do not retain its address. Dynamic registrations are + // committed only if this callback succeeds and their IDs do not collide + // with existing registrations. Other global side effects cannot be rolled + // back, so the callback must clean them up on failure. Each ID may be + // registered only once within a callback. + bool (*registerPlugin)(IreeCompilerPluginRegistrar *registrar); +} IreeCompilerPluginInfo; + +typedef const IreeCompilerPluginInfo *(*IreeCompilerPluginInfoGetter)(void); + +#ifdef __cplusplus +} // extern "C" + +// The per-id entry point serves a static link, the fixed one a dlopen. The +// fixed one is weak: a static link holds many plugins and calls none by that +// name. +// +// Indirect so the id may be a macro: ## does not expand its argument. +#define IREE_DEFINE_COMPILER_PLUGIN(plugin_id, register_fn, plugin_version) \ + IREE_DEFINE_COMPILER_PLUGIN_IMPL(plugin_id, register_fn, plugin_version) + +#define IREE_DEFINE_COMPILER_PLUGIN_IMPL(plugin_id, register_fn, \ + plugin_version) \ + extern "C" IREE_COMPILER_PLUGIN_EXPORT bool \ + iree_register_compiler_plugin_##plugin_id( \ + mlir::iree_compiler::PluginRegistrar *registrar) { \ + return register_fn(registrar); \ + } \ + extern "C" IREE_COMPILER_PLUGIN_EXPORT \ + IREE_COMPILER_PLUGIN_WEAK const IreeCompilerPluginInfo * \ + iree_get_compiler_plugin_info(void) { \ + static const IreeCompilerPluginInfo info = { \ + IREE_COMPILER_PLUGIN_ABI_HASH, \ + #plugin_id, \ + plugin_version, \ + &iree_register_compiler_plugin_##plugin_id, \ + }; \ + return &info; \ + } +#endif // __cplusplus + +#endif // IREE_COMPILER_PLUGINAPI_PLUGINENTRYPOINT_H_
diff --git a/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp b/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp index 3ac9025..b18f260 100644 --- a/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp +++ b/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp
@@ -6,13 +6,26 @@ #include "iree/compiler/PluginAPI/PluginManager.h" +#include <cinttypes> +#include <string> #include <utility> +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +#if !defined(_WIN32) +#include <dlfcn.h> +#endif + #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/DynamicLibrary.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/Process.h" #include "llvm/Support/raw_ostream.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/Location.h" +#include "mlir/IR/MLIRContext.h" // Declare entrypoints for each statically registered plugin. #define HANDLE_PLUGIN_ID(plugin_id) \ @@ -37,16 +50,287 @@ llvm::cl::cat(category)); } -PluginManager::PluginManager() {} +namespace { -bool PluginManager::loadAvailablePlugins() { +// --iree-load-plugin is parsed before LLVM command-line parsing, so plugins can +// add options. This sink keeps cl from rejecting it and lists it in --help. +struct PluginOptionsSink { + llvm::SmallVector<std::string> pluginOpts; + void bindOptions(OptionsBinder &binder) { + static llvm::cl::OptionCategory category("IREE dynamic plugin options"); + binder.list<std::string>( + "iree-load-plugin", pluginOpts, + llvm::cl::desc( + "Load an experimental compiler plugin library (repeatable). " + "Use --iree-print-plugin-info during compilation to list " + "IDs; activate a plugin with --iree-plugin=<id>."), + llvm::cl::value_desc("path"), llvm::cl::cat(category)); + } + using FromFlags = OptionsFromFlags<PluginOptionsSink>; +}; + +} // namespace + +IREE_DEFINE_COMPILER_OPTION_FLAGS(PluginOptionsSink); + +DynamicPluginRegistry &DynamicPluginRegistry::get() { + static DynamicPluginRegistry instance; + return instance; +} + +llvm::Error DynamicPluginRegistry::initialize(llvm::ArrayRef<const char *> args, + EnvPlugins envPlugins) { + assert(!initialized && "flags cannot be processed twice"); + initialized = true; + + // Registered even without a command line, so cl accepts and documents the + // flag. + (void)PluginOptionsSink::FromFlags::get(); + + loadPluginsFromCL(args); + if (envPlugins == EnvPlugins::Enabled) { + loadPluginPathsFromEnv(); + } + return std::move(loadErrors); +} + +void DynamicPluginRegistry::addPlugin(llvm::Expected<Plugin> plugin) { + if (!plugin) { + loadErrors = llvm::joinErrors(std::move(loadErrors), plugin.takeError()); + return; + } + for (const Plugin &existing : plugins) { + // Ignore repeated paths from command-line flags and the environment. + if (existing.path == plugin->path) { + return; + } + // Reject duplicate advertised IDs before running registration callbacks. + if (existing.pluginId == plugin->pluginId) { + loadErrors = llvm::joinErrors( + std::move(loadErrors), + llvm::createStringError( + llvm::inconvertibleErrorCode(), + "plugin '%s' reports id '%s', already taken by '%s'", + plugin->path.c_str(), plugin->pluginId.c_str(), + existing.path.c_str())); + return; + } + } + plugins.push_back(std::move(*plugin)); +} + +bool initializeDynamicPlugins(llvm::ArrayRef<const char *> args, + llvm::raw_ostream &os) { + // The first call loads plugins; subsequent calls reuse its result and ignore + // their arguments and any environment changes. + static const bool valid = [&] { + llvm::Error e = DynamicPluginRegistry::get().initialize( + args, DynamicPluginRegistry::EnvPlugins::Enabled); + if (!e) { + return true; + } + // logAllUnhandledErrors prefixes only the first error. + llvm::handleAllErrors(std::move(e), [&](const llvm::ErrorInfoBase &info) { + os << "[IREE Dynamic Plugin ERROR]: " << info.message() << "\n"; + }); + return false; + }(); + return valid; +} + +bool DynamicPluginRegistry::registerPlugins(PluginManager *registrar) const { + bool success = true; + for (const auto &plugin : plugins) { + if (registrar->registrations.count(plugin.pluginId)) { + llvm::errs() << "[IREE Dynamic Plugin ERROR]: '" << plugin.pluginId + << "' has the same id as an already registered plugin\n"; + success = false; + continue; + } + // Commit registrations only after the callback succeeds. A failing plugin + // must not leave partially registered sessions in an embedded compiler. + PluginManager pending; + if (!plugin.registerFunction(&pending)) { + llvm::errs() << "[IREE Dynamic Plugin ERROR]: registration function of '" + << plugin.pluginId << "' (" << plugin.path << ") failed\n"; + success = false; + continue; + } + bool unique = true; + for (const auto &entry : pending.registrations) { + if (registrar->registrations.count(entry.getKey())) { + llvm::errs() << "[IREE Dynamic Plugin ERROR]: '" << entry.getKey() + << "' has the same id as an already registered plugin\n"; + unique = false; + } + } + if (!unique) { + success = false; + continue; + } + for (auto &entry : pending.registrations) { + registrar->registerPlugin(std::move(entry.getValue())); + } + } + registrationFailed |= !success; + return success; +} + +llvm::SmallVector<std::string> DynamicPluginRegistry::getLoadedPlugins() const { + llvm::SmallVector<std::string> pluginNames; + for (const auto &plugin : plugins) { + pluginNames.push_back(plugin.pluginId); + } + return pluginNames; +} + +bool DynamicPluginRegistry::hasPluginPath(llvm::StringRef path) const { + return llvm::any_of( + plugins, [&](const Plugin &plugin) { return plugin.path == path; }); +} + +bool verifyDynamicPluginFlags(llvm::raw_ostream &os) { + const auto ®istry = DynamicPluginRegistry::get(); + bool valid = true; + for (const std::string &path : + PluginOptionsSink::FromFlags::get().pluginOpts) { + if (!registry.hasPluginPath(path)) { + os << "[IREE Dynamic Plugin ERROR]: --iree-load-plugin=" << path + << " was not loaded; this entry point takes plugins from " + "IREE_LOAD_PLUGINS\n"; + valid = false; + } + } + return valid; +} + +llvm::Expected<DynamicPluginRegistry::Plugin> +DynamicPluginRegistry::Plugin::loadFromPath(llvm::StringRef path) { + Plugin plugin; + plugin.path = path.str(); + +#if defined(_WIN32) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "dynamic plugins are not supported on Windows"); +#else + // llvm::sys::DynamicLibrary uses RTLD_LAZY, which defers a missing symbol to + // its first call. RTLD_LOCAL keeps two plugins carrying the same helper from + // binding to each other's copy. + plugin.library = ::dlopen(plugin.path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!plugin.library) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "could not load plugin library '%s': %s", + plugin.path.c_str(), ::dlerror()); + } + + // POSIX guarantees the cast. + auto getInfo = reinterpret_cast<IreeCompilerPluginInfoGetter>( + ::dlsym(plugin.library, IREE_COMPILER_PLUGIN_INFO_SYMBOL_NAME)); + if (!getInfo) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "plugin '%s' defines no %s; declare it with " + "IREE_DEFINE_COMPILER_PLUGIN", + plugin.path.c_str(), + IREE_COMPILER_PLUGIN_INFO_SYMBOL_NAME); + } + + const IreeCompilerPluginInfo *info = getInfo(); + if (!info) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "plugin '%s' returned no plugin info", + plugin.path.c_str()); + } + if (info->abiHash != IREE_COMPILER_PLUGIN_ABI_HASH) { + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "plugin '%s' was built against plugin API headers %016" PRIx64 + ", this compiler has %016" PRIx64 + "; rebuild the plugin against this compiler", + plugin.path.c_str(), info->abiHash, IREE_COMPILER_PLUGIN_ABI_HASH); + } + + // Read the remaining fields only after the compatibility ID matches. + if (!info->pluginId || !info->pluginVersion || !info->registerPlugin) { + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "plugin '%s' left a field of IreeCompilerPluginInfo null", + plugin.path.c_str()); + } + + plugin.pluginId = info->pluginId; + plugin.registerFunction = info->registerPlugin; + return plugin; +#endif // _WIN32 +} + +void DynamicPluginRegistry::loadPluginsFromCL( + llvm::ArrayRef<const char *> args) { + // Match every spelling cl accepts: one or two dashes, a separated value, + // response files. verifyDynamicPluginFlags rejects accepted flags whose + // paths were not loaded during initialization. + llvm::BumpPtrAllocator alloc; + llvm::SmallVector<const char *> expanded(args.begin(), args.end()); + llvm::cl::ExpansionContext expansion(alloc, llvm::cl::TokenizeGNUCommandLine); + if (llvm::Error e = expansion.expandResponseFiles(expanded)) { + loadErrors = llvm::joinErrors(std::move(loadErrors), std::move(e)); + } + + for (size_t i = 1; i < expanded.size(); ++i) { + llvm::StringRef arg = expanded[i]; + // cl treats everything after -- as positional. + if (arg == "--") { + break; + } + if (!arg.consume_front("--") && !arg.consume_front("-")) { + continue; + } + if (!arg.consume_front("iree-load-plugin")) { + continue; + } + llvm::StringRef path; + if (arg.consume_front("=")) { + path = arg; + } else if (arg.empty() && i + 1 < expanded.size()) { + path = expanded[++i]; + } else { + // A longer flag with the same prefix. + continue; + } + if (path.empty()) { + loadErrors = llvm::joinErrors( + std::move(loadErrors), + llvm::createStringError(llvm::inconvertibleErrorCode(), + "no path given to --iree-load-plugin")); + continue; + } + addPlugin(Plugin::loadFromPath(path)); + } +} + +void DynamicPluginRegistry::loadPluginPathsFromEnv() { + std::optional<std::string> envVar = + llvm::sys::Process::GetEnv("IREE_LOAD_PLUGINS"); + if (!envVar) { + return; + } + llvm::SmallVector<llvm::StringRef, 4> paths; + llvm::StringRef(*envVar).split(paths, ',', -1, /*KeepEmpty=*/false); + for (llvm::StringRef path : paths) { + addPlugin(Plugin::loadFromPath(path)); + } +} + +bool PluginManager::loadAvailablePlugins(bool tolerateDynamicFailures) { // Initialize static plugins. #define HANDLE_PLUGIN_ID(plugin_id) \ if (!iree_register_compiler_plugin_##plugin_id(this)) \ return false; #include "iree/compiler/PluginAPI/Config/StaticLinkedPlugins.inc" #undef HANDLE_PLUGIN_ID - return true; + + bool success = DynamicPluginRegistry::get().registerPlugins(this); + return success || tolerateDynamicFailures; } void PluginManager::globalInitialize() { @@ -73,11 +357,12 @@ } } -llvm::SmallVector<std::string> PluginManager::getLoadedPlugins() { +llvm::SmallVector<std::string> PluginManager::getLoadedPlugins() const { llvm::SmallVector<std::string> plugins; -#define HANDLE_PLUGIN_ID(plugin_id) plugins.push_back(#plugin_id); -#include "iree/compiler/PluginAPI/Config/StaticLinkedPlugins.inc" -#undef HANDLE_PLUGIN_ID + for (const auto &entry : registrations) { + plugins.push_back(entry.getKey().str()); + } + llvm::sort(plugins); return plugins; } @@ -167,9 +452,10 @@ } auto foundIt = allPluginSessions.find(pluginId); if (foundIt == allPluginSessions.end()) { - llvm::errs() - << "[IREE plugins error]: could not activate requested IREE plugin '" - << pluginId << "' because it is not registered (available plugins: "; + llvm::errs() << "[IREE plugins error]: could not activate requested " + "IREE plugin '" + << pluginId + << "' because it is not registered (available plugins: "; llvm::interleaveComma(getAvailableIds(), llvm::errs()); llvm::errs() << ")\n"; return failure();
diff --git a/compiler/src/iree/compiler/PluginAPI/PluginManager.h b/compiler/src/iree/compiler/PluginAPI/PluginManager.h index ef19499..c249b3d 100644 --- a/compiler/src/iree/compiler/PluginAPI/PluginManager.h +++ b/compiler/src/iree/compiler/PluginAPI/PluginManager.h
@@ -8,18 +8,21 @@ #define IREE_COMPILER_PLUGINAPI_PLUGINMANAGER_H_ #include <optional> +#include <string> #include <string_view> -#include <vector> #include "iree/compiler/PluginAPI/Client.h" #include "iree/compiler/Utils/OptionUtils.h" +#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" namespace mlir::iree_compiler { -class PluginManager; class PluginManagerSession; +class PluginManager; // Command line options for the plugin manager. class PluginManagerOptions { @@ -34,6 +37,60 @@ using FromFlags = OptionsFromFlags<PluginManagerOptions>; }; +// Initialized before LLVM command-line parsing so plugins can add options. +class DynamicPluginRegistry { +public: + DynamicPluginRegistry(const DynamicPluginRegistry &) = delete; + DynamicPluginRegistry &operator=(const DynamicPluginRegistry &) = delete; + + enum class EnvPlugins { Disabled, Enabled }; + + /// Empty until initialize(). + static DynamicPluginRegistry &get(); + + /// Loads every plugin named in |args| and, if enabled, in IREE_LOAD_PLUGINS. + /// Call once. The error joins every failure. + [[nodiscard]] llvm::Error initialize(llvm::ArrayRef<const char *> args, + EnvPlugins envPlugins); + + [[nodiscard]] bool registerPlugins(PluginManager *registrar) const; + llvm::SmallVector<std::string> getLoadedPlugins() const; + bool hasPluginPath(llvm::StringRef path) const; + bool hasRegistrationFailures() const { return registrationFailed; } + +private: + struct Plugin { + std::string path; + std::string pluginId; + // Never closed: registered code must outlive every session. + void *library = nullptr; + PluginRegistrationFunction registerFunction = nullptr; + + /// The id is read from the library. + static llvm::Expected<Plugin> loadFromPath(llvm::StringRef path); + }; + + DynamicPluginRegistry() = default; + + void loadPluginsFromCL(llvm::ArrayRef<const char *> args); + void loadPluginPathsFromEnv(); + void addPlugin(llvm::Expected<Plugin> plugin); + + bool initialized = false; + mutable bool registrationFailed = false; + llvm::Error loadErrors = llvm::Error::success(); + llvm::SmallVector<Plugin> plugins; +}; + +/// Loads once, however often it is called; later |args| are ignored. False if +/// any plugin failed, with the failures written to |os|. +bool initializeDynamicPlugins(llvm::ArrayRef<const char *> args, + llvm::raw_ostream &os); + +/// Catches an --iree-load-plugin that reached llvm::cl without passing through +/// initializeDynamicPlugins, as through the C API. +bool verifyDynamicPluginFlags(llvm::raw_ostream &os); + // Manages global registrations for available plugins. // Typically, there will be one PluginManager globally for the compiler, and // it is initialized in ireeCompilerGlobalInitialize() based on statically @@ -48,12 +105,10 @@ // an MLIRContext is available. class PluginManager : public PluginRegistrar { public: - PluginManager(); - - // Initializes the plugin manager. Since this may do shared library opening - // and use failable initializers, it can fail. There probably isn't much to - // do in that case but crash, but the choice is left to the caller. - bool loadAvailablePlugins(); + // Registers static plugins and previously loaded dynamic plugins. Returns + // false on registration failure. With tolerateDynamicFailures, skips failed + // dynamic registrations and returns false only for static failures. + bool loadAvailablePlugins(bool tolerateDynamicFailures = false); // Calls through to AbstractPluginRegistration::globalInitialize for all // available plugins. @@ -67,14 +122,15 @@ // available plugins. void initializeCLI(); - // Calls through to AbstractPluginRegistration::registerDialects for all + // Calls through to AbstractPluginRegistration::registerGlobalDialects for all // available plugins. void registerGlobalDialects(DialectRegistry ®istry); - // Gets a list of all loaded plugin names. - llvm::SmallVector<std::string> getLoadedPlugins(); + // Returns successfully registered plugin IDs in sorted order. + llvm::SmallVector<std::string> getLoadedPlugins() const; private: + friend class DynamicPluginRegistry; friend class PluginManagerSession; };
diff --git a/compiler/src/iree/compiler/PluginAPI/README.md b/compiler/src/iree/compiler/PluginAPI/README.md index c2282db..23795df 100644 --- a/compiler/src/iree/compiler/PluginAPI/README.md +++ b/compiler/src/iree/compiler/PluginAPI/README.md
@@ -2,13 +2,14 @@ This is a work in progress to enable IREE compiler plugin support per [RFC - Proposal to Build IREE Compiler Plugin Mechanism](https://github.com/iree-org/iree/issues/12520). -This document will be replaced with a more comprehensive single-source once -the work is complete. + +Dynamic loading is experimental: the API and C++ ABI may change without +backward compatibility. Rebuild plugins with the compiler they will run in. ## Interim Developer Docs The `PluginManager` mirrors the execution hierarchy of the C API bindings -(`compiler/bindings/c/iree/compiler/embedding_api`): +(`compiler/bindings/c/iree/compiler/embedding_api.h`): * Global Initialization * Global CLI setup @@ -36,8 +37,9 @@ ### Static linking -Plugins can be statically linked into the compiler by way of the -`-DIREE_COMPILER_PLUGINS=` option. This does two things: +In CMake, select statically linked compiler plugins with +`-DIREE_COMPILER_PLUGINS=<id1;id2>`. In Bazel, use +`--iree_compiler_plugins=<id1,id2>`. Selection does two things: * Causes the generated `PluginAPI/Config/StaticLinkedPlugins.inc` to have a `HANDLE_PLUGIN_ID(plugin_id)` line. @@ -51,16 +53,99 @@ ### Dynamic linking -(Not yet implemented) +Dynamic compiler plugins are experimental and supported on Linux and macOS. +In CMake, enable compiler symbol exports with +`-DIREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS=ON`. Bazel's compiler shared +library exports these symbols without an additional option. +Each library is opened with `dlopen()` and queried through one exported +symbol for its ID and compatibility metadata. A header hash mismatch is +rejected before registration. Plugins are named on the command line or in the environment: -Dynamic linking proceeds similarly, driven by a combination of environment -variables, API calls to load plugin libs or pre-parsed CLI flags. For each -plugin library located in such a way, it will be `dlopen()`'d and the -corresponding entry point found and used, similar to the static linking case. +```sh +iree-compile --iree-load-plugin=/path/to/libmy_plugin.so --iree-plugin=my_id ... +IREE_LOAD_PLUGINS=/path/to/libmy_plugin.so \ + iree-compile --iree-plugin=my_id ... +``` -Note that only compilers built with `-DIREE_COMPILER_BUILD_SHARED_LIBS=ON` is -supported for this case. That carries a number of restrictions and other issues -that are outside of the immediate scope of plugins. +Repeat `--iree-load-plugin=<path>` to load multiple libraries, or set +`IREE_LOAD_PLUGINS` to a comma-separated list of paths. Loading makes a plugin +available; `--iree-plugin=<id>` activates an explicitly selected plugin for a +session. Use `--iree-print-plugin-info` during compilation to list available +and activated IDs. + +Embedded users must set `IREE_LOAD_PLUGINS` before the first +`ireeCompilerGlobalInitialize()` call. Loading happens once per process; +subsequent initialization calls do not reread the environment or load new +plugins. `ireeCompilerSessionSetFlags` can select an already registered plugin +with `--iree-plugin=<id>`, but cannot load a library with `--iree-load-plugin`. + +The loader reports load and registration errors. The command-line tools exit +on these errors. An embedded compiler keeps successful registrations and skips +failed dynamic plugins. Registrations from a failed callback are discarded; +callbacks must clean up any other side effects before returning failure. +Registering the same ID twice within a callback is a programming error and +aborts the process. `IREE_DEFINE_COMPILER_PLUGIN` defines both entry points so +one source can support static and dynamic linking. + +Both build systems provide `iree_compiler_register_dynamic_plugin`, which +builds the library and applies the rename described below. An install tree +provides it through `find_package(IREECompiler)`; see +`samples/compiler_plugins/out_of_tree_example/README.md`. + +#### Build requirements + +Build the plugin against the host compiler's exact IREE and LLVM/MLIR revisions, +including generated headers from that build. Match these compilation settings: + +* Target architecture, pointer width, and platform C++ ABI. Using the host's + C++ compiler and toolchain is the simplest way to keep these aligned. +* C++ standard library and its ABI options, such as `_GLIBCXX_USE_CXX11_ABI`, + `_GLIBCXX_DEBUG`, or libc++ ABI configuration macros. +* RTTI and exception handling (`LLVM_ENABLE_RTTI`, `LLVM_ENABLE_EH`, and the + effective `-frtti`/`-fno-rtti` and `-fexceptions`/`-fno-exceptions` flags). +* Assertions (`NDEBUG`) and LLVM options that change header definitions or + layouts, such as `LLVM_ENABLE_ABI_BREAKING_CHECKS`. Check the actual compiler + flags: `IREE_ENABLE_ASSERTIONS` can enable assertions in a release build. +* Sanitizer instrumentation and its runtime requirements, when enabled. + +In-tree plugins inherit the build settings. External plugins must match them +explicitly. Matching only `CMAKE_BUILD_TYPE` is insufficient. Optimization levels +and debug information need not match unless they change one of the settings +above. + +The tools must link the shared compiler library, the default in both build +systems (`IREE_LINK_COMPILER_SHARED_LIBRARY` in CMake, +`//compiler/src/iree/compiler/API:link_shared` in Bazel). CMake also requires +`IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS=ON` and +`IREE_ENABLE_THIN_ARCHIVES=OFF`. Plugin archives must contain position-independent +code and must not be thin archives: `llvm-objcopy` rewrites their members. + +The build rules rename the plugin's `llvm::` and `mlir::` symbols to match the +compiler and resolve references against its shared library. Do not link another +copy of LLVM, MLIR, or the IREE compiler into the plugin. + +#### Compatibility checks and versions + +Use `IREE_DEFINE_COMPILER_PLUGIN(id, register_fn, "version")`. It fills in the +compatibility fields from the host headers; plugin authors do not maintain +compatibility version numbers. The supplied version string identifies the +plugin's own release; it is not checked for compatibility. + +`IREE_COMPILER_PLUGIN_ABI_HASH` is the single compatibility ID. The build +calculates it from `Client.h`, `PluginEntryPoint.h`, `Pipelines/Options.h`, and +`Utils/OptionUtils.h`, covering both the entry-point layout and the selected +C++ API headers. There is no manual API or ABI version to bump. IREE maintainers +must keep the registration contract in `PluginEntryPoint.h` current so contract +changes also change the ID. + +`IREE_COMPILER_ABI_PREFIX` is a symbol namespace, not a compatibility version. +Keep its default value; the installed CMake package supplies the host's value. +It does not need a bump when the plugin API changes. + +The loader checks the fixed-width header hash before reading the remaining +entry-point fields or calling registration. The hash does not cover LLVM/MLIR +headers or build settings. A matching hash and successful symbol resolution do not guarantee ABI compatibility: a mismatched plugin can +still load and crash or corrupt memory. ## Extension points @@ -73,17 +158,22 @@ from `globalInitialize()` below, but it is intended for regular use and called out separately to avoid triggering warnings related to use of global initialization. +* `onRegisterDialects(DialectRegistry&)`: Registers the session's dialects before + context initialization and activation. * `onActivate()` : Called when a plugin is activated for a session, having both `options` and `context` available. This is the recommended point to - provide a `DialectRegistry` and configure appropriate context hooks for - configuring MLIR prior to any parsing or operation creation. + configure appropriate context hooks for configuring MLIR prior to any + parsing or operation creation. HAL targets: +* `populateHALTargetDevices()` * `populateHALTargetBackends()` Input dialects: +* `extendInputConversionPreprocessingPassPipeline()`: Adds preprocessing passes + for a built-in input type. * `extendCustomInputConversionPassPipeline()`: Called to extend a pass pipeline with conversion passes for a given conversion type. * `populateCustomInputConversionTypes()`: Called to get a list of all @@ -91,22 +181,31 @@ * `populateDetectedCustomInputConversionTypes()`: Called to get a list of all conversion types this plugin _found_ within a given module +Preprocessing: + +* `extendPreprocessingPassPipeline()`: Adds passes at the end of preprocessing. + Less frequently used extension points: * `static globalInitialize()` : Perform once-only process level initialization, regardless of whether a plugin will be activated. This happens before command line processing and should only be used to massage process-wide static registration like things, as third party libraries may require. -* `static registerDialects(DialectRegistry&)` : Extends the process wide - initial dialect registry. This should not be used unless if absolutely - necessary or if interfacing to legacy codebases that require it. +* `static registerGlobalDialects(DialectRegistry&)` : Extends the process wide + initial dialect registry. Prefer `onRegisterDialects()` unless interfacing + to legacy codebases that require global registration. ## Current Status * Statically linked, named plugins are supported in CMake (with optional inclusion). -* Statically linked, named plugins are hardcoded in Bazel (no optionality). -* An example in-tree plugin is under `compiler/plugins/example`. +* Statically linked, named plugins are supported in Bazel (with optional + inclusion). +* Dynamic compiler plugins are experimental and supported on Linux and macOS. +* An example in-tree plugin is under `samples/compiler_plugins/example` and + supports static and dynamic linking from the same source. * See `iree_compiler_plugin.cmake` for the CMake integration. Specifically, the `-DIREE_COMPILER_PLUGINS=example` flag can be used to statically link the example plugin. +* [out_of_tree_example](../../../../../samples/compiler_plugins/out_of_tree_example/README.md) + adds a dialect and pass, with instructions for building against an install tree.
diff --git a/compiler/src/iree/compiler/PluginAPI/test/BUILD.bazel b/compiler/src/iree/compiler/PluginAPI/test/BUILD.bazel new file mode 100644 index 0000000..940df22 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/BUILD.bazel
@@ -0,0 +1,176 @@ +# Copyright 2026 The IREE Authors +# +# Licensed 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 + +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") +load("//build_tools/bazel:enforce_glob.bzl", "enforce_glob") +load("//build_tools/bazel:iree_lit_test.bzl", "iree_lit_test_suite") + +package( + features = ["layering_check"], + licenses = ["notice"], # Apache 2.0 +) + +# Built here so the tests need no compiler of their own. Plain C and not +# renamed: their callbacks do not reference compiler C++ symbols. +[ + cc_binary( + name = plugin, + testonly = True, + srcs = ["plugins/%s.c" % plugin], + linkshared = True, + ) + for plugin in [ + "invalid_plugin", + "layout_mismatch_plugin", + ] +] + +[ + cc_binary( + name = plugin, + testonly = True, + srcs = ["plugins/%s.c" % plugin], + linkshared = True, + deps = ["//compiler/src/iree/compiler/PluginAPI:PluginEntryPoint"], + ) + for plugin in [ + "valid_plugin", + "abi_hash_mismatch_plugin", + "null_field_plugin", + "duplicate_id_plugin", + "registration_failure_plugin", + ] +] + +cc_test( + name = "embedding_registration_test", + srcs = ["embedding_registration_test.c"], + args = [ + "$(location //lib:IREECompiler)", + "$(location :registration_failure_plugin)", + ], + data = [ + ":registration_failure_plugin", + "//lib:IREECompiler", + ], + tags = ["skip-bazel_to_cmake"], + target_compatible_with = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//compiler/bindings/c:headers", + "//compiler/bindings/c:loader", + ], +) + +cc_binary( + name = "static_collision_plugin", + testonly = True, + srcs = ["plugins/static_collision_plugin.c"], + linkshared = True, + deps = [ + "//compiler/src/iree/compiler/PluginAPI:PluginEntryPoint", + "//compiler/src/iree/compiler/PluginAPI/Config:StaticLinkedPlugins", + ], +) + +cc_test( + name = "embedding_static_collision_test", + srcs = ["embedding_registration_test.c"], + args = [ + "$(location //lib:IREECompiler)", + "$(location :static_collision_plugin)", + ], + data = [ + ":static_collision_plugin", + "//lib:IREECompiler", + ], + tags = ["skip-bazel_to_cmake"], + target_compatible_with = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//compiler/bindings/c:headers", + "//compiler/bindings/c:loader", + ], +) + +cc_binary( + name = "undefined_symbol_plugin", + testonly = True, + srcs = ["plugins/undefined_symbol_plugin.c"], + linkopts = select({ + # ELF leaves undefined symbols alone; ld64 has to be told. + "@platforms//os:macos": ["-Wl,-undefined,dynamic_lookup"], + "//conditions:default": [], + }), + linkshared = True, + deps = ["//compiler/src/iree/compiler/PluginAPI:PluginEntryPoint"], +) + +# Out-of-tree plugins are normally built with hidden visibility; the macro must +# export the entry point itself. +cc_binary( + name = "hidden_visibility_plugin", + testonly = True, + srcs = ["plugins/hidden_visibility_plugin.cpp"], + copts = ["-fvisibility=hidden"], + linkshared = True, + deps = ["//compiler/src/iree/compiler/PluginAPI:PluginEntryPoint"], +) + +iree_lit_test_suite( + name = "lit", + srcs = enforce_glob( + [ + "abi_hash_mismatch.txt", + "duplicate_id.txt", + "invalid_options.txt", + "invalid_plugin.txt", + "hidden_visibility.txt", + "null_field.txt", + "undefined_symbol.txt", + "valid_plugin.txt", + "layout_mismatch.txt", + ], + include = ["*.txt"], + ), + cfg = "//compiler:lit.cfg.py", + data = [ + ":abi_hash_mismatch_plugin", + ":duplicate_id_plugin", + ":hidden_visibility_plugin", + ":invalid_plugin", + ":layout_mismatch_plugin", + ":null_field_plugin", + ":undefined_symbol_plugin", + ":valid_plugin", + ], + env = { + "IREE_TEST_ABI_HASH_MISMATCH_PLUGIN": "$(rootpath :abi_hash_mismatch_plugin)", + "IREE_TEST_VALID_PLUGIN": "$(rootpath :valid_plugin)", + "IREE_TEST_INVALID_PLUGIN": "$(rootpath :invalid_plugin)", + "IREE_TEST_LAYOUT_MISMATCH_PLUGIN": "$(rootpath :layout_mismatch_plugin)", + "IREE_TEST_HIDDEN_VISIBILITY_PLUGIN": "$(rootpath :hidden_visibility_plugin)", + "IREE_TEST_UNDEFINED_SYMBOL_PLUGIN": "$(rootpath :undefined_symbol_plugin)", + "IREE_TEST_NULL_FIELD_PLUGIN": "$(rootpath :null_field_plugin)", + "IREE_TEST_DUPLICATE_ID_PLUGIN": "$(rootpath :duplicate_id_plugin)", + }, + target_compatible_with = select({ + "@platforms//os:macos": ["@platforms//os:macos"], + "@platforms//os:linux": ["@platforms//os:linux"], + "//conditions:default": ["@platforms//:incompatible"], + }), + tools = [ + "//tools:iree-compile", + "//tools:iree-opt", + "@llvm-project//llvm:FileCheck", + ], +)
diff --git a/compiler/src/iree/compiler/PluginAPI/test/CMakeLists.txt b/compiler/src/iree/compiler/PluginAPI/test/CMakeLists.txt new file mode 100644 index 0000000..a792201 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/CMakeLists.txt
@@ -0,0 +1,108 @@ +iree_add_all_subdirs() + +# Dynamic loading is unsupported on Windows. +if(NOT IREE_BUILD_TESTS OR WIN32) + return() +endif() + +# Built here so the tests need no compiler of their own. Plain C and not +# renamed: their callbacks do not reference compiler C++ symbols. +foreach(_PLUGIN "valid_plugin" "invalid_plugin" "layout_mismatch_plugin" + "undefined_symbol_plugin" "abi_hash_mismatch_plugin" + "null_field_plugin" "duplicate_id_plugin" + "registration_failure_plugin" "static_collision_plugin") + add_library(iree_compiler_PluginAPI_test_${_PLUGIN} MODULE + "plugins/${_PLUGIN}.c") + set_target_properties(iree_compiler_PluginAPI_test_${_PLUGIN} PROPERTIES + POSITION_INDEPENDENT_CODE ON) +endforeach() + +foreach(_PLUGIN "valid_plugin" "abi_hash_mismatch_plugin" "undefined_symbol_plugin" + "null_field_plugin" "duplicate_id_plugin" + "registration_failure_plugin" "static_collision_plugin") + target_link_libraries(iree_compiler_PluginAPI_test_${_PLUGIN} + PRIVATE iree::compiler::PluginAPI::PluginEntryPoint) +endforeach() + +iree_cc_test( + NAME embedding_registration_test + SRCS "embedding_registration_test.c" + ARGS + "$<TARGET_FILE:iree::compiler::API::SharedImpl>" + "$<TARGET_FILE:iree_compiler_PluginAPI_test_registration_failure_plugin>" + DEPS iree::compiler::bindings::c::loader +) +add_dependencies(iree_compiler_PluginAPI_test_embedding_registration_test + iree::compiler::API::SharedImpl + iree_compiler_PluginAPI_test_registration_failure_plugin + iree_compiler_PluginAPI_test_static_collision_plugin) +target_link_libraries(iree_compiler_PluginAPI_test_static_collision_plugin PRIVATE + iree::compiler::PluginAPI::Config::StaticLinkedPlugins) +add_test(NAME "iree/compiler/PluginAPI/test/embedding_static_collision" + COMMAND iree_compiler_PluginAPI_test_embedding_registration_test + "$<TARGET_FILE:iree::compiler::API::SharedImpl>" + "$<TARGET_FILE:iree_compiler_PluginAPI_test_static_collision_plugin>") + +foreach(_tool iree-compile iree-opt) + add_test(NAME "iree/compiler/PluginAPI/test/${_tool}_registration_failure" + COMMAND $<TARGET_FILE:${_tool}> + "--iree-load-plugin=$<TARGET_FILE:iree_compiler_PluginAPI_test_registration_failure_plugin>" + --help) + set_tests_properties("iree/compiler/PluginAPI/test/${_tool}_registration_failure" + PROPERTIES WILL_FAIL TRUE) +endforeach() + +# ELF leaves undefined symbols alone; ld64 has to be told. +if(APPLE) + target_link_options(iree_compiler_PluginAPI_test_undefined_symbol_plugin + PRIVATE "-Wl,-undefined,dynamic_lookup") +endif() + +# Out-of-tree plugins are normally built with hidden visibility; the macro must +# export the entry point itself. +add_library(iree_compiler_PluginAPI_test_hidden_visibility_plugin MODULE + "plugins/hidden_visibility_plugin.cpp") +set_target_properties(iree_compiler_PluginAPI_test_hidden_visibility_plugin + PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_compile_options(iree_compiler_PluginAPI_test_hidden_visibility_plugin + PRIVATE "-fvisibility=hidden") +target_link_libraries(iree_compiler_PluginAPI_test_hidden_visibility_plugin + PRIVATE iree::compiler::PluginAPI::PluginEntryPoint) + + +iree_lit_test_suite( + NAME + lit + SRCS + "invalid_options.txt" + "invalid_plugin.txt" + "valid_plugin.txt" + "layout_mismatch.txt" + "hidden_visibility.txt" + "undefined_symbol.txt" + "abi_hash_mismatch.txt" + "null_field.txt" + "duplicate_id.txt" + TOOLS + FileCheck + iree-compile + iree-opt +) + +iree_package_ns(_PACKAGE_NS) +string(REPLACE "::" "/" _PACKAGE_PATH ${_PACKAGE_NS}) +foreach(_TEST_SRC "invalid_plugin.txt" "valid_plugin.txt" "layout_mismatch.txt" + "hidden_visibility.txt" "undefined_symbol.txt" + "abi_hash_mismatch.txt" "null_field.txt" "duplicate_id.txt") + set_property(TEST "${_PACKAGE_PATH}/${_TEST_SRC}.test" APPEND PROPERTY + ENVIRONMENT + "IREE_TEST_VALID_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_valid_plugin>" + "IREE_TEST_INVALID_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_invalid_plugin>" + "IREE_TEST_LAYOUT_MISMATCH_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_layout_mismatch_plugin>" + "IREE_TEST_HIDDEN_VISIBILITY_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_hidden_visibility_plugin>" + "IREE_TEST_UNDEFINED_SYMBOL_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_undefined_symbol_plugin>" + "IREE_TEST_ABI_HASH_MISMATCH_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_abi_hash_mismatch_plugin>" + "IREE_TEST_NULL_FIELD_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_null_field_plugin>" + "IREE_TEST_DUPLICATE_ID_PLUGIN=$<TARGET_FILE:iree_compiler_PluginAPI_test_duplicate_id_plugin>" + ) +endforeach()
diff --git a/compiler/src/iree/compiler/PluginAPI/test/abi_hash_mismatch.txt b/compiler/src/iree/compiler/PluginAPI/test/abi_hash_mismatch.txt new file mode 100644 index 0000000..9b1a777 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/abi_hash_mismatch.txt
@@ -0,0 +1,5 @@ +// A mismatched compatibility ID must fail before registration. + +// RUN: not iree-opt --iree-load-plugin=%iree_test_abi_hash_mismatch_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: plugin{{.*}}was built against plugin API headers 0000000000000000
diff --git a/compiler/src/iree/compiler/PluginAPI/test/duplicate_id.txt b/compiler/src/iree/compiler/PluginAPI/test/duplicate_id.txt new file mode 100644 index 0000000..448eadb --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/duplicate_id.txt
@@ -0,0 +1,6 @@ +// One library named twice loads once. Two libraries with one id is an error. + +// RUN: iree-opt --iree-load-plugin=%iree_test_valid_plugin --iree-load-plugin=%iree_test_valid_plugin --help +// RUN: not iree-opt --iree-load-plugin=%iree_test_valid_plugin --iree-load-plugin=%iree_test_duplicate_id_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: plugin{{.*}}reports id 'sample_plugin', already taken by
diff --git a/compiler/src/iree/compiler/PluginAPI/test/embedding_registration_test.c b/compiler/src/iree/compiler/PluginAPI/test/embedding_registration_test.c new file mode 100644 index 0000000..a2453dd --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/embedding_registration_test.c
@@ -0,0 +1,33 @@ +// Copyright 2026 The IREE Authors +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "iree/compiler/embedding_api.h" +#include "iree/compiler/loader.h" + +static void check_plugin(const char *id, void *user_data) { + if (strcmp(id, "registration_failure") == 0) { + *(bool *)user_data = true; + } +} + +int main(int argc, char **argv) { + if (argc != 3 || setenv("IREE_LOAD_PLUGINS", argv[2], 1) != 0 || + !ireeCompilerLoadLibrary(argv[1])) { + return EXIT_FAILURE; + } + ireeCompilerGlobalInitialize(); + bool found_failure = false; + ireeCompilerEnumeratePlugins(check_plugin, &found_failure); + iree_compiler_session_t *session = ireeCompilerSessionCreate(); + ireeCompilerSessionDestroy(session); + ireeCompilerGlobalShutdown(); + if (found_failure) { + fprintf(stderr, "Failed plugin was advertised as registered\n"); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/hidden_visibility.txt b/compiler/src/iree/compiler/PluginAPI/test/hidden_visibility.txt new file mode 100644 index 0000000..a6b731d --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/hidden_visibility.txt
@@ -0,0 +1 @@ +// RUN: iree-opt --iree-load-plugin=%iree_test_hidden_visibility_plugin --help
diff --git a/compiler/src/iree/compiler/PluginAPI/test/invalid_options.txt b/compiler/src/iree/compiler/PluginAPI/test/invalid_options.txt new file mode 100644 index 0000000..291bda9 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/invalid_options.txt
@@ -0,0 +1,12 @@ +// Command line +// RUN: not iree-opt --iree-load-plugin= 2>&1 | FileCheck %s --check-prefix=NO_PATH +// RUN: not iree-compile --iree-load-plugin= 2>&1 | FileCheck %s --check-prefix=NO_PATH +// RUN: not iree-opt --iree-load-plugin=non_existent_path 2>&1 | FileCheck %s --check-prefix=NON_EXISTENT_PATH +// RUN: not iree-compile --iree-load-plugin=non_existent_path 2>&1 | FileCheck %s --check-prefix=NON_EXISTENT_PATH + +// Environment +// RUN: env IREE_LOAD_PLUGINS=non_existent_path not iree-opt 2>&1 | FileCheck %s --check-prefix=NON_EXISTENT_PATH +// RUN: env IREE_LOAD_PLUGINS=non_existent_path not iree-compile 2>&1 | FileCheck %s --check-prefix=NON_EXISTENT_PATH + +// NO_PATH: [IREE Dynamic Plugin ERROR]: no path given to --iree-load-plugin +// NON_EXISTENT_PATH: [IREE Dynamic Plugin ERROR]: could not load plugin library 'non_existent_path'
diff --git a/compiler/src/iree/compiler/PluginAPI/test/invalid_plugin.txt b/compiler/src/iree/compiler/PluginAPI/test/invalid_plugin.txt new file mode 100644 index 0000000..f1e8e60 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/invalid_plugin.txt
@@ -0,0 +1,3 @@ +// RUN: not iree-opt --iree-load-plugin=%iree_test_invalid_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: plugin{{.*}}defines no iree_get_compiler_plugin_info
diff --git a/compiler/src/iree/compiler/PluginAPI/test/layout_mismatch.txt b/compiler/src/iree/compiler/PluginAPI/test/layout_mismatch.txt new file mode 100644 index 0000000..67d1d90 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/layout_mismatch.txt
@@ -0,0 +1,3 @@ +// RUN: not iree-opt --iree-load-plugin=%iree_test_layout_mismatch_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: plugin{{.*}}was built against plugin API headers 0000000000000000
diff --git a/compiler/src/iree/compiler/PluginAPI/test/null_field.txt b/compiler/src/iree/compiler/PluginAPI/test/null_field.txt new file mode 100644 index 0000000..5fc4b71 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/null_field.txt
@@ -0,0 +1,3 @@ +// RUN: not iree-opt --iree-load-plugin=%iree_test_null_field_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: plugin{{.*}}left a field of IreeCompilerPluginInfo null
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/abi_hash_mismatch_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/abi_hash_mismatch_plugin.c new file mode 100644 index 0000000..ac11bfa --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/abi_hash_mismatch_plugin.c
@@ -0,0 +1,23 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// Claims other headers. Must be refused before registration. + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool +register_other_headers_plugin(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return true; +} + +static const IreeCompilerPluginInfo info = { + UINT64_C(0), "from_other_headers", "test", register_other_headers_plugin}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/duplicate_id_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/duplicate_id_plugin.c new file mode 100644 index 0000000..3629f59 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/duplicate_id_plugin.c
@@ -0,0 +1,23 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// Claims valid_plugin's id. + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool register_duplicate_plugin(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return true; +} + +static const IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, + "sample_plugin", "duplicate", + register_duplicate_plugin}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/hidden_visibility_plugin.cpp b/compiler/src/iree/compiler/PluginAPI/test/plugins/hidden_visibility_plugin.cpp new file mode 100644 index 0000000..6b2b00c --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/hidden_visibility_plugin.cpp
@@ -0,0 +1,14 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool registerHiddenPlugin(mlir::iree_compiler::PluginRegistrar *r) { + (void)r; + return true; +} + +IREE_DEFINE_COMPILER_PLUGIN(hidden_plugin, registerHiddenPlugin, "test")
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/invalid_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/invalid_plugin.c new file mode 100644 index 0000000..d861253 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/invalid_plugin.c
@@ -0,0 +1,11 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// No plugin entry point. + +#include <stdbool.h> + +bool some_function(void) { return false; }
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/layout_mismatch_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/layout_mismatch_plugin.c new file mode 100644 index 0000000..f4b533b --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/layout_mismatch_plugin.c
@@ -0,0 +1,21 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// An incompatible ABI with only a compatibility ID. The loader must reject it +// before reading fields from the current layout. + +#include <stdint.h> + +struct IncompatibleIreeCompilerPluginInfo { + uint64_t abiHash; +}; + +static const struct IncompatibleIreeCompilerPluginInfo info = {UINT64_C(0)}; + +const struct IncompatibleIreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/null_field_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/null_field_plugin.c new file mode 100644 index 0000000..34e4c08 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/null_field_plugin.c
@@ -0,0 +1,24 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// Matching compatibility ID, null plugin ID. Must fail before registration. + +#include <stddef.h> + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool register_null_field_plugin(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return true; +} + +static const IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, NULL, + "test", register_null_field_plugin}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/registration_failure_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/registration_failure_plugin.c new file mode 100644 index 0000000..8318ee0 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/registration_failure_plugin.c
@@ -0,0 +1,21 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool register_failure(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return false; +} + +static const IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, + "registration_failure", "test", + register_failure}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/static_collision_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/static_collision_plugin.c new file mode 100644 index 0000000..162a795 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/static_collision_plugin.c
@@ -0,0 +1,29 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 <stdlib.h> + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static const char *const static_ids[] = { +#define HANDLE_PLUGIN_ID(id) #id, +#include "iree/compiler/PluginAPI/Config/StaticLinkedPlugins.inc" +#undef HANDLE_PLUGIN_ID + "", +}; + +static bool register_collision(IreeCompilerPluginRegistrar *registrar) { + // The loader must detect the collision before calling us. + abort(); +} + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + static IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, NULL, + "test", register_collision}; + info.pluginId = static_ids[0]; + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/undefined_symbol_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/undefined_symbol_plugin.c new file mode 100644 index 0000000..6d1ae68 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/undefined_symbol_plugin.c
@@ -0,0 +1,25 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// Calls a function nothing provides; lazy binding would let it load. + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +extern bool iree_test_absent_function(void); + +static bool register_undefined_plugin(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return iree_test_absent_function(); +} + +static const IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, + "undefined_plugin", "test", + register_undefined_plugin}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/plugins/valid_plugin.c b/compiler/src/iree/compiler/PluginAPI/test/plugins/valid_plugin.c new file mode 100644 index 0000000..8035673 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/plugins/valid_plugin.c
@@ -0,0 +1,23 @@ +// Copyright 2026 The IREE Authors +// +// Licensed 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 + +// Plain C, to keep a C plugin loadable. + +#include "iree/compiler/PluginAPI/PluginEntryPoint.h" + +static bool register_sample_plugin(IreeCompilerPluginRegistrar *registrar) { + (void)registrar; + return true; +} + +static const IreeCompilerPluginInfo info = {IREE_COMPILER_PLUGIN_ABI_HASH, + "sample_plugin", "test", + register_sample_plugin}; + +IREE_COMPILER_PLUGIN_EXPORT const IreeCompilerPluginInfo * +iree_get_compiler_plugin_info(void) { + return &info; +}
diff --git a/compiler/src/iree/compiler/PluginAPI/test/undefined_symbol.txt b/compiler/src/iree/compiler/PluginAPI/test/undefined_symbol.txt new file mode 100644 index 0000000..4c0d63d --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/undefined_symbol.txt
@@ -0,0 +1,4 @@ +// RUN: not iree-opt --iree-load-plugin=%iree_test_undefined_symbol_plugin 2>&1 | FileCheck %s + +// CHECK: [IREE Dynamic Plugin ERROR]: could not load plugin library +// CHECK-SAME: iree_test_absent_function
diff --git a/compiler/src/iree/compiler/PluginAPI/test/valid_plugin.txt b/compiler/src/iree/compiler/PluginAPI/test/valid_plugin.txt new file mode 100644 index 0000000..92bcb64 --- /dev/null +++ b/compiler/src/iree/compiler/PluginAPI/test/valid_plugin.txt
@@ -0,0 +1 @@ +// RUN: iree-opt --iree-load-plugin=%iree_test_valid_plugin --help
diff --git a/compiler/src/iree/compiler/Tools/BUILD.bazel b/compiler/src/iree/compiler/Tools/BUILD.bazel index 5a8acd2..58353ce 100644 --- a/compiler/src/iree/compiler/Tools/BUILD.bazel +++ b/compiler/src/iree/compiler/Tools/BUILD.bazel
@@ -195,6 +195,7 @@ "//compiler/bindings/c:headers", "//compiler/src/iree/compiler/API/Internal:CompilerDriver", "//compiler/src/iree/compiler/Pipelines", + "//compiler/src/iree/compiler/PluginAPI:PluginManager", "@llvm-project//llvm:Support", ], )
diff --git a/compiler/src/iree/compiler/Tools/iree_compile_lib.cc b/compiler/src/iree/compiler/Tools/iree_compile_lib.cc index 94242f2..4abadbf 100644 --- a/compiler/src/iree/compiler/Tools/iree_compile_lib.cc +++ b/compiler/src/iree/compiler/Tools/iree_compile_lib.cc
@@ -6,12 +6,14 @@ #include "iree/compiler/Tools/iree_compile_lib.h" +#include <cstdlib> #include <functional> #include <memory> #include <string> #include <type_traits> #include "iree/compiler/Pipelines/Pipelines.h" +#include "iree/compiler/PluginAPI/PluginManager.h" #include "iree/compiler/embedding_api.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/InitLLVM.h" @@ -198,7 +200,19 @@ exit(0); })); + // Before CLI parsing: the plugins loaded here still have options to add. + if (!mlir::iree_compiler::initializeDynamicPlugins( + llvm::ArrayRef<const char *>(const_cast<const char **>(argv), argc), + llvm::errs())) { + return EXIT_FAILURE; + } + ireeCompilerGlobalInitialize(); + if (mlir::iree_compiler::DynamicPluginRegistry::get() + .hasRegistrationFailures()) { + ireeCompilerGlobalShutdown(); + return EXIT_FAILURE; + } ireeCompilerGetProcessCLArgs(&argc, const_cast<const char ***>(&argv)); ireeCompilerSetupGlobalCL(argc, const_cast<const char **>(argv), "IREE compilation driver\n",
diff --git a/compiler/src/iree/compiler/Utils/BUILD.bazel b/compiler/src/iree/compiler/Utils/BUILD.bazel index 050bed8..19fa760 100644 --- a/compiler/src/iree/compiler/Utils/BUILD.bazel +++ b/compiler/src/iree/compiler/Utils/BUILD.bazel
@@ -31,6 +31,9 @@ ], ) +# Hashed into the plugin ABI; see PluginAPI/BUILD.bazel. +exports_files(["OptionUtils.h"]) + iree_compiler_cc_library( name = "Utils", srcs = [
diff --git a/docs/website/docs/developers/building/cmake-options.md b/docs/website/docs/developers/building/cmake-options.md index 71e3992..3fd1965 100644 --- a/docs/website/docs/developers/building/cmake-options.md +++ b/docs/website/docs/developers/building/cmake-options.md
@@ -140,11 +140,16 @@ * type: BOOL -Experimental. Builds compiler plugins as shared libraries that `iree-compile` -loads at run time through `--iree-load-plugin`. Defaults to `OFF`. Requires -`-DIREE_ENABLE_THIN_ARCHIVES=OFF`, because the plugins resolve against a -renamed copy of the compiler's symbols and the rename cannot rewrite a thin -archive. See `compiler/src/iree/compiler/PluginAPI/README.md`. +Enables experimental dynamic compiler plugins and exports the compiler symbols +that they need. Defaults to `OFF`. Requires +`IREE_LINK_COMPILER_SHARED_LIBRARY=ON` and `IREE_ENABLE_THIN_ARCHIVES=OFF`. +Load a plugin with `--iree-load-plugin=<path>` and activate it with +`--iree-plugin=<id>`. + +Plugins must use the host compiler's IREE and LLVM/MLIR revisions and matching +ABI-affecting build settings. See the +[compiler plugin documentation](https://github.com/iree-org/iree/blob/main/compiler/src/iree/compiler/PluginAPI/README.md) +for build requirements and compatibility checks. ### `IREE_OUTPUT_FORMAT_C`