在这里,我们强调一些LLVM APIs,它们通常是有用的,并且当你编写转换时最好了解一下。
isa<>
,cast<>
,dyn_cast<>
模板
LLVM源代码库广泛使用了一种自定义形式的RTTI。 这些模板与C++ dynamic_cast<>操作符有许多相似之处,但它们没有一些缺点(主要源自dynamic_cast<>仅适用于具有v-table的类)。
因为它们经常被使用,你必须知道他们做什么以及它们如何工作。
所有这些模板都在llvm/Support/Casting.h
文件中定义(请注意,您很少需要直接包含此文件)。
isa<>
:isa<>
操作符就像Java的”instanceof”操作符.它根据一个引用或指针是否指向<>中指定的类类型来返回true或者false.这个操作符在各种约束检查中很有用(下面有例子).cast<>
:cast<>
操作符是一个”checked cast”操作.它将一个指针或引用从基类转换到派生类,如果类型错误将引发断言错误.这应该在当你相信实例的类型时使用.一个isa<> 和 cast<> 模板的例子是:
static bool isLoopInvariant(const Value *V, const Loop *L) {
if (isa<Constant>(V) || isa<Argument>(V) || isa<GlobalValue>(V))
return true;
// Otherwise, it must be an instruction...
return !L->contains(cast<Instruction>(V)->getParent());
}请注意,您不应该在后面跟着一个
cast <>
时使用isa<>
测试,对于那种情况使用dyn_cast<>
运算符。dyn_cast<>
:dyn_cast<>
操作符是一个“检查转换(checking cast)”操作。它检查操作数是否是指定的类型,如果是这样,返回一个指向它的指针(该操作符不适用于引用)。如果操作数不是正确的类型,则返回空指针。因此,这样做非常像C++中的dynamic_cast<>
运算符,并且应该在相同的情况下使用。 通常,dyn_cast<>运算符用于if语句或其他一些流程控制语句,如下所示:if (auto* AI = dyn_cast<AllocationInst>(Val)) {
// ...
}这种形式的if语句有效地组合了调用
isa<>
和调用cast<>
到一个语句中,这是非常方便的。请注意,
dyn_cast<>
运算符,如C++的dynamic_cast<>
或Java的instanceof
运算符,可能会被滥用。特别是,您不应该使用一系列if/then/else
块来检查许多不同类的变体。如果您发现自己想要这样做,使用InstVisitor
类直接发出指令类型更为清洁也更有效。isa_and_nonnull<>
:isa_and_nonnull<>
运算符的工作原理与isa<>
运算符类似,不同之处在于它允许将空指针作为参数(然后返回false)。这有时很有用,允许您将多个空检查合并为一个。cast_or_null<>
:cast_or_null<>
操作符与cast<>
操作符类似,只是它允许一个空指针作为参数(然后传播)。这有时可能是有用的,允许您将几个空检查合并为一个。dyn_cast_or_null<>
:dyn_cast_or_null<>
操作符与dyn_cast<>
运算符类似,只不过它允许一个空指针作为参数(然后传播)。这有时可能是有用的,允许您将几个空检查合并为一个。
这6个模板可以与任何类一起使用,无论它们是否具有虚表(v-table)。
如果要添加对这些模板的支持,请参阅文档如何为类层次结构设置LLVM风格的RTTI。
字符串传递
虽然LLVM通常不做太多字符串操作,但是我们有几个重要的APIs接受字符串。两个重要的例子是 Value
类(它有指令、函数等的名称)和 StringMap
类(在 LLVM 和 Clang 中广泛使用)。
这些是泛型类,它们需要能够接受可能包含空字符的字符串。因此,它们不能简单地接受const char*
,而接受const std::string&
要求客户机执行堆分配,这通常是不必要的。代替的是,许多LLVM APIs使用StringRef``或const Twine&
来有效地传递字符串。
StringRef类
StringRef
数据类型表示对常量字符串(一个字符数组和一个长度)的引用,并支持std::string
上可用的公共操作,但不需要堆分配。
它可以使用一个C风格的以null
结尾的字符串、一个std::string
隐式地被造,也可以使用一个字符指针和长度显式地构造。例如,StringRef find函数声明为:
iterator find(StringRef Key); |
client可以用以下任意一种方式调用这个函数:
Map.find("foo"); // Lookup "foo" |
类似地,需要返回string的APIs可能会返回一个StringRef
实例,该实例可以直接使用,也可以使用str成员函数将其转换为std::string
。有关更多信息,请查看 llvm/ADT/StringRef.h
。
您应该很少直接使用StringRef类,因为它包含指向外部内存的指针,所以存储该类的实例通常是不安全的(除非您知道不会释放外部存储)。StringRef在 LLVM 中足够小和普遍,因此它应该总是通过值传递。
Twine类
Twine
类是 APIs 接受连接字符串的有效方法。例如,一个常见的LLVM范型是根据带有后缀的另一条指令的名称来命名一条指令,例如:
New = CmpInst::Create(..., SO->getName() + ".cmp"); |
Twine类实际上是一个轻量级的rope,它指向临时(分配给栈的)对象。Twine可以隐式地构造为加运算符应用于字符串的结果(即,一个C字符串,一个std::string,或者一个StringRef)。Twine会延迟字符串的实际连接,直到实际需要它时,才会有效地将其直接呈现到字符数组中。这避免了在构造字符串连接的临时结果时涉及的不必要的堆分配。有关更多信息,请查看 llvm/ADT/Twine.h。
与StringRef一样,Twine对象指向外部内存,并且几乎不应该直接存储或提及。它们仅用于在定义一个应该能够有效接受连接字符串的函数时使用。
字符串格式化(formatv函数)
虽然LLVM不一定要做很多字符串操作和解析,但它确实做了很多字符串格式化。从诊断消息,到llvm工具输出(如llvm-readobj),再到打印详细的分解清单和LLDB运行时日志,字符串格式化的需求无处不在。
formatv
在本质上类似于printf
,但是使用了另一种语法,这种语法大量借鉴了Python和c#。与printf
不同,它推断要在编译时格式化的类型,因此不需要%d
之类的格式说明符。这减少了构造可移植格式字符串的脑力开销,特别是对于size_t或指针类型等特定于平台的类型。与printf
和Python不同的是,如果LLVM不知道如何格式化类型,它还不能编译。这两个属性确保函数比传统的格式化方法(如printf函数族)更安全,使用起来也更简单。
简单的格式化
formatv
调用涉及一个由0个或多个替换序列组成的格式字符串,然后是替换值的一个可变长度列表。一个替换序列是一个形式为{N[[,align]:style]}
的字符串。
N
表示替换值列表中参数的基于0的索引。注意,这意味着可以以任何顺序多次引用相同的参数,可能使用不同的样式和/或对齐选项。
align
是一个可选字符串,指定要将值格式化为的字段的宽度,以及字段内值的对齐方式。它被指定为一个可选的对齐样式,后跟一个正整数字段宽度。对齐样式可以是字符-
(左对齐)、=
(中对齐)或+
(右对齐)中的一个。默认值是右对齐的。
style
是一个可选字符串,由控制值格式的特定类型组成。例如,要将浮点值格式化为百分比,可以使用样式选项P。
自定义格式化
有两种方法可以定制一个类型的格式化行为。
使用适当的静态格式化方法为您的类型T提供
llvm::format_provider<T>
的模板专门化。namespace llvm {
template<>
struct format_provider<MyFooBar> {
static void format(const MyFooBar &V, raw_ostream &Stream, StringRef Style) {
// Do whatever is necessary to format `V` into `Stream`
}
};
void foo() {
MyFooBar X;
std::string S = formatv("{0}", X);
}
}这是一个有用的扩展机制,用于添加对使用自定义样式选项格式化自定义类型的支持。但是,当您想要扩展格式化库已经知道如何格式化的类型的机制时,它没有帮助。为此,我们需要别的东西。
提供从
llvm::FormatAdapter
继承的格式适配器namespace anything {
struct format_int_custom : public llvm::FormatAdapter<int> {
explicit format_int_custom(int N) : llvm::FormatAdapter<int>(N) {}
void format(llvm::raw_ostream &Stream, StringRef Style) override {
// Do whatever is necessary to format ``this->Item`` into ``Stream``
}
};
}
namespace llvm {
void foo() {
std::string S = formatv("{0}", anything::format_int_custom(42));
}
}如果检测到该类型派生自FormatAdapter
,formatv将对以指定样式传递的参数调用format方法。这允许提供任何类型的自定义格式,包括已经有内置格式提供程序的格式。
frtmatv示例
下面将提供一组不完整的示例,演示formatv的用法。通过阅读doxygen文档或查看单元测试套件可以找到更多信息。
std::string S; |
错误处理
正确的错误处理帮助我们识别代码中的错误,并帮助最终用户理解他们的工具使用中的错误。错误可以分为两大类:编程错误和可恢复性错误,它们具有不同的处理和报告策略。
编程错误
编程错误是对程序不变量或API契约的违反,并表示程序本身中的错误。我们的目标是记录不变量,并在不变量在运行时被破坏时在故障点快速中止(提供一些基本的诊断)。
处理编程错误的基本工具是断言和llvm_unaccessible函数。
断言用于表示不变条件,并且应该包含描述不变条件的消息:
assert(isPhysReg(R) && "All virt regs should have been allocated already."); |
llvm_unaccessible函数可用于记录控制流的区域,如果程序不变量保持:
enum { Foo, Bar, Baz } X = foo(); |
可恢复性错误
可恢复错误表示程序环境中的错误,例如资源故障(丢失的文件、丢失的网络连接等)或格式错误的输入。应该检测这些错误,并将其传达给程序的某个级别,以便对其进行适当处理。处理错误可能与向用户报告问题一样简单,也可能涉及尝试恢复。
注意
虽然在整个LLVM中使用这种错误处理方案是理想的,但是在某些地方这种方法并不实用。在绝对必须发出非编程错误且错误模型不可用的情况下,可以调用report_fatal_error,它将调用已安装的错误处理程序、打印一条消息并退出程序。
可恢复错误使用LLVM的错误模式建模。这个方案使用函数返回值表示错误,类似于经典的C整数错误代码,或者c++的std::error_code。然而,Error类实际上是用户定义错误类型的轻量级包装器,允许附加任意信息来描述错误。这类似于c++异常允许抛出用户定义类型的方式。
成功值是通过调用Error:: Success()创建的,例如:
Error foo() { |
成功值的构建和返回非常便宜——它们对程序性能的影响很小。
使用make_error构造失败值,其中T是继承自ErrorInfo实用程序的任何类,例如:
class BadFileFormat : public ErrorInfo<BadFileFormat> { |
错误值可以隐式地转换为bool: true for Error, false for success,启用以下习语:
Error mayFail(); |
对于可能失败但需要返回值的函数,可以使用预期的实用程序。这种类型的值可以用T或错误构造。预期值也可以隐式转换为布尔值,但与错误的约定相反:true表示成功,false表示错误。如果成功,可以通过取消引用操作符访问T值。如果失败,可以使用takeError()方法提取错误值。习惯用法如下:
Expected<FormattedFile> openFormattedFile(StringRef Path) { |
如果一个Expected值处于成功模式,则takeError()方法将返回一个成功值。利用这一事实,可以将上述函数改写为:
Error processFormattedFile(StringRef Path) { |
对于包含多个预期值的函数,第二种形式通常更具可读性,因为它限制了所需的缩进。
所有错误实例,无论是成功还是失败,都必须在销毁之前进行检查或从(通过std::move或return)移动(通过std::move或return)。意外丢弃未检查的错误将导致程序在未检查值的析构函数运行时中止,从而很容易识别和修复违反此规则的行为。
一旦测试成功值(通过调用布尔转换操作符),就认为它们已被检查:
if (auto Err = mayFail(...)) |
相反,下面的代码总是会导致中止,即使mayFail返回一个成功值:
mayFail(); |
一旦错误类型的处理程序被激活,就会认为检查了故障值:
handleErrors( |
handleErrors函数将一个错误作为它的第一个参数,然后是一个由“处理程序”组成的可变参数列表,其中每个处理程序必须是一个可调用类型(函数、lambda或带有调用操作符的类)和一个参数。handleErrors函数将访问序列中的每个处理程序,并根据错误的动态类型检查其参数类型,运行第一个匹配的处理程序。这与决定为c++异常运行哪个catch子句所用的决策过程相同。
由于传递给handleErrors的处理程序列表可能不能覆盖所有可能发生的错误类型,因此handleErrors函数还返回一个必须检查或传播的错误值。如果传递给handleErrors的错误值与任何处理程序不匹配,则将从handleErrors返回该值。因此,handleErrors的习惯用法如下:
if (auto Err = |
在您真正知道处理程序列表是详尽的情况下,可以使用handleAllErrors函数。这与handleErrors相同,只是如果传入未处理的错误,它将终止程序,因此可以返回void。通常应该避免handleAllErrors函数:在程序的其他地方引入新的错误类型可以很容易地将以前详尽的错误列表转换为非详尽的列表,从而冒着程序意外终止的风险。在可能的情况下,使用handleErrors并将未知的错误传播到堆栈中。
对于工具代码,可以通过打印错误消息然后使用错误代码退出来处理错误,ExitOnError实用程序可能是比handleErrors更好的选择,因为它简化了调用易出错函数时的控制流。
在已知的情况下,一个特定的调用的函数总是成功(例如,调用一个函数,它只能失败的一个子集的输入和输入,是安全的)cantFail函数可以用来删除错误类型,简化控制流。
StringError
许多类型的错误没有恢复策略,惟一可以采取的操作是将它们报告给用户,以便用户可以尝试修复环境。在本例中,将错误表示为字符串非常有意义。LLVM为此提供了StringError类。它接受两个参数:字符串错误消息和用于互操作性的等效std::error_code。它还提供了一个createStringError函数来简化这个类的常见用法:
// These two lines of code are equivalent: |
如果您确定您正在构建的错误永远不需要转换为std::error_code,那么您可以使用inconvertibleErrorCode()函数:
createStringError(inconvertibleErrorCode(), |
只有在仔细考虑之后才能这样做。如果试图将此错误转换为std::error_code,则会立即触发程序终止。除非您确定您的错误不需要互操作性,否则您应该寻找一个现有的std::error_code,您可以将其转换为它,甚至(尽管这很痛苦)考虑引入一个新的std::error_code作为权宜之计。
createStringError可以使用printf样式的格式说明符来提供格式化的消息:
createStringError(errc::executable_format_error, |
与std::error_code和ErrorOr的互操作性
许多现有的LLVM api都使用std::error_code及其合作伙伴ErrorOr(它的作用与Expected相同,但是包装的是std::error_code,而不是错误)。错误类型的传染性意味着,试图更改其中一个函数以返回Error或Expected,结果往往导致对调用者、调用者的调用者的大量更改,等等。(第一次尝试,从MachOObjectFile的构造函数返回一个错误,在diff达到3000行之后被放弃,影响了6个库,并且仍然在增长)。
为了解决这个问题,引入了Error/std::error_code互操作性需求。两对函数允许任何错误值转换为std::error_code,任何期望的转换为ErrorOr<T>,反之亦然:
std::error_code errorToErrorCode(Error Err); |
使用这些api,可以很容易地制作外科补丁,将单个函数从std::error_code更新为Error,并将ErrorOr<T>更新为Expected<T>。
从错误处理程序返回错误
错误恢复尝试本身可能会失败。因此,handleErrors实际上可以识别三种不同形式的处理程序签名:
// Error must be handled, no new errors produced: |
从处理程序返回的任何错误都将从handleErrors函数返回,以便它可以自己处理,或者向上传播堆栈。
使用ExitOnError简化工具代码
库代码不应该为可恢复错误调用exit,但是在工具代码中(尤其是命令行工具),这是一种合理的方法。遇到错误时调用exit可以极大地简化控制流,因为不再需要将错误传播到堆栈上。这允许以直线方式编写代码,只要每个容易出错的调用都封装在check中并调用退出即可。ExitOnError类支持这种模式,它提供检查错误值的调用操作符,在成功的情况下清除错误,并将日志记录到stderr,然后在失败的情况下退出。
要使用这个类,请在程序中声明一个全局ExitOnError变量:
ExitOnError ExitOnErr; |
对易出错函数的调用可以用对ExitOnErr的调用进行包装,将它们转换为非失败调用:
Error mayFail(); |
失败时,错误的日志消息将被写入stderr,前面可选地加上一个字符串“banner”,可以通过调用setBanner方法设置该字符串。还可以使用setExitCodeMapper方法从错误值映射到退出代码:
int main(int argc, char *argv[]) { |
在您的工具代码中尽可能使用ExitOnError,因为它可以极大地提高可读性。
使用cantFail可以简化安全的调用点
有些函数可能只对其输入的子集失败,因此可以假定使用已知安全输入的调用成功。
cantFail函数封装了这一点,它封装了一个断言,即它们的参数是一个成功值,并且,在预期的情况下,解包T值:
Error onlyFailsForSomeXValues(int X); |
与ExitOnError实用程序一样,cantFail简化了控制流。但是,它们对错误情况的处理非常不同:当ExitOnError保证在错误输入时终止程序时,cantFile简单地断言结果是成功的。在调试构建中,如果遇到错误,这将导致断言失败。在release构建中,cantFail的行为没有定义失败值。因此,在使用cantFail时必须非常小心:客户端必须确保cantFail包装的调用确实不能因为给定的参数而失败。
cantFail函数在库代码中应该很少使用,但是它们更可能用于工具和单元测试代码中,在这些代码中,输入和/或模拟的类或函数可能是安全的。
Fallible constructors
有些类需要资源获取或其他复杂的初始化,在构建过程中可能会失败。不幸的是,构造函数不能返回错误,而在构造完客户端测试对象以确保它们是有效的之后,让客户端测试对象很容易出错,因为很容易忘记测试。要解决这个问题,使用命名构造函数习惯用法并返回一个期望的:
class Foo { |
在这里,指定的构造函数通过引用将错误传递给实际的构造函数,然后构造函数可以使用该构造函数返回错误。ErrorAsOutParameter实用程序在进入构造函数时设置错误值的checked标志,以便将错误分配给构造函数,然后在退出时重置该标志,以强制客户机(指定的构造函数)检查错误。
通过使用这个习惯用法,试图构造Foo的客户端要么接收格式良好的Foo,要么接收错误,而不是处于无效状态的对象。
根据类型传播和消耗错误
在某些上下文中,某些类型的错误被认为是良性的。例如,在遍历归档文件时,一些客户机可能乐于跳过格式糟糕的目标文件,而不是立即终止遍历。可以使用一种精心设计的处理程序方法来跳过格式糟糕的对象,但是Error.h头提供了两个实用程序,使这个习惯用法更加简洁:类型检查方法isA和consumeError函数:
Error walkArchive(Archive A) { |
连接错误和joinErrors
在上面的归档行走示例中,BadFileFormat错误被简单地使用和忽略。如果客户想在完成归档后报告这些错误,他们可以使用joinErrors实用工具:
Error walkArchive(Archive A) { |
joinErrors例程构建一个名为ErrorList的特殊错误类型,其中包含用户定义的错误列表。handleErrors例程识别这种类型,并尝试按顺序处理每个包含的错误。如果所有包含的错误都可以处理,handleErrors将返回Error::success(),否则handleErrors将连接其余错误并返回结果ErrorList。
构建容易出错的迭代器和迭代器范围
上面的归档行走示例按索引检索归档成员,但是这需要相当多的样板文件来进行迭代和错误检查。我们可以使用“容易出错的迭代器”模式来清理这个问题,该模式支持对容易出错的容器(如存档)使用以下自然迭代习语:
Error Err; |
为了启用这种习惯用法,容易出错的容器上的迭代器是用自然的风格编写的,它们的++和——操作符被容易出错的Error inc()和Error dec()函数替换。例如:
class FallibleChildIterator { |
然后使用fallible_iterator实用程序对这种易出错迭代器接口的实例进行包装,该实用程序提供了操作符++和操作符\—\—,通过在构建时传递给包装器的引用返回任何错误。fallible_iterator包装负责(a)跳在误差范围的结束,和(b)标记错误检查每次迭代器相比,发现是不平等的(特别是:这标志着错误的全身检查基于范围for循环),使早期退出循环,没有多余的错误检查。
错误迭代器接口的实例(例如上面的错误迭代器)使用make_fallible_itr和make_fallible_end函数进行包装。例如:
class Archive { |
使用fallible_iterator实用程序允许自然构造容易出错的迭代器(使用失败的inc和dec操作)和相对自然地使用c++迭代器/循环习惯用法
有关错误及其相关实用程序的更多信息可以在Error.h头文件中找到。
传递函数和其他可调用对象
有时,您可能希望传递一个函数一个回调对象。为了支持lambda表达式和其他函数对象,你不应该使用传统的C方法来获取函数指针和一个不透明的cookie:
void takeCallback(bool (*Callback)(Function *, void *), void *Cookie); |
相反,使用以下方法之一:
函数模板
如果您不介意将函数的定义放入头文件中,请将其设置为可调用类型上的函数模板。
template<typename Callable> |
function_ref类模板
function_ref
类模板表示对可调用对象的引用,并在可调用对象的类型上进行模板化。如果在函数返回后不需要保留回调,那么这是向函数传递回调的好选择。这样,function_ref与std::函数的关系就像StringRef与std::string的关系一样。
function_ref<ret(param1, param2,…)>可以从任何可调用对象隐式构造,该对象可以使用参数类型param1、param2,…,并返回可转换为类型ret的值。
例如:
void visitBasicBlocks(Function *F, function_ref<bool (BasicBlock*)> Callback) { |
可以使用以下命令调用:
visitBasicBlocks(F, [&](BasicBlock *BB) { |
注意function_ref对象包含指向外部内存的指针,因此存储类的实例通常是不安全的(除非您知道外部存储将不会被释放)。如果需要此功能,请考虑使用std::函数。function_ref足够小,它应该总是通过值传递。
LLVM_DEBUG()宏和-debug选项
通常,在处理pass时,您会在pass中放入一组调试打印输出和其他代码。让它工作之后,您想要删除它,但是将来可能还需要它(为了解决遇到的新bug)。
当然,由于这个原因,您不希望删除调试打印输出,但也不希望它们总是有噪声。一个标准的折衷方法是将它们注释掉,以便在将来需要时启用它们。
llvm/Support/Debug.h (doxygen)文件提供了一个名为LLVM_DEBUG()的宏,它是这个问题的一个更好的解决方案。基本上,您可以将任意代码放入LLVM_DEBUG宏的参数中,并且只有在“opt”(或任何其他工具)使用“-debug”命令行参数运行时才会执行:
LLVM_DEBUG(dbgs() << "I am here!\n"); |
然后你就可以像这样运行你的pass了:
$ opt < a.bc > /dev/null -mypass |
使用LLVM_DEBUG()宏而不是自制的解决方案,允许您不必为您的传递的调试输出创建“另一个”命令行选项。注意,LLVM_DEBUG()宏对于非断言构建是禁用的,因此它们根本不会造成性能影响(出于同样的原因,它们也不应该包含副作用!)
LLVM_DEBUG()宏的另一个好处是,您可以在gdb中直接启用或禁用它。如果程序正在运行,只需从gdb中使用“set DebugFlag=0”或“set DebugFlag=1”。如果程序还没有启动,您总是可以使用-debug运行它。
带有DEBUG_TYPE和-debug-only选项的细粒度调试信息
有时,您可能会发现自己处于启用-debug只会打开太多信息的情况(例如在处理代码生成器时)。如果希望使用更细粒度的控制启用调试信息,应该定义DEBUG_TYPE宏,并使用-debug-only选项,如下所示:
#define DEBUG_TYPE "foo" |
然后你就可以像这样运行你的pass了:
$ opt < a.bc > /dev/null -mypass |
当然,在实践中,您应该只在文件的顶部设置DEBUG_TYPE,以便为整个模块指定调试类型。注意,您只在包含Debug.h之后才这样做,而不是在任何#include头文件周围。此外,您应该使用比“foo”和“bar”更有意义的名称,因为没有适当的系统来确保名称不冲突。如果两个不同的模块使用相同的字符串,那么当指定名称时,它们都将被打开。例如,这允许使用-debug-only=InstrSched启用所有用于指令调度的调试信息,即使源文件位于多个文件中。名称中不能包含逗号(,),因为逗号用于分隔-debug-only选项的参数。
由于性能原因,-debug-only在LLVM的优化构建(-enable-optimization)中不可用。
DEBUG_WITH_TYPE宏也适用于您想要设置DEBUG_TYPE的情况,但只适用于一个特定的调试语句。它接受一个附加的第一个参数,即要使用的类型。例如,上面的例子可以写成:
DEBUG_WITH_TYPE("foo", dbgs() << "'foo' debug type\n"); |