Skip to content

Commit 38b91ef

Browse files
committed
compiler: support file-level //go:linkname directives
Modern golang.org/x/sys/unix (v0.36+) declares its linknames detached from function declarations, e.g.: func syscall_syscall(...) //go:linkname syscall_syscall syscall.syscall TinyGo's pragma parser only inspected function doc comments and therefore missed these, producing link errors like: undefined symbol: _golang.org/x/sys/unix.syscall_syscall Extend parsePragmas to also walk the enclosing *ast.File's free-standing comments for //go:linkname directives matching the function's name. Function-attached directives still take precedence. The existing 'unsafe' import gate is preserved. Fixes #4395, #5365
1 parent 4204f3d commit 38b91ef

4 files changed

Lines changed: 124 additions & 1 deletion

File tree

compiler/compiler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ type compilerContext struct {
9090
astComments map[string]*ast.CommentGroup
9191
embedGlobals map[string][]*loader.EmbedFile
9292
pkg *types.Package
93-
packageDir string // directory for this package
93+
loaderPkg *loader.Package // current package being compiled (for AST access)
94+
packageDir string // directory for this package
9495
runtimePkg *types.Package
9596
}
9697

@@ -294,6 +295,7 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package,
294295
c.packageDir = pkg.OriginalDir()
295296
c.embedGlobals = pkg.EmbedGlobals
296297
c.pkg = pkg.Pkg
298+
c.loaderPkg = pkg
297299
c.runtimePkg = ssaPkg.Prog.ImportedPackage("runtime").Pkg
298300
c.program = ssaPkg.Prog
299301

compiler/symbol.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,51 @@ func (c *compilerContext) parsePragmas(info *functionInfo, f *ssa.Function) {
346346
}
347347
}
348348

349+
// Also scan file-level //go:linkname directives. These appear as
350+
// free-standing comments in *ast.File.Comments (not attached to any
351+
// declaration), and are used by modern golang.org/x/sys/unix and others.
352+
// Function-attached directives (above) take precedence — we only add
353+
// file-level ones if no doc-comment linkname was found for this function.
354+
//
355+
// TODO: the hasUnsafeImport gate enforced downstream (see the
356+
// //go:linkname case below) is package-level. gc enforces it per
357+
// file, on the file containing the directive. For file-level
358+
// linknames this is more important than for function-attached ones,
359+
// because the directive can live in a file separate from the
360+
// function. A stricter implementation would check whether the file
361+
// returned by fileForFunc imports "unsafe", not whether any file in
362+
// the package does.
363+
hasFunctionLinkname := false
364+
for _, comment := range pragmas {
365+
if strings.HasPrefix(comment.Text, "//go:linkname ") {
366+
parts := strings.Fields(comment.Text)
367+
if len(parts) == 3 && parts[1] == f.Name() {
368+
hasFunctionLinkname = true
369+
break
370+
}
371+
}
372+
}
373+
if !hasFunctionLinkname {
374+
if file := c.fileForFunc(f); file != nil {
375+
for _, group := range file.Comments {
376+
// Skip the function's own doc comment — already handled above.
377+
if decl, ok := syntax.(*ast.FuncDecl); ok && group == decl.Doc {
378+
continue
379+
}
380+
for _, comment := range group.List {
381+
if !strings.HasPrefix(comment.Text, "//go:linkname ") {
382+
continue
383+
}
384+
parts := strings.Fields(comment.Text)
385+
if len(parts) != 3 || parts[1] != f.Name() {
386+
continue
387+
}
388+
pragmas = append(pragmas, comment)
389+
}
390+
}
391+
}
392+
}
393+
349394
// Parse each pragma.
350395
for _, comment := range pragmas {
351396
parts := strings.Fields(comment.Text)
@@ -637,6 +682,34 @@ type globalInfo struct {
637682
section string // go:section
638683
}
639684

685+
// fileForFunc returns the *ast.File that contains the declaration of f, or
686+
// nil if it cannot be determined. File-level pragmas are only consulted for
687+
// functions in the package currently being compiled — functions imported from
688+
// other packages have their file-level pragmas processed when those packages
689+
// are compiled.
690+
func (c *compilerContext) fileForFunc(f *ssa.Function) *ast.File {
691+
if c.loaderPkg == nil || f.Pkg == nil || f.Pkg.Pkg != c.loaderPkg.Pkg {
692+
return nil
693+
}
694+
syntax := f.Syntax()
695+
if f.Origin() != nil {
696+
syntax = f.Origin().Syntax()
697+
}
698+
if syntax == nil {
699+
return nil
700+
}
701+
pos := syntax.Pos()
702+
if !pos.IsValid() {
703+
return nil
704+
}
705+
for _, file := range c.loaderPkg.Files {
706+
if file.FileStart <= pos && pos < file.FileEnd {
707+
return file
708+
}
709+
}
710+
return nil
711+
}
712+
640713
// loadASTComments loads comments on globals from the AST, for use later in the
641714
// program. In particular, they are required for //go:extern pragmas on globals.
642715
func (c *compilerContext) loadASTComments(pkg *loader.Package) {

compiler/testdata/pragma.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,31 @@ func doesNotEscapeParam(a *int, b []int, c chan int, d *[0]byte)
115115
//go:noescape
116116
func stillEscapes(a *int, b []int, c chan int, d *[0]byte) {
117117
}
118+
119+
// Define a function in a different package using a file-level go:linkname.
120+
// (Same as withLinkageName1, but with the //go:linkname directive detached
121+
// from the function declaration — see https://github.com/tinygo-org/tinygo/issues/4395)
122+
func withFileLevelLinkageName1() {
123+
}
124+
125+
// Import a function from a different package using a file-level go:linkname.
126+
// (Same as withLinkageName2, but with the //go:linkname directive detached
127+
// from the function declaration.)
128+
func withFileLevelLinkageName2()
129+
130+
//go:linkname withFileLevelLinkageName1 somepkg.someFileLevelFunction1
131+
//go:linkname withFileLevelLinkageName2 somepkg.someFileLevelFunction2
132+
133+
// File-level linkname directives can also appear between two function
134+
// declarations, in which case Go's AST attaches them as the doc comment
135+
// of the following function — even when the directive's localname refers
136+
// to a different function. Exercise that case: the directive below names
137+
// withAdjacentLinkageName, but Go will attach it to
138+
// sentinelAfterAdjacentLinkname's Doc. The file-level scan must find it
139+
// by walking comment groups regardless of which decl they're attached to.
140+
func withAdjacentLinkageName() {
141+
}
142+
143+
//go:linkname withAdjacentLinkageName somepkg.someAdjacentFunction
144+
func sentinelAfterAdjacentLinkname() {
145+
}

compiler/testdata/pragma.ll

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ entry:
9393
ret void
9494
}
9595

96+
; Function Attrs: nounwind
97+
define hidden void @somepkg.someFileLevelFunction1(ptr %context) unnamed_addr #2 {
98+
entry:
99+
ret void
100+
}
101+
102+
declare void @somepkg.someFileLevelFunction2(ptr) #1
103+
104+
; Function Attrs: nounwind
105+
define hidden void @somepkg.someAdjacentFunction(ptr %context) unnamed_addr #2 {
106+
entry:
107+
ret void
108+
}
109+
110+
; Function Attrs: nounwind
111+
define hidden void @main.sentinelAfterAdjacentLinkname(ptr %context) unnamed_addr #2 {
112+
entry:
113+
ret void
114+
}
115+
96116
attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
97117
attributes #1 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
98118
attributes #2 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }

0 commit comments

Comments
 (0)