From b4c22b4438aa776ce6fbaa639f253fa9e747c51b Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 8 Oct 2026 11:11:16 -0700 Subject: [PATCH 1/2] compiler: allow //go:wasmimport functions as function values Imports were rejected as function values because they are marked as exported and use a different ABI. Wrap them in a Go ABI function that calls the import instead. Other exported functions are still rejected. --- compiler/compiler.go | 15 ++++-- compiler/compiler_test.go | 49 +++++++++++++++++++ compiler/func.go | 57 +++++++++++++++++++++++ compiler/testdata/export-funcvalue.go | 13 ++++++ compiler/testdata/wasmimport-funcvalue.go | 19 ++++++++ 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 compiler/testdata/export-funcvalue.go create mode 100644 compiler/testdata/wasmimport-funcvalue.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 6f8b89a840..070dbab8d9 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2478,11 +2478,20 @@ func (b *builder) getValue(expr ssa.Value, pos token.Pos) llvm.Value { } return b.createConst(expr, pos) case *ssa.Function: + var fn llvm.Value if b.getFunctionInfo(expr).exported { - b.addError(expr.Pos(), "cannot use an exported function as value: "+expr.String()) - return llvm.Undef(b.getLLVMType(expr.Type())) + // Imports (//go:wasmimport) are wrapped in a Go ABI function. + // Other exported functions use a different ABI. + if expr.Blocks == nil { + fn = b.getImportFuncValueWrapper(expr) + } + if fn.IsNil() { + b.addError(expr.Pos(), "cannot use an exported function as value: "+expr.String()) + return llvm.Undef(b.getLLVMType(expr.Type())) + } + } else { + _, fn = b.getFunction(expr) } - _, fn := b.getFunction(expr) return b.createFuncValue(fn, llvm.Undef(b.dataPtrType), expr.Signature) case *ssa.Global: value := b.getGlobal(expr) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 4a36f83b9e..0d2705d9e5 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -793,3 +793,52 @@ func testCompilePackageWithDebug(t *testing.T, options *compileopts.Options, fil ssaPkg.Build() return CompilePackage(file, pkg, ssaPkg, machine, compilerConfig, false) } + +// Imports can be used as function values, //export functions cannot. +func TestImportFuncValue(t *testing.T) { + t.Parallel() + + options := &compileopts.Options{Target: "wasm"} + mod, errs := testCompilePackage(t, options, "wasmimport-funcvalue.go") + defer mod.Dispose() + for _, err := range errs { + t.Error(err) + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + + // Each wrapper takes the import parameters plus a context pointer. + for _, name := range []string{"voidimport", "intimport"} { + importFn := mod.NamedFunction("main." + name) + wrapper := mod.NamedFunction("main." + name + "$funcvalue") + if importFn.IsNil() || wrapper.IsNil() { + t.Errorf("%s: missing import (%t) or wrapper (%t)", name, importFn.IsNil(), wrapper.IsNil()) + continue + } + if got, want := len(wrapper.Params()), len(importFn.Params())+1; got != want { + t.Errorf("%s: wrapper has %d parameters, want %d", name, got, want) + } + if wrapper.IsDeclaration() { + t.Errorf("%s: wrapper has no body", name) + continue + } + called := false + for inst := wrapper.EntryBasicBlock().FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if !inst.IsACallInst().IsNil() && inst.CalledValue() == importFn { + called = true + } + } + if !called { + t.Errorf("%s: wrapper does not call the import", name) + } + } + + _, errs = testCompilePackage(t, options, "export-funcvalue.go") + if len(errs) != 1 { + t.Fatalf("got %d errors for exported function value, want 1: %v", len(errs), errs) + } + if want := "cannot use an exported function as value: main.exported"; !strings.Contains(errs[0].Error(), want) { + t.Errorf("got error %q, want it to contain %q", errs[0], want) + } +} diff --git a/compiler/func.go b/compiler/func.go index 517d585ea9..d15f66f15d 100644 --- a/compiler/func.go +++ b/compiler/func.go @@ -341,3 +341,60 @@ func (b *builder) parseMakeClosure(expr *ssa.MakeClosure) (llvm.Value, error) { _, fn := b.getFunction(f) return b.createFuncValue(fn, context, f.Signature), nil } + +// getImportFuncValueWrapper returns a Go ABI function that calls the given +// import, so the import can be used as a function value. It returns a nil +// value if the import and Go ABIs differ for this signature. +func (c *compilerContext) getImportFuncValueWrapper(fn *ssa.Function) llvm.Value { + importType, importFn := c.getFunction(fn) + wrapperName := importFn.Name() + "$funcvalue" + if wrapper := c.mod.NamedFunction(wrapperName); !wrapper.IsNil() { + return wrapper + } + + // The Go ABI parameters must match the import parameters. + abi := c.getFunctionABI(fn.Signature, false) + if abi.indirectResult { + return llvm.Value{} + } + var goParams []llvm.Type + for i, param := range getParams(fn.Signature) { + if abi.params[i].indirect { + return llvm.Value{} + } + for _, info := range c.expandDirectFormalParamType(abi.params[i].llvmType, param.Name(), param.Type()) { + goParams = append(goParams, info.llvmType) + } + } + importParams := importType.ParamTypes() + if len(goParams) != len(importParams) || abi.resultType != importType.ReturnType() { + return llvm.Value{} + } + for i := range goParams { + if goParams[i] != importParams[i] { + return llvm.Value{} + } + } + + paramTypes := append(append([]llvm.Type{}, importParams...), c.dataPtrType) + wrapper := llvm.AddFunction(c.mod, wrapperName, llvm.FunctionType(importType.ReturnType(), paramTypes, false)) + c.addStandardAttributes(wrapper) + wrapper.SetLinkage(llvm.LinkOnceODRLinkage) + wrapper.SetUnnamedAddr(true) + + b := builder{ + compilerContext: c, + Builder: c.ctx.NewBuilder(), + } + defer b.Builder.Dispose() + block := b.ctx.AddBasicBlock(wrapper, "entry") + b.SetInsertPointAtEnd(block) + args := wrapper.Params()[:len(importParams)] + result := b.CreateCall(importType, importFn, args, "") + if importType.ReturnType().TypeKind() == llvm.VoidTypeKind { + b.CreateRetVoid() + } else { + b.CreateRet(result) + } + return wrapper +} diff --git a/compiler/testdata/export-funcvalue.go b/compiler/testdata/export-funcvalue.go new file mode 100644 index 0000000000..3e3162819e --- /dev/null +++ b/compiler/testdata/export-funcvalue.go @@ -0,0 +1,13 @@ +package main + +//export exported +func exported(a int32) int32 { + return a +} + +var exportedValue func(int32) int32 + +func main() { + exportedValue = exported + println(exportedValue(1)) +} diff --git a/compiler/testdata/wasmimport-funcvalue.go b/compiler/testdata/wasmimport-funcvalue.go new file mode 100644 index 0000000000..7012ed07b2 --- /dev/null +++ b/compiler/testdata/wasmimport-funcvalue.go @@ -0,0 +1,19 @@ +package main + +import "unsafe" + +//go:wasmimport modulename voidimport +func voidimport(a int32, b float64) + +//go:wasmimport modulename intimport +func intimport(p unsafe.Pointer, n uint32) uint32 + +var ( + voidValue = voidimport + intValue = intimport +) + +func main() { + voidValue(1, 2) + println(intValue(nil, 0)) +} From 6e14d6ed5e76ce95f8bce49334434ffc6d6d8055 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 8 Oct 2026 11:11:16 -0700 Subject: [PATCH 2/2] runtime: add Pinner The GC does not move objects, so Pin only needs to keep the object alive until Unpin. The pins are kept in a global map. Generated bindings embed a Pinner in the object they pin, so storing the pins in the Pinner itself would let the GC free the object while only the host holds it. --- src/runtime/pinner.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/runtime/pinner.go diff --git a/src/runtime/pinner.go b/src/runtime/pinner.go new file mode 100644 index 0000000000..104b6a78ef --- /dev/null +++ b/src/runtime/pinner.go @@ -0,0 +1,21 @@ +package runtime + +// Pinner keeps objects alive until Unpin is called. The GC does not move +// objects. The pins are kept in a global map so that a Pinner embedded in +// the object it pins does not become unreachable. +type Pinner struct { + _ byte +} + +var pinned map[*Pinner][]any + +func (p *Pinner) Pin(pointer any) { + if pinned == nil { + pinned = make(map[*Pinner][]any) + } + pinned[p] = append(pinned[p], pointer) +} + +func (p *Pinner) Unpin() { + delete(pinned, p) +}