如何从 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.Len
和SliceHeader.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
的地址并得到它的长度之后,我们不再引用arr
(p
被uintptr
算作引用),并且积极的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)
或&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
如果您有一个名为p
的unsafe.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));
请注意:如果您直接传递指针,请务必使用&arr[0]
和不 &arr
。 &arr
是 *[10]byte
,而不是 *byte
。使用 &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 创建数组或切片?的主要内容,如果未能解决你的问题,请参考以下文章