如何从 golang 中的数组 unsafe.Pointer 创建数组或切片?

Posted

技术标签:

【中文标题】如何从 golang 中的数组 unsafe.Pointer 创建数组或切片?【英文标题】:How to create an array or a slice from an array unsafe.Pointer in golang? 【发布时间】:2018-12-13 17:44:30 【问题描述】:

一个指向数组的指针,比如说:

p := uintptr(unsafe.Pointer(&array))
size := 5

我无法访问变量array,上面的代码是为了让它更清晰。

另外,我知道数组的大小,但size 不是恒定的,它会根据运行时变化。

现在,我想用已知的指针、大小以及数据类型来初始化切片或数组。

我想出了以下代码:

data := make([]byte, size)
stepSize := unsafe.Sizeof(data[0])
for i := 0; i < size; i++ 
    data[i] = *(*byte)(unsafe.Pointer(p))
    p += stepSize

fmt.println(data)

但是这个方法是做内存拷贝,可能效率低下,反正不做拷贝有没有?

附:我也尝试了以下两种方法,

// method 1
data := *(*[]byte)(unsafe.Pointer(p))
// method 2
data := *(*[size]byte)(unsafe.Pointer(p))

但它会在运行时失败,我现在知道它的原因了。

【问题讨论】:

【参考方案1】:

前言:

您应该知道:如果您将指针作为 uintptr 类型的值,这不会阻止原始数组被垃圾收集(uintptr 值不计为引用)。所以在使用这样的值时要小心,不能保证它会指向一个有效的值/内存区域。

引用包unsafe.Pointer:

uintptr 是一个整数,而不是一个引用。将指针转换为 uintptr 会创建一个没有指针语义的整数值。即使一个 uintptr 持有某个对象的地址,如果该对象移动,垃圾收集器也不会更新该 uintptr 的值,该 uintptr 也不会阻止该对象被回收。

一般建议:尽可能远离包裹unsafe。留在 Go 的类型安全范围内。


声明一个切片类型的变量,并使用不安全的转换来获取它的reflect.SliceHeader描述符。

然后你可以修改它的字段,使用指针作为SliceHeader.Data的值,大小作为SliceHeader.LenSliceHeader.Cap

完成此操作后,切片变量将指向与初始指针相同的数组。

arr := [10]byte0, 1, 2, 3, 4, 5, 6, 7, 8, 9

size := len(arr)
p := uintptr(unsafe.Pointer(&arr))

var data []byte

sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
sh.Data = p
sh.Len = size
sh.Cap = size

fmt.Println(data)

runtime.KeepAlive(arr)

输出(在Go Playground上试试):

[0 1 2 3 4 5 6 7 8 9]

请注意,我使用了runtime.KeepAlive()。这是因为在获取arr 的地址并得到它的长度之后,我们不再引用arrpuintptr 算作引用),并且积极的GC 可能——正确地——在我们开始打印data(指向arr)之前擦除arr。将runtime.KeepAlive() 放在main() 的末尾将确保arr 不会被垃圾回收 在这个电话之前。有关详细信息,请参阅In Go, when will a variable become unreachable? 如果指针的提供者确保它不会被垃圾回收,则无需在代码中调用runtime.KeepAlive()

或者,您可以使用composite literal 创建reflect.SliceHeader,并使用不安全的转换从中获取切片,如下所示:

sh := &reflect.SliceHeader
    Data: p,
    Len:  size,
    Cap:  size,


data := *(*[]byte)(unsafe.Pointer(sh))

fmt.Println(data)

runtime.KeepAlive(arr)

输出将是相同的。在Go Playground 上试试这个。

这种可能性/用例记录在unsafe.Pointer,带有警告和警告:

(6) reflect.SliceHeader 或 reflect.StringHeader 数据字段与指针的转换。

与前一种情况一样,反射数据结构 SliceHeader 和 StringHeader 将字段 Data 声明为 uintptr 以防止调用者在未先导入“不安全”的情况下将结果更改为任意类型。但是,这意味着 SliceHeader 和 StringHeader 仅在解释实际切片或字符串值的内容时才有效。

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

在这种用法中,hdr.Data 实际上是引用切片头中的底层指针的另一种方式,而不是 uintptr 变量本身。

一般来说,reflect.SliceHeader 和 reflect.StringHeader 应该只用作指向实际切片或字符串的 *reflect.SliceHeader 和 *reflect.StringHeader,而不是普通结构。程序不应声明或分配这些结构类型的变量。

// INVALID: a directly-declared header will not hold Data as a reference.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost

【讨论】:

像魅力一样工作!很好的答案,谢谢!我必须与用 C 编写的 dll 交互,所以我必须使用unsafe @Allenlu 添加了另一种方法,并将runtime.KeepAlive() 包含在我的答案中。 您在 unsafe.Pointer 文档中的引用提到 reflect.SliceHeader 只能用作 *reflect.SliceHeader 指向实际切片,而不是普通结构。还有一点需要注意的是,直接声明的标头不会将 Data 作为参考。使用new(reflect.SliceHeader)&amp;reflect.SliceHeader 是例外吗?我运行了您在操场上使用复合文字的示例,并且 go vet 显示警告“可能滥用 reflect.SliceHeader”。这是误报吗?我只是对替代方法的有效性感到好奇。 @YenForYang 兽医警告是正确的,因为创建reflect.SliceHeader 不会被视为引用,因此不会阻止其Data 字段指向的对象被垃圾收集。但是,就我而言,这不是本意。为了确保指向的对象(通过Data 字段)仍然有效,我使用了runtime.KeepAlive()【参考方案2】:

在 Go 1.17 中,我们现在拥有 unsafe.Slice(ptr *ArbitraryType, len IntegerType)

您可以使用此函数创建一个切片,其底层数组以ptr 开始,长度和容量为len

假设你有一个数组:

arr := [10]byte0, 1, 2, 3, 4, 5, 6, 7, 8, 9

如果您有一个名为punsafe.Pointer

p := unsafe.Pointer(&arr); // Or unsafe.Pointer(&arr[0])
data := unsafe.Slice((*byte)(p), len(arr)); 

但如果您可以直接访问arr,则可以将其简化为:

data := unsafe.Slice(&arr[0], len(arr)); 

请注意:如果您直接传递指针,请务必使用&amp;arr[0] &amp;arr&amp;arr*[10]byte,而不是 *byte。使用 &amp;arr 将创建长度和容量为 10 的 [][10]byte,而不是长度和容量为 10 的 []byte


使用 Go 1.17,我们还可以conveniently cast from a slice to an array pointer。简而言之,如果您使用切片而不是 unsafe.Pointer(p),您的 method 2 将起作用:

array := *(*[len(arr)]byte)(data)

【讨论】:

以上是关于如何从 golang 中的数组 unsafe.Pointer 创建数组或切片?的主要内容,如果未能解决你的问题,请参考以下文章

从 Golang 中的数组中选择元素的最惯用方法?

如何将二维数组中的值与 GoLang 中的一维数组中的值进行比较?

#yyds干货盘点#带大家认识Golang中的切片数据类型

如何在golang中访问struct中的切片

golang 数据一   (字符串数组和数组指针)

golang怎么对日期和时间进行排序