How to call c++ code in LLVM IR

Hi,

I’m a green hand here. Is there an approach to call c++ codes in LLVM IR? For example, there is a c++ method defined outside the main function, how can we call it via LLVM IR?

int foo(int a, int b)
{
return a + b;
}

int main()
{

std::string err;
EngineBuilder EB(std::move(std::unique_ptr(module)));
EB.setEngineKind(EngineKind::JIT).setErrorStr(&err);
ExecutionEngine* EE = EB.create()
// How can we call foo() via LLVM IR?

}

And, if that foo function is an API defined in a dll, how to do it? I’d appreciate it if any example is provided.

Best Regards,
Lin

Hi!

I have only tried this with C myself. C is easier as you don’t have to worry about name mangling :slight_smile:

In any case, you probably want to use CreateCall from the IRBuilder API. InjectFuncCall from llvm-tutor might be helpful too.

-Andrzej

@banach-space

Thanks for your advice. Can you show me your codes about calling c function? To be honest, I don’t have any idea about that. :sweat_smile:

Best Regard,
Lin

The code for InjectFuncCall is available in InjectFuncCall.cpp :slight_smile: CreateCall invocation is here :slight_smile:

HTH,
-Andrzej

If you want to build LLVM IR which will call foo, then this is just the call instruction: LLVM Language Reference Manual — LLVM 15.0.0git documentation ; and @banach-space gave you some pointers.

If you want to make foo available to the jit through the ExecutionEngine, there is more plumbing to do but that’s a JIT question, we have a wrapper for this in MLIR and if you look at the implementation you can figure the JIT APIs for this: llvm-project/Invoke.cpp at main · llvm/llvm-project · GitHub

I think it should be the second. I want to invoke a function anytime and anyplace in llvm when I want to. And that function is a method defined in dll. So it’s a JIT question, yes?Actually I’m not quite clear about the concept JIT and LLVM IR.

I reviewed the example you mentioned. And I have 3 questions about that:

  1. What do I need to do to prepare for MLIR environment ?Now I just downloaded LLVM 13.0.0 and compiled.
  2. How is the function memrefMultiple is invoked?it’s registered into a map with “_mlir_ciface_callback”, but invoked by “caller_for_callback”?How does it wok?
  3. I noted that there’s a snippet about moduleStr in line 240~247. What is it used for?

The JIT means that you will build LLVM IR, turn it into machine code, and load it and execute in the current process. The non-JIT case is more like clang: you generate machine code, likely link it, and then execute it as a separate process.

To execute this unit test, see: Getting Started - MLIR
But note that I linked to this MLIR example here as one project that is using the underlying LLVM JIT

The moduleStr is the IR: it isn’t directly LLVM IR and instead is a piece of MLIR that will be turned to LLVM IR in two steps: lowerToLLVMDialect(*module) line 254 and then implicitly in ExecutionEngine::create(*module);.

The interesting part is that call @callback(%arg0, %coefficient) : (memref<?x?xf32>, i32) -> () is a function call to a function named callback in MLIR. When converting to LLVM IR, the MLIR machinery prefix it with _mlir_ciface_. So the LLVM IR will call a function _mlir_ciface_callback.

To visualize the LLVM IR, I quickly modified the code:

diff --git a/mlir/lib/ExecutionEngine/ExecutionEngine.cpp b/mlir/lib/ExecutionEngine/ExecutionEngine.cpp
index 00569e1d4242..4c31451ea4f5 100644
--- a/mlir/lib/ExecutionEngine/ExecutionEngine.cpp
+++ b/mlir/lib/ExecutionEngine/ExecutionEngine.cpp
@@ -317,7 +317,7 @@ Expected<std::unique_ptr<ExecutionEngine>> ExecutionEngine::create(
                    .setCompileFunctionCreator(compileFunctionCreator)
                    .setObjectLinkingLayerCreator(objectLinkingLayerCreator)
                    .create());
-
+  llvmModule->dump();
   // Add a ThreadSafemodule to the engine and return.
   ThreadSafeModule tsm(std::move(llvmModule), std::move(ctx));
   if (transformer)

and rebuilt and re-ran this test:

$ ninja tools/mlir/unittests/ExecutionEngine/MLIRExecutionEngineTests 
$ tools/mlir/unittests/ExecutionEngine/MLIRExecutionEngineTests 
...
[ RUN      ] NativeMemRefJit.JITCallback
; ModuleID = 'LLVMDialectModule'
source_filename = "LLVMDialectModule"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

declare i8* @malloc(i64 %0)

declare void @free(i8* %0)

define void @callback(float* %0, float* %1, i64 %2, i64 %3, i64 %4, i64 %5, i64 %6, i32 %7) !dbg !3 {
  ...
  call void @_mlir_ciface_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %16, i32 %7), !dbg !7
  ret void, !dbg !7
}

declare void @_mlir_ciface_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %0, i32 %1)

define void @caller_for_callback(float* %0, float* %1, i64 %2, i64 %3, i64 %4, i64 %5, i64 %6, i32 %7) !dbg !9 {
  ...
  call void @callback(float* %0, float* %1, i64 %2, i64 %3, i64 %4, i64 %5, i64 %6, i32 %7), !dbg !13
  ret void, !dbg !14
}

define void @_mlir_ciface_caller_for_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %0, i32 %1) !dbg !15 {
  ...
  call void @caller_for_callback(float* %4, float* %5, i64 %6, i64 %7, i64 %8, i64 %9, i64 %10, i32 %1), !dbg !16
  ret void, !dbg !16
}

define void @_mlir_callback(i8** %0) {
  ...
  call void @callback(float* %5, float* %9, i64 %13, i64 %17, i64 %21, i64 %25, i64 %29, i32 %33)
  ret void
}

define void @_mlir_caller_for_callback(i8** %0) {
  ...
  call void @caller_for_callback(float* %5, float* %9, i64 %13, i64 %17, i64 %21, i64 %25, i64 %29, i32 %33)
  ret void
}

define void @_mlir__mlir_ciface_caller_for_callback(i8** %0) {
  ...
  call void @_mlir_ciface_caller_for_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %5, i32 %9)
  ret void
}

...
[       OK ] NativeMemRefJit.JITCallback (24 ms)
...

I used ... to strip the long section of IR, the interesting part is likely that declare void @_mlir_ciface_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %0, i32 %1) is the only function that does not have a definition, even though it is called: call void @_mlir_ciface_callback({ float*, float*, i64, [2 x i64], [2 x i64] }* %16, i32 %7), !dbg !7
To be able to link the binary resulting from compiling this with LLVM, we’ll need to provide a definition for _mlir_ciface_callback.

This is where the unit-test is interesting, because we implemented this function with a local/static function that is named memrefMultiply. However we tell the JIT in the link I posted above to register this function with its internal dynamic linker so that it is known to provide the definition for _mlir_ciface_callback. That way when the LLVM IR above is compiled and then loaded in the JIT, the definition for _mlir_ciface_callback resolved to memrefMultiply.

@mehdi_amini ,

Thanks for your so so detailed explanation! I will try it as your instruction. Thank you again!

Hi @mehdi_amini ,

When I investigated this case, I reviewed git history and noted that there is a change made on llvm/examples/Kaleidoscope/Chapter4/toy.cpp, adding a prefix DLLEXPORT for functions putchard() and printd(). You made that change, am I right? :grin:

Actually the example in that file is what I want exactly, invoking putchard() via JIT. Now I have another question, related to that change. Is there an approach to make the function available without DLLEXPORT ahead of it? I have tried that and got a message ‘JIT session error: Sybols not found: [ putchard ]’. Do you have any idea about that?

Regards,
Lin