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
15 changes: 12 additions & 3 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
57 changes: 57 additions & 0 deletions compiler/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
13 changes: 13 additions & 0 deletions compiler/testdata/export-funcvalue.go
Original file line number Diff line number Diff line change
@@ -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))
}
19 changes: 19 additions & 0 deletions compiler/testdata/wasmimport-funcvalue.go
Original file line number Diff line number Diff line change
@@ -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))
}
21 changes: 21 additions & 0 deletions src/runtime/pinner.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading