Files
clang-p2996/llvm/tools/llvm-readtapi/llvm-readtapi.cpp
Chandler Carruth dd647e3e60 Rework the Option library to reduce dynamic relocations (#119198)
Apologies for the large change, I looked for ways to break this up and
all of the ones I saw added real complexity. This change focuses on the
option's prefixed names and the array of prefixes. These are present in
every option and the dominant source of dynamic relocations for PIE or
PIC users of LLVM and Clang tooling. In some cases, 100s or 1000s of
them for the Clang driver which has a huge number of options.

This PR addresses this by building a string table and a prefixes table
that can be referenced with indices rather than pointers that require
dynamic relocations. This removes almost 7k dynmaic relocations from the
`clang` binary, roughly 8% of the remaining dynmaic relocations outside
of vtables. For busy-boxing use cases where many different option tables
are linked into the same binary, the savings add up a bit more.

The string table is a straightforward mechanism, but the prefixes
required some subtlety. They are encoded in a Pascal-string fashion with
a size followed by a sequence of offsets. This works relatively well for
the small realistic prefixes arrays in use.

Lots of code has to change in order to land this though: both all the
option library code has to be updated to use the string table and
prefixes table, and all the users of the options library have to be
updated to correctly instantiate the objects.

Some follow-up patches in the works to provide an abstraction for this
style of code, and to start using the same technique for some of the
other strings here now that the infrastructure is in place.
2024-12-11 15:44:44 -08:00

580 lines
19 KiB
C++

//===-- llvm-readtapi.cpp - tapi file reader and transformer -----*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file defines the command-line driver for llvm-readtapi.
//
//===----------------------------------------------------------------------===//
#include "DiffEngine.h"
#include "llvm/BinaryFormat/Magic.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/Option.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/TextAPI/DylibReader.h"
#include "llvm/TextAPI/TextAPIError.h"
#include "llvm/TextAPI/TextAPIReader.h"
#include "llvm/TextAPI/TextAPIWriter.h"
#include "llvm/TextAPI/Utils.h"
#include <cstdlib>
#if !defined(_MSC_VER) && !defined(__MINGW32__)
#include <unistd.h>
#endif
using namespace llvm;
using namespace MachO;
using namespace object;
namespace {
using namespace llvm::opt;
enum ID {
OPT_INVALID = 0, // This is not an option ID.
#define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__),
#include "TapiOpts.inc"
#undef OPTION
};
#define OPTTABLE_STR_TABLE_CODE
#include "TapiOpts.inc"
#undef OPTTABLE_STR_TABLE_CODE
#define OPTTABLE_PREFIXES_TABLE_CODE
#include "TapiOpts.inc"
#undef OPTTABLE_PREFIXES_TABLE_CODE
static constexpr opt::OptTable::Info InfoTable[] = {
#define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),
#include "TapiOpts.inc"
#undef OPTION
};
class TAPIOptTable : public opt::GenericOptTable {
public:
TAPIOptTable()
: opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) {
setGroupedShortOptions(true);
}
};
struct StubOptions {
bool DeleteInput = false;
bool DeletePrivate = false;
bool TraceLibs = false;
};
struct CompareOptions {
ArchitectureSet ArchsToIgnore;
};
struct Context {
std::vector<std::string> Inputs;
StubOptions StubOpt;
CompareOptions CmpOpt;
std::unique_ptr<llvm::raw_fd_stream> OutStream;
FileType WriteFT = FileType::TBD_V5;
bool Compact = false;
Architecture Arch = AK_unknown;
};
// Use unique exit code to differentiate failures not directly caused from
// TextAPI operations. This is used for wrapping `compare` operations in
// automation and scripting.
const int NON_TAPI_EXIT_CODE = 2;
const std::string TOOLNAME = "llvm-readtapi";
ExitOnError ExitOnErr;
} // anonymous namespace
// Handle error reporting in cases where `ExitOnError` is not used.
static void reportError(Twine Message, int ExitCode = EXIT_FAILURE) {
errs() << TOOLNAME << ": error: " << Message << "\n";
errs().flush();
exit(ExitCode);
}
// Handle warnings.
static void reportWarning(Twine Message) {
errs() << TOOLNAME << ": warning: " << Message << "\n";
}
/// Get what the symlink points to.
/// This is a no-op on windows as it references POSIX level apis.
static void read_link(const Twine &Path, SmallVectorImpl<char> &Output) {
#if !defined(_MSC_VER) && !defined(__MINGW32__)
Output.clear();
if (Path.isTriviallyEmpty())
return;
SmallString<PATH_MAX> Storage;
auto P = Path.toNullTerminatedStringRef(Storage);
SmallString<PATH_MAX> Result;
ssize_t Len;
if ((Len = ::readlink(P.data(), Result.data(), PATH_MAX)) == -1)
reportError("unable to read symlink: " + Path);
Result.resize_for_overwrite(Len);
Output.swap(Result);
#else
reportError("unable to read symlink on windows: " + Path);
#endif
}
static std::unique_ptr<InterfaceFile>
getInterfaceFile(const StringRef Filename, bool ResetBanner = true) {
ExitOnErr.setBanner(TOOLNAME + ": error: '" + Filename.str() + "' ");
ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr =
MemoryBuffer::getFile(Filename, /*IsText=*/true);
if (BufferOrErr.getError())
ExitOnErr(errorCodeToError(BufferOrErr.getError()));
auto Buffer = std::move(*BufferOrErr);
std::unique_ptr<InterfaceFile> IF;
switch (identify_magic(Buffer->getBuffer())) {
case file_magic::macho_dynamically_linked_shared_lib:
case file_magic::macho_dynamically_linked_shared_lib_stub:
case file_magic::macho_universal_binary:
IF = ExitOnErr(DylibReader::get(Buffer->getMemBufferRef()));
break;
case file_magic::tapi_file:
IF = ExitOnErr(TextAPIReader::get(Buffer->getMemBufferRef()));
break;
default:
reportError(Filename + ": unsupported file type");
}
if (ResetBanner)
ExitOnErr.setBanner(TOOLNAME + ": error: ");
return IF;
}
static bool handleCompareAction(const Context &Ctx) {
if (Ctx.Inputs.size() != 2)
reportError("compare only supports two input files",
/*ExitCode=*/NON_TAPI_EXIT_CODE);
// Override default exit code.
ExitOnErr = ExitOnError(TOOLNAME + ": error: ",
/*DefaultErrorExitCode=*/NON_TAPI_EXIT_CODE);
auto LeftIF = getInterfaceFile(Ctx.Inputs.front());
auto RightIF = getInterfaceFile(Ctx.Inputs.at(1));
// Remove all architectures to ignore before running comparison.
auto removeArchFromIF = [](auto &IF, const ArchitectureSet &ArchSet,
const Architecture ArchToRemove) {
if (!ArchSet.has(ArchToRemove))
return;
if (ArchSet.count() == 1)
return;
auto OutIF = IF->remove(ArchToRemove);
if (!OutIF)
ExitOnErr(OutIF.takeError());
IF = std::move(*OutIF);
};
if (!Ctx.CmpOpt.ArchsToIgnore.empty()) {
const ArchitectureSet LeftArchs = LeftIF->getArchitectures();
const ArchitectureSet RightArchs = RightIF->getArchitectures();
for (const auto Arch : Ctx.CmpOpt.ArchsToIgnore) {
removeArchFromIF(LeftIF, LeftArchs, Arch);
removeArchFromIF(RightIF, RightArchs, Arch);
}
}
raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs();
return DiffEngine(LeftIF.get(), RightIF.get()).compareFiles(OS);
}
static bool handleWriteAction(const Context &Ctx,
std::unique_ptr<InterfaceFile> Out = nullptr) {
if (!Out) {
if (Ctx.Inputs.size() != 1)
reportError("write only supports one input file");
Out = getInterfaceFile(Ctx.Inputs.front());
}
raw_ostream &OS = Ctx.OutStream ? *Ctx.OutStream : outs();
ExitOnErr(TextAPIWriter::writeToStream(OS, *Out, Ctx.WriteFT, Ctx.Compact));
return EXIT_SUCCESS;
}
static bool handleMergeAction(const Context &Ctx) {
if (Ctx.Inputs.size() < 2)
reportError("merge requires at least two input files");
std::unique_ptr<InterfaceFile> Out;
for (StringRef FileName : Ctx.Inputs) {
auto IF = getInterfaceFile(FileName);
// On the first iteration copy the input file and skip merge.
if (!Out) {
Out = std::move(IF);
continue;
}
Out = ExitOnErr(Out->merge(IF.get()));
}
return handleWriteAction(Ctx, std::move(Out));
}
static void stubifyImpl(std::unique_ptr<InterfaceFile> IF, Context &Ctx) {
// TODO: Add inlining and magic merge support.
if (Ctx.OutStream == nullptr) {
std::error_code EC;
assert(!IF->getPath().empty() && "Unknown output location");
SmallString<PATH_MAX> OutputLoc = IF->getPath();
replace_extension(OutputLoc, ".tbd");
Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(OutputLoc, EC);
if (EC)
reportError("opening file '" + OutputLoc + ": " + EC.message());
}
handleWriteAction(Ctx, std::move(IF));
// Clear out output stream after file has been written incase more files are
// stubifed.
Ctx.OutStream = nullptr;
}
static void stubifyDirectory(const StringRef InputPath, Context &Ctx) {
assert(InputPath.back() != '/' && "Unexpected / at end of input path.");
StringMap<std::vector<SymLink>> SymLinks;
StringMap<std::unique_ptr<InterfaceFile>> Dylibs;
StringMap<std::string> OriginalNames;
std::set<std::pair<std::string, bool>> LibsToDelete;
std::error_code EC;
for (sys::fs::recursive_directory_iterator IT(InputPath, EC), IE; IT != IE;
IT.increment(EC)) {
if (EC == std::errc::no_such_file_or_directory) {
reportWarning(IT->path() + ": " + EC.message());
continue;
}
if (EC)
reportError(IT->path() + ": " + EC.message());
// Skip header directories (include/Headers/PrivateHeaders).
StringRef Path = IT->path();
if (sys::fs::is_directory(Path)) {
const StringRef Stem = sys::path::stem(Path);
if ((Stem == "include") || (Stem == "Headers") ||
(Stem == "PrivateHeaders") || (Stem == "Modules")) {
IT.no_push();
continue;
}
}
// Skip module files too.
if (Path.ends_with(".map") || Path.ends_with(".modulemap"))
continue;
// Check if the entry is a symlink. We don't follow symlinks but we record
// their content.
bool IsSymLink;
if (auto EC = sys::fs::is_symlink_file(Path, IsSymLink))
reportError(Path + ": " + EC.message());
if (IsSymLink) {
IT.no_push();
bool ShouldSkip;
auto SymLinkEC = shouldSkipSymLink(Path, ShouldSkip);
// If symlink is broken, for some reason, we should continue
// trying to repair it before quitting.
if (!SymLinkEC && ShouldSkip)
continue;
if (Ctx.StubOpt.DeletePrivate &&
isPrivateLibrary(Path.drop_front(InputPath.size()), true)) {
LibsToDelete.emplace(Path, false);
continue;
}
SmallString<PATH_MAX> SymPath;
read_link(Path, SymPath);
// Sometimes there are broken symlinks that are absolute paths, which are
// invalid during build time, but would be correct during runtime. In the
// case of an absolute path we should check first if the path exists with
// the known locations as prefix.
SmallString<PATH_MAX> LinkSrc = Path;
SmallString<PATH_MAX> LinkTarget;
if (sys::path::is_absolute(SymPath)) {
LinkTarget = InputPath;
sys::path::append(LinkTarget, SymPath);
// TODO: Investigate supporting a file manager for file system accesses.
if (sys::fs::exists(LinkTarget)) {
// Convert the absolute path to an relative path.
if (auto ec = MachO::make_relative(LinkSrc, LinkTarget, SymPath))
reportError(LinkTarget + ": " + EC.message());
} else if (!sys::fs::exists(SymPath)) {
reportWarning("ignoring broken symlink: " + Path);
continue;
} else {
LinkTarget = SymPath;
}
} else {
LinkTarget = LinkSrc;
sys::path::remove_filename(LinkTarget);
sys::path::append(LinkTarget, SymPath);
}
// For Apple SDKs, the symlink src is guaranteed to be a canonical path
// because we don't follow symlinks when scanning. The symlink target is
// constructed from the symlink path and needs to be canonicalized.
if (auto ec = sys::fs::real_path(Twine(LinkTarget), LinkTarget)) {
reportWarning(LinkTarget + ": " + ec.message());
continue;
}
SymLinks[LinkTarget.c_str()].emplace_back(LinkSrc.str(),
std::string(SymPath.str()));
continue;
}
bool IsDirectory = false;
if (auto EC = sys::fs::is_directory(Path, IsDirectory))
reportError(Path + ": " + EC.message());
if (IsDirectory)
continue;
if (Ctx.StubOpt.DeletePrivate &&
isPrivateLibrary(Path.drop_front(InputPath.size()))) {
IT.no_push();
LibsToDelete.emplace(Path, false);
continue;
}
auto IF = getInterfaceFile(Path);
if (Ctx.StubOpt.TraceLibs)
errs() << Path << "\n";
// Normalize path for map lookup by removing the extension.
SmallString<PATH_MAX> NormalizedPath(Path);
replace_extension(NormalizedPath, "");
if ((IF->getFileType() == FileType::MachO_DynamicLibrary) ||
(IF->getFileType() == FileType::MachO_DynamicLibrary_Stub)) {
OriginalNames[NormalizedPath.c_str()] = IF->getPath();
// Don't add this MachO dynamic library because we already have a
// text-based stub recorded for this path.
if (Dylibs.count(NormalizedPath.c_str()))
continue;
}
Dylibs[NormalizedPath.c_str()] = std::move(IF);
}
for (auto &Lib : Dylibs) {
auto &Dylib = Lib.second;
// Get the original file name.
SmallString<PATH_MAX> NormalizedPath(Dylib->getPath());
stubifyImpl(std::move(Dylib), Ctx);
replace_extension(NormalizedPath, "");
auto Found = OriginalNames.find(NormalizedPath.c_str());
if (Found == OriginalNames.end())
continue;
if (Ctx.StubOpt.DeleteInput)
LibsToDelete.emplace(Found->second, true);
// Don't allow for more than 20 levels of symlinks when searching for
// libraries to stubify.
StringRef LibToCheck = Found->second;
for (int i = 0; i < 20; ++i) {
auto LinkIt = SymLinks.find(LibToCheck);
if (LinkIt != SymLinks.end()) {
for (auto &SymInfo : LinkIt->second) {
SmallString<PATH_MAX> LinkSrc(SymInfo.SrcPath);
SmallString<PATH_MAX> LinkTarget(SymInfo.LinkContent);
replace_extension(LinkSrc, "tbd");
replace_extension(LinkTarget, "tbd");
if (auto EC = sys::fs::remove(LinkSrc))
reportError(LinkSrc + " : " + EC.message());
if (auto EC = sys::fs::create_link(LinkTarget, LinkSrc))
reportError(LinkTarget + " : " + EC.message());
if (Ctx.StubOpt.DeleteInput)
LibsToDelete.emplace(SymInfo.SrcPath, true);
LibToCheck = SymInfo.SrcPath;
}
} else
break;
}
}
// Recursively delete the directories. This will abort when they are not empty
// or we reach the root of the SDK.
for (const auto &[LibPath, IsInput] : LibsToDelete) {
if (!IsInput && SymLinks.count(LibPath))
continue;
if (auto EC = sys::fs::remove(LibPath))
reportError(LibPath + " : " + EC.message());
std::error_code EC;
auto Dir = sys::path::parent_path(LibPath);
do {
EC = sys::fs::remove(Dir);
Dir = sys::path::parent_path(Dir);
if (!Dir.starts_with(InputPath))
break;
} while (!EC);
}
}
static bool handleStubifyAction(Context &Ctx) {
if (Ctx.Inputs.empty())
reportError("stubify requires at least one input file");
if ((Ctx.Inputs.size() > 1) && (Ctx.OutStream != nullptr))
reportError("cannot write multiple inputs into single output file");
for (StringRef PathName : Ctx.Inputs) {
bool IsDirectory = false;
if (auto EC = sys::fs::is_directory(PathName, IsDirectory))
reportError(PathName + ": " + EC.message());
if (IsDirectory) {
if (Ctx.OutStream != nullptr)
reportError("cannot stubify directory'" + PathName +
"' into single output file");
stubifyDirectory(PathName, Ctx);
continue;
}
stubifyImpl(getInterfaceFile(PathName), Ctx);
if (Ctx.StubOpt.DeleteInput)
if (auto ec = sys::fs::remove(PathName))
reportError("deleting file '" + PathName + ": " + ec.message());
}
return EXIT_SUCCESS;
}
using IFOperation =
std::function<llvm::Expected<std::unique_ptr<InterfaceFile>>(
const llvm::MachO::InterfaceFile &, Architecture)>;
static bool handleSingleFileAction(const Context &Ctx, const StringRef Action,
IFOperation act) {
if (Ctx.Inputs.size() != 1)
reportError(Action + " only supports one input file");
if (Ctx.Arch == AK_unknown)
reportError(Action + " requires -arch <arch>");
auto IF = getInterfaceFile(Ctx.Inputs.front(), /*ResetBanner=*/false);
auto OutIF = act(*IF, Ctx.Arch);
if (!OutIF)
ExitOnErr(OutIF.takeError());
return handleWriteAction(Ctx, std::move(*OutIF));
}
static void setStubOptions(opt::InputArgList &Args, StubOptions &Opt) {
Opt.DeleteInput = Args.hasArg(OPT_delete_input);
Opt.DeletePrivate = Args.hasArg(OPT_delete_private_libraries);
Opt.TraceLibs = Args.hasArg(OPT_t);
}
int main(int Argc, char **Argv) {
InitLLVM X(Argc, Argv);
BumpPtrAllocator A;
StringSaver Saver(A);
TAPIOptTable Tbl;
Context Ctx;
ExitOnErr.setBanner(TOOLNAME + ": error:");
opt::InputArgList Args = Tbl.parseArgs(
Argc, Argv, OPT_UNKNOWN, Saver, [&](StringRef Msg) { reportError(Msg); });
if (Args.hasArg(OPT_help)) {
Tbl.printHelp(outs(),
"USAGE: llvm-readtapi <command> [-arch <architecture> "
"<options>]* <inputs> [-o "
"<output>]*",
"LLVM TAPI file reader and transformer");
return EXIT_SUCCESS;
}
if (Args.hasArg(OPT_version)) {
cl::PrintVersionMessage();
return EXIT_SUCCESS;
}
for (opt::Arg *A : Args.filtered(OPT_INPUT))
Ctx.Inputs.push_back(A->getValue());
if (opt::Arg *A = Args.getLastArg(OPT_output_EQ)) {
std::string OutputLoc = std::move(A->getValue());
std::error_code EC;
Ctx.OutStream = std::make_unique<llvm::raw_fd_stream>(OutputLoc, EC);
if (EC)
reportError("error opening the file '" + OutputLoc + EC.message(),
NON_TAPI_EXIT_CODE);
}
Ctx.Compact = Args.hasArg(OPT_compact);
if (opt::Arg *A = Args.getLastArg(OPT_filetype_EQ)) {
StringRef FT = A->getValue();
Ctx.WriteFT = TextAPIWriter::parseFileType(FT);
if (Ctx.WriteFT < FileType::TBD_V3)
reportError("deprecated filetype '" + FT + "' is not supported to write");
if (Ctx.WriteFT == FileType::Invalid)
reportError("unsupported filetype '" + FT + "'");
}
auto SanitizeArch = [&](opt::Arg *A) {
StringRef ArchStr = A->getValue();
auto Arch = getArchitectureFromName(ArchStr);
if (Arch == AK_unknown)
reportError("unsupported architecture '" + ArchStr);
return Arch;
};
if (opt::Arg *A = Args.getLastArg(OPT_arch_EQ))
Ctx.Arch = SanitizeArch(A);
for (opt::Arg *A : Args.filtered(OPT_ignore_arch_EQ))
Ctx.CmpOpt.ArchsToIgnore.set(SanitizeArch(A));
// Handle top level and exclusive operation.
SmallVector<opt::Arg *, 1> ActionArgs(Args.filtered(OPT_action_group));
if (ActionArgs.empty())
// If no action specified, write out tapi file in requested format.
return handleWriteAction(Ctx);
if (ActionArgs.size() > 1) {
std::string Buf;
raw_string_ostream OS(Buf);
OS << "only one of the following actions can be specified:";
for (auto *Arg : ActionArgs)
OS << " " << Arg->getSpelling();
reportError(OS.str());
}
switch (ActionArgs.front()->getOption().getID()) {
case OPT_compare:
return handleCompareAction(Ctx);
case OPT_merge:
return handleMergeAction(Ctx);
case OPT_extract:
return handleSingleFileAction(Ctx, "extract", &InterfaceFile::extract);
case OPT_remove:
return handleSingleFileAction(Ctx, "remove", &InterfaceFile::remove);
case OPT_stubify:
setStubOptions(Args, Ctx.StubOpt);
return handleStubifyAction(Ctx);
}
return EXIT_SUCCESS;
}