Skip to content

Commit 9b96dbe

Browse files
authored
compiler: go:linkname support method (#1152)
compiler: support `go:linkname` directive for methods This is similar to what the upstream compiler supports. This functionality is inherently unsafe, but can be useful for some certain libraries like [reflectx](https://github.com/goplusjs/reflectx/blob/main/name_js.go#L17). The first argument of the function will act as a receiver of the linked method. As long as underlying typed for the first arguments match, they will be converted at runtime, which allows linking to methods of unexported types. However, types of the other arguments are not converted, nor the signature is verified to match the linked method.
1 parent 5214468 commit 9b96dbe

File tree

9 files changed

+553
-13
lines changed

9 files changed

+553
-13
lines changed

compiler/compiler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type Decl struct {
113113
LinkingName SymName
114114
// A list of package-level JavaScript variable names this symbol needs to declare.
115115
Vars []string
116+
// NamedRecvType is method named recv declare.
117+
NamedRecvType string
116118
// JavaScript code that declares basic information about a symbol. For a type
117119
// it configures basic information about the type and its identity. For a function
118120
// or method it contains its compiled body.
@@ -318,7 +320,12 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS
318320
// This decl is referenced by a go:linkname directive, expose it to external
319321
// callers via $linkname object (declared in prelude). We are not using
320322
// $pkg to avoid clashes with exported symbols.
321-
code := fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.Vars[0])
323+
var code string
324+
if recv, method, ok := d.LinkingName.IsMethod(); ok {
325+
code = fmt.Sprintf("\t$linknames[%q] = $unsafeMethodToFunction(%v,%q,%t);\n", d.LinkingName.String(), d.NamedRecvType, method, strings.HasPrefix(recv, "*"))
326+
} else {
327+
code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.Vars[0])
328+
}
322329
if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil {
323330
return err
324331
}

compiler/linkname.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ func newSymName(o types.Object) SymName {
4141
sig := fun.Type().(*types.Signature)
4242
if recv := sig.Recv(); recv != nil {
4343
// Special case: disambiguate names for different types' methods.
44+
typ := recv.Type()
45+
if ptr, ok := typ.(*types.Pointer); ok {
46+
return SymName{
47+
PkgPath: o.Pkg().Path(),
48+
Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(),
49+
}
50+
}
4451
return SymName{
4552
PkgPath: o.Pkg().Path(),
46-
Name: recv.Type().(*types.Named).Obj().Name() + "." + o.Name(),
53+
Name: typ.(*types.Named).Obj().Name() + "." + o.Name(),
4754
}
4855
}
4956
}
@@ -55,17 +62,32 @@ func newSymName(o types.Object) SymName {
5562

5663
func (n SymName) String() string { return n.PkgPath + "." + n.Name }
5764

65+
func (n SymName) IsMethod() (recv string, method string, ok bool) {
66+
pos := strings.IndexByte(n.Name, '.')
67+
if pos == -1 {
68+
return
69+
}
70+
recv, method, ok = n.Name[:pos], n.Name[pos+1:], true
71+
size := len(recv)
72+
if size > 2 && recv[0] == '(' && recv[size-1] == ')' {
73+
recv = recv[1 : size-1]
74+
}
75+
return
76+
}
77+
5878
// parseGoLinknames processed comments in a source file and extracts //go:linkname
5979
// compiler directive from the comments.
6080
//
6181
// The following directive format is supported:
6282
// //go:linkname <localname> <importpath>.<name>
83+
// //go:linkname <localname> <importpath>.<type>.<name>
84+
// //go:linkname <localname> <importpath>.<(*type)>.<name>
6385
//
6486
// GopherJS directive support has the following limitations:
6587
//
6688
// - External linkname must be specified.
67-
// - The directive must be applied to a package-level function (variables and
68-
// methods are not supported).
89+
// - The directive must be applied to a package-level function or method (variables
90+
// are not supported).
6991
// - The local function referenced by the directive must have no body (in other
7092
// words, it can only "import" an external function implementation into the
7193
// local scope).
@@ -95,7 +117,11 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go
95117

96118
localPkg, localName := pkgPath, fields[1]
97119
extPkg, extName := "", fields[2]
98-
if idx := strings.LastIndexByte(extName, '.'); idx != -1 {
120+
if pos := strings.LastIndexByte(extName, '/'); pos != -1 {
121+
if idx := strings.IndexByte(extName[pos+1:], '.'); idx != -1 {
122+
extPkg, extName = extName[0:pos+idx+1], extName[pos+idx+2:]
123+
}
124+
} else if idx := strings.IndexByte(extName, '.'); idx != -1 {
99125
extPkg, extName = extName[0:idx], extName[idx+1:]
100126
}
101127

compiler/linkname_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestSymName(t *testing.T) {
4848
func AFunction() {}
4949
type AType struct {}
5050
func (AType) AMethod() {}
51-
func (AType) APointerMethod() {}
51+
func (*AType) APointerMethod() {}
5252
var AVariable int32
5353
`)
5454

@@ -66,8 +66,8 @@ func TestSymName(t *testing.T) {
6666
obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(),
6767
want: SymName{PkgPath: "testcase", Name: "AType.AMethod"},
6868
}, {
69-
obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "APointerMethod").Obj(),
70-
want: SymName{PkgPath: "testcase", Name: "AType.APointerMethod"},
69+
obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(),
70+
want: SymName{PkgPath: "testcase", Name: "(*AType).APointerMethod"},
7171
}, {
7272
obj: pkg.Scope().Lookup("AVariable"),
7373
want: SymName{PkgPath: "testcase", Name: "AVariable"},

compiler/package.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor
411411
FullName: o.FullName(),
412412
Blocking: len(funcInfo.Blocking) != 0,
413413
}
414+
d.LinkingName = newSymName(o)
414415
if fun.Recv == nil {
415-
d.LinkingName = newSymName(o)
416416
d.Vars = []string{funcCtx.objectName(o)}
417417
d.DceObjectFilter = o.Name()
418418
switch o.Name() {
@@ -431,14 +431,14 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor
431431
})
432432
d.DceObjectFilter = ""
433433
}
434-
}
435-
if fun.Recv != nil {
434+
} else {
436435
recvType := o.Type().(*types.Signature).Recv().Type()
437436
ptr, isPointer := recvType.(*types.Pointer)
438437
namedRecvType, _ := recvType.(*types.Named)
439438
if isPointer {
440439
namedRecvType = ptr.Elem().(*types.Named)
441440
}
441+
d.NamedRecvType = funcCtx.objectName(namedRecvType.Obj())
442442
d.DceObjectFilter = namedRecvType.Obj().Name()
443443
if !fun.Name.IsExported() {
444444
d.DceMethodFilter = o.Name() + "~"

compiler/prelude/prelude.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,46 @@ var $interfaceIsEqual = function(a, b) {
522522
}
523523
return $equal(a.$val, b.$val, a.constructor);
524524
};
525+
526+
var $unsafeMethodToFunction = function(typ, name, isPtr) {
527+
if (isPtr) {
528+
return function(r, ...args) {
529+
var ptrType = $ptrType(typ);
530+
if (r.constructor != ptrType) {
531+
switch (typ.kind) {
532+
case $kindStruct:
533+
r = $pointerOfStructConversion(r, ptrType);
534+
break;
535+
case $kindArray:
536+
r = new ptrType(r);
537+
break;
538+
default:
539+
r = new ptrType(r.$get,r.$set,r.$target);
540+
}
541+
}
542+
return r[name](...args);
543+
}
544+
} else {
545+
return function(r, ...args) {
546+
var ptrType = $ptrType(typ);
547+
if (r.constructor != ptrType) {
548+
switch (typ.kind) {
549+
case $kindStruct:
550+
r = $clone(r, typ);
551+
break;
552+
case $kindSlice:
553+
r = $convertSliceType(r, typ);
554+
break;
555+
case $kindComplex64:
556+
case $kindComplex128:
557+
r = new typ(r.$real, r.$imag);
558+
break;
559+
default:
560+
r = new typ(r);
561+
}
562+
}
563+
return r[name](...args);
564+
}
565+
}
566+
};
525567
`

compiler/prelude/prelude_min.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/pargma.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ Signatures of `remotename` and `localname` must be identical. Since this
2525
directive can subvert package incapsulation, the source file that uses the
2626
directive must also import `unsafe`.
2727

28+
The following directive format is supported:
29+
//go:linkname <localname> <importpath>.<name>
30+
//go:linkname <localname> <importpath>.<type>.<name>
31+
//go:linkname <localname> <importpath>.<(*type)>.<name>
32+
2833
Compared to the upstream Go, the following limitations exist in GopherJS:
2934

30-
- The directive only works on package-level functions (variables and methods
35+
- The directive only works on package-level functions or methods (variables
3136
are not supported).
3237
- The directive can only be used to "import" implementation from another
3338
package, and not to "provide" local implementation to another package.

tests/linkname_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/google/go-cmp/cmp"
7+
"github.com/gopherjs/gopherjs/tests/testdata/linkname/method"
78
"github.com/gopherjs/gopherjs/tests/testdata/linkname/one"
89
)
910

@@ -24,3 +25,12 @@ func TestLinknames(t *testing.T) {
2425
t.Fatalf("Callink linknamed functions returned a diff (-want,+got):\n%s", diff)
2526
}
2627
}
28+
29+
func TestLinknameMethods(t *testing.T) {
30+
defer func() {
31+
if err := recover(); err != nil {
32+
t.Fatalf("method.TestLinkname() paniced: %s", err)
33+
}
34+
}()
35+
method.TestLinkname(t)
36+
}

0 commit comments

Comments
 (0)