V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
guonaihong
V2EX  ›  程序员

golang cgo 小心 C.Bytes 函数

  •  
  •   guonaihong ·
    guonaihong · 2019-10-22 09:33:00 +08:00 · 3786 次点击
    这是一个创建于 1854 天前的主题,其中的信息可能已经有所发展或是发生改变。

    有时候为节约内存用了 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

    第 1 条附言  ·  2019-10-22 13:02:42 +08:00

    极致省内存方法(不推荐)

    • 在业务里面每次读取一个[]byte。给[]byte留空一位。专门放0,给c字符串使用。
    • 直接强转 go []byte到*C.char(要保证c里面的函数操作是安全的,最好是只读)
    package main
    
    /*
    #include<string.h>
    */
    import "C" 
    
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。\x00")
    
        p := (*C.char)(unsafe.Pointer(&b[0]))
    
        fmt.Printf("[]byte addr:%p:%d\n", b, C.strlen(p))
    
        fmt.Printf("%p\n", b) //告诉编译器b还在使用
    }
    
    
    12 条回复    2019-10-22 19:08:08 +08:00
    reus
        1
    reus  
       2019-10-22 09:43:37 +08:00 via Android
    我想一般人都不会想到用 CBytes… 字符串当然直接用 CString。
    CBytes 一样是复制一份,和 CString 的区别只在末尾的 0,哪来节约内存?
    guonaihong
        2
    guonaihong  
    OP
       2019-10-22 09:59:20 +08:00
    @reus 你的入参是[]byte。[]byte 到 string 要拷贝一次内存的。
    fuis
        3
    fuis  
       2019-10-22 10:02:03 +08:00
    为啥 C.Bytes 就节约内存了,没看懂
    guonaihong
        4
    guonaihong  
    OP
       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
        5
    petelin  
       2019-10-22 10:13:17 +08:00 via iPhone
    @guonaihong 强转就没有拷贝了?那怎么写才可能有拷贝
    reus
        6
    reus  
       2019-10-22 10:13:45 +08:00   ❤️ 1
    @guonaihong 入参是 []byte,那就不需要 string 啊,自己补个 0 不就行了。

    b := []byte("我是小妖怪,逍遥又自在,杀人不眨眼,吃人不放盐。")
    cstr := C.CBytes(append(b, 0))
    petelin
        7
    petelin  
       2019-10-22 10:20:53 +08:00 via iPhone
    https://www.cnblogs.com/zhangboyu/p/7623712.html
    我觉得直接让 string 的指针指向 byte 的地址然后保证 byte 不会再被修改更靠谱
    reus
        8
    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
        9
    guonaihong  
    OP
       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
        10
    guonaihong  
    OP
       2019-10-22 18:39:00 +08:00   ❤️ 1
    @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
        11
    reus  
       2019-10-22 18:48:14 +08:00
    @guonaihong append 是否分配内存,要看 cap,cap 够就不用分配,这是通用的规则。
    guonaihong
        12
    guonaihong  
    OP
       2019-10-22 19:08:08 +08:00
    @reus 用 append 还要依赖传参 空间是否够。觉得这个方式不是最优,可以用类型转换。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5554 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 08:19 · PVG 16:19 · LAX 00:19 · JFK 03:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.