-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
I've seen lots of instances of code like this:
pc, _, _, ok := runtime.Caller(skip)
if !ok { ... }
fmt.Printf("%s\n", runtime.FuncForPC(pc).Name())
(e.g. https://github.com/stretchr/testify/blob/ffdc059bfe9ce6a4e144ba849dbedead332c6053/mock/mock.go#L317)
This code won't work as expected when mid-stack inlining is enabled. If we have
func main() {
f() // f will be inlined here
}
func f() {
g()
}
func g() {
pc, _, _, _ := runtime.Caller(1)
fmt.Printf("%s\n", runtime.FuncForPC(pc).Name())
}
Then 1.12 prints main.main
, not main.f
as would be printed for 1.11.
This is because FuncForPC
is specified as follows:
If pc represents multiple functions because of inlining, it returns the *Func describing the outermost function.
The pc returned by runtime.Caller
is in main.main
on an instruction from the body of main.f
. Because of the spec worded as above, we get main.main
as a result.
The current "fix" for this is to require everyone to move to using runtime.CallersFrames
(see footnote 1) which handles inlined frames correctly. This is the long term solution, but requires users of runtime.Caller{,s}
+ runtime.FuncForPC
to do something active to keep their code working for 1.12.
I propose instead to change how FuncForPC
works so the above code does not need a fix. If we instead use this spec:
If pc represents multiple functions because of inlining, it returns the a *Func describing the innermost function, but with an entry of the outermost function.
(outermost -> innermost, plus some weasel words for what Entry() returns for an inlined function.)
This will fix the above code transparently. There are 2 wrinkles to this otherwise elegant solution:
- This changes the spec for an exported function.
- The runtime doesn't necessarily have a
Func
to return fromFuncForPC
for inlined functions.
I don't think number 1 is a huge problem. The only pcs that are exposed by the runtime are those returned by runtime.Caller{,s}
. Most inlinings we do in 1.11 are leaf inlinings, which runtime.Caller{,s}
can't observe. We also inline functions which call panic
. Those runtime.Caller{,s}
can observe, but would have to be called from within a deferred handler, which seems rare (and is ugly anyway because things like runtime.gopanic
are on the stack).
Of course, someone could get a pc by other means, using unsafe
, a profiling library, or who knows where else. Those users might see something unexpected.
I have a CL which will solve number 2. FuncForPC
has enough information to construct a Func
dynamically for the inlined body. It means FuncForPC
might allocate, but otherwise it's not observable by the user (other than the weird "what does Entry() return" problem mentioned above).
footnote 1: The testify
package fixed this problem by introducing //go:noinline
directives. That doesn't seem to be a good approach in general.