blob: d7410a9a5ebdb26dd994b3f6875ec9f9a7e833ba [file] [edit]
#!/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())