golang cgo 小心 C.Bytes 函数

2019-10-22 09:33:00 +08:00
 guonaihong

有时候为节约内存用了 C.CBytes 函数,这不往坑里跑了。c 里面字符串都是要以数字 0 结尾的。

复现代码(直接使用 C.CBytes 有问题)

为节约内存,想使用 C.CBytes 直接到 c 的 char *类型。但是有问题

package main

// #include <string.h>
import "C"

import (
    "fmt"
)

func main() {
    b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。")
    bs := string(b)
    p := C.CBytes(b)
    fmt.Printf("%d:%d:%d\n", int(C.strlen((*C.char)(p))), len(b), int(C.strlen((*C.char)(C.CString(bs)))))
}

// 输出
// Cbytes.strlen(75): len(72):CString.strlen(72)

发现长度不对

改进方法([]byte 到*C.char)

既然 C.Bytes 这条路走不同,换个思路,用类型强转吧 []byte->string。再使用 C.CString 不就得了

// #include <string.h>
import "C"

import (
    "fmt"
    "unsafe"
)

func main() {
    b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。")
    bs := *(*string)(unsafe.Pointer(&b))
    p := C.CString(bs)
    fmt.Printf("strlen(%d)\n", C.strlen((*C.char)(p)))
}


github

https://github.com/guonaihong/gout

3817 次点击
所在节点    程序员
12 条回复
reus
2019-10-22 09:43:37 +08:00
我想一般人都不会想到用 CBytes… 字符串当然直接用 CString。
CBytes 一样是复制一份,和 CString 的区别只在末尾的 0,哪来节约内存?
guonaihong
2019-10-22 09:59:20 +08:00
@reus 你的入参是[]byte。[]byte 到 string 要拷贝一次内存的。
fuis
2019-10-22 10:02:03 +08:00
为啥 C.Bytes 就节约内存了,没看懂
guonaihong
2019-10-22 10:08:39 +08:00
@fuis 在 go 里面 []byte 到 string 是有一次内存拷贝的。想把 go 里面的[]byte 转成 c 的*C.char 常见做法有两次拷贝
[]byte-->string-->*c.char

本来想用 C.Bytes 节约[]byte->string 的一次拷贝,发现没加'\0‘。后面用强制类型转化搞定了。
package main

import (
"fmt"
)

func main() {
s := "wowowo"
b := []byte(s)
fmt.Printf("%p:%p\n", &s, b)
}
petelin
2019-10-22 10:13:17 +08:00
@guonaihong 强转就没有拷贝了?那怎么写才可能有拷贝
reus
2019-10-22 10:13:45 +08:00
@guonaihong 入参是 []byte,那就不需要 string 啊,自己补个 0 不就行了。

b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。")
cstr := C.CBytes(append(b, 0))
petelin
2019-10-22 10:20:53 +08:00
https://www.cnblogs.com/zhangboyu/p/7623712.html
我觉得直接让 string 的指针指向 byte 的地址然后保证 byte 不会再被修改更靠谱
reus
2019-10-22 10:27:56 +08:00
@petelin bs := *(*string)(unsafe.Pointer(&b)) 这一行就是你说的“让 string 的指针指向 byte 的地址”

但具体到这个情况,[]byte 到 *C.char 是不需要 string 的,直接在 []byte 最后补零,再用 C.CBytes 就行了
guonaihong
2019-10-22 13:05:57 +08:00
@reus 加 0 的思路是好的。但是 append 发现空间不够还是会重新 malloc 内存。可以控制[]byte,留空一位专门放 0。
打印下面的代码证实, append 重新分配内存。
```go
package main

import "C"

import "fmt"

func main() {
b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。")
b1 := append(b, 0)
cstr := C.CBytes(b1)
fmt.Printf("b(%p):append address(%p):cbytes(%p)\n", b, b1, cstr)
}

```
guonaihong
2019-10-22 18:39:00 +08:00
@fuis "为啥 C.Bytes 就节约内存了,没看懂"。
如果 C.Bytes 可以用,只是把内存分配的过程从两次变为 1 次。你可以看下

改进方法([]byte 到*C.char) --->(1 次分配内存)
极致省内存方法(不推荐) ---->(0 次分配内存)

这两个标题下面的方法,把内存分配从 1 次,变为 0 次。当然推荐 1 次的方法,不容易出错。

## 内存两次分配的原因如下

要把 go 里面的[]byte 转成 c 里面的 char *。常规做法有两步[]byte-->string-->char *。每一步都有 malloc 内存的操作。

* 第一步 []byte--> string。(需要重新分配内存, 比如 string(bytes 类型的变量))
* 第二步 string --> char *。(需要重新分配内存,C.CString(s))
reus
2019-10-22 18:48:14 +08:00
@guonaihong append 是否分配内存,要看 cap,cap 够就不用分配,这是通用的规则。
guonaihong
2019-10-22 19:08:08 +08:00
@reus 用 append 还要依赖传参 空间是否够。觉得这个方式不是最优,可以用类型转换。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/611625

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX