Skip to content

Commit 6607971

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 2328d1e commit 6607971

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
localTypeNames typeutil.Map // *types.Named (synthetic local from generic instantiation) -> string
9697
}
@@ -308,6 +309,7 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package,
308309
c.packageDir = pkg.OriginalDir()
309310
c.embedGlobals = pkg.EmbedGlobals
310311
c.pkg = pkg.Pkg
312+
c.loaderPkg = pkg
311313
c.runtimePkg = ssaPkg.Prog.ImportedPackage("runtime").Pkg
312314
c.program = ssaPkg.Prog
313315

compiler/symbol.go

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

375+
// Also scan file-level //go:linkname directives. These appear as
376+
// free-standing comments in *ast.File.Comments (not attached to any
377+
// declaration), and are used by modern golang.org/x/sys/unix and others.
378+
// Function-attached directives (above) take precedence — we only add
379+
// file-level ones if no doc-comment linkname was found for this function.
380+
//
381+
// TODO: the hasUnsafeImport gate enforced downstream (see the
382+
// //go:linkname case below) is package-level. gc enforces it per
383+
// file, on the file containing the directive. For file-level
384+
// linknames this is more important than for function-attached ones,
385+
// because the directive can live in a file separate from the
386+
// function. A stricter implementation would check whether the file
387+
// returned by fileForFunc imports "unsafe", not whether any file in
388+
// the package does.
389+
hasFunctionLinkname := false
390+
for _, comment := range pragmas {
391+
if strings.HasPrefix(comment.Text, "//go:linkname ") {
392+
parts := strings.Fields(comment.Text)
393+
if len(parts) == 3 && parts[1] == f.Name() {
394+
hasFunctionLinkname = true
395+
break
396+
}
397+
}
398+
}
399+
if !hasFunctionLinkname {
400+
if file := c.fileForFunc(f); file != nil {
401+
for _, group := range file.Comments {
402+
// Skip the function's own doc comment — already handled above.
403+
if decl, ok := syntax.(*ast.FuncDecl); ok && group == decl.Doc {
404+
continue
405+
}
406+
for _, comment := range group.List {
407+
if !strings.HasPrefix(comment.Text, "//go:linkname ") {
408+
continue
409+
}
410+
parts := strings.Fields(comment.Text)
411+
if len(parts) != 3 || parts[1] != f.Name() {
412+
continue
413+
}
414+
pragmas = append(pragmas, comment)
415+
}
416+
}
417+
}
418+
}
419+
375420
// Parse each pragma.
376421
for _, comment := range pragmas {
377422
parts := strings.Fields(comment.Text)
@@ -666,6 +711,34 @@ type globalInfo struct {
666711
section string // go:section
667712
}
668713

714+
// fileForFunc returns the *ast.File that contains the declaration of f, or
715+
// nil if it cannot be determined. File-level pragmas are only consulted for
716+
// functions in the package currently being compiled — functions imported from
717+
// other packages have their file-level pragmas processed when those packages
718+
// are compiled.
719+
func (c *compilerContext) fileForFunc(f *ssa.Function) *ast.File {
720+
if c.loaderPkg == nil || f.Pkg == nil || f.Pkg.Pkg != c.loaderPkg.Pkg {
721+
return nil
722+
}
723+
syntax := f.Syntax()
724+
if f.Origin() != nil {
725+
syntax = f.Origin().Syntax()
726+
}
727+
if syntax == nil {
728+
return nil
729+
}
730+
pos := syntax.Pos()
731+
if !pos.IsValid() {
732+
return nil
733+
}
734+
for _, file := range c.loaderPkg.Files {
735+
if file.FileStart <= pos && pos < file.FileEnd {
736+
return file
737+
}
738+
}
739+
return nil
740+
}
741+
669742
// loadASTComments loads comments on globals from the AST, for use later in the
670743
// program. In particular, they are required for //go:extern pragmas on globals.
671744
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
@@ -120,3 +120,31 @@ func stillEscapes(a *int, b []int, c chan int, d *[0]byte) {
120120
func doesHeapAlloc() *int {
121121
return new(int)
122122
}
123+
124+
// Define a function in a different package using a file-level go:linkname.
125+
// (Same as withLinkageName1, but with the //go:linkname directive detached
126+
// from the function declaration — see https://github.com/tinygo-org/tinygo/issues/4395)
127+
func withFileLevelLinkageName1() {
128+
}
129+
130+
// Import a function from a different package using a file-level go:linkname.
131+
// (Same as withLinkageName2, but with the //go:linkname directive detached
132+
// from the function declaration.)
133+
func withFileLevelLinkageName2()
134+
135+
//go:linkname withFileLevelLinkageName1 somepkg.someFileLevelFunction1
136+
//go:linkname withFileLevelLinkageName2 somepkg.someFileLevelFunction2
137+
138+
// File-level linkname directives can also appear between two function
139+
// declarations, in which case Go's AST attaches them as the doc comment
140+
// of the following function — even when the directive's localname refers
141+
// to a different function. Exercise that case: the directive below names
142+
// withAdjacentLinkageName, but Go will attach it to
143+
// sentinelAfterAdjacentLinkname's Doc. The file-level scan must find it
144+
// by walking comment groups regardless of which decl they're attached to.
145+
func withAdjacentLinkageName() {
146+
}
147+
148+
//go:linkname withAdjacentLinkageName somepkg.someAdjacentFunction
149+
func sentinelAfterAdjacentLinkname() {
150+
}

compiler/testdata/pragma.ll

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ entry:
102102
; Function Attrs: allockind("alloc,zeroed") allocsize(0)
103103
declare noalias nonnull ptr @runtime.alloc_noheap(i32, ptr, ptr) #9
104104

105+
; Function Attrs: nounwind
106+
define hidden void @somepkg.someFileLevelFunction1(ptr %context) unnamed_addr #1 {
107+
entry:
108+
ret void
109+
}
110+
111+
declare void @somepkg.someFileLevelFunction2(ptr) #0
112+
113+
; Function Attrs: nounwind
114+
define hidden void @somepkg.someAdjacentFunction(ptr %context) unnamed_addr #1 {
115+
entry:
116+
ret void
117+
}
118+
119+
; Function Attrs: nounwind
120+
define hidden void @main.sentinelAfterAdjacentLinkname(ptr %context) unnamed_addr #1 {
121+
entry:
122+
ret void
123+
}
124+
105125
attributes #0 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
106126
attributes #1 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
107127
attributes #2 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "wasm-export-name"="extern_func" }

0 commit comments

Comments
 (0)