登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> CLQ工作室开源代码 >> 主题: [golang/raylib]raylib-go 一个兼容 golang 18.3 的修改     [回主站]     [分站链接]
标题
[golang/raylib]raylib-go 一个兼容 golang 18.3 的修改
clq
浏览(66) + 2024-07-30 19:05:50 发表 编辑

关键字:

[2024-07-30 21:50:51 最后更新]
[golang/raylib]raylib-go 一个兼容 golang 18.3 的修改

玩一下 raylib 发现现有的 go 18.3 编译不了。
但实际上只有一处错误,而且是不一定用得上的字体加载。于是想跳过去,不过其中的 unsafe 语法很不熟。查了一下资料修改如下后可运行

--------------------------------------------------------


_针对golang18.3有一处小修改


原理来自
https://zhuanlan.zhihu.com/p/682109749


在文件 D:\gopath1.10.8\src\_src_map_textfile_ios_g3n\vendor\github.com\gen2brain\raylib-go\raylib\rtext.go

//cfontChars := (*C.int)(unsafe.SliceData(codepoints)) //clq golang 18.3 不支持
cfontChars := (*C.int)(&codepoints[:1][0]); //根据 https://zhuanlan.zhihu.com/p/682109749 的说法,其结果与这个相同


--------------------------------------------------------
另外,似乎不能与 g3n 共存,提示 glfw 的 c 文件有两份。导致函数有两实现。还没试怎么处理。







clq
2024-07-30 21:50:51 发表 编辑



1、 如果 slice 的容量大于 0,SliceData 返回 &slice[:1][0]。

2、 如果 slice 为 nil,SliceData 返回 nil。

3、 否则,SliceData 返回一个非空指针,指向未指定的内存地址1。

--------------------------------------------------------

深入 GO unsafe.Pointer & uintptr
彭亚川Allen
彭亚川Allen

北京尘锋信息技术有限公司 资深Golang工程师
1 人赞同了该文章
GO unsafe.Pointer & uintptr

你是否经常看源码,源码里用 unsafe.Pointer, uintptr 各种骚操作,有没有想过为啥源码会这么用?若不了解 unsafe.Pointer, uintptr 使用姿势,代码很难看懂。虽然 GO 官方不建议大家使用,但作为一个 GO 工程师怎么能不了解 unsafe.Pointer 呢。

本文讲解案例采用 GO SDK 版本是 1.20.4,如果你的 GO SDK 版本较低,SDK 函数可能会有一些差异
GO 普通指针(*T)

Go 语言中的普通指针(即非 unsafe.Pointer)受到一些限制,以确保代码的安全性和可靠性,下面是普通指针的一些限制。
限制一:不能进行数学运算操作

1、 Go 语言不允许对普通指针进行数学运算,例如加法、减法等。

2、 不能直接对指针进行递增或递减操作。

func T() {
var (
x = 5
y = &x
)

y++
y = &x + 3
m := y * 2
}

上面这段代码编译会报错:

./T.go:9:2: invalid operation: y++ (non-numeric type *int)
./T.go:10:6: invalid operation: &x + 3 (mismatched types *int and untyped int)
./T.go:11:7: invalid operation: y * 2 (mismatched types *int and untyped int)

限制二:不同类型的指针不能相互转换

1、 不同类型的指针之间不能直接相互转换。

func TConvert() {
var (
n = int(100)
u *uint
)
u = &n
fmt.Println(u)
}

上面这段代码编译会报错:

cannot use &n (value of type *int) as *uint value in assignment

限制三:不能类型的指针不能用"=="、"!="比较、相互赋值

1、不同类型的指针之间不能进行比较操作,也不能相互赋值。

func Compare() {
var (
n = 100
f = 2.8
t = 100
g uint = 200
ptrN = &n
ptrF = &f
ptrT = &t
ptrG = &g
)
fmt.Printf("%v", ptrN == ptrT) // 同类型可以判等指针变量相等
fmt.Printf("%v", ptrG == ptrT) // 不同类型不能判断指针变量是否相等
fmt.Printf("%v", ptrF == ptrT) // 不同类型不能判断指针变量是否相等
}

上面这段代码编译会报错:

./T.go:37:27: invalid operation: ptrG == ptrT (mismatched types *uint and *int)
./T.go:38:27: invalid operation: ptrF == ptrT (mismatched types *float64 and *int)

2个指针变量类型相同可以相互转换的情况下,才可以进行比较。额外知识点指针变量可以通过"=="、"!="和nil进行比较
uintptr

uintptr 的定义在 builtin 包,定义如下:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

参考注释和定义我们知道:

1、 uintptr 是 integer 类型它足够大

2、 可以存储任何一种是数据结构对应的 Pointer 地址,通俗的解释 uintptr 本质存的是地址,uintptr 存的是10进制地址,举一栗子:

func out() {
var v int
pointer := unsafe.Pointer(&v)
address := uintptr(pointer)
fmt.Println(fmt.Sprintf("vAddress=%+v,pointerAddress=%+v,address=%v", &v, pointer, address))
}

日志输出:

=== RUN TestOut
v=0xc00010e190,pointerAddress=0xc00010e190,address=824634827152
--- PASS: TestOut (0.00s)
PASS

vAddress = pointerAddress 因为指向同一个内存块,address 也是内存地址为什么是824634827152?其实很简单,pointerAddress 和vAddress 是16进制,address 是10进制

特别注意

// 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.

intptr 并没有指针的语义,即使 uintptr 保存某个对象的地址,如果对象移动,uintptr 也不会阻止对象被 GC 回收。意思就是 uintptr 所指向的对象会被 gc 回收的。

unsafe 包主要提供3个函数支持【任意类型】=> uintptr 的转换:

// Sizeof takes an expression x of any type and returns the size in bytes
func Sizeof(x ArbitraryType) uintptr
// Offsetof returns the offset within the struct of the field represented by x
func Offsetof(x ArbitraryType) uintptr
// Alignof takes an expression x of any type and returns the required alignment
func Alignof(x ArbitraryType) uintptr

1、 第一个函数 Sizeof 简单好理解,获取任何类型大小返回的是字节

func sizeof() {
var v int
fmt.Println(unsafe.Sizeof(v))
}

日志输出:

=== RUN TestSizeOf
8
--- PASS: TestSizeOf (0.00s)
PASS

输出8个字节,具体场景和用法后续会拓展

2、 第二个函数Offsetof代表偏移量,主要用与struct field 偏移量

func offsetof() {
person := struct {
Name string
Age int
Address string
Phone uint
}{
Name: "李点点滴滴",
Age: 10,
Address: "ddddddd",
Phone: 12344,
}
fmt.Println(fmt.Sprintf("offsetName=%+v,offsetAge=%+v,offsetAddress=%+v,offsetPhone=%+v", unsafe.Offsetof(person.Name), unsafe.Offsetof(person.Age), unsafe.Offsetof(person.Address), unsafe.Offsetof(person.Phone)))
}

日志输出:

=== RUN TestOffsetOf
offsetName=0,offsetAge=16,offsetAddress=24,offsetPhone=40
--- PASS: TestOffsetOf (0.00s)
PASS

有内存对齐相关知识大家可以自己研究

3、 第三个函数Alignof接受任何类型的表达式x并返回所需的对齐方式,这个用的比较少了解下就行

func alignof() {
fmt.Println(unsafe.Alignof(int(0))) // 打印int类型的对齐要求
fmt.Println(unsafe.Alignof(float64(0.0))) // 打印float64类型的对齐要求
fmt.Println(unsafe.Alignof(struct{}{})) // 打印空结构体类型的对齐要求
fmt.Println(unsafe.Alignof("李四")) // 打印string的内存对其要求
}

日志输出:

=== RUN TestAlignOf
8
8
1
8
--- PASS: TestAlignOf (0.00s)
PASS

这三个函数开发者可以将任意类型变量传入获取对应的 uintptr,用来后续计算内存地址(比如基于一个结构体字段地址,获取下一个字段地址等)
unsafe.Pointer

我们看下unsafe 包下的 Pointer的定义和官方描述

// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
// - A pointer value of any type can be converted to a Pointer.
// - A Pointer can be converted to a pointer value of any type.
// - A uintptr can be converted to a Pointer.
// - A Pointer can be converted to a uintptr.
//
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.type
type Pointer *ArbitraryType

按照我个人的理解文档定义“ArbitraryType”是任意的类型,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。

官方提供了四种 Pointer 支持的场景:

1、 任何类型的指针值都可以被转换为 Pointer

2、 Pointer 可以被转换为任何类型的指针值

3、 uintptr 可以被转换为 Pointer

4、 Pointer 可以被转换为 uintptr
unsafe.Pointer 常见几种使用技巧
将T1转换为指向T2的指针

官方用了math.Float64bits案例

func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}

1、 unsafe.Pointer(&f) 将 float64 类型的参数 f 的地址转换为一个 unsafe.Pointer 类型的指针

2、 *(*uint64)(unsafe.Pointer(&f)) 将 unsafe.Pointer 类型的指针转换为 *uint64 类型的指针,然后再对其进行解引用,从而将 float64 类型的值转换为 uint64 类型的整数

本质上是将unsafe.Pointer作为一种媒介,它可以由任何类型转换得到,也可以将其转换为任意类型

但这里有几点限制

1、 T2 的大小不能超过 T1

2、 T1 和 T2 必须具有相等的内存布局(即相同的字段和对齐方式)

如果满足这些条件,我们可以进行指针类型的转换。

下面这段代码int8和float32是无法转换的

func float32ToInt8(in float32) int8 {
return *(*int8)(unsafe.Pointer(&in))
}

func int8ToFloat32(in int8) float32 {
return *(*float32)(unsafe.Pointer(&in))
}

日志输出:

=== RUN TestT12T2
0
-131072
--- PASS: TestT12T2 (0.00s)
PASS

float32 是一个单精度浮点数,占用四个字节(32 位),遵循 IEEE 754 标准。它的内存布局包含符号位、指数位和尾数位。

int8 是一个有符号的 8 位整数,占用一个字节(8 位),通常以二进制补码的形式表示。

尝试将这两种类型直接强制转换会导致浮点数的部分信息丢失,或者导致整数表示的范围溢出。
将 Pointer 转换为 uintptr(不转换回 Pointer)

将指针转换为 uintptr 会得到被指向值的内存地址,以整数的形式表示。通常情况下,这样的 uintptr 用于打印或记录内存地址。

如果对Go的数组和切片有更深的了解,肯定知道数组底层的内存地址是连续的,有没有测试过呢?举一个例子:

func main() {
var x int
size := unsafe.Sizeof(x)
fmt.Printf("int 占用 %d 个字节\n", size)

tmpList := []int{1, 2, 3, 4, 5}
for i := 0; i < len(tmpList); i++ {
fmt.Printf("16进制地址=%p,10进制地址=%d,值=%+v\n", &tmpList[i], uintptr(unsafe.Pointer(&tmpList[i])), tmpList[i])
}
}

上面这段代码打印的数据如下:

=== RUN TestSizeof2
int 占用 8 个字节
16进制地址=0xc00001a1e0,10进制地址=824633827808,值=1
16进制地址=0xc00001a1e8,10进制地址=824633827816,值=2
16进制地址=0xc00001a1f0,10进制地址=824633827824,值=3
16进制地址=0xc00001a1f8,10进制地址=824633827832,值=4
16进制地址=0xc00001a200,10进制地址=824633827840,值=5
--- PASS: TestSizeof2 (0.00s)

看出来了吗?uintptr 是10进制的地址,首地址+8代表下一个下标的内存地址,这里留一个思考题,数据的下标为啥是从0开始?能推导数组下标的寻址公式吗?
将 Pointer 转换为 uintptr 并进行算术运算后再转换回 Pointer
Offsetof 获取成员偏移量

如果 p 指向一个已分配的对象,可以通过将其转换为 uintptr,加上偏移量,并将其转换回指针来在对象内进行偏移。

这种模式最常见的用法是访问结构体的字段或数组的元素。举一个例子:

func T() {
employee := struct {
Name string
Age int
}{
Name: "李四",
Age: 18,
}

// 分别打印age和name的偏移量
fmt.Printf("nameOffset=%v,ageOffset=%v\n", unsafe.Offsetof(employee.Name), unsafe.Offsetof(employee.Age))
p := unsafe.Pointer(uintptr(unsafe.Pointer(&employee)) + unsafe.Offsetof(employee.Age)) // 转为 uintptr 并且通过算术运算计算 age 的内存地址
*((*int)(p)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300

fmt.Printf("age=%d\n", employee.Age)
}

上段代码打印的值:

=== RUN TestT
nameOffset=0,ageOffset=16
age=300
--- PASS: TestT (0.00s)
PASS

Sizeof 获取任意类型字节数

操作数组和struct有一些区别,再举一个数组的例子

func array() {
var (
tmpList = [6]int{2, 3, 1, 67, 8}
)
for i := 0; i < len(tmpList); i++ {
/*
1、tmpList[i] 转换为Pointer获取基地址,通过基地址+i位置角标对应值的字节数计算下一个元素的地址
2、做完算术运算后 uintptr 转 Pointer
*/
p := unsafe.Pointer(uintptr(unsafe.Pointer(&tmpList[i])) + unsafe.Sizeof(tmpList[i]))
//pp := unsafe.Add(p, unsafe.Sizeof(tmpList[i])) // 或者用这个姿势也是可以的,更简单
*(*int)(p) = i // 赋值
}

// 打印 tmpList
for i := 0; i < len(tmpList); i++ {
fmt.Printf("i=%d,v=%d\n", i, tmpList[i])
}
}

上段代码打印的值:

=== RUN TestT
i=0,v=2
i=1,v=0
i=2,v=1
i=3,v=2
i=4,v=3
i=5,v=4
--- PASS: TestT (0.00s)
PASS

数组为啥要用Sizeof?这跟数组寻址有关系,我先抛一个公式大家自行研究

a[i]_address=基地址(base_address)+i(数组下标)*字节数(int是8个字节...类推就好了)

在调用 syscall.Syscall 时将指针转换为 uintptr

基本用不着不过多解释

syscall.Syscall(syscall.SYS_PRCTL, PR_GET_KEEPCAPS, 0, 0); e != 0

将 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的结果从 uintptr 转换为指针

func TestT(t *testing.T) {
var (
n int
)
p := unsafe.Pointer(reflect.ValueOf(&n).Pointer())
*((*int)(p)) = 3
fmt.Printf("n=%v\n", n)
}

/* === RUN TestT
n=3
--- PASS: TestT (0.00s)
PASS*/

为什么Pointer不返回Pointer而是返回的是 uintptr ?

为了防止调用者在没有导入 unsafe 包并且可能会在不了解风险的情况下,将其转换为任意类型,从而导致不安全的操作。

这种转换过程需要注意下面这点:

当你使用 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 时,返回的结果是 uintptr。为了安全地处理这个结果,你应该在调用之后立即将其转换为 unsafe.Pointer。这个转换应该在同一个表达式中完成,以避免意外的行为。下面这段代码是官方给的错误例子

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))

将 reflect.SliceHeader 或 reflect.StringHeader 的 Data 字段与指针进行相互转换

主要是为了实现字符串和byte切片相互零拷贝转换。这个可以不用了解,官方建议在新的代码中使用 unsafe.String or unsafe.StringData、unsafe.Slice or unsafe.SliceData 。reflect.SliceHeader 和 reflect.StringHeader 应该会在后面的发布中标记为废弃。
Go1.20 引入的几个新方法
SliceData(slice []ArbitraryType) *ArbitraryType

返回指向参数切片底层数组的指针

1、 如果 slice 的容量大于 0,SliceData 返回 &slice[:1][0]。

2、 如果 slice 为 nil,SliceData 返回 nil。

3、 否则,SliceData 返回一个非空指针,指向未指定的内存地址1。

func slice2String() {
var (
b = []byte{72, 101, 108, 108, 111} // 字符串 "Hello" 对应的 ASCII 码
)

ptr := unsafe.SliceData(b)
fmt.Printf("address=%p,v=%v\n", ptr, *ptr) // 若 slice cap>0返回第一个元素指针
fmt.Printf("address=%p\n", &b[0]) // 打印第一个元素的地址
fmt.Println(unsafe.String(unsafe.SliceData(b), len(b))) // 转为字符串
}

数据的打印如下:

=== RUN TestTT
address=0xc000024288,v=72
address=0xc000024288
Hello
--- PASS: TestTT (0.00s)
PASS

String(ptr *byte, len IntegerType) string

String 函数的作用是获取一个字符串,其底层字节从指定的内存地址 ptr 开始,长度为 len。

func bytes2string() {
var (
b = []byte{72, 101, 108, 108, 111} // 字符串 "Hello" 对应的 ASCII 码
)
fmt.Println(unsafe.String(&b[0], len(b)-1))
}

数据的打印如下:

=== RUN TestTT
Hell
--- PASS: TestTT (0.00s)
PASS

StringData(str string) *byte

StringData 函数返回一个指向字符串 str 底层字节的指针。

func string2byte() {
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello1"))
fmt.Println(*unsafe.StringData("Hello")) // 值返回第一个字符的 ASCII 码
}

数据的打印如下:

=== RUN TestTT
0x1128c69
0x1128c69
0x1128ea1
72
--- PASS: TestTT (0.00s)
PASS

为啥字符串“Hello”打印的的地址是相同的?

在Go中,相同的字符串字面量会被编译器优化为同一个内存地址,这是因为字符串是不可变的,编译器可以安全地假设它们不会被修改,因此可以共享相同的内存空间,虽然变量是不同的,但是都指向同一个字符串字面量是相同的,所以的地址是一样的。
总结

在 Go 语言中,unsafe 包提供了一种与底层系统交互的手段,以及在必要时绕过 Go 语言的类型系统进行一些底层操作。

提供了以下操作

1、 直接操作指针和内存:unsafe 包允许程序员直接操作指针,例如读写内存、修改结构体的未导出成员等

2、 绕过类型系统的限制:Go 语言的指针相比 C 的指针有一些限制,例如不能进行数学运算、不同类型的指针不能相互转换等。

3、 性能优化:在某些场景下,使用 unsafe 包可以提高代码的性能。例如,通过直接操作内存,避免了一些不必要的类型转换和拷贝操作。(大家可以学习下零拷贝技术)
思考题

1、 如何通过上面学的知识获取 slice 长度和容量?

slice 源码位置 runtime/slice.go

通过源码看到 slice 的结构体定义如下:

type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}

tmpList := make([]int, 1, 2),通过 make 函数 创建一个切片底层初始化方式如下:

func makeslice(et *_type, len, cap int) unsafe.Pointer

我们可以通过 unsafe.Pointer 和 uintptr 获取计算偏移量获取长度和 len,代码如下:

func TestGetSliceLen(t *testing.T) {
// 首先打印 unsafe.Pointer 占用几个字节
var (
tmpp unsafe.Pointer
)
fmt.Printf("b=%d\n", unsafe.Sizeof(tmpp))

s := make([]int, 8, 12)
plen := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))
fmt.Printf("slice 的长度 len=%v\n", *(*int)(plen))
pcap := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))
fmt.Printf("slice 的容量 cap=%v\n", *(*int)(pcap))
fmt.Println("---------下面是通过 len 和 cap 直接获取长度和容量")

fmt.Printf("通过 len 和 cap 语法糖获取 len=%d,cap=%d\n", len(s), cap(s))
}

日志打印如下:

=== RUN TestGetSliceLen
b=8
slice 的长度 len=8
slice 的容量 cap=12
---------下面是通过 len 和 cap 直接获取长度和容量
通过 len 和 cap 语法糖获取 len=8,cap=12
--- PASS: TestGetSliceLen (0.00s)
PASS

大家看注释,这里不过多解释

2、 unsafe.Pointer 和任意类型、unsafe.Pointer 和 uintptr 相互转换可以拆开定义多个变量吗?为什么?(TT函数是多变量方案)

func T() {
employee := Employee{}
// 和TT函数的区别在下面这几行代码
p := unsafe.Pointer(uintptr(unsafe.Pointer(&employee)) + unsafe.Offsetof(employee.Age)) // 转为 uintptr 并且通过算术运算计算 age 的内存地址
*((*int)(p)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300
fmt.Printf("age=%d\n", employee.Age)
}

func TT() {
employee := Employee{}
// 和T函数的区别在下面这几行代码
p := unsafe.Pointer(&employee)
ptr := uintptr(p) + unsafe.Offsetof(employee.Age) // 转为 uintptr 并且通过算术运算计算 Age 的内存地址
pp := unsafe.Pointer(ptr) // 将 Age 内存地址转换为 Pointer
*((*int)(pp)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300
fmt.Printf("age=%d\n", employee.Age)
}

type Employee struct {
Name string
Age int
}

官方回答在转换回指针之前,不能将 uintptr 存储在变量中,主要原因是在进行指针到 uintptr 的转换时,我们无法保证得到的 uintptr 值会与原始指针一一对应,并且 uintptr 类型不会提供任何指针的语义信息,也不会阻止底层对象被垃圾回收。将 uintptr 类型存储在变量中可能导致不可预料的行为,在变量重新被使用时可能造成程序的安全隐患。 顺便附带一张官方文档截图



公众号原文链接:https://mp.weixin.qq.com/s?__biz=MzkzODY1MDQyMA==&mid=2247483656&idx=1&sn=1cc955dbf2b2a25fe93d55b6a14406db&chksm=c2fdbf53f58a36450086724482507d0f7bcf6cc204fb95b98e8a6ce5aed6c2b88b649073728b&token=1922909448&lang=zh_CN#rd
发布于 2024-02-12 10:02・IP 属地四川


总数:1 页次:1/1 首页 尾页  
总数:1 页次:1/1 首页 尾页  


所在合集/目录



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1