Type aliasing via unsafe.Pointer

I want to reinterpret a struct with a single interface as a struct with two pointer-sized ints.

My reading of the rules for unsafe.Pointer suggests that this is a valid usage: (*T2)(unsafe.Pointer(&T1{...})) where T1 and T2 share the same memory layout. However, I can’t help but wonder whether the garbage collector will get confused about what x really points to and consequently garbage-collect the contents of val.

The following code seems to work as intended, but is it safe?

package main

import (
	"fmt"
	"runtime"
	"time"
	"unsafe"
)

type Interface struct{ val interface{} }
type Uintptrs struct{ a, b uintptr }

func t() *Interface { return &Interface{val: "t=" + time.Now().String()} }

func main() {
	var x *Uintptrs = (*Uintptrs)(unsafe.Pointer(t()))
	runtime.GC()
	y := (*Interface)(unsafe.Pointer(x))
	fmt.Println(y.val)
}

https://goplay.space/#GKeGDekSXQH

From unsafe package - unsafe - Go Packages documentations.

Package unsafe contains operations that step around the type safety of Go programs.

Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.

I think that you can use unsafe package for some particular cases and not as current practice.

This is valid and the garbage collector will be fine. But I don’t think there’s any guarantee that your two struct types will in fact have an equivalent memory layout and size. (Even if that’s the case at the moment.)

Thanks @calmh. Is there any way to guarantee the same memory layout? What about the following?

type Interface struct{ val interface{} }
type SameLayout struct {
	a uintptr
	_ [unsafe.Sizeof(interface{}(nil)) - unsafe.Sizeof(uintptr(0))]byte
}

That’s a cool solution. I would probably just yolo it and test on each new Go release, for the very low risk that this ever changes & breaks. Your solution would at least guard against a uintptr magically becoming larger than an interface{}.

Thanks @calmh! I went one step further and added some compile-time assertions. At least, if anything does change, it won’t catch me off guard. :slight_smile:

// Compile-time assert that the two types have the same size and alignment.
const _ = -uint(unsafe.Sizeof(Interface{}) ^ unsafe.Sizeof(SameLayout{}))
const _ = -uint(unsafe.Alignof(Interface{}) ^ unsafe.Alignof(SameLayout{}))
1 Like

I also discovered that zero-padding isn’t possible. It seems that [0]byte occupies space in the struct, so I’ve also added:

const padding = unsafe.Sizeof(interface{}(nil)) - unsafe.Sizeof(uintptr(0))
const _ = -uint(padding) // assert(padding == 0)

type SameLayout struct {
	a uintptr
	// _ [padding]byte // only required if padding != 0
}

Mind you, this might be getting a little overcomplicated. The original Sizeof assertion with a comment is probably sufficient to avoid future pain.

1 Like

Ah yes, a [0]byte takes space when it’s the last element in a struct, otherwise a pointer to it would be a pointer outside the struct, potentially a pointer to the next struct if it was in array, and that’d be confusing.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.