Writing LLVM Pass in 2018 — Part IV
Clang Integration — For Fun & For Profit
One of the most asked LLVM questions on stackoverflow is: I wrote a Hello World Pass, how do I run it with clang instead of opt?
One of most common solutions is to use (legacy)PassManager extension point alone with -Xclang -load -Xclang MyPass.so
command line options. However, it makes me wonder:
Can I run my Pass or custom features by simply passing one command line option to clang?
Of course this requires some changes in the LLVM source tree. But I believe it would be a good way to learn the internal of clang and how it interacts with LLVM. So here is a simple but yet entertaining tutorial for you. Let’s get started!
Goal
We’re going to enable ExtraProteinPass
, a Pass that would modify trip counts of all loops in your code, by giving a command line option, -add-extra-protein
, to clang.
The option has several variants:
-add-extra-protein=2x
. Double the trip counts of all loops.-add-extra-protein=5g
. Only add extra 5 iterations for all loops.-add-extra-protein=1lb
. Add extra 454 iterations for all loops. Since one pound = 453.59 grams.
By default, -add-extra-protein
would equal to -add-extra-protein=2x
.
Code
All of the modifications to the LLVM / Clang source tree in this tutorial can be found here:
- LLVM tree (branch ‘extra-protein’): https://github.com/mshockwave/llvm/tree/extra-protein
- Clang tree (also in branch ‘extra-protein’): https://github.com/mshockwave/clang/tree/extra-protein
The LLVM Pass
I’m not going to go through details of the Pass here. The Pass would be put in lib/Transforms/Scalar/ExtraProtein.cpp
and include/llvm/Transforms/Scalar/ExtraProtein.h
. We will use the createExtraProteinLegacyPass(uint32_t, uint32_t)
factory function in the header file to construct a new Pass instance later.
Clang Internal: Driver, Frontend and CodeGen in a Nutshell
Lightening round: Is the clang
executable a “compiler”?
Strictly speaking, the answer should be No.
A typical compiler requires lots of bootstrapping before getting into the real compilation process(i.e. lexer, parser,…). For example, finding the default / system header path. Modern “compilers”, e.g. gcc and clang, often offload this kind of trivial tasks into another separated instance, called compiler driver, or simply driver. So the executable clang
you run, is actually a driver, it would invoke the “real compiler” later after setting up all the requirements.
In folder lib/Driver/ToolChains
(relative to clang’s project root), we can see various of compiler drivers. For example, developers from the Fuchsia OS create their own driver in Fuchsia.cpp
and Fuchsia.h
, which can grab the correct header paths in the Fuchsia OS and setup default flags .etc. Strictly speaking, again, files in that folder are not just drivers, but toolchains, which also describe other parts in the compilation pipeline, like what assembler and linker it’s gonna use.
Beginning of the “real compiler” is the frontend, which is the one we know from textbooks: lexer and parser. In clang, frontend is alternatively called cc1. Sometimes some magic solutions you found online tell you to run commands like:
clang -cc1 -fsome_flag -some_option ...
Or
clang -Xclang -fsome_flag -Xclang -some_option
this is equivalent to passing the flags or options directly to the frontend.
You can also see what options did driver pass to frontend by adding -v option:
> clang++ -v -c hello.cc
...
"/path/to/clang" -cc1 -triple x86_64-apple-macosx10.13.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -emit-obj -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names ... -o hello.o -x c++ ./hello.cc
As you can see above that originally we only give options -c hello.cc
. But the driver add many additional options, shown after -cc1
, that would be passed to frontend then.
When the frontend in clang finally construct an AST(abstract syntax tree), it needs to generate corresponding LLVM IR code. This phase is called CodeGen, which might be confused with CodeGen in LLVM, where the latter one generate native code from LLVM IR.
- CodeGen in clang: AST -> LLVM IR
- CodeGen in LLVM: LLVM IR -> Native Code
Step 1. Add New Command Line Options for Driver
Clang and LLVM are not just famous for its generated code quality, but also for their great frameworks. In this case, adding a new command line option for the driver only takes you less than five lines.
Common command line options for a driver is defined in a TableGen file: include/clang/Driver/Options.td
. (If you’re not familiar with TableGen, that’s OK, as the syntax used here is so simple, that you probably figure it out by yourself within minutes) Find somewhere in the file and add the following lines:
The extra_protein_EQ
in the first line is the flag variable name. Things after the colon, i.e. Joined<…>
, Flags<…>
, HelpText<…>
, is something that describes the flag. For example, Joined<[“-”, “ — “], “add-extra-protein=”>
tells the formats when it’s used in command line. The bottom part defines the alias rules. Such that when you pass -add-extra-protein
without any value, it would still provide a default value to the extra_protein_EQ
option.
Now you can use the -add-extra-protein
option with clang
— But nothing would happen, of course. We will define its associated action later. Before that, we’re going to add new options for frontend first.
Step 2. Add New Command Line Options for Frontend
As seen earlier, driver is responsible for the bootstrapping process, it would “expand” the driver options into another bunch of options, which would then be passed to the frontend. And since driver and frontend are two different instances, basically, they have different sets of options.
Options for frontend are also defined in a TableGen file: include/clang/Driver/CC1Options.td
. Add the following lines to any place but some inner brackets(e.g. let Group = Action_Group in {…}
) in the file.
def extra_protein_amount : Joined<["-"], "extra-protein-amount=">,
HelpText<"Amount of extra protein want to add for all loops">;
Step 3. Connect Driver and Frontend
Now we’re going to transform command line options for driver to those for frontend. We’re going to modify the “clang” driver. In file lib/Driver/ToolChains/Clang.cpp
. We’ll add the following line to Clang::ConstructJob
method:
Basically we do nothing but converting “pounds” to “grams”. Then in line 15 to 17, we use the command line option for frontend to pass down our information.
Step 4. Add New CodeGen Options
We finally arrive at the last phase: CodeGen. Though CodeGen in clang is not an individual instance or executable, it has its own option set, which is put in the CodeGenOptions
class in include/clang/Frontend/CodeGenOptions.h
. We’re going to add a simple member field for it:
struct ProteinAmount {
uint32_t Duplicate;
uint32_t Amend;
ProteinAmount() : Duplicate(0U), Amend(0U) {}
inline bool empty() const {
return !Duplicate && !Amend;
}
};
ProteinAmount ExtraProteinAmount;
The Duplicate
field stores the 2x
, 3x
kinds protein amount, and Amend field stores those using “grams” as protein unit.
Next, we’re going to configure the ExtraProteinAmount
CodeGen option with command line options passed from frontend. We’re going to modify the ParseCodeGenArgs
function, which is used to populate most of the CodeGen options, in lib/Frontend/CompilerInvocation.cpp
. Put the following code in any place in the function.
This is the place we finally transform textual representation of our protein amount into in-memory values.
Step 5(Final). Add LLVM Pass
At the final step, we’re going to add our ExtraProteinPass
to the Pass pipeline that would run by clang CodeGen. The thing we’ll touch is put in lib/CodeGen/BackendUtil.cpp
. EmitAssemblyHelper::CreatePasses
method, as its name suggested, create LLVM passes that would be run after CodeGen. We’ll put our code that adds ExtraProteinPass
here.
The code itself is pretty simple, and this time we add the code at the very last line of EmitAssemblyHelper::CreatePasses
, since we need two optimization Passes to run ahead: SROA and Mem2Reg. These two can give us a more concise code shape for our ExtraProteinPass
to process.
However, by default clang runs in optimization level zero(i.e. -O0
), if no extra optimization flag is given. And SROA and Mem2Reg won’t be add to the Pass pipeline in -O0
. If we want to use only one clang command line option to enable our feature, we need to add SROA and Mem2Reg to the Pass pipeline even in -O0
:
In addition, in -O0
, clang would add an attribute, optnone
, to all functions. The attribute would prevent any optimization Passes from running on the attached functions. Thus, we also need to tell clang not to add this attribute if there is any “extra protein”. The function we’re going to touch is CodeGenModule::SetLLVMFunctionAttributesForDefinition
in lib/CodeGen/CodeGenModule.cpp
. Modify the lines related to the ShouldAddOptNone
variable, which is used to control the optnone
generating process, by adding a new guard statement:
...
ShouldAddOptNone &= !D->hasAttr<AlwaysInlineAttr>();
ShouldAddOptNone &= CodeGenOpts.ExtraProteinAmount.empty();
...
This tutorial provides an entertaining but yet thorough way to see the internal structure of Clang. As you can see, codes in this article are not hard, most of them are self-explaining. Hope this can attract your curiosity toward the amazing world of Clang / LLVM :-)