ClangCompiler.jl provides a Julia interface to the Clang C++ API, and can be used for tasks such as declaration lookup, parsing, code analysis, etc.
pkg> add ClangCompiler
Each release series works with exactly one Julia minor version, because the package binds the Clang that matches the LLVM that Julia is built on:
| ClangCompiler | Julia | LLVM |
|---|---|---|
| 0.2 | 1.13 | 20 |
| 0.1 | 1.12 | 18 |
Look a C++ declaration up by its qualified name:
import ClangCompiler as CC
# Create an interpreter
I = CC.create_interpreter(["-include", "vector"])
# Run a real C++ name lookup. The result is resolved to the class clang built
# for it, so `isa` tests on it mean what they say.
decl = CC.find_decl(I, "std::vector")
CC.qualified_name(decl) # "std::vector"
decl isa CC.AbstractNamedDecl # true
# Clean up resources
CC.dispose(I)find_decl returns nothing when the name is not found, and find_decls returns the whole
overload set.
Dispatch on the abstract, not the carrier. Carriers are leaves, mirroring clang's classes
one-for-one, so d isa CC.CXXMethodDecl is an exact-class test that a constructor fails even
though clang's CXXConstructorDecl derives from CXXMethodDecl. The abstract supertypes are
the spelling of C++'s isa<T>, and they are what this package promises across an LLVM upgrade:
julia> CC.find_decl(I, "app::Widget") isa CC.AbstractRecordDecl
trueAbstractDecl, AbstractNamedDecl, AbstractFunctionDecl, AbstractTagDecl,
AbstractVarDecl and AbstractRecordDecl are public for this reason. The concrete carriers
are not: they are named after clang's AST classes, and those get renamed and deleted between
LLVM releases.
Walking what a declaration contains:
import ClangCompiler as CC
I = CC.create_interpreter()
CC.parse(I, "namespace app { struct Widget { int w; int area() const; }; }")
# `definition` first: a lookup can land on a forward declaration, which has no members
w = CC.definition(CC.find_decl(I, "app::Widget"))
for m in CC.members(w)
println(CC.decl_name(m), " ", m isa CC.AbstractFunctionDecl ? "(function)" : "")
end
CC.dispose(I)Everything below src/clang/ — dump, resolve, children, getTemplatedDecl,
DeclIterator and the rest of the wrapper layer — is reachable as CC.name and is what the
package is built on, but it carries no compatibility promise; see
the note in src/ClangCompiler.jl for why.
The following example demonstrates how to compile and invoke function:
julia> import ClangCompiler as CC
julia> I = CC.create_interpreter(["-I", normpath(joinpath(Sys.BINDIR, "..", "include", "julia"))])
ClangCompiler.CxxInterpreter(ClangCompiler.Interpreter(Ptr{ClangCompiler.LibClangEx.CXInterpreterImpl}(0x000000012ce70500)))
julia> CC.compile(I,
"""
#include <julia.h>
#include <iostream>
#include <vector>
extern "C" void copy_from_julia_array(jl_array_t* julia_array) {
size_t len = jl_array_len(julia_array);
double* data = jl_array_data(julia_array, double);
for (size_t i = 0; i < len; ++i) {
data[i] *= 2.0;
}
std::vector<double> cpp_vec(data, data + len);
for (double val : cpp_vec) {
std::cout << val << " ";
}
std::cout << std::endl;
}
""")
julia> p = CC.get_function_pointer(I, "copy_from_julia_array")
Ptr{Nothing}(0x0000000123ffc000)
julia> v = [1.0, 2.0, 3.0, 4.0, 5.0]
5-element Vector{Float64}:
1.0
2.0
3.0
4.0
5.0
julia> @ccall $p(v::Any)::Cvoid
2 4 6 8 10
julia> v
5-element Vector{Float64}:
2.0
4.0
6.0
8.0
10.0
julia> CC.dispose(I)
create_interpreter compiles increment by increment and hands each one straight to its own
JIT. create_compiler compiles a whole translation unit in one go, which puts the entire
module in your hands before anything runs it:
julia> import ClangCompiler as CC
julia> cc = CC.create_compiler("""
extern "C" int fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
"""; args=["-O2"])
julia> mod = CC.take_module(cc) # the whole unit as one LLVM module: inspect it, transform it
julia> CC.compile(cc, mod) # ... and hand it back
julia> p = CC.get_function_pointer(cc, "fib")
Ptr{Nothing}(0x000000010f5b4000)
julia> @ccall $p(20::Cint)::Cint
6765
julia> CC.dispose(cc)CC.create_irgenerator is the same frontend without the JIT, for when the IR itself is the
product. Neither leaves an AST behind — the frontend has finished by the time the call
returns — so use create_interpreter or create_parser for anything that traverses one.
examples/ has seven worked programs that run — a JIT'd C++ function called from
Julia, an AST tour, record layout, template instantiation, cross-target ABI inspection, the
type-safety guarantees the handle layer provides, and JIT'd C++ calling back into the hosting
Julia session. They are executed by CI, so they cannot drift away from the API:
julia --project examples/runall.jl