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/internal/obj | |
| 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/internal/obj')
| -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 |
6 files changed, 221 insertions, 23 deletions
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 } } |
