I recently updated the JIT back-end of my VHDL simulator to use LLVM’s new ORC API which was added in version 3.9. It has a couple of advantages, the two important ones for me were re-introduction of lazy-JIT-ing of functions, and that it works on Windows. Both features that were lost moving from the legacy JIT API to the newer MCJIT one. ORC is actually built as a layer on top of MCJIT though.

Documentation seems to be pretty scarce. There’s some example code but it all uses the C++ API so I thought it might be useful to write some notes on how I use it with the C API. You simply need to include this header and then either link against the LLVM shared library or llvm-config --libs orcjit.

#include <llvm-c/OrcBindings.h>

Initialise the LLVM libraries and MCJIT back-end which ORC is built on:

LLVMInitializeNativeTarget();
LLVMInitializeNativeAsmPrinter();
LLVMLinkInMCJIT();

Let’s assume you already have an LLVM bitcode module from somewhere else:

LLVMModuleRef module = ...;

Unlike the MCJIT API and the original LLVM JIT API you need a “target machine” reference to create the ORC object. This is probably the only non-obvious part but a bit of searching in the other headers finds some functions to do it:

char *def_triple = LLVMGetDefaultTargetTriple();   // E.g. "x86_64-linux-gnu"
char *error;
LLVMTargetRef target_ref;
if (LLVMGetTargetFromTriple(def_triple, &target_ref, &error)) {
   // Fatal error
}
 
if (!LLVMTargetHasJIT(target_ref)) {
   // Fatal error, cannot do JIT on this platform
}
 
LLVMTargetMachineRef tm_ref =
   LLVMCreateTargetMachine(target_ref, def_triple, "", "",
                           LLVMCodeGenLevelDefault,
                           LLVMRelocDefault,
                           LLVMCodeModelJITDefault);
assert(tm_ref);
LLVMDisposeMessage(def_triple);

The two empty string arguments to LLVMCreateTargetMachine are CPU and Features. I can’t work out what these are used for and everything works fine if you pass an empty string. On LLVM 4.0 you can pass NULL here but this crashes on 3.9.

I haven’t experimented with anything other the default relocation model, which seems to work everywhere I tried it, or the optimisation level.

Now we can actually create the ORC object:

LLVMOrcJITStackRef orc_ref = LLVMOrcCreateInstance(tm_ref);
LLVMOrcAddLazilyCompiledIR(orc_ref, module, orc_sym_resolver, NULL);

The only interesting argument is orc_sym_resolver. This is a pointer to callback ORC will use when it needs you to resolve a symbol.

static uint64_t orc_sym_resolver(const char *name, void *ctx)
{
   return (uint64_t)(uintptr_t)LLVMOrcGetSymbolAddress(orc_ref, name);
}

The function LLVMOrcGetSymbolAddress seems to do exactly what we want, but you could do some custom symbol lookup if required.

You can also use this function to trigger the lazy compilation and get a function pointer to the result. For example:

int (*main_fn)(void) = LLVMOrcGetSymbolAddress(orc_ref, "main");
int result = (*main_fn)();