[Compiler] Rename the compiler's LLVM/MLIR symbols (#24891) Allow a process that already contains another LLVM to load it. This is a prerequisite for dynamic plugins. This is considered experimental, renaming is disabled by default. 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> --------- Co-authored-by: maxbartel <bartel@roofline.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt index f59326f..bacf38a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -142,6 +142,10 @@ "Build optimization profile to apply. One of '', 'lto', 'size'.") set(IREE_LTO_MODE "full" CACHE STRING "LTO type, 'thin' or 'full'. Only consulted on clang-like compilers.") option(IREE_VISIBILITY_HIDDEN "Builds all C/C++ libraries with hidden visibility" ON) +option(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS "Experimental: builds libIREECompiler with a renamed, exported LLVM/MLIR ABI so dynamically loaded compiler plugins can resolve compiler symbols" OFF) +if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS AND WIN32) + message(FATAL_ERROR "IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS is not supported on Windows") +endif() #------------------------------------------------------------------------------- # Custom runtime allocator configuration @@ -559,6 +563,11 @@ option(IREE_ENABLE_SPLIT_DWARF "Enable gsplit-dwarf for debug information if the platform supports it" OFF) option(IREE_ENABLE_THIN_ARCHIVES "Enables thin ar archives (elf systems only). Disable for released static archives" OFF) option(IREE_LINK_COMPILER_SHARED_LIBRARY "Links IREE tools using the compiler compiled into a shared library" ON) +# Must match build_tools/bazel/abi_prefix.bzl, which explains the value. A +# plugin renames its symbols to match, so a consumer needs this value; +# IREECompilerConfig.cmake exports it. +set(IREE_COMPILER_ABI_PREFIX "IREE18" CACHE STRING + "Logical namespace the compiler's llvm/mlir symbols are renamed into") option(IREE_ENABLE_WERROR_FLAG "Enable `-Werror` flag, treat error as warning" ON) option(IREE_ENABLE_POSITION_INDEPENDENT_CODE "Enable position independent code" TRUE) option(IREE_REVERSE_ITERATION "Reverse iteration over in unordered LLVM containers" OFF) @@ -570,6 +579,23 @@ "be unloaded before Tracy finishes, static linking is one workaround)") endif() +if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS AND NOT IREE_LINK_COMPILER_SHARED_LIBRARY) + message(SEND_ERROR + "IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS requires " + "-DIREE_LINK_COMPILER_SHARED_LIBRARY=ON: a plugin resolves the renamed " + "ABI from libIREECompiler, which statically linked tools do not carry. " + "IREE_ENABLE_COMPILER_TRACING forces static linking, so it cannot be " + "combined with dynamic plugins") +endif() + +if(IREE_ENABLE_THIN_ARCHIVES AND IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS) + message(SEND_ERROR + "IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS requires -DIREE_ENABLE_THIN_ARCHIVES=OFF " + "(the symbol rename runs llvm-objcopy over every archive; on a thin " + "archive it cannot resolve the member objects, and the archive it " + "writes carries member paths relative to the wrong directory)") +endif() + option(IREE_ENABLE_CCACHE "[DEPRECATED: Use CMAKE_<LANG>_COMPILER_LAUNCHER configure options or environment variables instead.] Use ccache if installed." OFF)
diff --git a/build_tools/bazel/BUILD.bazel b/build_tools/bazel/BUILD.bazel index dcf0f30..afffddd 100644 --- a/build_tools/bazel/BUILD.bazel +++ b/build_tools/bazel/BUILD.bazel
@@ -14,6 +14,20 @@ exports_files(["assert_empty_query.sh"]) +# Helper tool for the IREE compiler ABI symbol rename (see renamed_link.bzl). +py_binary( + name = "gen_rename_map", + srcs = ["gen_rename_map.py"], +) + +py_test( + name = "gen_rename_map_test", + srcs = [ + "gen_rename_map.py", + "gen_rename_map_test.py", + ], +) + # Note that the "proper" way to do this is via a query on @bazel_tools, # but as with so many things bazel, it doesn't work reliably across versions, # variants, etc. So we just define our own since we also own the config
diff --git a/build_tools/bazel/abi_prefix.bzl b/build_tools/bazel/abi_prefix.bzl new file mode 100644 index 0000000..44d73b1 --- /dev/null +++ b/build_tools/bazel/abi_prefix.bzl
@@ -0,0 +1,13 @@ +# 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 + +"""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. +IREE_COMPILER_ABI_PREFIX = "IREE18"
diff --git a/build_tools/bazel/gen_rename_map.py b/build_tools/bazel/gen_rename_map.py new file mode 100644 index 0000000..d7410a9 --- /dev/null +++ b/build_tools/bazel/gen_rename_map.py
@@ -0,0 +1,181 @@ +#!/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 +"""Generates an llvm-objcopy --redefine-syms map for IREE ABI renaming. + +Renaming lets the compiler library share a process with another LLVM/MLIR copy, +TensorFlow's say. Plugins rename to match, so shared symbols resolve against the +library and MLIR TypeIDs coalesce across the dlopen boundary. + +The decision is made on the demangled name, which must mention `llvm::` or +`mlir::` as a namespace. Matching the mangled string instead would be unsound: +`_Z9good4mlirv` (`good4mlir()`) contains `4mlir` inside a longer source-name. +Symbols merely naming an llvm/mlir type in their signature are renamed too, or +plugin functions would disagree with their call sites. + +The inserted component is length-prefixed so the result still demangles: + + _ZN4mlir3fooEv -> _ZN6IREE184mlir3fooEv IREE18::mlir::foo() + _ZTVN4mlir6WalkerE -> _ZTVN6IREE184mlir6WalkerE vtable for IREE18::... + _Z3fooN4mlir5ValueE -> _Z6IREE183fooN4mlir5ValueE IREE18(foo, mlir::Value) + +The transformation is a pure function of the mangled name, so the library and +every renamed plugin archive arrive at the same new name independently. +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys + +ITANIUM_MANGLED_PREFIXES = ("__Z", "_Z") + +# Special encodings that may precede the nested-name: vtable, VTT, typeinfo, +# typeinfo name, guard variable, reference temporary, thread-local init and +# wrapper, and `Z` for function-local scope (these chain, e.g. `GVZN...` is +# the guard variable of a local static inside a namespaced function). The +# component is inserted after the `N` so the result stays demanglable. +# Anything else (thunks and other rare forms) falls back to insertion right +# after the mangling prefix: the result may not demangle, but the rename +# stays consistent everywhere. +_MARKED_NESTED_RE = re.compile(r"^(?:GV|GR|TV|TT|TI|TS|TH|TW|Z)*N") + +# Namespace qualifier of llvm:: or mlir:: anywhere in the demangled name, +# including argument and template positions. The look-behind rejects +# namespaces that merely end in llvm/mlir (e.g. `myllvm::`). +_NEEDS_RENAME_RE = re.compile(r"(?<![A-Za-z0-9_])(llvm|mlir)::") + + +def _itanium_component(symbol_prefix: str) -> str: + """Returns the length-prefixed Itanium source-name component.""" + return f"{len(symbol_prefix)}{symbol_prefix}" + + +def _needs_rename(demangled: str) -> bool: + return _NEEDS_RENAME_RE.search(demangled) is not None + + +def _split_itanium_name(name: str) -> tuple[str, str] | None: + for prefix in ITANIUM_MANGLED_PREFIXES: + if name.startswith(prefix): + return prefix, name[len(prefix) :] + return None + + +def _renamed(name: str, demangled: str, component: str) -> str | None: + """Returns the renamed symbol, or None when the symbol is out of scope.""" + split_name = _split_itanium_name(name) + if split_name is None: + return None + if not _needs_rename(demangled): + return None + + prefix, body = split_name + marked_nested = _MARKED_NESTED_RE.match(body) + if marked_nested: + head = marked_nested.group(0) + return prefix + head + component + body[len(head) :] + return prefix + component + body + + +def _demangle(cxxfilt: str, names: list[str]) -> list[str]: + """Demangles names 1:1 via llvm-cxxfilt; failures echo the input name.""" + if not names: + return [] + result = subprocess.run( + [cxxfilt], + input="\n".join(names) + "\n", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + demangled = result.stdout.splitlines() + if len(demangled) != len(names): + raise RuntimeError( + f"{cxxfilt} returned {len(demangled)} lines for {len(names)} symbols" + ) + return demangled + + +def _write_rename_map(renames: set[tuple[str, str]], out_path: str) -> None: + # Both builds give each invocation its own output path, so a plain write + # suffices. + with open(out_path, "w", encoding="utf-8") as f: + for old, new in sorted(renames): + f.write(f"{old} {new}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate an llvm-objcopy --redefine-syms file for IREE ABI renaming.", + ) + parser.add_argument("--nm", required=True, help="Path to llvm-nm.") + parser.add_argument("--cxxfilt", required=True, help="Path to llvm-cxxfilt.") + parser.add_argument( + "--input", required=True, help="Object file or static archive to inspect." + ) + parser.add_argument("--out", required=True, help="Output rename-map path.") + parser.add_argument( + "--symbol-prefix", + required=True, + help="Logical ABI prefix (e.g. IREE18); inserted as a length-prefixed " + "Itanium component.", + ) + parser.add_argument( + "--defined-only", + action="store_true", + help="Pass --defined-only to llvm-nm before generating the map.", + ) + args = parser.parse_args() + + # The map keys must be the raw assembler-level names, so nm must not + # demangle; demangling for the rename decision happens separately. + nm_args = [args.nm, "--no-demangle"] + if args.defined_only: + nm_args.append("--defined-only") + nm_args.append(args.input) + + result = subprocess.run( + nm_args, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + # Unsupported inputs, corrupt archives, or toolchain mismatches should + # fail the Bazel action with llvm-nm's own diagnostic. + sys.stderr.write(result.stderr) + return result.returncode + + mangled_names = set() + for line in result.stdout.splitlines(): + fields = line.split() + if not fields: + continue + name = fields[-1] + if _split_itanium_name(name) is not None: + mangled_names.add(name) + + ordered_names = sorted(mangled_names) + demangled_names = _demangle(args.cxxfilt, ordered_names) + + component = _itanium_component(args.symbol_prefix) + renames: set[tuple[str, str]] = set() + for name, demangled in zip(ordered_names, demangled_names): + new_name = _renamed(name, demangled, component) + if new_name: + renames.add((name, new_name)) + + _write_rename_map(renames, args.out) + return 0 + + +if __name__ == "__main__": + sys.exit(main())
diff --git a/build_tools/bazel/gen_rename_map_test.py b/build_tools/bazel/gen_rename_map_test.py new file mode 100644 index 0000000..d571f4c --- /dev/null +++ b/build_tools/bazel/gen_rename_map_test.py
@@ -0,0 +1,170 @@ +#!/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_rename_map.""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(__file__)) +import gen_rename_map + +PREFIX = "IREE18" +COMPONENT = gen_rename_map._itanium_component(PREFIX) + + +class ItaniumComponentTest(unittest.TestCase): + def test_component_is_length_prefixed(self): + self.assertEqual(COMPONENT, "6IREE18") + + def test_length_digit_follows_the_prefix(self): + # A fork setting its own prefix gets the right length digit, so the + # prefix is never spelled out with the digit attached. + self.assertEqual(gen_rename_map._itanium_component("AB"), "2AB") + self.assertEqual( + gen_rename_map._itanium_component("LONGERPREFIX"), "12LONGERPREFIX" + ) + + def test_rename_uses_the_given_component(self): + self.assertEqual( + gen_rename_map._renamed( + "_ZN4mlir3fooEv", "mlir::foo()", gen_rename_map._itanium_component("AB") + ), + "_ZN2AB4mlir3fooEv", + ) + + +class NeedsRenameTest(unittest.TestCase): + def test_namespace_qualifiers_match(self): + self.assertTrue(gen_rename_map._needs_rename("mlir::func::FuncOp::print()")) + self.assertTrue(gen_rename_map._needs_rename("foo(mlir::Value)")) + self.assertTrue(gen_rename_map._needs_rename("vtable for llvm::raw_ostream")) + self.assertTrue( + gen_rename_map._needs_rename( + "std::vector<mlir::Value>::push_back(mlir::Value&&)" + ) + ) + + def test_review_false_positive_good4mlir(self): + # `_Z9good4mlirv` has the raw `4mlir` marker but no mlir:: namespace. + self.assertFalse(gen_rename_map._needs_rename("good4mlir()")) + + def test_namespace_suffix_does_not_match(self): + self.assertFalse(gen_rename_map._needs_rename("myllvm::foo()")) + self.assertFalse(gen_rename_map._needs_rename("notmlir::bar()")) + + def test_unrelated_names_do_not_match(self): + self.assertFalse(gen_rename_map._needs_rename("iree_compiler::foo()")) + self.assertFalse(gen_rename_map._needs_rename("plain_c_symbol")) + + +class RenamedTest(unittest.TestCase): + def test_nested_name(self): + self.assertEqual( + gen_rename_map._renamed("_ZN4mlir3fooEv", "mlir::foo()", COMPONENT), + "_ZN" + COMPONENT + "4mlir3fooEv", + ) + + def test_macho_extra_underscore(self): + self.assertEqual( + gen_rename_map._renamed("__ZN4mlir3barEv", "mlir::bar()", COMPONENT), + "__ZN" + COMPONENT + "4mlir3barEv", + ) + + def test_vtable_typeinfo_guard(self): + self.assertEqual( + gen_rename_map._renamed( + "_ZTVN4mlir6WalkerE", "vtable for mlir::Walker", COMPONENT + ), + "_ZTVN" + COMPONENT + "4mlir6WalkerE", + ) + self.assertEqual( + gen_rename_map._renamed( + "_ZTIN4llvm5ErrorE", "typeinfo for llvm::Error", COMPONENT + ), + "_ZTIN" + COMPONENT + "4llvm5ErrorE", + ) + self.assertEqual( + gen_rename_map._renamed( + "_ZGVN4mlir3ctxE", "guard variable for mlir::ctx", COMPONENT + ), + "_ZGVN" + COMPONENT + "4mlir3ctxE", + ) + + def test_local_scope_and_guard_chains(self): + # Guard variable of a function-local static (the MLIR TypeID pattern). + self.assertEqual( + gen_rename_map._renamed( + "__ZGVZN4mlir6detail14TypeIDResolverIN4llvm5APIntEvE13resolveTypeIDEvE2id", + "guard variable for mlir::detail::TypeIDResolver<llvm::APInt, " + "void>::resolveTypeID()::id", + COMPONENT, + ), + "__ZGVZN" + + COMPONENT + + "4mlir6detail14TypeIDResolverIN4llvm5APIntEvE13resolveTypeIDEvE2id", + ) + # The local static itself. + self.assertEqual( + gen_rename_map._renamed( + "_ZZN4mlir3fooEvE5local", + "mlir::foo()::local", + COMPONENT, + ), + "_ZZN" + COMPONENT + "4mlir3fooEvE5local", + ) + + def test_free_function_with_mlir_argument(self): + # A plugin function naming MLIR types must agree with its call sites. + self.assertEqual( + gen_rename_map._renamed( + "_Z3fooN4mlir5ValueE", "foo(mlir::Value)", COMPONENT + ), + "_Z" + COMPONENT + "3fooN4mlir5ValueE", + ) + + def test_review_false_positive_good4mlir(self): + self.assertIsNone( + gen_rename_map._renamed("_Z9good4mlirv", "good4mlir()", COMPONENT) + ) + + def test_non_itanium_symbols_pass_through(self): + self.assertIsNone( + gen_rename_map._renamed("plain_c_symbol", "plain_c_symbol", COMPONENT) + ) + self.assertIsNone( + gen_rename_map._renamed("mlirContextCreate", "mlirContextCreate", COMPONENT) + ) + + def test_iree_namespace_not_renamed(self): + self.assertIsNone( + gen_rename_map._renamed("_ZN4iree3bazEv", "iree::baz()", COMPONENT) + ) + + +class WriteRenameMapTest(unittest.TestCase): + def test_writes_sorted_map(self): + with tempfile.TemporaryDirectory() as temp_dir: + out_path = os.path.join(temp_dir, "rename.map") + gen_rename_map._write_rename_map( + { + ("_ZN4mlir3barEv", "_ZN" + COMPONENT + "4mlir3barEv"), + ("_ZN4llvm3fooEv", "_ZN" + COMPONENT + "4llvm3fooEv"), + }, + out_path, + ) + with open(out_path, encoding="utf-8") as f: + self.assertEqual( + f.read(), + "_ZN4llvm3fooEv _ZN" + COMPONENT + "4llvm3fooEv\n" + "_ZN4mlir3barEv _ZN" + COMPONENT + "4mlir3barEv\n", + ) + + +if __name__ == "__main__": + unittest.main()
diff --git a/build_tools/bazel/renamed_link.bzl b/build_tools/bazel/renamed_link.bzl new file mode 100644 index 0000000..3e071c8 --- /dev/null +++ b/build_tools/bazel/renamed_link.bzl
@@ -0,0 +1,466 @@ +# 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 + +"""C++ provider transforms for the renamed IREE compiler ABI. + +The compiler library renames every llvm/mlir C++ symbol before link (see +gen_rename_map.py), so anything linking against it must rename to match, +undefined references included. + +Rewritten archives are declared as new provider values rather than mutating the +inputs, keeping Bazel's C++ provider graph intact. + +The ABI target and the CcInfo transform stay separate so each can be tested +alone. +""" + +load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain") +load("//build_tools/bazel:abi_prefix.bzl", "IREE_COMPILER_ABI_PREFIX") + +RenamedCompilerAbiInfo = provider(fields = { + "symbol_prefix": "Logical ABI prefix inserted as an Itanium component.", + "provided_owners": "Labels, as strings, of the archives inside the library.", +}) + +def _bzlmod_repo_name_matches(workspace_name, repo_name): + return ( + workspace_name.endswith("+" + repo_name) or + workspace_name.endswith("~" + repo_name) + ) + +def _provided_by_compiler(owner, compiler_workspace_name): + workspace_name = owner.workspace_name + owner_text = str(owner) + + if "llvm-project" in workspace_name or "llvm-project" in owner_text: + return True + + # libbacktrace is already in the dylib and exports plain C, so let consumers + # resolve it there rather than carry every platform backend themselves. + if "libbacktrace" in workspace_name or "libbacktrace" in owner_text: + return True + + # Whatever repository the compiler target came from is inside the dylib + # already. In-tree that is the main workspace; from a plugin repository it + # is @iree_core, and there the plugin's own main-workspace archives still + # need renaming. The caller keeps its direct deps regardless, so an in-tree + # plugin sharing the main workspace with the compiler is not mistaken for + # part of it. + if compiler_workspace_name != None and workspace_name == compiler_workspace_name: + return True + + # Fallbacks for a consumer that names no compiler target. + if workspace_name == "iree_core": + return True + return _bzlmod_repo_name_matches(workspace_name, "iree_core") or \ + _bzlmod_repo_name_matches(workspace_name, "iree") + +def _artifact_suffix(file, suffixes, description): + path = file.path + for suffix in suffixes: + if path.endswith(suffix): + return suffix + fail("Expected {}, got {}".format(description, file.path)) + +def _sanitize_path_fragment(value): + """Returns a stable file-name fragment. + + Example: `@llvm-project//mlir:IR.pic.a` becomes + `_llvm_project__mlir_IR_pic_a`, which is safe to use in declared outputs. + """ + result = "" + for i in range(len(value)): + c = value[i] + if c.isalnum() or c == "_": + result += c + else: + result += "_" + return result + +def _rename_file(ctx, input_file, output_base, symbol_prefix, defined_only, suffixes, description): + suffix = _artifact_suffix(input_file, suffixes, description) + output_base = _sanitize_path_fragment(output_base) + out = ctx.actions.declare_file( + "{}/{}{}".format(ctx.label.name + "_renamed", output_base, suffix), + ) + map_file = ctx.actions.declare_file( + "{}/{}.rename_map".format(ctx.label.name + "_renamed", output_base), + ) + + map_args = ctx.actions.args() + map_args.add("--nm", ctx.executable._nm) + map_args.add("--cxxfilt", ctx.executable._cxxfilt) + map_args.add("--input", input_file) + map_args.add("--out", map_file) + map_args.add("--symbol-prefix", symbol_prefix) + if defined_only: + map_args.add("--defined-only") + ctx.actions.run( + executable = ctx.executable._gen_map, + arguments = [map_args], + inputs = [input_file], + tools = [ctx.executable._nm, ctx.executable._cxxfilt], + outputs = [map_file], + mnemonic = "RenameMap", + progress_message = "Computing llvm/mlir rename map for %s" % input_file.short_path, + ) + + objcopy_args = ctx.actions.args() + objcopy_args.add("--redefine-syms=" + map_file.path) + objcopy_args.add(input_file) + objcopy_args.add(out) + ctx.actions.run( + executable = ctx.executable._objcopy, + arguments = [objcopy_args], + inputs = [input_file, map_file], + outputs = [out], + mnemonic = "RenameSymbols", + progress_message = "Renaming llvm/mlir symbols in %s" % input_file.short_path, + ) + + return out + +def _rename_archive(ctx, archive, output_base, symbol_prefix, defined_only): + return _rename_file( + ctx, + archive, + output_base, + symbol_prefix, + defined_only, + [".pic.a", ".lo", ".a", ".lib"], + "a static archive-like file", + ) + +def _archive_objects(ctx, objects, output_base): + if len(objects) == 0: + return None + output_base = _sanitize_path_fragment(output_base) + out = ctx.actions.declare_file( + "{}/{}.a".format(ctx.label.name + "_renamed", output_base), + ) + args = ctx.actions.args() + args.add("rcs") + args.add(out) + args.add_all(objects) + ctx.actions.run( + executable = ctx.executable._llvm_ar, + arguments = [args], + inputs = objects, + outputs = [out], + mnemonic = "ArchiveObjects", + progress_message = "Archiving object-file linker input for %s" % ctx.label.name, + ) + return out + +def _has_objects(value): + return value != None and len(value) != 0 + +def _library_identity(library): + """Returns a de-duplication key, or None for flag/input-only libraries.""" + parts = [] + for artifact in [ + library.static_library, + library.pic_static_library, + library.dynamic_library, + library.interface_library, + ]: + if artifact != None: + parts.append(artifact.path) + if _has_objects(library.objects): + for artifact in library.objects: + parts.append(artifact.path) + if _has_objects(library.pic_objects): + for artifact in library.pic_objects: + parts.append(artifact.path) + if len(parts) == 0: + return None + return "|".join(parts) + +def _rename_library_to_link( + ctx, + cc_toolchain, + feature_configuration, + library, + output_base, + symbol_prefix, + defined_only, + force_alwayslink): + static_library = None + pic_static_library = None + object_static_library = None + object_pic_static_library = None + + # llvm-objcopy takes one file, so bare object lists are archived first. + if library.static_library: + static_library = _rename_archive( + ctx, + library.static_library, + output_base + "_static", + symbol_prefix, + defined_only, + ) + if library.pic_static_library: + pic_static_library = _rename_archive( + ctx, + library.pic_static_library, + output_base + "_pic_static", + symbol_prefix, + defined_only, + ) + if _has_objects(library.objects) and library.static_library == None: + object_archive = _archive_objects(ctx, library.objects, output_base + "_objects") + object_static_library = _rename_archive( + ctx, + object_archive, + output_base + "_objects_renamed", + symbol_prefix, + defined_only, + ) + if _has_objects(library.pic_objects) and library.pic_static_library == None: + object_pic_archive = _archive_objects(ctx, library.pic_objects, output_base + "_pic_objects") + object_pic_static_library = _rename_archive( + ctx, + object_pic_archive, + output_base + "_pic_objects_renamed", + symbol_prefix, + defined_only, + ) + + # Nothing to rewrite. + renamed_files = [ + f + for f in [ + static_library, + pic_static_library, + object_static_library, + object_pic_static_library, + ] + if f != None + ] + if not renamed_files: + return [library], [] + + alwayslink = force_alwayslink or library.alwayslink + + # create_library_to_link mangles its input, so it rejects the _solib_ symlink + # Bazel already mangled. Give it the original. + dynamic_library = library.resolved_symlink_dynamic_library or library.dynamic_library + interface_library = library.resolved_symlink_interface_library or library.interface_library + + libraries = [] + + # Recreate the same static/dynamic shape the input had. + if ( + static_library != None or + pic_static_library != None or + dynamic_library != None or + interface_library != None + ): + libraries.append(cc_common.create_library_to_link( + actions = ctx.actions, + static_library = static_library, + pic_static_library = pic_static_library, + dynamic_library = dynamic_library, + interface_library = interface_library, + cc_toolchain = cc_toolchain if dynamic_library != None or interface_library != None else None, + feature_configuration = feature_configuration if dynamic_library != None or interface_library != None else None, + alwayslink = alwayslink, + )) + if object_static_library != None or object_pic_static_library != None: + libraries.append(cc_common.create_library_to_link( + actions = ctx.actions, + static_library = object_static_library, + pic_static_library = object_pic_static_library, + alwayslink = alwayslink, + )) + return libraries, renamed_files + +def _renamed_cc_info_impl(ctx): + # Normally taken from the ABI target so plugins and the compiler agree. The + # attr keeps this usable alone, in tests or standalone wiring. + symbol_prefix = ctx.attr.symbol_prefix + compiler_workspace_name = None + provided_owners = {} + if ctx.attr.compiler: + abi = ctx.attr.compiler[RenamedCompilerAbiInfo] + symbol_prefix = abi.symbol_prefix + compiler_workspace_name = ctx.attr.compiler.label.workspace_name + provided_owners = abi.provided_owners or {} + cc_toolchain = find_cpp_toolchain(ctx) + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + + direct_owners = {str(dep.label): True for dep in ctx.attr.deps} + excluded_owners = {str(dep.label): True for dep in ctx.attr.exclude} + linker_inputs = [] + renamed_files = [] + seen_libraries = {} + library_ordinal = 0 + + for dep in ctx.attr.deps: + for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list(): + owner = linker_input.owner + owner_label = str(owner) + if owner_label in excluded_owners: + continue + + # Leaf deps only; their MLIR closure still comes from the dylib. + if ctx.attr.direct_only and owner_label not in direct_owners: + continue + + # The compiler names what it holds, so anything else belongs to + # the plugin however deep in its graph it sits. Direct deps are + # kept outright, and the workspace test is the fallback for a + # consumer that named no compiler. + if not ctx.attr.include_provided and owner_label not in direct_owners: + if provided_owners: + if owner_label in provided_owners: + continue + elif _provided_by_compiler(owner, compiler_workspace_name): + continue + + libraries = [] + for library in linker_input.libraries: + library_id = _library_identity(library) + + # Flags-only entry. Keep going so those fields survive. + if library_id != None: + if library_id in seen_libraries: + continue + seen_libraries[library_id] = True + + renamed_libraries, files = _rename_library_to_link( + ctx, + cc_toolchain, + feature_configuration, + library, + "lib_{}".format(library_ordinal), + symbol_prefix, + ctx.attr.defined_only, + ctx.attr.force_alwayslink, + ) + library_ordinal += 1 + libraries.extend(renamed_libraries) + renamed_files.extend(files) + + if libraries or linker_input.user_link_flags or linker_input.additional_inputs: + linker_inputs.append(cc_common.create_linker_input( + owner = owner, + libraries = depset(direct = libraries), + user_link_flags = depset(direct = linker_input.user_link_flags), + additional_inputs = depset(direct = linker_input.additional_inputs), + )) + + linking_context = cc_common.create_linking_context( + linker_inputs = depset(direct = linker_inputs, order = "topological"), + ) + return [ + DefaultInfo(files = depset(renamed_files)), + CcInfo(linking_context = linking_context), + ] + +renamed_cc_info = rule( + implementation = _renamed_cc_info_impl, + doc = ( + "Low-level CcInfo transform that rewrites selected static archives to " + + "the IREE compiler ABI." + ), + attrs = { + "deps": attr.label_list(providers = [CcInfo], default = []), + "compiler": attr.label( + providers = [[CcInfo, RenamedCompilerAbiInfo]], + default = None, + doc = "Optional compiler ABI target that supplies the symbol prefix.", + ), + "exclude": attr.label_list( + providers = [CcInfo], + default = [], + doc = "Linker-input owners to omit entirely.", + ), + "include_provided": attr.bool( + default = False, + doc = "Keep archives from repos already provided by libIREECompiler.", + ), + "direct_only": attr.bool( + default = False, + doc = "Keep only linker inputs whose owner is one of deps' direct labels.", + ), + "defined_only": attr.bool( + default = False, + doc = "Rename only defined symbols in each archive.", + ), + "force_alwayslink": attr.bool( + default = False, + doc = "Force replacement static archives into whole-archive linking.", + ), + "symbol_prefix": attr.string(default = IREE_COMPILER_ABI_PREFIX), + "_nm": attr.label(default = "@llvm-project//llvm:llvm-nm", executable = True, cfg = "exec"), + "_cxxfilt": attr.label(default = "@llvm-project//llvm:llvm-cxxfilt", executable = True, cfg = "exec"), + "_objcopy": attr.label(default = "@llvm-project//llvm:llvm-objcopy", executable = True, cfg = "exec"), + "_llvm_ar": attr.label(default = "@llvm-project//llvm:llvm-ar", executable = True, cfg = "exec"), + "_gen_map": attr.label(default = "//build_tools/bazel:gen_rename_map", executable = True, cfg = "exec"), + }, + fragments = ["cpp"], + toolchains = use_cpp_toolchain(), +) + +def _renamed_compiler_abi_impl(ctx): + cc_toolchain = find_cpp_toolchain(ctx) + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + + # What the library already holds. A plugin resolves these rather than + # carrying its own copy, and anything absent here is the plugin's own. + provided_owners = {} + if ctx.attr.static_closure: + for linker_input in ctx.attr.static_closure[CcInfo].linking_context.linker_inputs.to_list(): + provided_owners[str(linker_input.owner)] = True + + library_to_link = cc_common.create_library_to_link( + actions = ctx.actions, + dynamic_library = ctx.file.shared_library, + cc_toolchain = cc_toolchain, + feature_configuration = feature_configuration, + ) + linker_input = cc_common.create_linker_input( + owner = ctx.label, + libraries = depset(direct = [library_to_link]), + ) + linking_context = cc_common.create_linking_context( + linker_inputs = depset(direct = [linker_input]), + ) + return [ + DefaultInfo(files = depset([ctx.file.shared_library])), + RenamedCompilerAbiInfo( + symbol_prefix = ctx.attr.symbol_prefix, + provided_owners = provided_owners, + ), + CcInfo(linking_context = linking_context), + ] + +iree_renamed_compiler_abi = rule( + implementation = _renamed_compiler_abi_impl, + doc = "Wraps the renamed libIREECompiler shared library and its ABI prefix.", + attrs = { + "shared_library": attr.label(allow_single_file = True, mandatory = True), + "static_closure": attr.label( + providers = [CcInfo], + doc = "What the shared library was linked from, so a plugin can " + + "tell the compiler's archives from its own.", + ), + "symbol_prefix": attr.string(default = IREE_COMPILER_ABI_PREFIX), + }, + fragments = ["cpp"], + toolchains = use_cpp_toolchain(), +)
diff --git a/build_tools/cmake/build_all.sh b/build_tools/cmake/build_all.sh index 44a93a3..98748c3 100755 --- a/build_tools/cmake/build_all.sh +++ b/build_tools/cmake/build_all.sh
@@ -30,6 +30,12 @@ # needed, but some of the deps are too large to enable by default for all # developers. IREE_TARGET_BACKEND_WEBGPU_SPIRV="${IREE_TARGET_BACKEND_WEBGPU_SPIRV:-${OFF_IF_DARWIN}}" +# Off by default: the rename relinks all of libIREECompiler, which most builds +# have no use for. +IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS="${IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS:-OFF}" +# Mutually exclusive with the rename: llvm-objcopy cannot follow a thin +# archive's member paths. +IREE_ENABLE_THIN_ARCHIVES="${IREE_ENABLE_THIN_ARCHIVES:-ON}" # Enable building the `iree-test-deps` target. IREE_BUILD_TEST_DEPS="${IREE_BUILD_TEST_DEPS:-1}" # If set to 1, exit after CMake configure (useful for generating compile_commands.json). @@ -58,7 +64,7 @@ # Enable split dwarf and thin archives for smaller object files and faster # linking (for builds with debug info). "-DIREE_ENABLE_SPLIT_DWARF=ON" - "-DIREE_ENABLE_THIN_ARCHIVES=ON" + "-DIREE_ENABLE_THIN_ARCHIVES=${IREE_ENABLE_THIN_ARCHIVES}" # Enable docs build on the CI. The additional builds are pretty fast and # give us early warnings for some types of website publication errors. @@ -74,6 +80,7 @@ "-DIREE_TARGET_BACKEND_CUDA=${IREE_TARGET_BACKEND_CUDA}" "-DIREE_TARGET_BACKEND_ROCM=${IREE_TARGET_BACKEND_ROCM}" "-DIREE_TARGET_BACKEND_WEBGPU_SPIRV=${IREE_TARGET_BACKEND_WEBGPU_SPIRV}" + "-DIREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS=${IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS}" ) # Include the first simulator target's test modules in the initial build.
diff --git a/build_tools/cmake/iree_renamed_link.cmake b/build_tools/cmake/iree_renamed_link.cmake new file mode 100644 index 0000000..b5647bc --- /dev/null +++ b/build_tools/cmake/iree_renamed_link.cmake
@@ -0,0 +1,271 @@ +# 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 + +# CMake side of the renamed IREE compiler ABI (IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS). +# +# 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. +# +# Everything becomes a static archive first, because that is what llvm-objcopy +# takes. The linked library cannot be renamed instead: rewriting .dynsym breaks +# the ELF hash sections. + +# Target names reach declared outputs, and some contain dots (`obj.MLIRCAPIIR`). +function(iree_renamed_link_sanitize_name VALUE OUT_VAR) + string(REGEX REPLACE "[^A-Za-z0-9_]" "_" _result "${VALUE}") + set(${OUT_VAR} "${_result}" PARENT_SCOPE) +endfunction() + +# Evaluates $<BOOL:...>, where a path is true. if() would call it false under +# CMP0054, inverting the very paths LLVM guards its link flags with. +function(iree_renamed_link_genex_bool VALUE OUT_VAR) + string(TOUPPER "${VALUE}" _upper) + if(_upper STREQUAL "" OR _upper STREQUAL "0" OR _upper STREQUAL "FALSE" + OR _upper STREQUAL "OFF" OR _upper STREQUAL "N" OR _upper STREQUAL "NO" + OR _upper STREQUAL "IGNORE" OR _upper STREQUAL "NOTFOUND" + OR _upper MATCHES "-NOTFOUND$") + set(${OUT_VAR} FALSE PARENT_SCOPE) + else() + set(${OUT_VAR} TRUE PARENT_SCOPE) + endif() +endfunction() + +# Rewrites one static archive to the renamed ABI. +# +# Parameters: +# OUT_VAR: variable receiving the renamed archive path. +# NAME: base name for the outputs (must be unique in this directory). +# INPUT: archive path (generator expressions allowed). +# DEPENDS: extra dependencies of the rename actions. +function(iree_renamed_archive OUT_VAR) + cmake_parse_arguments(_RULE "" "NAME;INPUT" "DEPENDS" ${ARGN}) + set(_dir "${CMAKE_CURRENT_BINARY_DIR}/renamed") + set(_map "${_dir}/${_RULE_NAME}.rename_map") + set(_out "${_dir}/lib${_RULE_NAME}.renamed.a") + set(_script "${IREE_ROOT_DIR}/build_tools/bazel/gen_rename_map.py") + add_custom_command( + OUTPUT "${_map}" + COMMAND + "${Python3_EXECUTABLE}" "${_script}" + --nm "$<TARGET_FILE:llvm-nm>" + --cxxfilt "$<TARGET_FILE:llvm-cxxfilt>" + --input "${_RULE_INPUT}" + --out "${_map}" + --symbol-prefix "${IREE_COMPILER_ABI_PREFIX}" + DEPENDS "${_RULE_INPUT}" "${_script}" llvm-nm llvm-cxxfilt ${_RULE_DEPENDS} + COMMENT "Computing llvm/mlir rename map for ${_RULE_NAME}" + ) + add_custom_command( + OUTPUT "${_out}" + COMMAND + "$<TARGET_FILE:llvm-objcopy>" "--redefine-syms=${_map}" + "${_RULE_INPUT}" "${_out}" + DEPENDS "${_RULE_INPUT}" "${_map}" llvm-objcopy ${_RULE_DEPENDS} + COMMENT "Renaming llvm/mlir symbols in ${_RULE_NAME}" + ) + set(${OUT_VAR} "${_out}" PARENT_SCOPE) +endfunction() + +# As iree_renamed_archive, but starting from an object library's objects. +function(iree_renamed_archive_from_objects OUT_VAR) + cmake_parse_arguments(_RULE "" "NAME;TARGET" "" ${ARGN}) + set(_dir "${CMAKE_CURRENT_BINARY_DIR}/renamed") + set(_archive "${_dir}/lib${_RULE_NAME}.objects.a") + add_custom_command( + OUTPUT "${_archive}" + COMMAND "$<TARGET_FILE:llvm-ar>" rcs "${_archive}" + "$<TARGET_OBJECTS:${_RULE_TARGET}>" + DEPENDS "$<TARGET_OBJECTS:${_RULE_TARGET}>" ${_RULE_TARGET} llvm-ar + COMMAND_EXPAND_LISTS + COMMENT "Archiving objects of ${_RULE_TARGET}" + ) + iree_renamed_archive(_renamed + NAME "${_RULE_NAME}" + INPUT "${_archive}" + ) + set(${OUT_VAR} "${_renamed}" PARENT_SCOPE) +endfunction() + +# 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. +function(iree_collect_static_link_closure STATIC_LIBS_VAR OTHER_VAR) + set(_worklist ${ARGN}) + set(_visited "") + set(_static_libs "") + set(_other "") + while(_worklist) + list(POP_FRONT _worklist _item) + if(_item IN_LIST _visited) + continue() + endif() + list(APPEND _visited "${_item}") + # Directory-scope markers CMake embeds in LINK_LIBRARIES values. + if(_item MATCHES "^::@") + continue() + endif() + if(_item MATCHES "^\\$<LINK_ONLY:(.+)>$") + list(APPEND _worklist "${CMAKE_MATCH_1}") + continue() + endif() + # Decidable here: every closure target exists by the time this walk runs. + if(_item MATCHES "^\\$<TARGET_NAME_IF_EXISTS:([^>]+)>$") + if(TARGET "${CMAKE_MATCH_1}") + list(APPEND _worklist "${CMAKE_MATCH_1}") + endif() + continue() + endif() + if(_item MATCHES "^\\$<IF:\\$<TARGET_EXISTS:([^>]+)>,([^,]*),([^>]*)>$") + if(TARGET "${CMAKE_MATCH_1}") + set(_resolved "${CMAKE_MATCH_2}") + else() + set(_resolved "${CMAKE_MATCH_3}") + endif() + if(_resolved) + list(APPEND _worklist "${_resolved}") + endif() + continue() + endif() + # iree_cc_library reaches usage requirements through this indirection. + if(_item MATCHES "^\\$<TARGET_PROPERTY:([^,>]+),([A-Za-z_]+)>$") + if(TARGET "${CMAKE_MATCH_1}") + get_target_property(_prop_value "${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}") + if(_prop_value) + list(APPEND _worklist ${_prop_value}) + endif() + endif() + continue() + endif() + # The compiler dylib is no MLIR aggregate, so this exclusion list is empty + # and the guard always admits the library. + if(_item MATCHES "^\\$<\\$<NOT:\\$<IN_LIST:.*MLIR_AGGREGATE_EXCLUDE_LIBS.*>:([A-Za-z0-9_]+)>$") + list(APPEND _worklist "${CMAKE_MATCH_1}") + continue() + endif() + # LLVM guards platform link flags this way. The probe is a literal by now, + # so decide it rather than warn; an empty one contributes nothing. + if(_item MATCHES "^\\$<\\$<BOOL:(.*)>:(.*)>$") + set(_bool_cond "${CMAKE_MATCH_1}") + set(_bool_value "${CMAKE_MATCH_2}") + iree_renamed_link_genex_bool("${_bool_cond}" _bool_true) + if(_bool_true) + list(APPEND _worklist "${_bool_value}") + endif() + continue() + endif() + if(_item MATCHES "^\\$<") + # An unevaluated one would corrupt the build files. Dropping it yields an + # undefined symbol instead, which can be diagnosed. + message(WARNING + "iree_collect_static_link_closure: dropping unsupported generator " + "expression link item: ${_item}") + continue() + endif() + if(NOT TARGET "${_item}") + list(APPEND _other "${_item}") + continue() + endif() + get_target_property(_aliased "${_item}" ALIASED_TARGET) + if(_aliased) + list(APPEND _worklist "${_aliased}") + continue() + endif() + get_target_property(_imported "${_item}" IMPORTED) + get_target_property(_type "${_item}" TYPE) + if(_imported) + list(APPEND _other "${_item}") + elseif(_type STREQUAL "STATIC_LIBRARY") + list(APPEND _static_libs "${_item}") + elseif(_type STREQUAL "INTERFACE_LIBRARY" OR _type STREQUAL "OBJECT_LIBRARY") + # Recurse only. Object libraries are the walk's seeds; their objects are + # archived and whole-archive linked by the caller, so linking the + # library target itself would duplicate them unrenamed. + else() + # Shared libraries, executables: pass through. + list(APPEND _other "${_item}") + endif() + foreach(_prop LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) + get_target_property(_deps "${_item}" ${_prop}) + if(_deps) + list(APPEND _worklist ${_deps}) + endif() + endforeach() + endwhile() + set(${STATIC_LIBS_VAR} "${_static_libs}" PARENT_SCOPE) + set(${OTHER_VAR} "${_other}" PARENT_SCOPE) +endfunction() + +# Assembles the renamed libIREECompiler link. +# +# Deferred to the end of the top-level directory so the closure walk sees every +# target: the API directory is configured before most of the compiler tree, so +# aliases like iree::compiler::Codegen::Common do not exist yet. +# +# Reads IREE_RENAMED_SHARED_IMPL_OBJECT_LIBS, set by compiler/src/iree/compiler/API. +function(iree_renamed_shared_impl_finalize) + get_property(_object_libs GLOBAL PROPERTY IREE_RENAMED_SHARED_IMPL_OBJECT_LIBS) + set(_target iree_compiler_API_SharedImpl) + + # Whole-archive: these are the exported surface, so nothing references them. + set(_whole_archives) + foreach(_object_lib ${_object_libs}) + iree_renamed_link_sanitize_name("${_object_lib}" _base) + iree_renamed_archive_from_objects(_renamed + NAME "${_base}" + TARGET "${_object_lib}" + ) + list(APPEND _whole_archives "${_renamed}") + endforeach() + + # The closure is renamed too, or the references above find nothing. It keeps + # selective extraction, so the same objects land as in an unrenamed link. + iree_collect_static_link_closure(_closure_static_libs _closure_other + ${_object_libs} + MLIRExportSMTLIB + MLIRTargetLLVMIRImport + ) + set(_closure_archives) + foreach(_lib ${_closure_static_libs}) + iree_renamed_link_sanitize_name("${_lib}" _base) + iree_renamed_archive(_renamed + NAME "closure_${_base}" + INPUT "$<TARGET_FILE:${_lib}>" + DEPENDS "${_lib}" + ) + list(APPEND _closure_archives "${_renamed}") + endforeach() + + if(APPLE) + set(_whole_archive_flags) + foreach(_archive ${_whole_archives}) + list(APPEND _whole_archive_flags "-Wl,-force_load,${_archive}") + endforeach() + # ld64 ignores command-line order. + target_link_options(${_target} PRIVATE ${_whole_archive_flags}) + target_link_libraries(${_target} PRIVATE + ${_closure_archives} + ${_closure_other} + ) + else() + # Grouped so single-pass ELF linkers do not care about archive order. + target_link_libraries(${_target} PRIVATE + "-Wl,--whole-archive" ${_whole_archives} "-Wl,--no-whole-archive" + "-Wl,--start-group" ${_closure_archives} "-Wl,--end-group" + ${_closure_other} + ) + endif() + + # Orders archive generation before the link. + add_custom_target(${_target}_renamed_archives + DEPENDS ${_whole_archives} ${_closure_archives} + ) + add_dependencies(${_target} ${_target}_renamed_archives) + set_property(TARGET ${_target} APPEND PROPERTY + LINK_DEPENDS ${_whole_archives} ${_closure_archives} + ) +endfunction()
diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 79dae71..024d0d5 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt
@@ -21,6 +21,13 @@ set(IREE_COMPILER_DYLIB_INSTALL_PREFIX "lib/") endif() +# Plugins resolve compiler symbols by name, and hidden visibility would strip +# them before the linker sees them, beyond recovery. Scoped here so the runtime +# keeps hidden visibility. +if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS) + list(REMOVE_ITEM IREE_DEFAULT_COPTS "-fvisibility=hidden") +endif() + # Always build the C bindings, since the API is available apart from # actually building the compiler. add_subdirectory(bindings/c) @@ -43,6 +50,11 @@ PLUGIN_CMAKE_FILE "iree_compiler_plugin.cmake" ) + if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS) + message(STATUS "Dynamically loaded compiler plugins enabled (renamed LLVM/MLIR ABI)") + else() + message(STATUS "Dynamically loaded compiler plugins disabled (IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS=OFF)") + endif() add_subdirectory(src) # Reset BUILD_SHARED_LIBS.
diff --git a/compiler/bindings/c/BUILD.bazel b/compiler/bindings/c/BUILD.bazel index ad4abd4..81943bb 100644 --- a/compiler/bindings/c/BUILD.bazel +++ b/compiler/bindings/c/BUILD.bazel
@@ -47,10 +47,10 @@ "iree/compiler/loader/loader_test.c", ], args = [ - "lib/libIREECompiler.so", + "$(location //lib:IREECompiler)", ], data = [ - "//lib:libIREECompiler.so", + "//lib:IREECompiler", ], tags = ["skip-bazel_to_cmake"], deps = [
diff --git a/compiler/src/iree/compiler/API/BUILD.bazel b/compiler/src/iree/compiler/API/BUILD.bazel index f10c922..59fd351 100644 --- a/compiler/src/iree/compiler/API/BUILD.bazel +++ b/compiler/src/iree/compiler/API/BUILD.bazel
@@ -17,7 +17,7 @@ "api_exports.c", "api_exports.def", "api_exports.ld", - "api_exports.lst", + "api_exports.macos.lst", ]) # IMPORTANT: If modifying the list of dependencies, mirror the changes @@ -56,9 +56,7 @@ iree_compiler_cc_library( name = "SharedImpl", - srcs = [ - "//lib:libIREECompiler.so", - ], + srcs = ["//lib:IREECompiler"], tags = ["skip-bazel_to_cmake"], )
diff --git a/compiler/src/iree/compiler/API/CMakeLists.txt b/compiler/src/iree/compiler/API/CMakeLists.txt index d0a0a69..af3edc9 100644 --- a/compiler/src/iree/compiler/API/CMakeLists.txt +++ b/compiler/src/iree/compiler/API/CMakeLists.txt
@@ -160,6 +160,29 @@ # library structures. set(CMAKE_PLATFORM_NO_VERSIONED_SONAME ON) +if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS) + # Renamed ABI (see build_tools/cmake/iree_renamed_link.cmake). Exports + # everything but the runtime's `iree_*` C-API, so plugins renamed the same way + # resolve against this library at dlopen. + set(_SHARED_IMPL_LINKOPTS + "$<$<PLATFORM_ID:Linux>:-Wl,--version-script=${IREE_ROOT_DIR}/lib/hide_iree_runtime_capi.ld>" + "$<$<PLATFORM_ID:Darwin>:-Wl,-unexported_symbols_list,${IREE_ROOT_DIR}/lib/hide_iree_runtime_capi.macos.txt>" + ) + set(_SHARED_IMPL_LINK_DEPENDS + "${IREE_ROOT_DIR}/lib/hide_iree_runtime_capi.ld;${IREE_ROOT_DIR}/lib/hide_iree_runtime_capi.macos.txt" + ) +else() + set(_SHARED_IMPL_LINKOPTS + # Linux uses a linker script to version symbols. + "$<$<PLATFORM_ID:Linux>:-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/api_version.ld>" + # Hide all symbols from static archives (.a) linked into the shared library. + # Without this, LLVM/MLIR internal symbols leak through the `global: *` + # version script since LLVM is not compiled with -fvisibility=hidden. + "$<$<PLATFORM_ID:Linux>:LINKER:--exclude-libs,ALL>" + ) + set(_SHARED_IMPL_LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/api_version.ld") +endif() + iree_cc_library( SHARED NAME @@ -168,40 +191,54 @@ # Force this to be a regular library with an empty source file. empty.c LINKOPTS - # Linux uses a linker script to version symbols. - $<$<PLATFORM_ID:Linux>:-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/api_version.ld> - # Hide all symbols from static archives (.a) linked into the shared library. - # Without this, LLVM/MLIR internal symbols leak through the `global: *` - # version script since LLVM is not compiled with -fvisibility=hidden. - $<$<PLATFORM_ID:Linux>:LINKER:--exclude-libs,ALL> + ${_SHARED_IMPL_LINKOPTS} DEPS iree::compiler::bindings::c::headers ) # Make sure that re-link if extra linker inputs change. set_target_properties(iree_compiler_API_SharedImpl PROPERTIES - LINK_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/api_version.ld + LINK_DEPENDS "${_SHARED_IMPL_LINK_DEPENDS}" ) -# We need to add the object sources directly to the SHARED sub-target, not -# the .objects sub-target, which is what would happen if added to SRCS. -target_sources(iree_compiler_API_SharedImpl PRIVATE - ${_EXPORT_OBJECT_SRCS} -) +if(IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS) + include(iree_renamed_link) -# Explicitly link deps as PRIVATE (the default rule uses PUBLIC) so that usage -# requirements do not cascade: The shared library is intended to be used as an -# isolated boundary for C consumers. -target_link_libraries(iree_compiler_API_SharedImpl PRIVATE - ${_EXPORT_OBJECT_DEPS} -) + # The payload arrives as archive paths, so CMake sees only empty.c and would + # pick the C driver, which omits libstdc++. The branch below infers CXX from + # the sources it adds. + set_target_properties(iree_compiler_API_SharedImpl PROPERTIES + LINKER_LANGUAGE CXX + ) -# Link MLIRTargetLLVMIRImport and MLIRExportSMTLIB directly since they are -# not exported as object libraries. -target_link_libraries(iree_compiler_API_SharedImpl PRIVATE - MLIRExportSMTLIB - MLIRTargetLLVMIRImport -) + # Deferred to the end of the top-level directory, where every target exists: + # the closure walk needs them, and most of the compiler tree is unconfigured + # while this directory runs. The branch below defers to generate time instead. + set_property(GLOBAL PROPERTY + IREE_RENAMED_SHARED_IMPL_OBJECT_LIBS "${_EXPORT_OBJECT_LIBS}") + cmake_language(DEFER DIRECTORY "${IREE_ROOT_DIR}" + CALL iree_renamed_shared_impl_finalize) +else() + # We need to add the object sources directly to the SHARED sub-target, not + # the .objects sub-target, which is what would happen if added to SRCS. + target_sources(iree_compiler_API_SharedImpl PRIVATE + ${_EXPORT_OBJECT_SRCS} + ) + + # Explicitly link deps as PRIVATE (the default rule uses PUBLIC) so that usage + # requirements do not cascade: The shared library is intended to be used as an + # isolated boundary for C consumers. + target_link_libraries(iree_compiler_API_SharedImpl PRIVATE + ${_EXPORT_OBJECT_DEPS} + ) + + # Link MLIRTargetLLVMIRImport and MLIRExportSMTLIB directly since they are + # not exported as object libraries. + target_link_libraries(iree_compiler_API_SharedImpl PRIVATE + MLIRExportSMTLIB + MLIRTargetLLVMIRImport + ) +endif() # If not using sanitizers, ask linkers to error on undefined symbols. # For shared libraries, using sanitizers implies undefined symbols on Unix-like.
diff --git a/docs/website/docs/building-from-source/getting-started.md b/docs/website/docs/building-from-source/getting-started.md index 525ad86..6479d20 100644 --- a/docs/website/docs/building-from-source/getting-started.md +++ b/docs/website/docs/building-from-source/getting-started.md
@@ -136,6 +136,12 @@ -DIREE_ENABLE_ASSERTIONS=ON ``` +!!! Note "Note - thin archives and dynamic compiler plugins" + `-DIREE_ENABLE_THIN_ARCHIVES=ON` cannot be combined with + `-DIREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS=ON`; configuring both fails. Dynamic + plugins resolve against a renamed copy of the compiler's symbols, and the + rename cannot rewrite a thin archive. + ???+ Tip "Tip - CMAKE_BUILD_TYPE values" We recommend using the `RelWithDebInfo` build type by default for a good balance of debug info and performance. The `Debug`, `Release`, and
diff --git a/docs/website/docs/developers/building/cmake-options.md b/docs/website/docs/developers/building/cmake-options.md index 978097d..71e3992 100644 --- a/docs/website/docs/developers/building/cmake-options.md +++ b/docs/website/docs/developers/building/cmake-options.md
@@ -136,6 +136,16 @@ * `IREE_INPUT_TORCH` * `IREE_INPUT_TOSA` +### `IREE_EXPERIMENTAL_COMPILER_DYNAMIC_PLUGINS` + +* 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`. + ### `IREE_OUTPUT_FORMAT_C` * type: BOOL
diff --git a/lib/BUILD.bazel b/lib/BUILD.bazel index 0a229e1..12b64aa 100644 --- a/lib/BUILD.bazel +++ b/lib/BUILD.bazel
@@ -4,7 +4,13 @@ # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +load("//build_tools/bazel:abi_prefix.bzl", "IREE_COMPILER_ABI_PREFIX") load("//build_tools/bazel:build_defs.oss.bzl", "iree_compiler_cc_binary") +load( + "//build_tools/bazel:renamed_link.bzl", + "iree_renamed_compiler_abi", + "renamed_cc_info", +) package( default_visibility = ["//visibility:public"], @@ -12,19 +18,82 @@ licenses = ["notice"], # Apache 2.0 ) +# Hide the bundled runtime's `iree_*` C-API from the compiler library's +# exports. See IREECompiler below for why. +exports_files([ + "hide_iree_runtime_capi.ld", + "hide_iree_runtime_capi.macos.txt", +]) + +# The IREE compiler shared library. Named "IREECompiler" because Bazel prefixes +# "lib" to cc_binary(linkshared=1) outputs. +# +# Every llvm/mlir C++ symbol in the StaticImpl closure is renamed before link +# (`_ZN4mlir...` -> `_ZN6IREE184mlir...`), so the library can share a process +# with a foreign LLVM/MLIR without colliding, or weak-coalescing on macOS. The +# renamed internals are exported for dynamic plugins. +# +# The runtime's `extern "C"` C-API is hidden by export list instead, the rename +# reaching only mangled C++ names. Neither the compiler C-API (`ireeCompiler*`) +# nor the renamed internals match `iree_*`, so nothing else is caught. iree_compiler_cc_binary( - name = "libIREECompiler.so", + name = "IREECompiler", srcs = [ "//compiler/src/iree/compiler/API:api_exports.c", ], - linkopts = [ - "-Wl,--version-script=$(location //compiler/src/iree/compiler/API:api_exports.ld)", - "-Wl,--no-undefined", - ], + additional_linker_inputs = select({ + "//conditions:default": [":hide_iree_runtime_capi.ld"], + "@platforms//os:macos": [":hide_iree_runtime_capi.macos.txt"], + }), + linkopts = select({ + "//conditions:default": [ + "-Wl,--no-undefined", + "-Wl,--version-script=$(location :hide_iree_runtime_capi.ld)", + ], + "@platforms//os:macos": [ + "-Wl,-install_name,@rpath/libIREECompiler.dylib", + "-Wl,-unexported_symbols_list,$(location :hide_iree_runtime_capi.macos.txt)", + ], + }), linkshared = 1, + tags = ["skip-bazel_to_cmake"], deps = [ + ":IREEStaticImplRenamed", "//compiler/bindings/c:headers", - "//compiler/src/iree/compiler/API:StaticImpl", - "//compiler/src/iree/compiler/API:api_exports.ld", ], ) + +# Renaming the CcInfo leaves the dylib the linker-input shape a normal Bazel +# C++ link would see. +renamed_cc_info( + name = "IREEStaticImplRenamed", + include_provided = True, + tags = ["skip-bazel_to_cmake"], + deps = ["//compiler/src/iree/compiler/API:StaticImpl"], +) + +# Entry point for out-of-tree plugins: the library plus the prefix they must +# rename to. +iree_renamed_compiler_abi( + name = "IREECompilerShared", + shared_library = ":IREECompiler", + static_closure = "//compiler/src/iree/compiler/API:StaticImpl", + symbol_prefix = IREE_COMPILER_ABI_PREFIX, + tags = ["skip-bazel_to_cmake"], +) + +# Guards the export invariants the linker scripts above rely on. +py_test( + name = "exported_symbols_test", + srcs = ["exported_symbols_test.py"], + args = [ + "--nm=$(location @llvm-project//llvm:llvm-nm)", + "--library=$(location :IREECompiler)", + "--symbol-prefix=" + IREE_COMPILER_ABI_PREFIX, + ], + data = [ + ":IREECompiler", + "@llvm-project//llvm:llvm-nm", + ], + tags = ["skip-bazel_to_cmake"], +)
diff --git a/lib/exported_symbols_test.py b/lib/exported_symbols_test.py new file mode 100644 index 0000000..54f9189 --- /dev/null +++ b/lib/exported_symbols_test.py
@@ -0,0 +1,103 @@ +#!/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 +"""Export invariants of libIREECompiler. + +The single renamed compiler library relies on naming conventions to keep three +symbol families apart: + + 1. snake_case `iree_*` -- the bundled runtime C-API; must be hidden so it + can never shadow a co-resident iree.runtime. + 2. camelCase `ireeCompiler*` -- the compiler embedding C-API; must stay + exported. + 3. the renamed llvm/mlir C++ internals, carrying the ABI prefix given by + --symbol-prefix; used by dynamic plugins and must stay exported. + +A convention violation (a snake_case export sneaking into the compiler, or the +hide-script over-matching) silently binds the wrong symbol at load time, so +this test turns it into a CI failure instead. + +Extension point: a co-installed runtime library can be added to `data` and +checked for an empty exported-name intersection with the compiler library. +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +import unittest + +NM: str = "" +LIBRARY: str = "" +SYMBOL_PREFIX: str = "" + +# Mach-O adds a leading underscore to C-level names. +_RUNTIME_CAPI_RE = re.compile(r"^_?iree_[a-z0-9_]+$") +_COMPILER_CAPI_RE = re.compile(r"^_?ireeCompiler[A-Za-z0-9]+$") +# Built from the prefix, whose length is part of the mangled component. +_RENAMED_CXX_RE: re.Pattern[str] | None = None + + +def _exported_symbols() -> list[str]: + result = subprocess.run( + [NM, "--extern-only", "--defined-only", "--no-demangle", LIBRARY], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + symbols = [] + for line in result.stdout.splitlines(): + fields = line.split() + if len(fields) >= 2 and fields[-2] not in ("U",): + symbols.append(fields[-1]) + return symbols + + +class ExportedSymbolsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.symbols = _exported_symbols() + + def test_runtime_capi_is_hidden(self): + leaked = [s for s in self.symbols if _RUNTIME_CAPI_RE.match(s)] + self.assertEqual( + leaked[:20], + [], + msg="snake_case iree_* runtime C-API symbols leaked into the " + "compiler export table; hide_iree_runtime_capi.* must cover them", + ) + + def test_compiler_capi_is_exported(self): + self.assertTrue( + any(_COMPILER_CAPI_RE.match(s) for s in self.symbols), + msg="no ireeCompiler* embedding C-API symbol exported", + ) + + def test_renamed_internals_are_exported(self): + self.assertTrue( + any(_RENAMED_CXX_RE.match(s) for s in self.symbols), + msg=f"no renamed ({SYMBOL_PREFIX}) llvm/mlir C++ symbol exported; " + "the rename pipeline or export scripts are broken", + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--nm", required=True) + parser.add_argument("--library", required=True) + parser.add_argument("--symbol-prefix", required=True) + args, remaining = parser.parse_known_args() + NM = args.nm + LIBRARY = args.library + SYMBOL_PREFIX = args.symbol_prefix + _RENAMED_CXX_RE = re.compile( + r"^_?_Z(N|TVN|TTN|TIN|TSN|GVN)?%d%s" + % (len(SYMBOL_PREFIX), re.escape(SYMBOL_PREFIX)) + ) + unittest.main(argv=[sys.argv[0]] + remaining)
diff --git a/lib/hide_iree_runtime_capi.ld b/lib/hide_iree_runtime_capi.ld new file mode 100644 index 0000000..92453b0 --- /dev/null +++ b/lib/hide_iree_runtime_capi.ld
@@ -0,0 +1,16 @@ +# Version script for libIREECompiler (Linux/ELF). +# +# The compiler lib bundles the IREE runtime, so it also carries the runtime's +# `iree_*` C-API under its real names, the rename reaching only mangled C++. +# Compile-then-run flows load both libraries at once, and a runtime plugin's +# undefined iree_hal_* may then bind to the compiler's copy, leaving its device +# without the loaders the real runtime registered. +# +# So hide every snake_case `iree_*` from the export table. Nothing else resolves +# here by that spelling: the embedding C-API is camelCase, the MLIR C-API is +# mlir*, and the renamed internals carry the ABI prefix. +# exported_symbols_test.py keeps it that way. +{ + global: *; + local: iree_*; +};
diff --git a/lib/hide_iree_runtime_capi.macos.txt b/lib/hide_iree_runtime_capi.macos.txt new file mode 100644 index 0000000..845c8db --- /dev/null +++ b/lib/hide_iree_runtime_capi.macos.txt
@@ -0,0 +1,8 @@ +# -unexported_symbols_list for libIREECompiler (macOS/Mach-O). +# +# See hide_iree_runtime_capi.ld for why. +# +# Mach-O prefixes C names with an underscore, so the runtime C-API is `_iree_*` +# here. The embedding C-API and renamed internals do not match, and stay +# exported. +_iree_*