Link a pre-compiled bitcodes with my jit code, enabling IPO (original) (raw)

August 14, 2025, 9:42am 1

Hi everyone,

I’m implementing a JIT engine using LLVM’s ORCv2 framework. Currently, my engine generates LLVM IR from user-supplied scripts, as shown in this simplified example:

declare ptr @CreateWrappedValueFromInt(i32) local_unnamed_addr

define noundef i32 @f_() local_unnamed_addr #0 {
  ret i32 23333
}

define ptr @f_WRAPPED(ptr nocapture readnone %0, ptr nocapture readnone %1) local_unnamed_addr {
  %3 = tail call ptr @CreateWrappedValueFromInt(i32 23333)
  ret ptr %3
}

Here, f_ is the user’s translated function, while f_WRAPPED is a generated wrapper that adapts the result for the interpreter (e.g., passing f_’s output to CreateWrappedValueFromInt). The interpreter invokes f_WRAPPED directly.

I need Inter-Procedural Optimization (IPO) like inlining for helper functions (e.g., CreateWrappedValueFromInt). These functions are implemented in C++ and rely heavily on host-program definitions, making IRBuilder-based generation impractical for maintenance.

Current Approach

I precompile these helper functions via clang -emit-llvm, then merge them into a single bitcode file using llvm-link. At runtime:

  1. Load this bitcode into an llvm::Module.
  2. Link it with my module created from AST.
  3. Apply optimizations (Just use -O3)
  4. Add the final module to ExecutionSession, lookup symbol (e.g. f_WRAPPED)

Problem: Module Linking

I’m struggling to link modules correctly without triggering MaterializationResponsibility assertions/errors. What I’ve tried:

  1. llvm::Linker::linkModules:
    • If used in IRTransformLayer, the final module contains extra definitions conflicting with MaterializationResponsibility.
    • If linked before added to ExecutionSession, some symbols vanish after optimizations, again breaking MaterializationResponsibility.
    • Question: Can/should MaterializationResponsibility be modified to resolve this?(Or add an extra layer to create the MaterializationResponsibility manually)
    • After I pass a InternalizeCallback to linkModules(), the final module works. Is it a correct way? My bitcodes contains some global object used in helper functions, it means that should I only import function definitions from the bitcode module?
    • I discovered llvm::FunctionImporter in llvm source. But it was used in llvm LTO only. Could I use it standalone?
  2. llvm::LTO:
    • This appears tailored for static linking scenarios. Is it viable (or overkill) for JIT?

Additional Concern

My bitcode may contain global initializers. Does this imply the bitcode module must also be jit-ed and managed by ORC? (Contains the global variables, other module will import function definitions only)

I’d greatly appreciate insights into best practices for linking external bitcode with ORCv2 JIT modules while preserving IPO and avoiding materialization issues.

Thanks in advance!

j20w August 14, 2025, 3:58pm 2

I found some posts similar to this:

Almost did the same thing as I did!
However, this post did not mention the issue of global objects. If the bitcode module has been link to multiple modules and all things have been internalized, the global object may exists multiple instances?

This reminded me of these functions! I will try to use them. I found that LTO also uses several of the functions mentioned here to create pipelines.