diff options
| author | Evan Phoenix <evan@phx.io> | 2023-01-22 15:30:59 -0800 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2023-03-02 05:28:55 +0000 |
| commit | 02411bcd7c8eda9c694a5755aff0a516d4983952 (patch) | |
| tree | 71106b1c22a459f53d59f68751267a35c4ddf65a /src/cmd | |
| parent | af9f21289fff0c513df3a785c97d8ca35e1829b2 (diff) | |
| download | go-02411bcd7c8eda9c694a5755aff0a516d4983952.tar.xz | |
all: implement wasmimport directive
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 #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
Reviewed-on: https://go-review.googlesource.com/c/go/+/463018
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Reviewed-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Diffstat (limited to 'src/cmd')
| -rw-r--r-- | src/cmd/compile/internal/gc/compile.go | 4 | ||||
| -rw-r--r-- | src/cmd/compile/internal/ir/func.go | 10 | ||||
| -rw-r--r-- | src/cmd/compile/internal/ir/sizeof_test.go | 2 | ||||
| -rw-r--r-- | src/cmd/compile/internal/noder/linker.go | 11 | ||||
| -rw-r--r-- | src/cmd/compile/internal/noder/noder.go | 37 | ||||
| -rw-r--r-- | src/cmd/compile/internal/noder/reader.go | 12 | ||||
| -rw-r--r-- | src/cmd/compile/internal/noder/writer.go | 26 | ||||
| -rw-r--r-- | src/cmd/compile/internal/ssagen/abi.go | 88 | ||||
| -rw-r--r-- | src/cmd/internal/goobj/objfile.go | 1 | ||||
| -rw-r--r-- | src/cmd/internal/obj/link.go | 74 | ||||
| -rw-r--r-- | src/cmd/internal/obj/objfile.go | 17 | ||||
| -rw-r--r-- | src/cmd/internal/obj/sym.go | 12 | ||||
| -rw-r--r-- | src/cmd/internal/obj/wasm/a.out.go | 3 | ||||
| -rw-r--r-- | src/cmd/internal/obj/wasm/anames.go | 3 | ||||
| -rw-r--r-- | src/cmd/internal/obj/wasm/wasmobj.go | 135 | ||||
| -rw-r--r-- | src/cmd/link/internal/loader/loader.go | 23 | ||||
| -rw-r--r-- | src/cmd/link/internal/wasm/asm.go | 118 |
17 files changed, 533 insertions, 43 deletions
diff --git a/src/cmd/compile/internal/gc/compile.go b/src/cmd/compile/internal/gc/compile.go index cfce77d828..4795297e7e 100644 --- a/src/cmd/compile/internal/gc/compile.go +++ b/src/cmd/compile/internal/gc/compile.go @@ -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) diff --git a/src/cmd/compile/internal/ir/func.go b/src/cmd/compile/internal/ir/func.go index 967ebb02c2..76ab952157 100644 --- a/src/cmd/compile/internal/ir/func.go +++ b/src/cmd/compile/internal/ir/func.go @@ -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 { diff --git a/src/cmd/compile/internal/ir/sizeof_test.go b/src/cmd/compile/internal/ir/sizeof_test.go index 754d1a8de0..307f40d484 100644 --- a/src/cmd/compile/internal/ir/sizeof_test.go +++ b/src/cmd/compile/internal/ir/sizeof_test.go @@ -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}, } diff --git a/src/cmd/compile/internal/noder/linker.go b/src/cmd/compile/internal/noder/linker.go index 0f39fdec05..44de017ae5 100644 --- a/src/cmd/compile/internal/noder/linker.go +++ b/src/cmd/compile/internal/noder/linker.go @@ -5,6 +5,7 @@ package noder import ( + "internal/buildcfg" "internal/pkgbits" "io" @@ -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) diff --git a/src/cmd/compile/internal/noder/noder.go b/src/cmd/compile/internal/noder/noder.go index 10619bf569..16113e37a3 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" @@ -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 { @@ -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. @@ -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) { diff --git a/src/cmd/compile/internal/noder/reader.go b/src/cmd/compile/internal/noder/reader.go index b7605e9317..e4ab80b2d0 100644 --- a/src/cmd/compile/internal/noder/reader.go +++ b/src/cmd/compile/internal/noder/reader.go @@ -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() { diff --git a/src/cmd/compile/internal/noder/writer.go b/src/cmd/compile/internal/noder/writer.go index da5c1e910d..5dd8d1de2d 100644 --- a/src/cmd/compile/internal/noder/writer.go +++ b/src/cmd/compile/internal/noder/writer.go @@ -6,6 +6,7 @@ package noder import ( "fmt" + "internal/buildcfg" "internal/pkgbits" "cmd/compile/internal/base" @@ -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 @@ -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") } } @@ -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) @@ -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) diff --git a/src/cmd/compile/internal/ssagen/abi.go b/src/cmd/compile/internal/ssagen/abi.go index fa26ae1f06..9c725b898d 100644 --- a/src/cmd/compile/internal/ssagen/abi.go +++ b/src/cmd/compile/internal/ssagen/abi.go @@ -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 @@ -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 +} diff --git a/src/cmd/internal/goobj/objfile.go b/src/cmd/internal/goobj/objfile.go index 547b826495..0364f856cf 100644 --- a/src/cmd/internal/goobj/objfile.go +++ b/src/cmd/internal/goobj/objfile.go @@ -442,6 +442,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 d153afbfae..077562a267 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 73c29d9686..78fa4c1076 100644 --- a/src/cmd/internal/obj/objfile.go +++ b/src/cmd/internal/obj/objfile.go @@ -605,7 +605,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) + } } } @@ -703,6 +708,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 } @@ -759,8 +770,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 e0817d5f74..4a01af3927 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 83ce0a6738..0262630d5a 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 c9bc15d270..6f1a662960 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 96a2ef4a6f..fd0faec84b 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, @@ -135,6 +134,14 @@ const ( WasmImport = 1 << 0 ) +const ( + // This is a special wasm module name that when used as the module name + // in //go:wasmimport will cause the generated code to pass the stack pointer + // directly to the imported function. In other words, any function that + // uses the gojs module understands the internal Go WASM ABI directly. + GojsModule = "gojs" +) + func instinit(ctxt *obj.Link) { morestack = ctxt.Lookup("runtime.morestack") morestackNoCtxt = ctxt.Lookup("runtime.morestack_noctxt") @@ -177,7 +184,121 @@ 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 s.Func().Text.From.Sym.Wrapper() { + // If the function exits just to call out to a wasmimport, then + // generate the code to translate from our internal Go-stack + // based call convention to the native webassembly call convention. + 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 the module that the import is for is our magic "gojs" module, then this + // indicates that the called function understands the Go stack-based call convention + // so we just pass the stack pointer to it, knowing it will read the params directly + // off the stack and push the results into memory based on the stack pointer. + if wi.Module == GojsModule { + // The called function has a signature of 'func(sp int)'. It has access to the memory + // value somewhere to be able to address the memory based on the "sp" value. + + p = appendp(p, AGet, regAddr(REG_SP)) + p = appendp(p, ACall, to) + + p.Mark = WasmImport + } else { + if len(wi.Results) > 1 { + // TODO(evanphx) implement support for the multi-value proposal: + // https://github.com/WebAssembly/multi-value/blob/master/proposals/multi-value/Overview.md + panic("invalid results type") // impossible until multi-value proposal has landed + } + if len(wi.Results) == 1 { + // If we have a result (rather than returning nothing at all), then + // we'll write the result to the Go stack relative to the current stack pointer. + // We cache the current stack pointer value on the wasm stack here and then use + // it after the Call instruction to store the result. + p = appendp(p, AGet, regAddr(REG_SP)) + } + for _, f := range wi.Params { + // Each load instructions will consume the value of sp on the stack, so + // we need to read sp for each param. WASM appears to not have a stack dup instruction + // (a strange ommission for a stack-based VM), if it did, we'd be using the dup here. + p = appendp(p, AGet, regAddr(REG_SP)) + + // Offset is the location of the param on the Go stack (ie relative to sp). + // Because of our call convention, the parameters are located an additional 8 bytes + // from sp because we store the return address as a int64 at the bottom of the stack. + // Ie the stack looks like [return_addr, param3, param2, param1, etc] + + // Ergo, we add 8 to the true byte offset of the param to skip the return address. + loadOffset := f.Offset + 8 + + // We're reading the value from the Go stack onto the WASM stack and leaving it there + // for CALL to pick them up. + switch f.Type { + case obj.WasmI32: + p = appendp(p, AI32Load, constAddr(loadOffset)) + case obj.WasmI64: + p = appendp(p, AI64Load, constAddr(loadOffset)) + case obj.WasmF32: + p = appendp(p, AF32Load, constAddr(loadOffset)) + case obj.WasmF64: + p = appendp(p, AF64Load, constAddr(loadOffset)) + case obj.WasmPtr: + p = appendp(p, AI64Load, constAddr(loadOffset)) + p = appendp(p, AI32WrapI64) + default: + panic("bad param type") + } + } + + // The call instruction is marked as being for a wasm import so that a later phase + // will generate relocation information that allows us to patch this with then + // offset of the imported function in the wasm imports. + p = appendp(p, ACall, to) + p.Mark = WasmImport + + if len(wi.Results) == 1 { + f := wi.Results[0] + + // Much like with the params, we need to adjust the offset we store the result value + // to by 8 bytes to account for the return address on the Go stack. + storeOffset := f.Offset + 8 + + // This code is paired the code above that reads the stack pointer onto the wasm + // stack. We've done this so we have a consistent view of the sp value as it might + // be manipulated by the call and we want to ignore that manipulation here. + switch f.Type { + case obj.WasmI32: + p = appendp(p, AI32Store, constAddr(storeOffset)) + case obj.WasmI64: + p = appendp(p, AI64Store, constAddr(storeOffset)) + case obj.WasmF32: + p = appendp(p, AF32Store, constAddr(storeOffset)) + case obj.WasmF64: + p = appendp(p, AF64Store, constAddr(storeOffset)) + case obj.WasmPtr: + p = appendp(p, AI64ExtendI32U) + p = appendp(p, AI64Store, constAddr(storeOffset)) + default: + panic("bad result type") + } + } + } + + p = appendp(p, obj.ARET) + + // It should be 0 already, but we'll set it to 0 anyway just to be sure + // that the code below which adds frame expansion code to the function body + // isn't run. We don't want the frame expansion code because our function + // body is just the code to translate and call the imported function. + framesize = 0 + } else if s.Func().Text.From.Sym.Wrapper() { // if g._panic != nil && g._panic.argp == FP { // g._panic.argp = bottom-of-frame // } @@ -241,7 +362,9 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { p.Spadj = int32(framesize) } - needMoreStack := !s.Func().Text.From.Sym.NoSplit() + // If the framesize is 0, then imply nosplit because it's a specially + // generated function. + needMoreStack := framesize > 0 && !s.Func().Text.From.Sym.NoSplit() // If the maymorestack debug option is enabled, insert the // call to maymorestack *before* processing resume points so @@ -707,12 +830,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 } } diff --git a/src/cmd/link/internal/loader/loader.go b/src/cmd/link/internal/loader/loader.go index e3ee819a9d..fa8c0c6b20 100644 --- a/src/cmd/link/internal/loader/loader.go +++ b/src/cmd/link/internal/loader/loader.go @@ -1618,6 +1618,29 @@ func (l *Loader) Aux(i Sym, j int) Aux { return Aux{r.Aux(li, j), r, l} } +// WasmImportSym returns the auxiliary 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, bool) { + 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()), true + } + } + + return 0, false +} + // 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 b5685701f2..30d0dc7ff2 100644 --- a/src/cmd/link/internal/wasm/asm.go +++ b/src/cmd/link/internal/wasm/asm.go @@ -6,10 +6,14 @@ package wasm import ( "bytes" + "cmd/internal/obj" + "cmd/internal/obj/wasm" "cmd/internal/objabi" "cmd/link/internal/ld" "cmd/link/internal/loader" "cmd/link/internal/sym" + "encoding/binary" + "fmt" "internal/buildcfg" "io" "regexp" @@ -44,9 +48,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 +59,59 @@ 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)) } + + data := ldr.Data(s) + + readUint32 := func() (v uint32) { + v = binary.LittleEndian.Uint32(data) + data = data[4:] + return + } + + readUint64 := func() (v uint64) { + v = binary.LittleEndian.Uint64(data) + data = data[8:] + return + } + + readByte := func() byte { + if len(data) == 0 { + reportError(io.EOF) + } + + b := data[0] + data = data[1:] + return b + } + + readString := func() string { + n := readUint32() + + s := string(data[:n]) + + data = data[n:] + + return s + } + + var 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 = int64(readUint64()) + } + wi.Results = make([]obj.WasmField, readUint32()) + for i := range wi.Results { + wi.Results[i].Type = obj.WasmFieldType(readByte()) + wi.Results[i].Offset = int64(readUint64()) + } + return wi +} + var wasmFuncTypes = map[string]*wasmFuncType{ "_rt0_wasm_js": {Params: []byte{}}, // "wasm_export_run": {Params: []byte{I32, I32}}, // argc, argv @@ -136,23 +194,30 @@ 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 + // these are added as import statements to the top of the WebAssembly binary + var hostImports []*wasmFunc hostImportMap := make(map[loader.Sym]int64) 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)) - hostImports = append(hostImports, &wasmFunc{ - Name: ldr.SymName(r.Sym()), - Type: lookupType(&wasmFuncType{Params: []byte{I32}}, &types), - }) + if lsym, ok := ldr.WasmImportSym(fn); ok { + wi := readWasmImport(ldr, lsym) + hostImportMap[fn] = int64(len(hostImports)) + hostImports = append(hostImports, &wasmFunc{ + Module: wi.Module, + Name: wi.Name, + Type: lookupType(&wasmFuncType{ + Params: fieldsToTypes(wi.Params), + Results: fieldsToTypes(wi.Results), + }, &types), + }) + } else { + panic(fmt.Sprintf("missing wasm symbol for %s", ldr.SymName(r.Sym()))) + } } } } @@ -288,7 +353,11 @@ 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 + if fn.Module != "" { + writeName(ctxt.Out, fn.Module) + } else { + writeName(ctxt.Out, wasm.GojsModule) // provided by the import object in wasm_exec.js + } writeName(ctxt.Out, fn.Name) ctxt.Out.WriteByte(0x00) // func import writeUleb128(ctxt.Out, uint64(fn.Type)) @@ -610,3 +679,22 @@ 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 + default: + panic(fmt.Sprintf("fieldsToTypes: unknown field type: %d", f.Type)) + } + } + return b +} |
