Go linknames
The Go codebase has a number of interesting tricks up its sleeve. While I wouldn’t easily greenlight them in a production codebase, they are still nice to know about.
In today’s edition, let’s see one that (among other things) allows accessing unexported symbols from other packages.
The //go:linkname directive
Linknames are briefly mentioned in the compile command docs. One of their uses in the Go tree is to allow flexibility while maintaining the compatibility promise.
In contrast to other directives (eg. //go:norace
) that affect the code block immediately under them, they can appear anywhere in a file, using the //go:linkname localname [importpath.name]
format.
This linkname directive tells the compiler to ‘rename’ or ‘link’ the variable or function localname
to importpath.name
; in other words, whenever the code calls importpath.name
it will reach out to localname
instead.
Omitting the importpath.name
will just make the localname
symbol accessible (eg. so it can be called in assembly code), even if it doesn’t start with a capital letter.
Keep in mind you’ll either have to provide a correct go tool compile
command with the -complete
flag so that the compiler skips checking for partially defined functions, or just add a dummy assembly (.s) file for the same effect.
Linknames in action
Here’s an example of linknames in action!
// foo/bar/bar.go
package bar
import "encoding/base64"
import "fmt"
import _ "unsafe"
//go:linkname encode main.bar_myencode
//go:linkname decode main.fromString
func Setup() {
fmt.Println("starting..")
}
func encode(input []byte) string {
return base64.StdEncoding.EncodeToString(input)
}
func decode(input string) (string, error) {
res, err := base64.StdEncoding.DecodeString(input)
if err != nil {
return "", err
}
return string(res), nil
}
// foo/main.go
package main
import (
"fmt"
"foo/bar"
_ "unsafe"
)
func bar_myencode(input []byte) string
func fromString(input string) (string, error)
func main() {
bar.Setup()
fmt.Println(bar_myencode([]byte("greetings")))
fmt.Println(fromString("R28gPDMK"))
}
How is this implemented?
Linknames are parsed and stored in a linkname
slice, which records the symbol’s position on the file, as well as the local and remapped symbol names. The actual parsing of the directive happens a few lines below. This slice is ultimately used to add these symbols as nodes in the AST tree. Linknames can only be used as long as the unsafe
package has been imported.
Linknamed functions are the only ones allowed to not have a body, as they still need their signature defined, like in our example.
How is the check for ‘unsafe’ implemented?
In noder.go
, there is a comparison with imported_unsafe
.
That boolean is switched to ‘true’ if importfile
encounters ‘unsafe’ in the import list. Unsafe is characterized as a ‘pseudopackage’ since it is mostly used for enabling features like this, which circumvent the type safety of Go programs, and not an actual package containing functionality.
Outro
That’s all about linknames; make sure to not use this new toy in your arsenal. Unless you have loads of experience, a robust testing infrastructure and you’re working with low-level primitives or reaching for extreme performance optimizations, you probably shouldn’t be messing with unsafe.
Until next time!