github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/compiler/linkname.go (about)

     1  package compiler
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"strings"
     8  
     9  	"github.com/gopherjs/gopherjs/compiler/astutil"
    10  	"github.com/gopherjs/gopherjs/compiler/internal/symbol"
    11  )
    12  
    13  // GoLinkname describes a go:linkname compiler directive found in the source code.
    14  //
    15  // GopherJS treats these directives in a way that resembles a symbolic link,
    16  // where for a single given symbol implementation there may be zero or more
    17  // symbols referencing it. This is subtly different from the upstream Go
    18  // implementation, which simply overrides symbol name the linker will use.
    19  type GoLinkname struct {
    20  	Implementation symbol.Name
    21  	Reference      symbol.Name
    22  }
    23  
    24  // parseGoLinknames processed comments in a source file and extracts //go:linkname
    25  // compiler directive from the comments.
    26  //
    27  // The following directive format is supported:
    28  // //go:linkname <localname> <importpath>.<name>
    29  // //go:linkname <localname> <importpath>.<type>.<name>
    30  // //go:linkname <localname> <importpath>.<(*type)>.<name>
    31  //
    32  // GopherJS directive support has the following limitations:
    33  //
    34  //   - External linkname must be specified.
    35  //   - The directive must be applied to a package-level function or method (variables
    36  //     are not supported).
    37  //   - The local function referenced by the directive must have no body (in other
    38  //     words, it can only "import" an external function implementation into the
    39  //     local scope).
    40  func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) {
    41  	var errs ErrorList = nil
    42  	var directives []GoLinkname
    43  
    44  	isUnsafe := astutil.ImportsUnsafe(file)
    45  
    46  	processComment := func(comment *ast.Comment) error {
    47  		if !strings.HasPrefix(comment.Text, "//go:linkname ") {
    48  			return nil // Not a linkname compiler directive.
    49  		}
    50  
    51  		// TODO(nevkontakte): Ideally we should check that the directive comment
    52  		// is on a line by itself, line Go compiler does, but ast.Comment doesn't
    53  		// provide an easy way to find that out.
    54  
    55  		if !isUnsafe {
    56  			return fmt.Errorf(`//go:linkname is only allowed in Go files that import "unsafe"`)
    57  		}
    58  
    59  		fields := strings.Fields(comment.Text)
    60  		if len(fields) != 3 {
    61  			return fmt.Errorf(`usage (all fields required): //go:linkname localname importpath.extname`)
    62  		}
    63  
    64  		localPkg, localName := pkgPath, fields[1]
    65  		extPkg, extName := "", fields[2]
    66  		if pos := strings.LastIndexByte(extName, '/'); pos != -1 {
    67  			if idx := strings.IndexByte(extName[pos+1:], '.'); idx != -1 {
    68  				extPkg, extName = extName[0:pos+idx+1], extName[pos+idx+2:]
    69  			}
    70  		} else if idx := strings.IndexByte(extName, '.'); idx != -1 {
    71  			extPkg, extName = extName[0:idx], extName[idx+1:]
    72  		}
    73  
    74  		obj := file.Scope.Lookup(localName)
    75  		if obj == nil {
    76  			return fmt.Errorf("//go:linkname local symbol %q is not found in the current source file", localName)
    77  		}
    78  
    79  		if obj.Kind != ast.Fun {
    80  			if pkgPath == "math/bits" || pkgPath == "reflect" {
    81  				// These standard library packages are known to use go:linkname with
    82  				// variables, which GopherJS doesn't support. We silently ignore such
    83  				// directives, since it doesn't seem to cause any problems.
    84  				return nil
    85  			}
    86  			return fmt.Errorf("gopherjs: //go:linkname is only supported for functions, got %q", obj.Kind)
    87  		}
    88  
    89  		decl := obj.Decl.(*ast.FuncDecl)
    90  		if decl.Body != nil {
    91  			if pkgPath == "runtime" || pkgPath == "internal/bytealg" || pkgPath == "internal/fuzz" {
    92  				// These standard library packages are known to use unsupported
    93  				// "insert"-style go:linkname directives, which we ignore here and handle
    94  				// case-by-case in native overrides.
    95  				return nil
    96  			}
    97  			return fmt.Errorf("gopherjs: //go:linkname can not insert local implementation into an external package %q", extPkg)
    98  		}
    99  		// Local function has no body, treat it as a reference to an external implementation.
   100  		directives = append(directives, GoLinkname{
   101  			Reference:      symbol.Name{PkgPath: localPkg, Name: localName},
   102  			Implementation: symbol.Name{PkgPath: extPkg, Name: extName},
   103  		})
   104  		return nil
   105  	}
   106  
   107  	for _, cg := range file.Comments {
   108  		for _, c := range cg.List {
   109  			if err := processComment(c); err != nil {
   110  				errs = append(errs, ErrorAt(err, fset, c.Pos()))
   111  			}
   112  		}
   113  	}
   114  
   115  	return directives, errs.ErrOrNil()
   116  }
   117  
   118  // goLinknameSet is a utility that enables quick lookup of whether a decl is
   119  // affected by any go:linkname directive in the program.
   120  type goLinknameSet struct {
   121  	byImplementation map[symbol.Name][]GoLinkname
   122  	byReference      map[symbol.Name]GoLinkname
   123  }
   124  
   125  // Add more GoLinkname directives into the set.
   126  func (gls *goLinknameSet) Add(entries []GoLinkname) error {
   127  	if gls.byImplementation == nil {
   128  		gls.byImplementation = map[symbol.Name][]GoLinkname{}
   129  	}
   130  	if gls.byReference == nil {
   131  		gls.byReference = map[symbol.Name]GoLinkname{}
   132  	}
   133  	for _, e := range entries {
   134  		gls.byImplementation[e.Implementation] = append(gls.byImplementation[e.Implementation], e)
   135  		if prev, found := gls.byReference[e.Reference]; found {
   136  			return fmt.Errorf("conflicting go:linkname directives: two implementations for %q: %q and %q",
   137  				e.Reference, prev.Implementation, e.Implementation)
   138  		}
   139  		gls.byReference[e.Reference] = e
   140  	}
   141  	return nil
   142  }
   143  
   144  // IsImplementation returns true if there is a directive referencing this symbol
   145  // as an implementation.
   146  func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool {
   147  	_, found := gls.byImplementation[sym]
   148  	return found
   149  }
   150  
   151  // FindImplementation returns a symbol name, which provides the implementation
   152  // for the given symbol. The second value indicates whether the implementation
   153  // was found.
   154  func (gls *goLinknameSet) FindImplementation(sym symbol.Name) (symbol.Name, bool) {
   155  	directive, found := gls.byReference[sym]
   156  	return directive.Implementation, found
   157  }