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 – Represents llvm/include/llvm-c/Context.h. Holds uniqued objects like types and constants, isolating independent compilations.
  • LLVMModuleRef – Defined in llvm/include/llvm-c/Core.h. Acts as the top-level container for functions, globals, and metadata, analogous to a translation unit.
  • LLVMTypeRef and LLVMValueRef – Both declared in llvm/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 like LLVMBuildAdd and LLVMBuildRet for appending operations to basic blocks.
  • LLVMExecutionEngineRef – Found in llvm/include/llvm-c/ExecutionEngine.h. Enables JIT compilation and runtime function invocation.
  • Target Configuration – llvm/include/llvm-c/Target.h exposes LLVMTargetRef and initialization routines like LLVMInitializeNativeTarget.

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:

  1. Initialize the Native Target – Call LLVMInitializeNativeTarget() and LLVMInitializeNativeAsmPrinter() (from llvm/include/llvm-c/Target.h) to register the host CPU for JIT compilation.

  2. Create a Context – Invoke LLVMContextCreate() to generate an LLVMContextRef. All subsequent type and value creation must reference this context.

  3. Allocate a Module – Use LLVMModuleCreateWithNameInContext() to produce an LLVMModuleRef, specifying the context from step two.

  4. Construct Types and Functions – Build function signatures using LLVMInt32TypeInContext() and LLVMFunctionType(), then insert functions into the module with LLVMAddFunction().

  5. Generate IR – Append basic blocks with LLVMAppendBasicBlock(), position an LLVMBuilderRef using LLVMCreateBuilderInContext() and LLVMPositionBuilderAtEnd(), then emit instructions via builder functions.

  6. Verify the Module – Pass the module to LLVMVerifyModule() (declared in llvm/include/llvm-c/Analysis.h) with LLVMAbortProcessAction to catch malformed IR before execution.

  7. Instantiate the Execution Engine – Create a JIT compiler using LLVMCreateJITCompilerForModule(), which produces an LLVMExecutionEngineRef capable of emitting machine code.

  8. Execute and Cleanup – Run functions with LLVMRunFunction(), passing LLVMGenericValueRef arguments created via LLVMCreateGenericValueOfInt(). Release resources using LLVMDisposeExecutionEngine(), LLVMDisposeModule(), and LLVMContextDispose().

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:

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* or LLVM*Create* call requires a corresponding LLVMDispose* call to prevent leaks.
  • Key headers reside in llvm/include/llvm-c/, with Core.h, IRBuilder.h, and ExecutionEngine.h providing 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →