Skip to content

Commit

Permalink
all: implement wasmimport directive
Browse files Browse the repository at this point in the history
Go programs can now use the //go:wasmimport module_name function_name
directive to import functions from the WebAssembly runtime.

For now, the directive is restricted to the runtime and syscall/js
packages.

* Derived from CL 350737
* Original work modified to work with changes to the IR conversion code.
* Modification of CL 350737 changes to fully exist in Unified IR path (emp)
* Original work modified to work with changes to the ABI configuration code.
* Fixes golang#38248

Co-authored-by: Vedant Roy <vroy101@gmail.com>
Co-authored-by: Richard Musiol <mail@richard-musiol.de>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Change-Id: I740719735d91c306ac718a435a78e1ee9686bc16
  • Loading branch information
4 people committed Feb 28, 2023
1 parent 65fa9ce commit a664424
Show file tree
Hide file tree
Showing 29 changed files with 588 additions and 146 deletions.
9 changes: 8 additions & 1 deletion misc/wasm/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}

const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}

const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
Expand Down Expand Up @@ -206,7 +210,10 @@

const timeOrigin = Date.now() - performance.now();
this.importObject = {
go: {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// 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
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
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 @@ -43,6 +43,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
10 changes: 10 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,16 @@ 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
}

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

func NewFunc(pos src.XPos) *Func {
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/compile/internal/ir/sizeof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestSizeof(t *testing.T) {
_32bit uintptr // size on 32bit platforms
_64bit uintptr // size on 64bit platforms
}{
{Func{}, 184, 320},
{Func{}, 188, 328},
{Name{}, 100, 176},
}

Expand Down
11 changes: 11 additions & 0 deletions src/cmd/compile/internal/noder/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package noder

import (
"internal/buildcfg"
"internal/pkgbits"
"io"

Expand Down Expand Up @@ -269,6 +270,16 @@ func (l *linker) relocFuncExt(w *pkgbits.Encoder, name *ir.Name) {
l.pragmaFlag(w, name.Func.Pragma)
l.linkname(w, name)

if buildcfg.GOARCH == "wasm" {
if name.Func.WasmImport != nil {
w.String(name.Func.WasmImport.Module)
w.String(name.Func.WasmImport.Name)
} else {
w.String("")
w.String("")
}
}

// Relocated extension data.
w.Bool(true)

Expand Down
37 changes: 34 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 @@ -166,9 +167,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 @@ -192,6 +201,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 @@ -219,6 +231,25 @@ func (p *noder) pragma(pos syntax.Pos, blankLine bool, text string, old syntax.P
}

switch {
case strings.HasPrefix(text, "go:wasmimport "):
f := strings.Fields(text)
if len(f) != 3 {
p.error(syntax.Error{Pos: pos, Msg: "usage: //go:wasmimport importmodule importname"})
break
}
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"})
break
}

if buildcfg.GOARCH == "wasm" {
// Only actually use them if we're compiling to WASM though.
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
12 changes: 12 additions & 0 deletions src/cmd/compile/internal/noder/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,18 @@ func (r *reader) funcExt(name *ir.Name, method *types.Sym) {
fn.Pragma = r.pragmaFlag()
r.linkname(name)

if buildcfg.GOARCH == "wasm" {
xmod := r.String()
xname := r.String()

if xmod != "" && xname != "" {
fn.WasmImport = &ir.WasmImport{
Module: xmod,
Name: xname,
}
}
}

typecheck.Func(fn)

if r.Bool() {
Expand Down
26 changes: 25 additions & 1 deletion src/cmd/compile/internal/noder/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package noder

import (
"fmt"
"internal/buildcfg"
"internal/pkgbits"

"cmd/compile/internal/base"
Expand Down Expand Up @@ -1003,11 +1004,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 +1033,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 All @@ -1041,6 +1047,17 @@ func (w *writer) funcExt(obj *types2.Func) {
w.Sync(pkgbits.SyncFuncExt)
w.pragmaFlag(pragma)
w.linkname(obj)

if buildcfg.GOARCH == "wasm" {
if wi != nil {
w.String(wi.Module)
w.String(wi.Name)
} else {
w.String("")
w.String("")
}
}

w.Bool(false) // stub extension
w.Reloc(pkgbits.RelocBody, body)
w.Sync(pkgbits.SyncEOF)
Expand Down Expand Up @@ -2728,6 +2745,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
88 changes: 88 additions & 0 deletions src/cmd/compile/internal/ssagen/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ 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"
"cmd/internal/obj/wasm"
)

// SymABIs records information provided by the assembler about symbol
Expand Down Expand Up @@ -336,3 +339,88 @@ 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)

setupWasmABI(fn)

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 setupWasmABI(f *ir.Func) {
wi := obj.WasmImport{
Module: f.WasmImport.Module,
Name: f.WasmImport.Name,
}
if wi.Module == wasm.GojsModule {
// Functions that are imported from the "gojs" module use a special
// ABI that just accepts the stack pointer.
// Example:
//
// //go:wasmimport gojs add
// func importedAdd(a, b uint) uint
//
// will roughly become
//
// (import "gojs" "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 @@ -442,6 +442,7 @@ const (
AuxPcline
AuxPcinline
AuxPcdata
AuxWasmImport
)

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

0 comments on commit a664424

Please sign in to comment.