Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CApiBindings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "CApiBindings.h"
#include "llvm-c/IRReader.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/MemoryBuffer.h"

LLVMValueRef LLVMGoGetOrInsertFunction(LLVMModuleRef M, const char *Name,
size_t NameLen, LLVMTypeRef FunctionTy) {
#if LLVM_VERSION_MAJOR >= 22
return LLVMGetOrInsertFunction(M, Name, NameLen, FunctionTy);
#else
return llvm::wrap(llvm::unwrap(M)->getOrInsertFunction(
llvm::StringRef(Name, NameLen), llvm::unwrap<llvm::FunctionType>(FunctionTy))
.getCallee());
#endif
}

LLVMBool LLVMGoParseIRInContext(LLVMContextRef Context, LLVMMemoryBufferRef Buffer,
LLVMModuleRef *OutModule, char **OutMessage) {
#if LLVM_VERSION_MAJOR >= 22
return LLVMParseIRInContext2(Context, Buffer, OutModule, OutMessage);
#else
// The legacy parser consumes its input even on failure. Give it an owned
// copy so the caller keeps the same ownership contract on older LLVMs.
LLVMMemoryBufferRef Copy = LLVMCreateMemoryBufferWithMemoryRangeCopy(
LLVMGetBufferStart(Buffer), LLVMGetBufferSize(Buffer),
llvm::unwrap(Buffer)->getBufferIdentifier().str().c_str());
return LLVMParseIRInContext(Context, Copy, OutModule, OutMessage);
#endif
}
19 changes: 19 additions & 0 deletions CApiBindings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Header files miss the LLVM banner/license block used repo-wide

Every other bindings source in this repo (IRBindings.h/.cpp, backports.cpp, SupportBindings.h, ...) opens with the LLVM //===- File - description -*- C++ -*-===// banner plus the full license block. CApiBindings.h and CApiBindings.cpp use only a bare // SPDX-License-Identifier line. Consider adding the standard banner for consistency with the surrounding files.

#ifndef LLVM_BINDINGS_GO_C_API_BINDINGS_H
#define LLVM_BINDINGS_GO_C_API_BINDINGS_H

#include "llvm-c/Core.h"

#ifdef __cplusplus
extern "C" {
#endif

LLVMValueRef LLVMGoGetOrInsertFunction(LLVMModuleRef M, const char *Name,
size_t NameLen, LLVMTypeRef FunctionTy);
LLVMBool LLVMGoParseIRInContext(LLVMContextRef Context, LLVMMemoryBufferRef Buffer,
LLVMModuleRef *OutModule, char **OutMessage);

#ifdef __cplusplus
}
#endif
#endif
21 changes: 21 additions & 0 deletions function_callee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
package llvm

/*
#include "CApiBindings.h"
#include <stdlib.h>
*/
import "C"
import "unsafe"

// GetOrInsertFunction returns the callee for name, creating an external function
// declaration with type ft if the name is absent. Existing symbols, types, and
// attributes are preserved. The result can be an alias or, with typed pointers,
// a constant-expression cast rather than a Function; callers should use ft when

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] GetOrInsertFunction doc describes an unreachable typed-pointer case

The comment says the result "can be an alias or, with typed pointers, a constant-expression cast rather than a Function." All LLVM versions this binding compiles against use opaque pointers, so the constant-expression-cast case (typed-pointer mode) can no longer occur. The clause is hedged and not strictly wrong, but it documents a path unreachable here and could lead readers to expect a cast expression. Consider dropping the typed-pointer clause; the alias caveat and IsAFunction guidance remain accurate.

// constructing calls and check IsAFunction before accessing function attributes.
func (m Module) GetOrInsertFunction(name string, ft Type) (v Value) {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
v.C = C.LLVMGoGetOrInsertFunction(m.C, cname, C.size_t(len(name)), ft.C)
return
}
60 changes: 60 additions & 0 deletions function_callee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
package llvm

import "testing"

func TestGetOrInsertFunction(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
m := ctx.NewModule("callee")
defer m.Dispose()
ft := FunctionType(ctx.VoidType(), nil, false)
fn := m.GetOrInsertFunction("target", ft)
if fn.IsAFunction().IsNil() || fn.GlobalValueType() != ft || fn.Linkage() != ExternalLinkage {
t.Fatalf("unexpected declaration: %s", fn)
}
attr := ctx.CreateEnumAttribute(AttributeKindID("nounwind"), 0)
fn.AddFunctionAttr(attr)
if got := m.GetOrInsertFunction("target", ft); got != fn || got.GetEnumFunctionAttribute(AttributeKindID("nounwind")) != attr {
t.Fatal("existing function or attributes were not preserved")
}
// Reusing a name with a different function type must preserve the original
// declaration and return a callable value, without creating target.1.
otherType := FunctionType(ctx.VoidType(), []Type{ctx.Int32Type()}, false)
callee := m.GetOrInsertFunction("target", otherType)
if m.NamedFunction("target") != fn || fn.GlobalValueType() != ft || !m.NamedFunction("target.1").IsNil() {
t.Fatal("type mismatch changed or duplicated the existing declaration")
}
caller := AddFunction(m, "caller", ft)
b := ctx.NewBuilder()
defer b.Dispose()
b.SetInsertPointAtEnd(ctx.AddBasicBlock(caller, "entry"))
b.CreateCall(otherType, callee, []Value{ConstInt(ctx.Int32Type(), 7, false)}, "")
b.CreateRetVoid()
if err := VerifyModule(m, ReturnStatusAction); err != nil {
t.Fatalf("callee is not callable with requested type: %v\n%s", err, m)
}
}

func TestGetOrInsertFunctionAlias(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
m := ctx.NewModule("alias")
defer m.Dispose()
ft := FunctionType(ctx.VoidType(), nil, false)
fn := AddFunction(m, "implementation", ft)
b := ctx.NewBuilder()
defer b.Dispose()
b.SetInsertPointAtEnd(ctx.AddBasicBlock(fn, "entry"))
b.CreateRetVoid()
alias := AddAlias(m, ft, 0, fn, "entrypoint")
if got := m.GetOrInsertFunction("entrypoint", ft); got != alias || got.IsAGlobalAlias().IsNil() {
t.Fatalf("expected existing alias, got %s", got)
}
if !m.NamedFunction("entrypoint.1").IsNil() {
t.Fatal("created a duplicate declaration for an existing alias")
}
if err := VerifyModule(m, ReturnStatusAction); err != nil {
t.Fatal(err)
}
}
20 changes: 18 additions & 2 deletions irreader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package llvm
/*
#include "llvm-c/Core.h"
#include "llvm-c/IRReader.h"
#include "CApiBindings.h"
#include <stdlib.h>
*/
import "C"
Expand All @@ -23,8 +24,9 @@ import (
"errors"
)

// ParseIR parses the textual IR given in the memory buffer and returns a new
// LLVM module in this context.
// ParseIR parses LLVM assembly or bitcode into a new module in this context.
// It consumes buf on both success and failure. The caller must not access or
// dispose buf after calling ParseIR. Use ParseIRBuffer to retain ownership.
func (c *Context) ParseIR(buf MemoryBuffer) (Module, error) {
var m Module
var errmsg *C.char
Expand All @@ -35,3 +37,17 @@ func (c *Context) ParseIR(buf MemoryBuffer) (Module, error) {
}
return m, nil
}

// ParseIRBuffer parses LLVM assembly or bitcode into a new module in this
// context without consuming buf, on either success or failure. The caller may
// reuse buf and is responsible for calling buf.Dispose when finished.
func (c *Context) ParseIRBuffer(buf MemoryBuffer) (Module, error) {
var m Module
var errmsg *C.char
if C.LLVMGoParseIRInContext(c.C, buf.C, &m.C, &errmsg) != 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] No nil-buffer guard in ParseIRBuffer

The legacy shim dereferences Buffer via LLVMGetBufferStart/LLVMGetBufferSize/llvm::unwrap(Buffer)->getBufferIdentifier() with no NULL check, so a nil MemoryBuffer from Go crashes here. This matches upstream LLVMParseIRInContext2 behavior (not a regression), but since buf is a trust boundary from Go callers, a buf.IsNil() early return in ParseIRBuffer (and ParseIR) would be a cheap defense-in-depth guard. Optional.

err := errors.New(C.GoString(errmsg))
C.LLVMDisposeMessage(errmsg)
return Module{}, err
}
return m, nil
}
74 changes: 74 additions & 0 deletions irreader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
package llvm

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)

func TestParseIRBufferOwnership(t *testing.T) {
for _, format := range []string{"assembly", "bitcode", "invalid"} {
t.Run(format, func(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
var buf MemoryBuffer
var inputPath string
if format == "bitcode" {
m := ctx.NewModule("source")
AddFunction(m, "target", FunctionType(ctx.VoidType(), nil, false))
buf = WriteBitcodeToMemoryBuffer(m)
m.Dispose()
} else {
src := "declare void @target()\n"
if format == "invalid" {
src = "define void @broken( {\n"
}
path := filepath.Join(t.TempDir(), "input.ll")
inputPath = path
if err := os.WriteFile(path, []byte(src), 0600); err != nil {
t.Fatal(err)
}
var err error
buf, err = NewMemoryBufferFromFile(path)
if err != nil {
t.Fatal(err)
}
}
defer buf.Dispose()
want := buf.Bytes()
// Parse twice, with an explicit read after each module is disposed.
// This covers ownership on success and on the diagnostic path.
for i := 0; i < 2; i++ {
m, err := ctx.ParseIRBuffer(buf)
if format == "invalid" {
if err == nil || err.Error() == "" || !m.IsNil() {
t.Fatalf("invalid IR: error=%v", err)
}
if !strings.Contains(err.Error(), inputPath) {
t.Fatalf("diagnostic lost buffer identifier: %v", err)
}
} else {
if err != nil {
t.Fatal(err)
}
if m.NamedFunction("target").IsNil() {
t.Fatal("parsed module is missing target")
}
if err := VerifyModule(m, ReturnStatusAction); err != nil {
t.Fatal(err)
}
if format == "assembly" && !strings.Contains(m.String(), `source_filename = "`+inputPath+`"`) {
t.Fatalf("module lost buffer identifier: %s", m)
}
m.Dispose()
}
if !bytes.Equal(buf.Bytes(), want) {
t.Fatal("parser changed the caller's buffer")
}
}
})
}
}
4 changes: 3 additions & 1 deletion metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ func TestNamedMetadataRoundTrip(t *testing.T) {
t.Fatal(err)
}

parsed, err := (&parseCtx).ParseIR(buf)
defer buf.Dispose()

parsed, err := (&parseCtx).ParseIRBuffer(buf)
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 3 additions & 1 deletion switch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ case1:
t.Fatal(err)
}

m, err := ctx.ParseIR(buf)
defer buf.Dispose()

m, err := ctx.ParseIRBuffer(buf)
if err != nil {
t.Fatal(err)
}
Expand Down
Loading