Blog 5 min read

A quick overview of how Clang works internally

Share this article
A quick overview of how Clang works internally

Clang has proven to be a mature compiler for C and C++, like GCC and Microsoft’s compilers. What makes it special, however, is that it is not just a compiler: it is also an infrastructure for building tools. Thanks to its library-based architecture, components can be reused more easily, and new features can be integrated more flexibly into other projects.

Clang Design

Like many other compiler designs, the Clang compiler has three phases:

  • The front end parses source code, checks it for errors, and builds a language-specific Abstract Syntax Tree (AST) to represent the input code.
  • The optimizer performs optimizations on the representation generated by the front end.
  • The back end generates the final code to be executed by the machine; it depends on the target.

What’s the difference between Clang and other compilers?

The most important difference in its design is that Clang is based on LLVM; the idea behind LLVM is to use the LLVM Intermediate Representation (IR), which is like the bytecode for Java. LLVM IR is designed to host mid-level analyses and transformations that you find in the optimizer section of a compiler. It was designed with many specific goals in mind, including supporting lightweight runtime optimizations, cross-function/interprocedural optimizations, whole program analysis, and aggressive restructuring transformations, etc. The most important aspect of it, though, is that it is itself defined as a first class language with well-defined semantics.

With this design, we can reuse a large part of the compiler to create other compilers. For example, we can change only the front end to process other languages.

I- Front End

Clang is designed to be modular, and each compilation phase is done by a specific module. Here are some of the projects involved in the front-end phase:

As with any front-end parser, we need a lexer and a semantic analysis. The Clang front end can be executed by passing the -cc1 argument. It supports several features, including AST generation:

clang -cc1 -ast-dump test.c

This command line is handled by the cc1_main function; here is the sequence of some of the interesting methods that are executed:

clang11

The ExecuteAction method has a parameter of type FrontendAction; its purpose is to specify which front-end action to execute. FrontendAction is abstract, so we need to inherit from it to implement a concrete front-end action.

Let’s explore all the front-end actions implemented by Clang using CQLinq; for that, we can search for all classes inheriting directly or indirectly from it.

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0  >= 0 orderby depth0
select new { t, depth0 }

Many front-end actions are available; for example, ASTDumpAction makes it possible to generate the AST without creating the final executable. Almost all the front-end actions inherit from ASTFrontendAction, which means that they work with the generated AST.

What’s interesting about this design is that we can easily plug in our own FrontendAction; we simply have to implement a new one.

How can we perform some processing on the AST?

Each ASTFrontendAction creates one or more ASTConsumers; the ASTConsumer class is abstract, and we have to implement our own AST consumer to meet our specific needs.

The FrontendAction will invoke the AST consumer as shown by the following graph.

Let’s search for all ASTConsumer classes using CQLinq:

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0  == 1
select new { t, depth0 }

CodeGenerator is an example of an AST consumer

As mentioned earlier, one of LLVM’s strengths is its use of IR, and generating it requires processing the AST. CodeGenerator is the class derived from ASTConsumer that is responsible for generating the IR. Interestingly, this processing is isolated in another project called ClangCodeGen.

Here are some classes involved in the LLVM IR generation:

II- Optimizer

To explain this phase, I can’t say it better than Chris Lattner, the father of LLVM, in this post:

“To give some intuition for how optimizations work, it is useful to walk through some examples. There are lots of different kinds of compiler optimizations, so it is hard to provide a recipe for how to solve an arbitrary problem. That said, most optimizations follow a simple three-part structure:

  • Look for a pattern to be transformed.
  • Verify that the transformation is safe/correct for the matched instance.
  • Do the transformation, updating the code.
The optimizer reads LLVM IR in, chews on it a bit, then emits LLVM IR, which hopefully will execute faster. In LLVM (as in many other compilers) the optimizer is organized as a pipeline of distinct optimization passes each of which is run on the input and has a chance to do something. Common examples of passes are the inliner (which substitutes the body of a function into call sites), expression reassociation, loop invariant code motion, etc. Depending on the optimization level, different passes are run: for example at -O0 (no optimization) the Clang compiler runs no passes, at -O3 it runs a series of 67 passes in its optimizer (as of LLVM 2.8).

Let’s explore the LLVMCore passes by searching for classes that inherit from the “Pass” class.

from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0  >= 0 orderby depth0
select new { t, depth0 }

Of course, many other passes exist in other LLVM modules.

III- BackEnd

Like the other phases, the back end is responsible for generating output for a specific target. In Clang, it is highly modular. Take LLVMX86Target, the module that generates code for the x86 target, as an example.

Here is a graph showing all the modules involved in generating binaries for the x86 target.

Many modules are involved in this phase, each with a specific responsibility. This promotes cohesion, clean APIs, and separation of concerns, making the system easier for developers to understand because they can focus on smaller pieces of the overall architecture.

Conclusion

The LLVM/Clang duo is not just a C/C++ compiler; it is also an infrastructure for building tools, and its behavior is easy to extend. Many tools are included out of the box in the LLVM/Clang source code, and many others can be found on the web.

If you need a C/C++ parser to build a tool, Clang is a very good candidate.

Share this article