Skip to content

Commit

Permalink
cmd/compile: replace CallImport with go:wasmimport directive
Browse files Browse the repository at this point in the history
This change replaces the special assembler instruction CallImport
of the wasm architecture with a new go:wasmimport directive. This new
directive is cleaner and has more flexibility with regards to how
parameters get passed to WebAssembly function imports. This is a
preparation for adding support for wasi (WebAssembly System Interface).

The default mode of the directive passes Go parameters as individual
WebAssembly parameters. This mode will be used with wasi. The second
mode "abi0" only passes the current SP as a single parameter. The
called function then reads its arguments from memory. This is the
method currently used by wasm_exec.js and the goal is to eventually
remove this mode.

* Fixes golang#38248

Co-authored-by: Vedant Roy <vroy101@gmail.com>
Co-authored-by: Richard Musiol <mail@richard-musiol.de>
Co-authored-by: David Blyth <davidcooperblyth@gmail.com>
Change-Id: I2baee4cca5d6c6ecfa26042a5aa233e33ea6f06f
  • Loading branch information
3 people authored and johanbrandhorst committed Dec 24, 2022
1 parent 38cfb3b commit bfd4239
Show file tree
Hide file tree
Showing 31 changed files with 525 additions and 146 deletions.
2 changes: 1 addition & 1 deletion misc/wasm/go_js_wasm_exec
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright 2018 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
Expand Down
3 changes: 3 additions & 0 deletions misc/wasm/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@

const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
go: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/compile/internal/gc/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func enqueueFunc(fn *ir.Func) {
return // we'll get this as part of its enclosing function
}

if ssagen.CreateWasmImportWrapper(fn) {
return
}

if len(fn.Body) == 0 {
// Initialize ABI wrappers if necessary.
ir.InitLSym(fn, false)
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/compile/internal/ir/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ type Func struct {
// For wrapper functions, WrappedFunc point to the original Func.
// Currently only used for go/defer wrappers.
WrappedFunc *Func

// WasmImport is used by the //go:wasmimport directive to store info about
// a WebAssembly function import.
WasmImport *WasmImport
}

func NewFunc(pos src.XPos) *Func {
Expand Down
6 changes: 6 additions & 0 deletions src/cmd/compile/internal/ir/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,12 @@ const (

)

// WasmImport stores metadata associated with the //go:wasmimport pragma
type WasmImport struct {
Module string
Name string
}

func AsNode(n types.Object) Node {
if n == nil {
return nil
Expand Down
4 changes: 2 additions & 2 deletions src/cmd/compile/internal/ir/sizeof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func TestSizeof(t *testing.T) {
_32bit uintptr // size on 32bit platforms
_64bit uintptr // size on 64bit platforms
}{
{Func{}, 184, 320},
{Name{}, 100, 176},
{Func{}, 196, 328},
{Name{}, 112, 176},
}

for _, tt := range tests {
Expand Down
19 changes: 19 additions & 0 deletions src/cmd/compile/internal/noder/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ func (g *irgen) funcDecl(out *ir.Nodes, decl *syntax.FuncDecl) {
}
}

if p, ok := decl.Pragma.(*pragmas); ok && p.WasmImport != nil {
if decl.Body != nil {
base.ErrorfAt(fn.Pos(), "can only use //go:wasmimport with external func implementations")
}
name := typecheck.Lookup(decl.Name.Value).Def.(*ir.Name)
f := name.Defn.(*ir.Func)
f.WasmImport = &ir.WasmImport{
Module: p.WasmImport.Module,
Name: p.WasmImport.Name,
}
// While functions annotated with //go:wasmimport are
// bodyless, the compiler generates a WebAssembly body for
// them. However, the body will never grow the Go stack.
f.Pragma |= ir.Nosplit
}

if decl.Body != nil {
if fn.Pragma&ir.Noescape != 0 {
base.ErrorfAt(fn.Pos(), "can only use //go:noescape with external func implementations")
Expand Down Expand Up @@ -349,4 +365,7 @@ func (g *irgen) reportUnused(pragma *pragmas) {
base.ErrorfAt(g.makeXPos(e.Pos), "misplaced go:embed directive")
}
}
if pragma.WasmImport != nil {
base.ErrorfAt(g.makeXPos(pragma.WasmImport.Pos), "misplaced go:wasmimport directive")
}
}
2 changes: 1 addition & 1 deletion src/cmd/compile/internal/noder/irgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ Outer:
if base.Flag.Complete {
for _, n := range g.target.Decls {
if fn, ok := n.(*ir.Func); ok {
if fn.Body == nil && fn.Nname.Sym().Linkname == "" {
if fn.Body == nil && fn.Nname.Sym().Linkname == "" && fn.WasmImport == nil {
base.ErrorfAt(fn.Pos(), "missing function body")
}
}
Expand Down
34 changes: 31 additions & 3 deletions src/cmd/compile/internal/noder/noder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package noder
import (
"errors"
"fmt"
"internal/buildcfg"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -219,9 +220,17 @@ var allowedStdPragmas = map[string]bool{

// *pragmas is the value stored in a syntax.pragmas during parsing.
type pragmas struct {
Flag ir.PragmaFlag // collected bits
Pos []pragmaPos // position of each individual flag
Embeds []pragmaEmbed
Flag ir.PragmaFlag // collected bits
Pos []pragmaPos // position of each individual flag
Embeds []pragmaEmbed
WasmImport *WasmImport
}

// WasmImport stores metadata associated with the //go:wasmimport pragma
type WasmImport struct {
Pos syntax.Pos
Module string
Name string
}

type pragmaPos struct {
Expand All @@ -245,6 +254,9 @@ func (p *noder) checkUnusedDuringParse(pragma *pragmas) {
p.error(syntax.Error{Pos: e.Pos, Msg: "misplaced go:embed directive"})
}
}
if pragma.WasmImport != nil {
p.error(syntax.Error{Pos: pragma.WasmImport.Pos, Msg: "misplaced go:wasmimport directive"})
}
}

// pragma is called concurrently if files are parsed concurrently.
Expand Down Expand Up @@ -272,6 +284,22 @@ func (p *noder) pragma(pos syntax.Pos, blankLine bool, text string, old syntax.P
}

switch {
case strings.HasPrefix(text, "go:wasmimport "):
if buildcfg.GOARCH == "wasm" {
f := strings.Fields(text)
if len(f) != 3 {
p.error(syntax.Error{Pos: pos, Msg: "usage: //go:wasmimport module_name import_name"})
}
if !base.Flag.CompilingRuntime && base.Ctxt.Pkgpath != "syscall/js" && base.Ctxt.Pkgpath != "syscall/js_test" {
p.error(syntax.Error{Pos: pos, Msg: "//go:wasmimport directive cannot be used outside of runtime or syscall/js"})
}
pragma.WasmImport = &WasmImport{
Pos: pos,
Module: f[1],
Name: f[2],
}
}

case strings.HasPrefix(text, "go:linkname "):
f := strings.Fields(text)
if !(2 <= len(f) && len(f) <= 3) {
Expand Down
14 changes: 13 additions & 1 deletion src/cmd/compile/internal/noder/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1003,11 +1003,15 @@ func (w *writer) funcExt(obj *types2.Func) {
if pragma&ir.Systemstack != 0 && pragma&ir.Nosplit != 0 {
w.p.errorf(decl, "go:nosplit and go:systemstack cannot be combined")
}
wi := asWasmImport(decl.Pragma)

if decl.Body != nil {
if pragma&ir.Noescape != 0 {
w.p.errorf(decl, "can only use //go:noescape with external func implementations")
}
if wi != nil {
w.p.errorf(decl, "can only use //go:wasmimport with external func implementations")
}
if (pragma&ir.UintptrKeepAlive != 0 && pragma&ir.UintptrEscapes == 0) && pragma&ir.Nosplit == 0 {
// Stack growth can't handle uintptr arguments that may
// be pointers (as we don't know which are pointers
Expand All @@ -1028,7 +1032,8 @@ func (w *writer) funcExt(obj *types2.Func) {
if base.Flag.Complete || decl.Name.Value == "init" {
// Linknamed functions are allowed to have no body. Hopefully
// the linkname target has a body. See issue 23311.
if _, ok := w.p.linknames[obj]; !ok {
// Wasmimport functions are also allowed to have no body.
if _, ok := w.p.linknames[obj]; !ok && wi == nil {
w.p.errorf(decl, "missing function body")
}
}
Expand Down Expand Up @@ -2728,6 +2733,13 @@ func asPragmaFlag(p syntax.Pragma) ir.PragmaFlag {
return p.(*pragmas).Flag
}

func asWasmImport(p syntax.Pragma) *WasmImport {
if p == nil {
return nil
}
return p.(*pragmas).WasmImport
}

// isPtrTo reports whether from is the type *to.
func isPtrTo(from, to types2.Type) bool {
ptr, ok := from.(*types2.Pointer)
Expand Down
128 changes: 128 additions & 0 deletions src/cmd/compile/internal/ssagen/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"os"
"strings"

"cmd/compile/internal/abi"
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/objw"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/obj"
Expand Down Expand Up @@ -339,3 +341,129 @@ func makeABIWrapper(f *ir.Func, wrapperABI obj.ABI) {
typecheck.DeclContext = savedclcontext
ir.CurFunc = savedcurfn
}

// CreateWasmImportWrapper creates a wrapper for imported WASM functions to
// adapt them to the Go calling convention. The body for this function is
// generated in cmd/internal/obj/wasm/wasmobj.go
func CreateWasmImportWrapper(fn *ir.Func) bool {
if fn.WasmImport == nil {
return false
}
if buildcfg.GOARCH != "wasm" {
base.FatalfAt(fn.Pos(), "CreateWasmImportWrapper call not supported on %s: func was %v", buildcfg.GOARCH, fn)
}

ir.InitLSym(fn, true)

pp := objw.NewProgs(fn, 0)
defer pp.Free()
pp.Text.To.Type = obj.TYPE_TEXTSIZE
pp.Text.To.Val = int32(types.RoundUp(fn.Type().ArgWidth(), int64(types.RegSize)))
// Wrapper functions never need their own stack frame
pp.Text.To.Offset = 0
pp.Flush()

return true
}

func toWasmFields(result *abi.ABIParamResultInfo, abiParams []abi.ABIParamAssignment) []obj.WasmField {
wfs := make([]obj.WasmField, len(abiParams))
for i, p := range abiParams {
t := p.Type
switch {
case t.IsInteger() && t.Size() == 4:
wfs[i].Type = obj.WasmI32
case t.IsInteger() && t.Size() == 8:
wfs[i].Type = obj.WasmI64
case t.IsFloat() && t.Size() == 4:
wfs[i].Type = obj.WasmF32
case t.IsFloat() && t.Size() == 8:
wfs[i].Type = obj.WasmF64
case t.IsPtr():
wfs[i].Type = obj.WasmPtr
default:
base.Fatalf("wasm import has bad function signature")
}
wfs[i].Offset = p.FrameOffset(result)
}
return wfs
}

// setupTextLSym initializes the LSym for a with-body text symbol.
func setupTextLSym(f *ir.Func, flag int) {
if f.Dupok() {
flag |= obj.DUPOK
}
if f.Wrapper() {
flag |= obj.WRAPPER
}
if f.ABIWrapper() {
flag |= obj.ABIWRAPPER
}
if f.Needctxt() {
flag |= obj.NEEDCTXT
}
if f.Pragma&ir.Nosplit != 0 {
flag |= obj.NOSPLIT
}
if f.ReflectMethod() {
flag |= obj.REFLECTMETHOD
}

// Clumsy but important.
// For functions that could be on the path of invoking a deferred
// function that can recover (runtime.reflectcall, reflect.callReflect,
// and reflect.callMethod), we want the panic+recover special handling.
// See test/recover.go for test cases and src/reflect/value.go
// for the actual functions being considered.
//
// runtime.reflectcall is an assembly function which tailcalls
// WRAPPER functions (runtime.callNN). Its ABI wrapper needs WRAPPER
// flag as well.
fnname := f.Sym().Name
if base.Ctxt.Pkgpath == "runtime" && fnname == "reflectcall" {
flag |= obj.WRAPPER
} else if base.Ctxt.Pkgpath == "reflect" {
switch fnname {
case "callReflect", "callMethod":
flag |= obj.WRAPPER
}
}

base.Ctxt.InitTextSym(f.LSym, flag, f.Pos())

if f.WasmImport != nil {
wi := obj.WasmImport{
Module: f.WasmImport.Module,
Name: f.WasmImport.Name,
}
if wi.Module == "go" {
// Functions that are imported from the "go" module use a special
// ABI that just accepts the stack pointer.
// Example:
//
// //go:wasmimport go add
// func importedAdd(a, b uint) uint
//
// will roughly become
//
// (import "go" "add" (func (param i32)))
wi.Params = []obj.WasmField{{Type: obj.WasmI32}}
} else {
// All other imported functions use the normal WASM ABI.
// Example:
//
// //go:wasmimport a_module add
// func importedAdd(a, b uint) uint
//
// will roughly become
//
// (import "a_module" "add" (func (param i32 i32) (result i32)))
abiConfig := AbiForBodylessFuncStackMap(f)
abiInfo := abiConfig.ABIAnalyzeFuncType(f.Type().FuncType())
wi.Params = toWasmFields(abiInfo, abiInfo.InParams())
wi.Results = toWasmFields(abiInfo, abiInfo.OutParams())
}
f.LSym.Func().WasmImport = &wi
}
}
1 change: 1 addition & 0 deletions src/cmd/internal/goobj/objfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ const (
AuxPcline
AuxPcinline
AuxPcdata
AuxWasmImport
)

func (a *Aux) Type() uint8 { return a[0] }
Expand Down
Loading

0 comments on commit bfd4239

Please sign in to comment.