From 38630e83892d7e77acdba23817dbc12554867a3b Mon Sep 17 00:00:00 2001 From: David Blyth Date: Fri, 17 Sep 2021 10:31:11 -0700 Subject: [PATCH] cmd/compile: replace CallImport with go:wasmimport directive 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 #38248 Co-authored-by: Vedant Roy Co-authored-by: Richard Musiol Co-authored-by: David Blyth Change-Id: I2baee4cca5d6c6ecfa26042a5aa233e33ea6f06f --- misc/wasm/go_js_wasm_exec | 2 +- misc/wasm/wasm_exec.js | 3 + src/cmd/compile/internal/gc/compile.go | 4 + src/cmd/compile/internal/ir/func.go | 4 + src/cmd/compile/internal/ir/node.go | 6 + src/cmd/compile/internal/ir/sizeof_test.go | 4 +- src/cmd/compile/internal/noder/decl.go | 19 +++ src/cmd/compile/internal/noder/irgen.go | 2 +- src/cmd/compile/internal/noder/noder.go | 34 +++++- src/cmd/compile/internal/noder/writer.go | 14 ++- src/cmd/compile/internal/ssagen/abi.go | 128 +++++++++++++++++++++ src/cmd/internal/goobj/objfile.go | 1 + src/cmd/internal/obj/link.go | 74 +++++++++++- src/cmd/internal/obj/objfile.go | 17 ++- src/cmd/internal/obj/sym.go | 12 +- src/cmd/internal/obj/wasm/a.out.go | 3 +- src/cmd/internal/obj/wasm/anames.go | 3 +- src/cmd/internal/obj/wasm/wasmobj.go | 79 +++++++++++-- src/cmd/link/internal/loader/loader.go | 22 ++++ src/cmd/link/internal/wasm/asm.go | 92 ++++++++++++--- src/runtime/lock_js.go | 8 +- src/runtime/mem_js.go | 2 + src/runtime/os_js.go | 2 + src/runtime/stubs3.go | 1 + src/runtime/sys_wasm.go | 1 + src/runtime/sys_wasm.s | 32 ------ src/runtime/timestub2.go | 1 + src/syscall/js/func.go | 5 +- src/syscall/js/js.go | 16 +++ src/syscall/js/js_js.s | 68 +---------- src/syscall/js/js_test.go | 12 ++ 31 files changed, 525 insertions(+), 146 deletions(-) diff --git a/misc/wasm/go_js_wasm_exec b/misc/wasm/go_js_wasm_exec index fcbd0e4fc8ce0e..db4d2cb608e788 100755 --- a/misc/wasm/go_js_wasm_exec +++ b/misc/wasm/go_js_wasm_exec @@ -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. diff --git a/misc/wasm/wasm_exec.js b/misc/wasm/wasm_exec.js index e6c8921091ec12..c4c25e2f875607 100644 --- a/misc/wasm/wasm_exec.js +++ b/misc/wasm/wasm_exec.js @@ -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 diff --git a/src/cmd/compile/internal/gc/compile.go b/src/cmd/compile/internal/gc/compile.go index 6951d7ed5a9ca5..6a9dd645b3b85e 100644 --- a/src/cmd/compile/internal/gc/compile.go +++ b/src/cmd/compile/internal/gc/compile.go @@ -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) diff --git a/src/cmd/compile/internal/ir/func.go b/src/cmd/compile/internal/ir/func.go index 31c11f8297ec19..9c1285f39a1788 100644 --- a/src/cmd/compile/internal/ir/func.go +++ b/src/cmd/compile/internal/ir/func.go @@ -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 { diff --git a/src/cmd/compile/internal/ir/node.go b/src/cmd/compile/internal/ir/node.go index bda3957af95611..d95bc061262efb 100644 --- a/src/cmd/compile/internal/ir/node.go +++ b/src/cmd/compile/internal/ir/node.go @@ -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 diff --git a/src/cmd/compile/internal/ir/sizeof_test.go b/src/cmd/compile/internal/ir/sizeof_test.go index 754d1a8de070bf..e8717c7935327b 100644 --- a/src/cmd/compile/internal/ir/sizeof_test.go +++ b/src/cmd/compile/internal/ir/sizeof_test.go @@ -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 { diff --git a/src/cmd/compile/internal/noder/decl.go b/src/cmd/compile/internal/noder/decl.go index 07353cc17eaaf2..b8de46cacfa6b7 100644 --- a/src/cmd/compile/internal/noder/decl.go +++ b/src/cmd/compile/internal/noder/decl.go @@ -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") @@ -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") + } } diff --git a/src/cmd/compile/internal/noder/irgen.go b/src/cmd/compile/internal/noder/irgen.go index d0349260e85468..4d2a440c046cf8 100644 --- a/src/cmd/compile/internal/noder/irgen.go +++ b/src/cmd/compile/internal/noder/irgen.go @@ -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") } } diff --git a/src/cmd/compile/internal/noder/noder.go b/src/cmd/compile/internal/noder/noder.go index d0d95451ac0d35..f2ab3ec9b90ddc 100644 --- a/src/cmd/compile/internal/noder/noder.go +++ b/src/cmd/compile/internal/noder/noder.go @@ -7,6 +7,7 @@ package noder import ( "errors" "fmt" + "internal/buildcfg" "os" "path/filepath" "runtime" @@ -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 { @@ -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. @@ -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) { diff --git a/src/cmd/compile/internal/noder/writer.go b/src/cmd/compile/internal/noder/writer.go index da5c1e910d7409..8951a2abe2d54d 100644 --- a/src/cmd/compile/internal/noder/writer.go +++ b/src/cmd/compile/internal/noder/writer.go @@ -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 @@ -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") } } @@ -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) diff --git a/src/cmd/compile/internal/ssagen/abi.go b/src/cmd/compile/internal/ssagen/abi.go index 84d5b5951c2faa..a209b4b9f644c5 100644 --- a/src/cmd/compile/internal/ssagen/abi.go +++ b/src/cmd/compile/internal/ssagen/abi.go @@ -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" @@ -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 + } +} diff --git a/src/cmd/internal/goobj/objfile.go b/src/cmd/internal/goobj/objfile.go index ae215dfef5e996..00e228dc41b7dc 100644 --- a/src/cmd/internal/goobj/objfile.go +++ b/src/cmd/internal/goobj/objfile.go @@ -440,6 +440,7 @@ const ( AuxPcline AuxPcinline AuxPcdata + AuxWasmImport ) func (a *Aux) Type() uint8 { return a[0] } diff --git a/src/cmd/internal/obj/link.go b/src/cmd/internal/obj/link.go index 80370173af4ef1..c65bef7fb7cb6e 100644 --- a/src/cmd/internal/obj/link.go +++ b/src/cmd/internal/obj/link.go @@ -37,6 +37,7 @@ import ( "cmd/internal/objabi" "cmd/internal/src" "cmd/internal/sys" + "encoding/binary" "fmt" "sync" "sync/atomic" @@ -499,7 +500,9 @@ type FuncInfo struct { WrapInfo *LSym // for wrapper, info of wrapped function JumpTables []JumpTable - FuncInfoSym *LSym + FuncInfoSym *LSym + WasmImportSym *LSym + WasmImport *WasmImport } // JumpTable represents a table used for implementing multi-way @@ -558,6 +561,75 @@ func (s *LSym) File() *FileInfo { return f } +// WasmImport represents a WebAssembly (WASM) imported function with +// parameters and results translated into WASM types based on the Go function +// declaration. +type WasmImport struct { + // Module holds the WASM module name specified by the //go:wasmimport + // directive. + Module string + // Name holds the WASM imported function name specified by the + // //go:wasmimport directive. + Name string + // Params holds the imported function parameter fields. + Params []WasmField + // Results holds the imported function result fields. + Results []WasmField +} + +func (wi *WasmImport) CreateSym(ctxt *Link) *LSym { + var sym LSym + + var b [8]byte + writeByte := func(x byte) { + sym.WriteBytes(ctxt, sym.Size, []byte{x}) + } + writeUint32 := func(x uint32) { + binary.LittleEndian.PutUint32(b[:], x) + sym.WriteBytes(ctxt, sym.Size, b[:4]) + } + writeInt64 := func(x int64) { + binary.LittleEndian.PutUint64(b[:], uint64(x)) + sym.WriteBytes(ctxt, sym.Size, b[:]) + } + writeString := func(s string) { + writeUint32(uint32(len(s))) + sym.WriteString(ctxt, sym.Size, len(s), s) + } + writeString(wi.Module) + writeString(wi.Name) + writeUint32(uint32(len(wi.Params))) + for _, f := range wi.Params { + writeByte(byte(f.Type)) + writeInt64(f.Offset) + } + writeUint32(uint32(len(wi.Results))) + for _, f := range wi.Results { + writeByte(byte(f.Type)) + writeInt64(f.Offset) + } + + return &sym +} + +type WasmField struct { + Type WasmFieldType + // Offset holds the frame-pointer-relative locations for Go's stack-based + // ABI. This is used by the src/cmd/internal/wasm package to map WASM + // import parameters to the Go stack in a wrapper function. + Offset int64 +} + +type WasmFieldType byte + +const ( + WasmI32 WasmFieldType = iota + WasmI64 + WasmF32 + WasmF64 + WasmPtr +) + type InlMark struct { // When unwinding from an instruction in an inlined body, mark // where we should unwind to. diff --git a/src/cmd/internal/obj/objfile.go b/src/cmd/internal/obj/objfile.go index ff0968ecf4c2c0..4ded273449f952 100644 --- a/src/cmd/internal/obj/objfile.go +++ b/src/cmd/internal/obj/objfile.go @@ -602,7 +602,12 @@ func (w *writer) Aux(s *LSym) { for _, pcSym := range fn.Pcln.Pcdata { w.aux1(goobj.AuxPcdata, pcSym) } - + if fn.WasmImportSym != nil { + if fn.WasmImportSym.Size == 0 { + panic("wasmimport aux sym must have non-zero size") + } + w.aux1(goobj.AuxWasmImport, fn.WasmImportSym) + } } } @@ -700,6 +705,12 @@ func nAuxSym(s *LSym) int { n++ } n += len(fn.Pcln.Pcdata) + if fn.WasmImport != nil { + if fn.WasmImportSym == nil || fn.WasmImportSym.Size == 0 { + panic("wasmimport aux sym must exist and have non-zero size") + } + n++ + } } return n } @@ -756,8 +767,8 @@ func genFuncInfoSyms(ctxt *Link) { fn.FuncInfoSym = isym b.Reset() - dwsyms := []*LSym{fn.dwarfRangesSym, fn.dwarfLocSym, fn.dwarfDebugLinesSym, fn.dwarfInfoSym} - for _, s := range dwsyms { + auxsyms := []*LSym{fn.dwarfRangesSym, fn.dwarfLocSym, fn.dwarfDebugLinesSym, fn.dwarfInfoSym, fn.WasmImportSym} + for _, s := range auxsyms { if s == nil || s.Size == 0 { continue } diff --git a/src/cmd/internal/obj/sym.go b/src/cmd/internal/obj/sym.go index e5b052c5377c40..b95e72a1a68f82 100644 --- a/src/cmd/internal/obj/sym.go +++ b/src/cmd/internal/obj/sym.go @@ -416,16 +416,16 @@ func (ctxt *Link) traverseFuncAux(flag traverseFlag, fsym *LSym, fn func(parent } } - dwsyms := []*LSym{fninfo.dwarfRangesSym, fninfo.dwarfLocSym, fninfo.dwarfDebugLinesSym, fninfo.dwarfInfoSym} - for _, dws := range dwsyms { - if dws == nil || dws.Size == 0 { + auxsyms := []*LSym{fninfo.dwarfRangesSym, fninfo.dwarfLocSym, fninfo.dwarfDebugLinesSym, fninfo.dwarfInfoSym, fninfo.WasmImportSym} + for _, s := range auxsyms { + if s == nil || s.Size == 0 { continue } - fn(fsym, dws) + fn(fsym, s) if flag&traverseRefs != 0 { - for _, r := range dws.R { + for _, r := range s.R { if r.Sym != nil { - fn(dws, r.Sym) + fn(s, r.Sym) } } } diff --git a/src/cmd/internal/obj/wasm/a.out.go b/src/cmd/internal/obj/wasm/a.out.go index 83ce0a67385e70..0262630d5a78a0 100644 --- a/src/cmd/internal/obj/wasm/a.out.go +++ b/src/cmd/internal/obj/wasm/a.out.go @@ -18,8 +18,7 @@ const ( * wasm */ const ( - ACallImport = obj.ABaseWasm + obj.A_ARCHSPECIFIC + iota - AGet + AGet = obj.ABaseWasm + obj.A_ARCHSPECIFIC + iota ASet ATee ANot // alias for I32Eqz diff --git a/src/cmd/internal/obj/wasm/anames.go b/src/cmd/internal/obj/wasm/anames.go index c9bc15d27007bc..6f1a6629602800 100644 --- a/src/cmd/internal/obj/wasm/anames.go +++ b/src/cmd/internal/obj/wasm/anames.go @@ -5,8 +5,7 @@ package wasm import "cmd/internal/obj" var Anames = []string{ - obj.A_ARCHSPECIFIC: "CallImport", - "Get", + obj.A_ARCHSPECIFIC: "Get", "Set", "Tee", "Not", diff --git a/src/cmd/internal/obj/wasm/wasmobj.go b/src/cmd/internal/obj/wasm/wasmobj.go index 9b0aabe919538b..f92fee50dd296d 100644 --- a/src/cmd/internal/obj/wasm/wasmobj.go +++ b/src/cmd/internal/obj/wasm/wasmobj.go @@ -100,7 +100,6 @@ var unaryDst = map[obj.As]bool{ ATee: true, ACall: true, ACallIndirect: true, - ACallImport: true, ABr: true, ABrIf: true, ABrTable: true, @@ -133,7 +132,7 @@ var ( const ( /* mark flags */ - WasmImport = 1 << 0 + CallWasmImport = 1 << 0 ) func instinit(ctxt *obj.Link) { @@ -179,6 +178,72 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { s.Func().Args = s.Func().Text.To.Val.(int32) s.Func().Locals = int32(framesize) + if wi := s.Func().WasmImport; wi != nil { + s.Func().WasmImportSym = wi.CreateSym(ctxt) + p := s.Func().Text + if p.Link != nil { + panic("wrapper functions for WASM imports should not have a body") + } + to := obj.Addr{ + Type: obj.TYPE_MEM, + Name: obj.NAME_EXTERN, + Sym: s, + } + if wi.Module == "go" { + p = appendp(p, AGet, regAddr(REG_SP)) + p = appendp(p, ACall, to) + p.Mark = CallWasmImport + } else { + if len(wi.Results) > 1 { + panic("invalid results type") // impossible until multi-value proposal has landed + } + if len(wi.Results) == 1 { + p = appendp(p, AGet, regAddr(REG_SP)) // address has to be before the value + } + for _, f := range wi.Params { + p = appendp(p, AGet, regAddr(REG_SP)) + f.Offset += 8 + switch f.Type { + case obj.WasmI32: + p = appendp(p, AI32Load, constAddr(f.Offset)) + case obj.WasmI64: + p = appendp(p, AI64Load, constAddr(f.Offset)) + case obj.WasmF32: + p = appendp(p, AF32Load, constAddr(f.Offset)) + case obj.WasmF64: + p = appendp(p, AF64Load, constAddr(f.Offset)) + case obj.WasmPtr: + p = appendp(p, AI64Load, constAddr(f.Offset)) + p = appendp(p, AI32WrapI64) + default: + panic("bad param type") + } + } + p = appendp(p, ACall, to) + p.Mark = CallWasmImport + if len(wi.Results) == 1 { + f := wi.Results[0] + f.Offset += 8 + switch f.Type { + case obj.WasmI32: + p = appendp(p, AI32Store, constAddr(f.Offset)) + case obj.WasmI64: + p = appendp(p, AI64Store, constAddr(f.Offset)) + case obj.WasmF32: + p = appendp(p, AF32Store, constAddr(f.Offset)) + case obj.WasmF64: + p = appendp(p, AF64Store, constAddr(f.Offset)) + case obj.WasmPtr: + p = appendp(p, AI64ExtendI32U) + p = appendp(p, AI64Store, constAddr(f.Offset)) + default: + panic("bad result type") + } + } + } + p = appendp(p, obj.ARET) + } + if s.Func().Text.From.Sym.Wrapper() { // if g._panic != nil && g._panic.argp == FP { // g._panic.argp = bottom-of-frame @@ -714,12 +779,6 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { default: panic("bad MOV type") } - - case ACallImport: - p.As = obj.ANOP - p = appendp(p, AGet, regAddr(REG_SP)) - p = appendp(p, ACall, obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: s}) - p.Mark = WasmImport } } @@ -1020,7 +1079,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { r.Siz = 1 // actually variable sized r.Off = int32(w.Len()) r.Type = objabi.R_CALL - if p.Mark&WasmImport != 0 { + if p.Mark&CallWasmImport != 0 { r.Type = objabi.R_WASMIMPORT } r.Sym = p.To.Sym @@ -1065,7 +1124,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { case AI32Load, AI64Load, AF32Load, AF64Load, AI32Load8S, AI32Load8U, AI32Load16S, AI32Load16U, AI64Load8S, AI64Load8U, AI64Load16S, AI64Load16U, AI64Load32S, AI64Load32U: if p.From.Offset < 0 { - panic("negative offset for *Load") + panic(fmt.Sprintf("negative offset (%v) for *Load", p.From.Offset)) } if p.From.Type != obj.TYPE_CONST { panic("bad type for *Load") diff --git a/src/cmd/link/internal/loader/loader.go b/src/cmd/link/internal/loader/loader.go index 8e1575a5a29df2..5357ee42027267 100644 --- a/src/cmd/link/internal/loader/loader.go +++ b/src/cmd/link/internal/loader/loader.go @@ -1601,6 +1601,28 @@ func (l *Loader) Aux(i Sym, j int) Aux { return Aux{r.Aux(li, j), r, l} } +// WasmImportSym returns the auxilary WebAssembly import symbol associated with +// a given function symbol. The aux sym only exists for Go function stubs that +// have been annotated with the //go:wasmimport directive. The aux sym +// contains the information necessary for the linker to add a WebAssembly +// import statement. +// (https://webassembly.github.io/spec/core/syntax/modules.html#imports) +func (l *Loader) WasmImportSym(fnSymIdx Sym) Sym { + if l.SymType(fnSymIdx) != sym.STEXT { + log.Fatalf("error: non-function sym %d/%s t=%s passed to WasmImportSym", fnSymIdx, l.SymName(fnSymIdx), l.SymType(fnSymIdx).String()) + } + r, li := l.toLocal(fnSymIdx) + auxs := r.Auxs(li) + for i := range auxs { + a := &auxs[i] + switch a.Type() { + case goobj.AuxWasmImport: + return l.resolve(r, a.Sym()) + } + } + panic("WasmImportSym called for func without aux Wasm import sym") +} + // GetFuncDwarfAuxSyms collects and returns the auxiliary DWARF // symbols associated with a given function symbol. Prior to the // introduction of the loader, this was done purely using name diff --git a/src/cmd/link/internal/wasm/asm.go b/src/cmd/link/internal/wasm/asm.go index 99018c807916bc..c793a008a51018 100644 --- a/src/cmd/link/internal/wasm/asm.go +++ b/src/cmd/link/internal/wasm/asm.go @@ -6,10 +6,13 @@ package wasm import ( "bytes" + "cmd/internal/obj" "cmd/internal/objabi" "cmd/link/internal/ld" "cmd/link/internal/loader" "cmd/link/internal/sym" + "encoding/binary" + "fmt" "internal/buildcfg" "io" "regexp" @@ -44,9 +47,10 @@ func gentext(ctxt *ld.Link, ldr *loader.Loader) { } type wasmFunc struct { - Name string - Type uint32 - Code []byte + Module string + Name string + Type uint32 + Code []byte } type wasmFuncType struct { @@ -54,6 +58,47 @@ type wasmFuncType struct { Results []byte } +func readWasmImport(ldr *loader.Loader, s loader.Sym) obj.WasmImport { + reportError := func(err error) { panic(fmt.Sprintf("failed to read WASM import in sym %v: %v", s, err)) } + + r := bytes.NewReader(ldr.Data(s)) + readBinary := func(v interface{}) { + if err := binary.Read(r, binary.LittleEndian, v); err != nil { + reportError(err) + } + } + readUint32 := func() (v uint32) { readBinary(&v); return } + readInt64 := func() (v int64) { readBinary(&v); return } + readByte := func() byte { + b, err := r.ReadByte() + if err != nil { + reportError(err) + } + return b + } + readString := func() string { + buf := make([]byte, readUint32()) + if _, err := io.ReadFull(r, buf); err != nil { + reportError(err) + } + return string(buf) + } + wi := obj.WasmImport{} + wi.Module = readString() + wi.Name = readString() + wi.Params = make([]obj.WasmField, readUint32()) + for i := range wi.Params { + wi.Params[i].Type = obj.WasmFieldType(readByte()) + wi.Params[i].Offset = readInt64() + } + wi.Results = make([]obj.WasmField, readUint32()) + for i := range wi.Results { + wi.Results[i].Type = obj.WasmFieldType(readByte()) + wi.Results[i].Offset = readInt64() + } + return wi +} + var wasmFuncTypes = map[string]*wasmFuncType{ "_rt0_wasm_js": {Params: []byte{}}, // "wasm_export_run": {Params: []byte{I32, I32}}, // argc, argv @@ -128,22 +173,26 @@ func asmb2(ctxt *ld.Link, ldr *loader.Loader) { } // collect host imports (functions that get imported from the WebAssembly host, usually JavaScript) - hostImports := []*wasmFunc{ - { - Name: "debug", - Type: lookupType(&wasmFuncType{Params: []byte{I32}}, &types), - }, - } + // we store the import index of each imported function, so the R_WASMIMPORT relocation + // can write the correct index after a `call` instruction hostImportMap := make(map[loader.Sym]int64) + // these are added as import statements to the top of the WebAssembly binary + var hostImports []*wasmFunc + for _, fn := range ctxt.Textp { relocs := ldr.Relocs(fn) for ri := 0; ri < relocs.Count(); ri++ { r := relocs.At(ri) if r.Type() == objabi.R_WASMIMPORT { - hostImportMap[r.Sym()] = int64(len(hostImports)) + wi := readWasmImport(ldr, ldr.WasmImportSym(fn)) + hostImportMap[fn] = int64(len(hostImports)) hostImports = append(hostImports, &wasmFunc{ - Name: ldr.SymName(r.Sym()), - Type: lookupType(&wasmFuncType{Params: []byte{I32}}, &types), + Module: wi.Module, + Name: wi.Name, + Type: lookupType(&wasmFuncType{ + Params: fieldsToTypes(wi.Params), + Results: fieldsToTypes(wi.Results), + }, &types), }) } } @@ -280,7 +329,7 @@ func writeImportSec(ctxt *ld.Link, hostImports []*wasmFunc) { writeUleb128(ctxt.Out, uint64(len(hostImports))) // number of imports for _, fn := range hostImports { - writeName(ctxt.Out, "go") // provided by the import object in wasm_exec.js + writeName(ctxt.Out, fn.Module) writeName(ctxt.Out, fn.Name) ctxt.Out.WriteByte(0x00) // func import writeUleb128(ctxt.Out, uint64(fn.Type)) @@ -602,3 +651,20 @@ func writeSleb128(w io.ByteWriter, v int64) { w.WriteByte(c) } } + +func fieldsToTypes(fields []obj.WasmField) []byte { + b := make([]byte, len(fields)) + for i, f := range fields { + switch f.Type { + case obj.WasmI32, obj.WasmPtr: + b[i] = I32 + case obj.WasmI64: + b[i] = I64 + case obj.WasmF32: + b[i] = F32 + case obj.WasmF64: + b[i] = F64 + } + } + return b +} diff --git a/src/runtime/lock_js.go b/src/runtime/lock_js.go index f71e7a2b4a59cd..39eae6656fd404 100644 --- a/src/runtime/lock_js.go +++ b/src/runtime/lock_js.go @@ -6,9 +6,7 @@ package runtime -import ( - _ "unsafe" -) +import _ "unsafe" // js/wasm has no support for threads yet. There is no preemption. @@ -232,9 +230,13 @@ func pause(newsp uintptr) // scheduleTimeoutEvent tells the WebAssembly environment to trigger an event after ms milliseconds. // It returns a timer id that can be used with clearTimeoutEvent. +// +//go:wasmimport go runtime.scheduleTimeoutEvent func scheduleTimeoutEvent(ms int64) int32 // clearTimeoutEvent clears a timeout event scheduled by scheduleTimeoutEvent. +// +//go:wasmimport go runtime.clearTimeoutEvent func clearTimeoutEvent(id int32) // handleEvent gets invoked on a call from JavaScript into Go. It calls the event handler of the syscall/js package diff --git a/src/runtime/mem_js.go b/src/runtime/mem_js.go index e87c5f26ae23a1..d5be9bb5756dc5 100644 --- a/src/runtime/mem_js.go +++ b/src/runtime/mem_js.go @@ -79,6 +79,8 @@ func growMemory(pages int32) int32 // resetMemoryDataView signals the JS front-end that WebAssembly's memory.grow instruction has been used. // This allows the front-end to replace the old DataView object with a new one. +// +//go:wasmimport go runtime.resetMemoryDataView func resetMemoryDataView() func sysMapOS(v unsafe.Pointer, n uintptr) { diff --git a/src/runtime/os_js.go b/src/runtime/os_js.go index 7481fb92bf8836..af5ac20dd5bfe5 100644 --- a/src/runtime/os_js.go +++ b/src/runtime/os_js.go @@ -27,6 +27,7 @@ func closefd(fd int32) int32 { panic("not implemented") func read(fd int32, p unsafe.Pointer, n int32) int32 { panic("not implemented") } //go:noescape +//go:wasmimport go runtime.wasmWrite func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) func usleep(usec uint32) @@ -117,6 +118,7 @@ func crash() { *(*int32)(nil) = 0 } +//go:wasmimport go runtime.getRandomData func getRandomData(r []byte) func goenvs() { diff --git a/src/runtime/stubs3.go b/src/runtime/stubs3.go index 891663b1109b19..dc6e0b8742f77a 100644 --- a/src/runtime/stubs3.go +++ b/src/runtime/stubs3.go @@ -6,4 +6,5 @@ package runtime +//go:wasmimport go runtime.nanotime1 func nanotime1() int64 diff --git a/src/runtime/sys_wasm.go b/src/runtime/sys_wasm.go index bf5756984ae1d6..17484a273bc76e 100644 --- a/src/runtime/sys_wasm.go +++ b/src/runtime/sys_wasm.go @@ -21,6 +21,7 @@ func wasmDiv() func wasmTruncS() func wasmTruncU() +//go:wasmimport go runtime.wasmExit func wasmExit(code int32) // adjust Gobuf as it if executed a call to fn with context ctxt diff --git a/src/runtime/sys_wasm.s b/src/runtime/sys_wasm.s index f706e00ab285b3..bd60e1d419be96 100644 --- a/src/runtime/sys_wasm.s +++ b/src/runtime/sys_wasm.s @@ -101,35 +101,3 @@ TEXT runtime·growMemory(SB), NOSPLIT, $0 GrowMemory I32Store ret+8(FP) RET - -TEXT ·resetMemoryDataView(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·wasmExit(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·wasmWrite(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·nanotime1(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·walltime(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·scheduleTimeoutEvent(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·clearTimeoutEvent(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·getRandomData(SB), NOSPLIT, $0 - CallImport - RET diff --git a/src/runtime/timestub2.go b/src/runtime/timestub2.go index b9a5cc6345ee2d..4fe8b9b0056541 100644 --- a/src/runtime/timestub2.go +++ b/src/runtime/timestub2.go @@ -6,4 +6,5 @@ package runtime +//go:wasmimport go runtime.walltime func walltime() (sec int64, nsec int32) diff --git a/src/syscall/js/func.go b/src/syscall/js/func.go index cc9497236450bb..277b2880f68c12 100644 --- a/src/syscall/js/func.go +++ b/src/syscall/js/func.go @@ -6,7 +6,10 @@ package js -import "sync" +import ( + "sync" + _ "unsafe" +) var ( funcsMu sync.Mutex diff --git a/src/syscall/js/js.go b/src/syscall/js/js.go index 2f4f5adda02738..ed00e0de82ab06 100644 --- a/src/syscall/js/js.go +++ b/src/syscall/js/js.go @@ -58,6 +58,7 @@ func makeValue(r ref) Value { return Value{ref: r, gcPtr: gcPtr} } +//go:wasmimport go syscall/js.finalizeRef func finalizeRef(r ref) func predefValue(id uint32, typeFlag byte) Value { @@ -209,6 +210,7 @@ func ValueOf(x any) Value { } } +//go:wasmimport go syscall/js.stringVal func stringVal(x string) ref // Type represents the JavaScript type of a Value. @@ -292,6 +294,7 @@ func (v Value) Get(p string) Value { return r } +//go:wasmimport go syscall/js.valueGet func valueGet(v ref, p string) ref // Set sets the JavaScript property p of value v to ValueOf(x). @@ -306,6 +309,7 @@ func (v Value) Set(p string, x any) { runtime.KeepAlive(xv) } +//go:wasmimport go syscall/js.valueSet func valueSet(v ref, p string, x ref) // Delete deletes the JavaScript property p of value v. @@ -318,6 +322,7 @@ func (v Value) Delete(p string) { runtime.KeepAlive(v) } +//go:wasmimport go syscall/js.valueDelete func valueDelete(v ref, p string) // Index returns JavaScript index i of value v. @@ -331,6 +336,7 @@ func (v Value) Index(i int) Value { return r } +//go:wasmimport go syscall/js.valueIndex func valueIndex(v ref, i int) ref // SetIndex sets the JavaScript index i of value v to ValueOf(x). @@ -345,6 +351,7 @@ func (v Value) SetIndex(i int, x any) { runtime.KeepAlive(xv) } +//go:wasmimport go syscall/js.valueSetIndex func valueSetIndex(v ref, i int, x ref) func makeArgs(args []any) ([]Value, []ref) { @@ -369,6 +376,7 @@ func (v Value) Length() int { return r } +//go:wasmimport go syscall/js.valueLength func valueLength(v ref) int // Call does a JavaScript call to the method m of value v with the given arguments. @@ -391,6 +399,7 @@ func (v Value) Call(m string, args ...any) Value { return makeValue(res) } +//go:wasmimport go syscall/js.valueCall func valueCall(v ref, m string, args []ref) (ref, bool) // Invoke does a JavaScript call of the value v with the given arguments. @@ -410,6 +419,7 @@ func (v Value) Invoke(args ...any) Value { return makeValue(res) } +//go:wasmimport go syscall/js.valueInvoke func valueInvoke(v ref, args []ref) (ref, bool) // New uses JavaScript's "new" operator with value v as constructor and the given arguments. @@ -429,6 +439,7 @@ func (v Value) New(args ...any) Value { return makeValue(res) } +//go:wasmimport go syscall/js.valueNew func valueNew(v ref, args []ref) (ref, bool) func (v Value) isNumber() bool { @@ -528,8 +539,10 @@ func jsString(v Value) string { return string(b) } +//go:wasmimport go syscall/js.valuePrepareString func valuePrepareString(v ref) (ref, int) +//go:wasmimport go syscall/js.valueLoadString func valueLoadString(v ref, b []byte) // InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator. @@ -540,6 +553,7 @@ func (v Value) InstanceOf(t Value) bool { return r } +//go:wasmimport go syscall/js.valueInstanceOf func valueInstanceOf(v ref, t ref) bool // A ValueError occurs when a Value method is invoked on @@ -566,6 +580,7 @@ func CopyBytesToGo(dst []byte, src Value) int { return n } +//go:wasmimport go syscall/js.copyBytesToGo func copyBytesToGo(dst []byte, src ref) (int, bool) // CopyBytesToJS copies bytes from src to dst. @@ -580,4 +595,5 @@ func CopyBytesToJS(dst Value, src []byte) int { return n } +//go:wasmimport go syscall/js.copyBytesToJS func copyBytesToJS(dst ref, src []byte) (int, bool) diff --git a/src/syscall/js/js_js.s b/src/syscall/js/js_js.s index 47ad6b83e56398..abdccc9cb0cec7 100644 --- a/src/syscall/js/js_js.s +++ b/src/syscall/js/js_js.s @@ -2,68 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -#include "textflag.h" - -TEXT ·finalizeRef(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·stringVal(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueGet(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueSet(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueDelete(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueIndex(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueSetIndex(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueCall(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueInvoke(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueNew(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueLength(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valuePrepareString(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueLoadString(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·valueInstanceOf(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·copyBytesToGo(SB), NOSPLIT, $0 - CallImport - RET - -TEXT ·copyBytesToJS(SB), NOSPLIT, $0 - CallImport - RET +// The runtime package uses //go:linkname to push the setEventHandler to this +// package. To prevent the go tool from passing -complete to the compile tool, +// this file must remain stubbed out. diff --git a/src/syscall/js/js_test.go b/src/syscall/js/js_test.go index f860a5bb50581b..8823421b894bd0 100644 --- a/src/syscall/js/js_test.go +++ b/src/syscall/js/js_test.go @@ -44,6 +44,18 @@ var dummys = js.Global().Call("eval", `({ objBooleanFalse: new Boolean(false), })`) +//go:wasmimport _gotest add +func testAdd(uint32, uint32) uint32 + +func TestWasmImport(t *testing.T) { + a := uint32(3) + b := uint32(5) + want := a + b + if got := testAdd(a, b); got != want { + t.Errorf("got %v, want %v", got, want) + } +} + func TestBool(t *testing.T) { want := true o := dummys.Get("someBool")