0

Edit

I'm basically trying to do this but with llvm's orc Jit api (llvm-13)


I have a library that JIT's some code using llvm (13). I have some functions in that library that I want to make available to the JIT without writing them in LLVM IR.

Here's some code:


#include "llvm/Analysis/AliasAnalysis.h"
#include "llvm/ExecutionEngine/JITSymbol.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/IRTransformLayer.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/Mangling.h"
#include "llvm/ExecutionEngine/Orc/LLJIT.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/ExecutionEngine/Orc/ExecutorProcessControl.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/Support/TargetSelect.h"


using namespace llvm;
using namespace llvm::orc;

// this is just a demo module that creates a function that adds 1 to an int
ThreadSafeModule makeSimpleModule() {
    auto Context = std::make_unique<LLVMContext>();
    auto M = std::make_unique<Module>("test", *Context);

    // Create the add1 function entry and insert this entry into module M.  The
    // function will have a return type of "int" and take an argument of "int".
    Function *Add1F =
            Function::Create(FunctionType::get(Type::getInt32Ty(*Context),
                                               {Type::getInt32Ty(*Context)}, false),
                             Function::ExternalLinkage, "add1", M.get());

    // Add a basic block to the function. As before, it automatically inserts
    // because of the last argument.
    BasicBlock *BB = BasicBlock::Create(*Context, "EntryBlock", Add1F);

    // Create a basic block builder with default parameters.  The builder will
    // automatically append instructions to the basic block `BB'.
    IRBuilder<> builder(BB);

    // Get pointers to the constant `1'.
    Value *One = builder.getInt32(1);

    // Get pointers to the integer argument of the add1 function...
    assert(Add1F->arg_begin() != Add1F->arg_end()); // Make sure there's an arg
    Argument *ArgX = &*Add1F->arg_begin();          // Get the arg
    ArgX->setName("AnArg"); // Give it a nice symbolic name for fun.

    // Create the add instruction, inserting it into the end of BB.
    Value *Add = builder.CreateAdd(One, ArgX);

    // Create the return instruction and add it to the basic block
    builder.CreateRet(Add);

    return {std::move(M), std::move(Context)};
}

// this represents a function in my library that I want to make available to the JIT. 
namespace mylibsubnamespace {
extern "C" {

    int add2(int a) {
        return a + 2;
    }
}
}

int main(int argc, const char *argv[]) {

    // do some JIT initialization 
    llvm::InitLLVM X(argc, argv);
    llvm::InitializeNativeTarget();
    llvm::InitializeNativeTargetAsmPrinter();
    llvm::InitializeNativeTargetAsmParser();

    // Create an LLJIT instance.
    auto J = LLJITBuilder().create();

    // this code seems to enable symbol resolution for when the missing symbol is 
    // in the standard C library (and presumably included). 
    // This is what allows the "cos" function below to work (comment it out and we get a seg fault)
    auto DLSG = llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess(
            (*J)->getDataLayout().getGlobalPrefix());
    if (!DLSG) {
        llvm::logAllUnhandledErrors(
                std::move(DLSG.takeError()),
                llvm::errs(),
                "DynamicLibrarySearchGenerator not built successfully"
        );
    }
    (*J)->getMainJITDylib().addGenerator(std::move(*DLSG));


    auto M = makeSimpleModule();

    (*J)->addIRModule(std::move(M));

    // Look up the JIT'd function, cast it to a function pointer, then call it.
    // This function is written in LLVM IR directly. 
    auto Add1Sym = (*J)->lookup("add1");
    int (*Add1)(int) = (int (*)(int)) Add1Sym->getAddress();

    int Result = Add1(42);
    outs() << "add1(42) = " << Result << "\n";

    // Look up the JIT'd function, cast it to a function pointer, then call it.
    // This function is defined in the standard C library. Its symbol is resolved 
    // by DynamicLibrarySearchGenerator above
    auto CosSym = (*J)->lookup("cos");
    double (*Cos)(double) = (double (*)(double)) CosSym->getAddress();

    outs() << "Cos(50) = " << Cos(50) << "\n";

So far so good. What I haven't been able to work out is how to make the add2 function available in a cachable way. I have successfully been able to follow the instructions here to enable hardcoding of the address in the current session, like so:

    auto symbolStringPool = (*J)->getExecutionSession().getExecutorProcessControl().getSymbolStringPool();
    orc::SymbolStringPtr symbPtr = symbolStringPool->intern("add2");

    // JITTargetAddress is uint64 typedefd
    llvm::JITSymbolFlags flg;
    llvm::JITEvaluatedSymbol symb((std::int64_t) &mylibsubnamespace::add2, flg);
    if (llvm::Error err = (*J)->getMainJITDylib().define(
            llvm::orc::absoluteSymbols({{symbPtr, symb}}))) {
        llvm::logAllUnhandledErrors(std::move(err), llvm::errs(), "Could not add symbol add2");
    }

But the instructions explicitly advice against this strategy, since symbols resolved this way are not cachable. However, resolving the symbol with something like how the instructions suggest:

JD.addGenerator(DynamicLibrarySearchGenerator::Load("/path/to/lib"
                                                    DL.getGlobalPrefix()));

isn't possible because there is no /path/to/lib. What is the normal way to handle such situations?

CiaranWelsh
  • 7,014
  • 10
  • 53
  • 106

1 Answers1

0

What you need is to add -rdynamic or -Wl, -export-dynamic flag to the linker.

-E --export-dynamic

When creating a dynamically linked executable, add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time. If you do not use this option, the dynamic symbol table will normally contain only those symbols which are referenced by some dynamic object mentioned in the link. If you use dlopen to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object, then you will probably need to use this option when linking the program itself.

aneax
  • 1
  • 3