Reputation: 290
The Go Programming Language says in Section 13.2 that this is code is safe and x
will always
be visible to the garbage collector:
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
And that this code is unsafe, because x
is temporarily not visible to the
garbage collector, which could move it, making pb
a dangling pointer:
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
But I can't see the difference between these two examples.
In the case described as safe, after uintptr
has been called, the only
reference to the x
is the uintptr
value, isn't it? There's a Pointer
to it on the same line, but it was an argument to uintptr
, which has run,
so nothing is referencing the arguments, and so the Pointer
is not live and the uintptr
is the only reference to the object.
I can't see how storing the uintptr
in a local variable instead of as an
expression intermediate value makes it any more safe. Aren't local variables
like tmp
removed in compiler phases anyway, becoming anonymous dataflow edges,
so that the generated code should be semantically equivalent? Or does Go have
some rules for when garbage collection can run? Such as having safepoints only
between statements? But the code in the first example has method calls so I
would presume they would always be safepoints?
Upvotes: 3
Views: 1052
Reputation: 76423
Found the reference I hinted at in my comments here
A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.
What this means is that this expression:
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
Is safe because you're creating a uintptr
, which is seen as an integer, not a reference, but it's immediately assigned (unless there's a race condition somewhere else, the object that x
references cannot be GC'ed) until after the assignment). The uintptr
(again: integer type) is also immediately cast to a pointer, turning it into a reference so the GC will manage pb
. This means that:
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))
: all safe, because x
clearly is an existing reference to an objectpb
is assigned an integer that is (through the cast) marked as a reference to an int16
objectHowever, when you write this:
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
There is a chance that, between assigning tmp
(remember integer, not reference), the actual object in memory is moved. As it says in the docs: tmp
will not be updated. Thus, when you assign pb
, you could end up with an invalid pointer.
think of tmp
in this case as x
in the first case. Rather than being a reference to an object, it's as if you wrote
tmp := 123456 // a random integer
pb := (*int16) (unsafe.Pointer(tmp)) // not safe, obviously
For example:
var pb *int16
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
go func() {
time.Sleep(1 * time.Second)
pb = (*int16)(unsafe.Pointer(tmp))
}()
// original value of x could be GC'ed here, before the goroutine starts, or the time.Sleep call returns
x = TypeOfX{
b: 123,
}
Upvotes: 2