How to Use LLVM's C API (LLVM-C) to Build Custom Tools: A Complete Guide
LLVM's C API (LLVM-C) provides stable, opaque C bindings to LLVM's C++ core, enabling you to generate IR, create JIT compilers, and execute code using functions like LLVMContextCreate, LLVMBuildAdd, and LLVMCreateJITCompilerForModule.
LLVM's C API (LLVM-C) is a thin, binary-compatible wrapper around the core LLVM C++ libraries, residing in the llvm/llvm-project repository. It exposes compiler infrastructure—including contexts, modules, type systems, and execution engines—through plain C functions and opaque handles. This design allows developers to build custom LLVM-based tools in C, Rust, Python (via ctypes), or any language supporting C FFI, without linking against the C++ standard library.
Core Architectural Components
The LLVM-C interface centers on opaque reference types that isolate LLVM objects across API boundaries. Each component maps to a specific header in llvm/include/llvm-c/:
LLVMContextRef– Representsllvm/include/llvm-c/Context.h. Holds uniqued objects like types and constants, isolating independent compilations.LLVMModuleRef– Defined inllvm/include/llvm-c/Core.h. Acts as the top-level container for functions, globals, and metadata, analogous to a translation unit.LLVMTypeRefandLLVMValueRef– Both declared inllvm/include/llvm-c/Core.h. Describe value types (integers, floats, structs) and represent instructions, arguments, and constants respectively.- IR Builder Functions – Declared in
llvm/include/llvm-c/IRBuilder.h. Provide instruction constructors likeLLVMBuildAddandLLVMBuildRetfor appending operations to basic blocks. LLVMExecutionEngineRef– Found inllvm/include/llvm-c/ExecutionEngine.h. Enables JIT compilation and runtime function invocation.- Target Configuration –
llvm/include/llvm-c/Target.hexposesLLVMTargetRefand initialization routines likeLLVMInitializeNativeTarget.
All handles are opaque pointers, guaranteeing binary compatibility across LLVM releases when linking against libLLVM-C.
Step-by-Step Implementation Workflow
Building a custom tool with LLVM-C follows a predictable lifecycle. Each stage utilizes specific functions from the source headers:
-
Initialize the Native Target – Call
LLVMInitializeNativeTarget()andLLVMInitializeNativeAsmPrinter()(fromllvm/include/llvm-c/Target.h) to register the host CPU for JIT compilation. -
Create a Context – Invoke
LLVMContextCreate()to generate anLLVMContextRef. All subsequent type and value creation must reference this context. -
Allocate a Module – Use
LLVMModuleCreateWithNameInContext()to produce anLLVMModuleRef, specifying the context from step two. -
Construct Types and Functions – Build function signatures using
LLVMInt32TypeInContext()andLLVMFunctionType(), then insert functions into the module withLLVMAddFunction(). -
Generate IR – Append basic blocks with
LLVMAppendBasicBlock(), position anLLVMBuilderRefusingLLVMCreateBuilderInContext()andLLVMPositionBuilderAtEnd(), then emit instructions via builder functions. -
Verify the Module – Pass the module to
LLVMVerifyModule()(declared inllvm/include/llvm-c/Analysis.h) withLLVMAbortProcessActionto catch malformed IR before execution. -
Instantiate the Execution Engine – Create a JIT compiler using
LLVMCreateJITCompilerForModule(), which produces anLLVMExecutionEngineRefcapable of emitting machine code. -
Execute and Cleanup – Run functions with
LLVMRunFunction(), passingLLVMGenericValueRefarguments created viaLLVMCreateGenericValueOfInt(). Release resources usingLLVMDisposeExecutionEngine(),LLVMDisposeModule(), andLLVMContextDispose().
Complete Working Example: JIT-Compiling an Add Function
The following self-contained example creates a module containing add(i32, i32) -> i32, JIT-compiles it, and invokes the function from C. It demonstrates the essential API calls required for custom tool development.
/* compile: gcc -g -O2 example.c `llvm-config --cflags --libs --system-libs` -lLLVM-C -o example */
#include <stdio.h>
#include <stdlib.h>
#include "llvm-c/Core.h"
#include "llvm-c/ExecutionEngine.h"
#include "llvm-c/Target.h"
#include "llvm-c/Analysis.h"
int main(void) {
/* 1. Initialise the native target (required for JIT). */
LLVMInitializeNativeTarget();
LLVMInitializeNativeAsmPrinter();
/* 2. Context & Module. */
LLVMContextRef ctx = LLVMContextCreate();
LLVMModuleRef mod = LLVMModuleCreateWithNameInContext("my_module", ctx);
/* 3. Types. */
LLVMTypeRef i32 = LLVMInt32TypeInContext(ctx);
LLVMTypeRef param_types[] = { i32, i32 };
LLVMTypeRef fn_type = LLVMFunctionType(i32, param_types, 2, 0);
/* 4. Function and basic block. */
LLVMValueRef add_fn = LLVMAddFunction(mod, "add", fn_type);
LLVMSetFunctionCallConv(add_fn, LLVMCCallConv); /* optional */
LLVMBasicBlockRef entry = LLVMAppendBasicBlock(add_fn, "entry");
LLVMBuilderRef builder = LLVMCreateBuilderInContext(ctx);
LLVMPositionBuilderAtEnd(builder, entry);
/* 5. Build body: %a = param0, %b = param1, %sum = add %a, %b, ret %sum */
LLVMValueRef a = LLVMGetParam(add_fn, 0);
LLVMValueRef b = LLVMGetParam(add_fn, 1);
LLVMValueRef sum = LLVMBuildAdd(builder, a, b, "sum");
LLVMBuildRet(builder, sum);
/* 6. Verify & optionally dump. */
char *error = NULL;
if (LLVMVerifyModule(mod, LLVMAbortProcessAction, &error)) {
fprintf(stderr, "Module verification failed: %s\n", error);
LLVMDisposeMessage(error);
exit(1);
}
// LLVMDumpModule(mod); /* useful for debugging */
/* 7. Create JIT execution engine. */
LLVMExecutionEngineRef engine;
if (LLVMCreateJITCompilerForModule(&engine, mod, 0, &error)) {
fprintf(stderr, "Failed to create JIT: %s\n", error);
LLVMDisposeMessage(error);
exit(1);
}
/* 8. Run the compiled function. */
LLVMGenericValueRef args[2];
args[0] = LLVMCreateGenericValueOfInt(i32, 40, 0);
args[1] = LLVMCreateGenericValueOfInt(i32, 2, 0);
LLVMGenericValueRef ret = LLVMRunFunction(engine, add_fn, 2, args);
unsigned long long result = LLVMGenericValueToInt(ret, 0);
printf("add(40, 2) = %llu\n", result); /* prints 42 */
/* 9. Clean‑up. */
LLVMDisposeGenericValue(args[0]);
LLVMDisposeGenericValue(args[1]);
LLVMDisposeGenericValue(ret);
LLVMDisposeBuilder(builder);
LLVMDisposeExecutionEngine(engine);
LLVMContextDispose(ctx);
return 0;
}
This example links against libLLVM-C using llvm-config to resolve flags. The LLVMBuildAdd and LLVMBuildRet functions, though used here, are formally declared in llvm/include/llvm-c/IRBuilder.h, which you should include for complex IR generation.
Essential Header Files for Custom Tools
When developing with LLVM-C, include these headers based on functionality:
llvm/include/llvm-c/Core.h– Context creation, module management, type construction, and value manipulation.llvm/include/llvm-c/IRBuilder.h– Instruction building utilities likeLLVMBuildAddandLLVMBuildRet.llvm/include/llvm-c/ExecutionEngine.h– JIT compiler creation (LLVMCreateJITCompilerForModule) and function invocation (LLVMRunFunction).llvm/include/llvm-c/Target.h– Target initialization (LLVMInitializeNativeTarget) and target data layout.llvm/include/llvm-c/TargetMachine.h– Fine-grained code-generation configuration for MCJIT.llvm/include/llvm-c/Analysis.h– Module verification viaLLVMVerifyModule.llvm/include/llvm-c/Types.h– Opaque type definitions (LLVMContextRef,LLVMModuleRef,LLVMValueRef).
Summary
- LLVM's C API provides stable, opaque handles (
LLVMContextRef,LLVMModuleRef) that insulate your tool from C++ ABI changes. - The standard workflow requires initializing targets, creating a context and module, building IR with type constructors and builder functions, then instantiating an execution engine.
- Memory management is explicit: every
LLVMCreate*orLLVM*Create*call requires a correspondingLLVMDispose*call to prevent leaks. - Key headers reside in
llvm/include/llvm-c/, withCore.h,IRBuilder.h, andExecutionEngine.hproviding the majority of functionality for custom JIT tools.
Frequently Asked Questions
How do I initialize LLVM for JIT compilation using the C API?
You must call LLVMInitializeNativeTarget() and LLVMInitializeNativeAsmPrinter() from llvm/include/llvm-c/Target.h before creating a JIT execution engine. These functions register the host architecture with the LLVM backend, enabling LLVMCreateJITCompilerForModule() to generate machine code for the local CPU.
What is the difference between LLVMCreateJITCompilerForModule and LLVMCreateInterpreterForModule?
LLVMCreateJITCompilerForModule (declared in llvm/include/llvm-c/ExecutionEngine.h) compiles LLVM IR to native machine code for fast execution, while LLVMCreateInterpreterForModule executes IR instruction-by-instruction without compilation, sacrificing performance for debugging flexibility. Most custom tools use the JIT compiler for production workloads.
How do I prevent memory leaks when using LLVM-C opaque handles?
Every creation function requires explicit disposal: call LLVMDisposeExecutionEngine() for engines, LLVMDisposeModule() for modules, LLVMDisposeBuilder() for builders, and LLVMContextDispose() for contexts. Additionally, LLVMDisposeMessage() frees error strings returned by functions like LLVMVerifyModule.
Can LLVM-C generate object files for targets other than the host?
Yes. While the example uses LLVMInitializeNativeTarget() for convenience, you can initialize specific targets using LLVMInitializeX86Target(), LLVMInitializeARMTarget(), or similar architecture-specific functions from llvm/include/llvm-c/Target.h. Combined with LLVMTargetMachineRef from llvm/include/llvm-c/TargetMachine.h, you can configure cross-compilation settings, CPUs, and feature sets before emitting object code via the LLVMTargetMachineEmitToFile interface.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →