The magic of go:linkname

Summary

Go's //go:linkname directive allows linking a function declaration without a body, such as time.Sleep, to its actual implementation in a different package, like runtime.timeSleep. This powerful mechanism subverts Go's type system and package modularity, necessitating an import "unsafe" statement. Developers can utilize go:linkname to access unexported functions across packages, but this often results in a "missing function body" build error. The common workaround involves adding an empty assembly file (.s) to the package containing the function declaration. While useful for specific advanced scenarios, this "hacky" feature should be employed with significant caution.

When writing Go program, there is frequent need on using time.Sleep() function to pause the logic for some time. And if jumping to the definition of this function, can see below definition:

// Sleep pauses the current goroutine for at least the duration d.
// A negative or zero duration causes Sleep to return immediately.
func Sleep(d Duration)

I's strange that there is no function body defined here. What happened? The actual definition of the function body is residing at runtime/time.go indeed.

// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
    if ns <= 0 {
        return
    }

    gp := getg()
    t := gp.timer
    if t == nil {
        t = new(timer)
        gp.timer = t
    }
    *t = timer{}
    t.when = nanotime() + ns
    t.f = goroutineReady
    t.arg = gp
    tb := t.assignBucket()
    lock(&tb.lock)
    if !tb.addtimerLocked(t) {
        unlock(&tb.lock)
        badTimer()
    }
    goparkunlock(&tb.lock, waitReasonSleep, traceEvGoSleep, 2)
}

How is this linked to the original time.Sleep() function? The magic is at the line:

//go:linkname timeSleep time.Sleep

According to the official documentation:

//go:linkname localname importpath.name

The //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".

In above example, localname refers to timeSleep while importpath.name refers to time.Sleep. The reason for doing this is that time.Sleep is in time package and it's exported while timeSleep is in runtime package and it's un-exported. To be able to use it, this go:linkname is used. This is a bit hacky though.

Here is another example to demonstrate the usage. Assume there is a directory structure

➜  demo git:(master) ✗ tree
.
├── linkname
│   └── a.go
├── main.go
└── outer
    └── world.go

The contents in a.go

package linkname

import _ "unsafe"

//go:linkname hello examples/demo/outer.World
func hello() {
    println("hello,world!")
}

The contents in world.go

package outer

import (
    _ "examples/demo/linkname"
)

func World()

The contents in main.go

package main

import (
    "examples/demo/outer"
)

func main() {
    outer.World()
}

The output is

# examples/demo/outer
outer/world.go:7:6: missing function body

The reason for the error is that go build will run with -complete flag which detects the function has no body and will report error as it assume package has no non-Go components. To workaround this, just add a file with extension name .s.

➜  demo git:(master) ✗ tree
.
├── linkname
│   └── a.go
├── main.go
└── outer
    ├── i.s
    └── world.go

Run the program again and it will output

hello,world!

This trick is useful in some cases where there is really a need to access some un-exported functions in some packages. Use it cautiously.

Reference: https://studygolang.com/articles/15842

TRICKS GOLANG GO:LINKNAME

  RELATED

  COMMENTS

0

No comment for this article.