From 980fcc6eec0ec78c9af5ad6e45a33acf90008dc8 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sun, 6 Sep 2026 18:53:34 +0800 Subject: [PATCH] llvm: expose reusable callees and non-consuming IR parsing --- CApiBindings.cpp | 31 +++++++++++++++++ CApiBindings.h | 19 +++++++++++ function_callee.go | 21 ++++++++++++ function_callee_test.go | 60 +++++++++++++++++++++++++++++++++ irreader.go | 20 +++++++++-- irreader_test.go | 74 +++++++++++++++++++++++++++++++++++++++++ metadata_test.go | 4 ++- switch_test.go | 4 ++- 8 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 CApiBindings.cpp create mode 100644 CApiBindings.h create mode 100644 function_callee.go create mode 100644 function_callee_test.go create mode 100644 irreader_test.go diff --git a/CApiBindings.cpp b/CApiBindings.cpp new file mode 100644 index 0000000..a678c6f --- /dev/null +++ b/CApiBindings.cpp @@ -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(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 +} diff --git a/CApiBindings.h b/CApiBindings.h new file mode 100644 index 0000000..0c8d7cd --- /dev/null +++ b/CApiBindings.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +#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 diff --git a/function_callee.go b/function_callee.go new file mode 100644 index 0000000..8aed1be --- /dev/null +++ b/function_callee.go @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +package llvm + +/* +#include "CApiBindings.h" +#include +*/ +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 +// 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 +} diff --git a/function_callee_test.go b/function_callee_test.go new file mode 100644 index 0000000..5cbd5e7 --- /dev/null +++ b/function_callee_test.go @@ -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) + } +} diff --git a/irreader.go b/irreader.go index ee084bf..8b9703a 100644 --- a/irreader.go +++ b/irreader.go @@ -15,6 +15,7 @@ package llvm /* #include "llvm-c/Core.h" #include "llvm-c/IRReader.h" +#include "CApiBindings.h" #include */ import "C" @@ -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 @@ -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 { + err := errors.New(C.GoString(errmsg)) + C.LLVMDisposeMessage(errmsg) + return Module{}, err + } + return m, nil +} diff --git a/irreader_test.go b/irreader_test.go new file mode 100644 index 0000000..274d99c --- /dev/null +++ b/irreader_test.go @@ -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") + } + } + }) + } +} diff --git a/metadata_test.go b/metadata_test.go index f0350b5..4dfb260 100644 --- a/metadata_test.go +++ b/metadata_test.go @@ -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) } diff --git a/switch_test.go b/switch_test.go index aa7ca26..e465778 100644 --- a/switch_test.go +++ b/switch_test.go @@ -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) }