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")