Skip to content

About

Clang compiler infrastructure for Julia

Resources

Contributing

Stars

24 stars

Watchers

2 watching

Forks

Repository files navigation

ClangCompiler

CI TagBot codecov

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.

Installation

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

Examples

Decl Lookup

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
true

AbstractDecl, 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.

AST Traversal

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.

Execution

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)

Batch Compilation

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.

More

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

About

Clang compiler infrastructure for Julia

Resources

Contributing

Stars

24 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages