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