5

I'm writing code to generate LLVM bytecode from a custom VM bytecode using the LLVM Go bindings; the code is then JITed and executed in-process.

The custom VM bytecode has several operations that can't be implemented directly in LLVM, because they require changing external state; the functionality for these opcodes is implemented as Go functions.

There are excellent guides on getting started with generating LLVM bytecode from Go, but none address the issue of callbacks or exported functions. Is it possible to generate LLVM instructions to call back into the Go functions? If so, how?

I've tried the way described below by @arrowd, and it doesn't appear to work. Source code, adapted from Felix Angell's blog post:

package main

import (
    "C"
    "fmt"
    "llvm.org/llvm/final/bindings/go/llvm"
)

// export AddInts
func AddInts(arg1, arg2 int) int {
    return arg1 + arg2;
}

func main() {
    // setup our builder and module
    builder := llvm.NewBuilder()
    mod := llvm.NewModule("my_module")

    // create our function prologue
    main := llvm.FunctionType(llvm.Int32Type(), []llvm.Type{}, false)
    llvm.AddFunction(mod, "main", main)
    block := llvm.AddBasicBlock(mod.NamedFunction("main"), "entry")
    builder.SetInsertPoint(block, block.FirstInstruction())

    // int a = 32
    a := builder.CreateAlloca(llvm.Int32Type(), "a")
    builder.CreateStore(llvm.ConstInt(llvm.Int32Type(), 32, false), a)

    // int b = 16
    b := builder.CreateAlloca(llvm.Int32Type(), "b")
    builder.CreateStore(llvm.ConstInt(llvm.Int32Type(), 16, false), b)

    // return a + b
    bVal := builder.CreateLoad(b, "b_val")
    aVal := builder.CreateLoad(a, "a_val")
    addIntsType := llvm.FunctionType(llvm.Int32Type(), []llvm.Type{llvm.Int32Type(), llvm.Int32Type()}, false)
    addInts := llvm.AddFunction(mod, "AddInts", addIntsType)
    call := builder.CreateCall(addInts, []llvm.Value{aVal, bVal}, "AddInts")
    builder.CreateRet(call)

    // verify it's all good
    if ok := llvm.VerifyModule(mod, llvm.ReturnStatusAction); ok != nil {
        fmt.Println(ok.Error())
    }
    mod.Dump()

    // create our exe engine
    engine, err := llvm.NewExecutionEngine(mod)
    if err != nil {
        fmt.Println(err.Error())
    }

    // run the function!
    funcResult := engine.RunFunction(mod.NamedFunction("main"), []llvm.GenericValue{})
    fmt.Printf("%d\n", funcResult.Int(false))
}

Returns:

; ModuleID = 'my_module'

define i32 @main() {
entry:
  %a = alloca i32
  store i32 32, i32* %a
  %b = alloca i32
  store i32 16, i32* %b
  %b_val = load i32* %b
  %a_val = load i32* %a
  %AddInts = call i32 @AddInts(i32 %a_val, i32 %b_val)
  ret i32 %AddInts
}

declare i32 @AddInts(i32, i32)
LLVM ERROR: Tried to execute an unknown external function: AddInts
exit status 1
Nick Johnson
  • 100,655
  • 16
  • 128
  • 198
  • Haskell provides a way to export code as C functions, and because of that it is possible to call Haskell back from anything, including LLVM. Does Go have such feature? – arrowd Apr 26 '16 at 16:09
  • @arrowd My understanding is that CGo provides this (while the native Go implementation doesn't, as it uses its own calling convention). That's a step in the right direction, but I'm still not certain how to actually call that function from LLVM. – Nick Johnson Apr 26 '16 at 16:10

2 Answers2

3

According to this answer you can export Go functions as C ones by adding "export" comment. If you do this for your function, the LLVM JIT should be able to resolve this symbol during runtime linking. The answer didn't provide the C code which calls that exported function, but assuming it would be just goProgressCB(args); then you can create a CallInst to this function and JIT it.

Updated.

Take a look at this answer. It seems that recent Go can generate C headers for you to call Go back. With this you can compile them with clang -S -emit-llvm or clang -march=cpp and obtain textual IR or C++ calls to the LLVM API that would make up call instructions.

Community
  • 1
  • 1
arrowd
  • 33,231
  • 8
  • 79
  • 110
  • Thanks! I'm still hoping someone will come up with a simple end-to-end LLVM example (in part because I'm sure I won't be the last person to ask this), though. – Nick Johnson Apr 26 '16 at 16:30
  • Incidentally, the LLVM bindings for go don't have `CallInst`; is there something equivalent to that I should be looking for? – Nick Johnson Apr 26 '16 at 19:57
  • So, unfortunately this doesn't seem to work - see my update to the post. – Nick Johnson Apr 26 '16 at 20:17
3

The main problem is that you're not actually using the JIT here, you're using the interpreter. llvm.NewExecutionEngine will construct a JIT compiler if it is available, and fall back to the interpreter otherwise.

You should use llvm.NewMCJITCompiler. This will fail without additional changes, for the same reason why NewExecutionEngine did not yield a JIT in the first place. You need to add the following to your "func main()":

llvm.LinkInMCJIT()               
llvm.InitializeNativeTarget()    
llvm.InitializeNativeAsmPrinter()

Another, smaller, issue with your code is that (lack of) whitespace is important in the "//export" magic.

axw
  • 6,908
  • 1
  • 24
  • 14