Skip to content

Commit 02411bc

Browse files
evanphxvedantroyneelancejohanbrandhorst
authored andcommitted
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>
1 parent af9f212 commit 02411bc

29 files changed

+585
-145
lines changed

Diff for: misc/wasm/wasm_exec.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@
113113
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
114114
}
115115

116+
const setInt32 = (addr, v) => {
117+
this.mem.setUint32(addr + 0, v, true);
118+
}
119+
116120
const getInt64 = (addr) => {
117121
const low = this.mem.getUint32(addr + 0, true);
118122
const high = this.mem.getInt32(addr + 4, true);
@@ -206,7 +210,10 @@
206210

207211
const timeOrigin = Date.now() - performance.now();
208212
this.importObject = {
209-
go: {
213+
_gotest: {
214+
add: (a, b) => a + b,
215+
},
216+
gojs: {
210217
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
211218
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
212219
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).

Diff for: src/cmd/compile/internal/gc/compile.go

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func enqueueFunc(fn *ir.Func) {
4343
return // we'll get this as part of its enclosing function
4444
}
4545

46+
if ssagen.CreateWasmImportWrapper(fn) {
47+
return
48+
}
49+
4650
if len(fn.Body) == 0 {
4751
// Initialize ABI wrappers if necessary.
4852
ir.InitLSym(fn, false)

Diff for: src/cmd/compile/internal/ir/func.go

+10
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ type Func struct {
133133
// For wrapper functions, WrappedFunc point to the original Func.
134134
// Currently only used for go/defer wrappers.
135135
WrappedFunc *Func
136+
137+
// WasmImport is used by the //go:wasmimport directive to store info about
138+
// a WebAssembly function import.
139+
WasmImport *WasmImport
140+
}
141+
142+
// WasmImport stores metadata associated with the //go:wasmimport pragma.
143+
type WasmImport struct {
144+
Module string
145+
Name string
136146
}
137147

138148
func NewFunc(pos src.XPos) *Func {

Diff for: src/cmd/compile/internal/ir/sizeof_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestSizeof(t *testing.T) {
2020
_32bit uintptr // size on 32bit platforms
2121
_64bit uintptr // size on 64bit platforms
2222
}{
23-
{Func{}, 184, 320},
23+
{Func{}, 188, 328},
2424
{Name{}, 100, 176},
2525
}
2626

Diff for: src/cmd/compile/internal/noder/linker.go

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package noder
66

77
import (
8+
"internal/buildcfg"
89
"internal/pkgbits"
910
"io"
1011

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

273+
if buildcfg.GOARCH == "wasm" {
274+
if name.Func.WasmImport != nil {
275+
w.String(name.Func.WasmImport.Module)
276+
w.String(name.Func.WasmImport.Name)
277+
} else {
278+
w.String("")
279+
w.String("")
280+
}
281+
}
282+
272283
// Relocated extension data.
273284
w.Bool(true)
274285

Diff for: src/cmd/compile/internal/noder/noder.go

+34-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package noder
77
import (
88
"errors"
99
"fmt"
10+
"internal/buildcfg"
1011
"os"
1112
"path/filepath"
1213
"runtime"
@@ -166,9 +167,17 @@ var allowedStdPragmas = map[string]bool{
166167

167168
// *pragmas is the value stored in a syntax.pragmas during parsing.
168169
type pragmas struct {
169-
Flag ir.PragmaFlag // collected bits
170-
Pos []pragmaPos // position of each individual flag
171-
Embeds []pragmaEmbed
170+
Flag ir.PragmaFlag // collected bits
171+
Pos []pragmaPos // position of each individual flag
172+
Embeds []pragmaEmbed
173+
WasmImport *WasmImport
174+
}
175+
176+
// WasmImport stores metadata associated with the //go:wasmimport pragma
177+
type WasmImport struct {
178+
Pos syntax.Pos
179+
Module string
180+
Name string
172181
}
173182

174183
type pragmaPos struct {
@@ -192,6 +201,9 @@ func (p *noder) checkUnusedDuringParse(pragma *pragmas) {
192201
p.error(syntax.Error{Pos: e.Pos, Msg: "misplaced go:embed directive"})
193202
}
194203
}
204+
if pragma.WasmImport != nil {
205+
p.error(syntax.Error{Pos: pragma.WasmImport.Pos, Msg: "misplaced go:wasmimport directive"})
206+
}
195207
}
196208

197209
// 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
219231
}
220232

221233
switch {
234+
case strings.HasPrefix(text, "go:wasmimport "):
235+
f := strings.Fields(text)
236+
if len(f) != 3 {
237+
p.error(syntax.Error{Pos: pos, Msg: "usage: //go:wasmimport importmodule importname"})
238+
break
239+
}
240+
if !base.Flag.CompilingRuntime && base.Ctxt.Pkgpath != "syscall/js" && base.Ctxt.Pkgpath != "syscall/js_test" {
241+
p.error(syntax.Error{Pos: pos, Msg: "//go:wasmimport directive cannot be used outside of runtime or syscall/js"})
242+
break
243+
}
244+
245+
if buildcfg.GOARCH == "wasm" {
246+
// Only actually use them if we're compiling to WASM though.
247+
pragma.WasmImport = &WasmImport{
248+
Pos: pos,
249+
Module: f[1],
250+
Name: f[2],
251+
}
252+
}
222253
case strings.HasPrefix(text, "go:linkname "):
223254
f := strings.Fields(text)
224255
if !(2 <= len(f) && len(f) <= 3) {

Diff for: src/cmd/compile/internal/noder/reader.go

+12
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,18 @@ func (r *reader) funcExt(name *ir.Name, method *types.Sym) {
10811081
fn.Pragma = r.pragmaFlag()
10821082
r.linkname(name)
10831083

1084+
if buildcfg.GOARCH == "wasm" {
1085+
xmod := r.String()
1086+
xname := r.String()
1087+
1088+
if xmod != "" && xname != "" {
1089+
fn.WasmImport = &ir.WasmImport{
1090+
Module: xmod,
1091+
Name: xname,
1092+
}
1093+
}
1094+
}
1095+
10841096
typecheck.Func(fn)
10851097

10861098
if r.Bool() {

Diff for: src/cmd/compile/internal/noder/writer.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package noder
66

77
import (
88
"fmt"
9+
"internal/buildcfg"
910
"internal/pkgbits"
1011

1112
"cmd/compile/internal/base"
@@ -1003,11 +1004,15 @@ func (w *writer) funcExt(obj *types2.Func) {
10031004
if pragma&ir.Systemstack != 0 && pragma&ir.Nosplit != 0 {
10041005
w.p.errorf(decl, "go:nosplit and go:systemstack cannot be combined")
10051006
}
1007+
wi := asWasmImport(decl.Pragma)
10061008

10071009
if decl.Body != nil {
10081010
if pragma&ir.Noescape != 0 {
10091011
w.p.errorf(decl, "can only use //go:noescape with external func implementations")
10101012
}
1013+
if wi != nil {
1014+
w.p.errorf(decl, "can only use //go:wasmimport with external func implementations")
1015+
}
10111016
if (pragma&ir.UintptrKeepAlive != 0 && pragma&ir.UintptrEscapes == 0) && pragma&ir.Nosplit == 0 {
10121017
// Stack growth can't handle uintptr arguments that may
10131018
// be pointers (as we don't know which are pointers
@@ -1028,7 +1033,8 @@ func (w *writer) funcExt(obj *types2.Func) {
10281033
if base.Flag.Complete || decl.Name.Value == "init" {
10291034
// Linknamed functions are allowed to have no body. Hopefully
10301035
// the linkname target has a body. See issue 23311.
1031-
if _, ok := w.p.linknames[obj]; !ok {
1036+
// Wasmimport functions are also allowed to have no body.
1037+
if _, ok := w.p.linknames[obj]; !ok && wi == nil {
10321038
w.p.errorf(decl, "missing function body")
10331039
}
10341040
}
@@ -1041,6 +1047,17 @@ func (w *writer) funcExt(obj *types2.Func) {
10411047
w.Sync(pkgbits.SyncFuncExt)
10421048
w.pragmaFlag(pragma)
10431049
w.linkname(obj)
1050+
1051+
if buildcfg.GOARCH == "wasm" {
1052+
if wi != nil {
1053+
w.String(wi.Module)
1054+
w.String(wi.Name)
1055+
} else {
1056+
w.String("")
1057+
w.String("")
1058+
}
1059+
}
1060+
10441061
w.Bool(false) // stub extension
10451062
w.Reloc(pkgbits.RelocBody, body)
10461063
w.Sync(pkgbits.SyncEOF)
@@ -2728,6 +2745,13 @@ func asPragmaFlag(p syntax.Pragma) ir.PragmaFlag {
27282745
return p.(*pragmas).Flag
27292746
}
27302747

2748+
func asWasmImport(p syntax.Pragma) *WasmImport {
2749+
if p == nil {
2750+
return nil
2751+
}
2752+
return p.(*pragmas).WasmImport
2753+
}
2754+
27312755
// isPtrTo reports whether from is the type *to.
27322756
func isPtrTo(from, to types2.Type) bool {
27332757
ptr, ok := from.(*types2.Pointer)

Diff for: src/cmd/compile/internal/ssagen/abi.go

+88
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import (
1111
"os"
1212
"strings"
1313

14+
"cmd/compile/internal/abi"
1415
"cmd/compile/internal/base"
1516
"cmd/compile/internal/ir"
17+
"cmd/compile/internal/objw"
1618
"cmd/compile/internal/typecheck"
1719
"cmd/compile/internal/types"
1820
"cmd/internal/obj"
21+
"cmd/internal/obj/wasm"
1922
)
2023

2124
// SymABIs records information provided by the assembler about symbol
@@ -336,3 +339,88 @@ func makeABIWrapper(f *ir.Func, wrapperABI obj.ABI) {
336339
typecheck.DeclContext = savedclcontext
337340
ir.CurFunc = savedcurfn
338341
}
342+
343+
// CreateWasmImportWrapper creates a wrapper for imported WASM functions to
344+
// adapt them to the Go calling convention. The body for this function is
345+
// generated in cmd/internal/obj/wasm/wasmobj.go
346+
func CreateWasmImportWrapper(fn *ir.Func) bool {
347+
if fn.WasmImport == nil {
348+
return false
349+
}
350+
if buildcfg.GOARCH != "wasm" {
351+
base.FatalfAt(fn.Pos(), "CreateWasmImportWrapper call not supported on %s: func was %v", buildcfg.GOARCH, fn)
352+
}
353+
354+
ir.InitLSym(fn, true)
355+
356+
setupWasmABI(fn)
357+
358+
pp := objw.NewProgs(fn, 0)
359+
defer pp.Free()
360+
pp.Text.To.Type = obj.TYPE_TEXTSIZE
361+
pp.Text.To.Val = int32(types.RoundUp(fn.Type().ArgWidth(), int64(types.RegSize)))
362+
// Wrapper functions never need their own stack frame
363+
pp.Text.To.Offset = 0
364+
pp.Flush()
365+
366+
return true
367+
}
368+
369+
func toWasmFields(result *abi.ABIParamResultInfo, abiParams []abi.ABIParamAssignment) []obj.WasmField {
370+
wfs := make([]obj.WasmField, len(abiParams))
371+
for i, p := range abiParams {
372+
t := p.Type
373+
switch {
374+
case t.IsInteger() && t.Size() == 4:
375+
wfs[i].Type = obj.WasmI32
376+
case t.IsInteger() && t.Size() == 8:
377+
wfs[i].Type = obj.WasmI64
378+
case t.IsFloat() && t.Size() == 4:
379+
wfs[i].Type = obj.WasmF32
380+
case t.IsFloat() && t.Size() == 8:
381+
wfs[i].Type = obj.WasmF64
382+
case t.IsPtr():
383+
wfs[i].Type = obj.WasmPtr
384+
default:
385+
base.Fatalf("wasm import has bad function signature")
386+
}
387+
wfs[i].Offset = p.FrameOffset(result)
388+
}
389+
return wfs
390+
}
391+
392+
// setupTextLSym initializes the LSym for a with-body text symbol.
393+
func setupWasmABI(f *ir.Func) {
394+
wi := obj.WasmImport{
395+
Module: f.WasmImport.Module,
396+
Name: f.WasmImport.Name,
397+
}
398+
if wi.Module == wasm.GojsModule {
399+
// Functions that are imported from the "gojs" module use a special
400+
// ABI that just accepts the stack pointer.
401+
// Example:
402+
//
403+
// //go:wasmimport gojs add
404+
// func importedAdd(a, b uint) uint
405+
//
406+
// will roughly become
407+
//
408+
// (import "gojs" "add" (func (param i32)))
409+
wi.Params = []obj.WasmField{{Type: obj.WasmI32}}
410+
} else {
411+
// All other imported functions use the normal WASM ABI.
412+
// Example:
413+
//
414+
// //go:wasmimport a_module add
415+
// func importedAdd(a, b uint) uint
416+
//
417+
// will roughly become
418+
//
419+
// (import "a_module" "add" (func (param i32 i32) (result i32)))
420+
abiConfig := AbiForBodylessFuncStackMap(f)
421+
abiInfo := abiConfig.ABIAnalyzeFuncType(f.Type().FuncType())
422+
wi.Params = toWasmFields(abiInfo, abiInfo.InParams())
423+
wi.Results = toWasmFields(abiInfo, abiInfo.OutParams())
424+
}
425+
f.LSym.Func().WasmImport = &wi
426+
}

Diff for: src/cmd/internal/goobj/objfile.go

+1
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ const (
442442
AuxPcline
443443
AuxPcinline
444444
AuxPcdata
445+
AuxWasmImport
445446
)
446447

447448
func (a *Aux) Type() uint8 { return a[0] }

0 commit comments

Comments
 (0)