FFI实战之对接GO(CGO)

Posted 不得闲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FFI实战之对接GO(CGO)相关的知识,希望对你有一定的参考价值。

简要说明

Go语言发行到现在已经超过了10个年头了,虽然已经过了那么久了,也已经很稳定了,生态也很强大了。但是从编程世界来说,也依然是个儿童,前辈们也依然活力满满,所以为了使用前辈们留下来的武器和库,咱们必须要和前辈们进行必要的交互,在咱们编程世界中,称之为FFI,也就是外部函数交互接口,Go中用来做这一块的,保不齐的会需要用到CGO,本文不会涉及太多的深度的CGO方面的内容,不过是记录一些实用的技巧,以及日常实用需要掌握的常规的转换,关于CGo比较详细的教程请参考《Go语言高级编程》

明确目标

咱们用CGO的目的是什么,前面也说了,就是FFI,用来和其他的语言进行交互,那么交互的主要规则就是在双方都能识别出对方,主要包括:

  1. 调用方式,CGO使用GCC编译,默认的调用方式就是cdecl,stdcall是win下独有的调用方式,主要区别就是cdecl是由调用者去清理堆栈,而windows是由于系统本身很多时候需要在堆栈上操作,所以其设计的stdcall是被调用的函数执行完毕之后自己清理堆栈,他们两个的传参方式都是由堆栈传参,从右向左传递,其他并无不同,至于比较详细解释,可以网络上查看相关的资料
  2. 参数类型的话主要就是表现在入栈的参数的内容长度一致则可以。

把这两点搞明确了,那么我们就能明确的用其他的语言写动态库或者静态库去给Go调用,也可以用Go写动态库去给其他语言调用,这个就是咱们的最终极目标,就我个人来说,多数时候是用Go写动态库去给Delphi调用,因为Delphi这已经日薄西山的老头语言,当前的最新的流行的各种库都不给提供Delphi的适配,而如果自己去适配,需要花费的时间就比较多,所以,很多时候,就会用Go写一个给Delphi调用。

初入门槛

无论如何,咱们还是先从一个最简单的例子入手,第一步先写一个hello from cgo,先在GO自身调用成功。先来一个官方例子:

package main

/*
#include <stdio.h>
#include <stdlib.h>
void print(char* msg)
    printf("recv from go :%s",msg);

*/
import "C"
import "unsafe"

func main() 
    goString := C.CString("Hello from cgo \\n")
    C.print(goString)
    C.free(unsafe.Pointer(goString))

这个官方例子比较简单,咱们只用了一个数据类型C.CString来将Go的数据传递到C语言中去使用;乍一看来,这个C.CString不知道是啥,黑盒子中呢,只是被告知要这样使用,作为一个有理想的猿,当然不能满足在这种黑盒状态,一定要搞清楚情况。下面,咱们不使用这个C.CString来传递,然后试试能不能成。

分析构造自定义类型

我们来分析一下GO语言的string类型的结构类型,这一块,在Go的相关文档都有介绍,而且在Go自带的源码中,也有给出这个结构,那就是反射包中的StringHeader结构,构造原型如下:

type StringHeader struct 
    Data uintptr
    Len  int

其中Data实际上就是字符串的真实数据地址,而Go使用UTF8存放字符串,所以,这个Data就是一个指向Utf8数据串的指针,Len就是这个数据的长度。这样来看,是不是就比较明确了呢, 现在咱们将这个结构在C语言中声明一个相似的结构体。

typedef struct _goString
    char*   utf8Data;
    size_t  datalen;
goString,*pgoString;

然后,咱们使用这个结构体来构造我们要显示的函数

void printData(pgoString data,int intValue)
    char nData[data->datalen+1];
    nData[data->datalen] = 0;
    memcpy(nData,data->utf8Data,data->datalen);
    printf("recv from go :%s, intValue=%d",nData,intValue);

最后咱们在GO中调用

func main() 
    goString := C.CString("Hello from cgo \\n")
    C.print(goString)
    C.free(unsafe.Pointer(goString))
    temp := "this is from go"
    C.printData(C.pgoString(unsafe.Pointer(&temp)), C.int(231))

解释说明

以上通过构造了一个类似和Go结构体相同的类型进去了,实际上C.CString也和这个差不多,只是C.CString做的更安全一些,因为CGO是单独运行在一个goroutine中的,而go在某些GC的时候,可能会将对象转移,所以像上面搞的那种模式,如果在对象被转移了之后,那么那个temp对象的地址肯定就无效了,从而会导致达到一个非预期的效果。所以,具体情况可以具体去使用不同的方式,自己需要确保的就是地址不变的话,用上面的方法是有效的,一般只要能确保这个对象是在某个触发函数的局部对象,并且调用的CGO函数中不会启动线程去访问这个对象地址的话,一般不会有问题。其他的结构体类型按照相似的方式去处理就好,简单的类型int,有C.int等,具体可以查看相关的文档,这里不细描述了。

反向输出,交相辉映

前面讲了在go语言中调用C语言的方法,实际上两者是相辅相成的,互相调用才是FFI的基石,所以下面咱们再来试试在C中调用Go的函数,高级编程中的写法,直接使用原始标记类型

//export SayHello
func SayHello(s *C.char) 
    fmt.Print(C.GoString(s))

上面使用了C.char类型,然后用的时候,直接使用了C.GoString类型来实现,而在上面,我们已经声明了一个我们对应的go字符串类型_goString,所以,这里我们依然可以使用我们自己实现的方式来写代码,如下:

//export goPrint
func goPrint(cmsg C.pgoString) 
    //这个cmsg就是一个pgoString,这里直接使用
    data := reflect.StringHeader
        Data: uintptr(unsafe.Pointer(cmsg.utf8Data)),
        Len:  int(cmsg.datalen),
    
    msg := *(*string)(unsafe.Pointer(&data))
    fmt.Println("cmsg from c=", msg)

上面咱们构造了一个reflect.StringHeader结构,然后进行赋值,而实际上,pgoString本身就是Go的数据类型,所以咱们就没必要再转一遍,可以直接一步到位

//export goPrint
func goPrint(cmsg C.pgoString) 
    //这个cmsg就是一个pgoString,这里直接使用
    msg := *(*string)(unsafe.Pointer(cmsg))
    fmt.Println("cmsg from c=", msg)

//大家可以试试,然后咱们就可以在C语言中使用goPrint函数了,如下
static void printGoPrint()
    char nData[] = "这是来自于C的";
    goString cstr;
    cstr.utf8Data = &nData[0];
    cstr.datalen = strlen(nData);
    goPrint(&cstr);
    SayHello("SayHello from C");

细枝末节

通过前面的讲解,基本上已经在Go中调用C和在C中调用Go都能支持了,但是细心的人可能就发现了,我在上面的printGoPrint函数前面加上了static,这是为啥呢。

这个主要原因就是,咱们在这个代码中有了GO的导出函数//export goPrint和//export SayHello,只要有这个,那么和这些导出函数写在一个GO文件中的,最终在编译连接的时候,由于obj文件会进行几次连接,然后就会发现多个相同的C函数,就会连接错误,而咱们加上static就表示这函数只在这个文件内有效,其他的文件中无法访问,所以就能祛除这个问题,如果需要在多个文件中都能访问,应该将这些函数提出去,放到单独的C语言文件中,然后在需要使用的地方,声明一下就行了,这样就不用添加static了。

下面咱们将整体代码拆分成三个文件,test.h,test.c,main.go

//test.h
#ifndef testh
#define testh
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _goString
    char*   utf8Data;
    size_t  datalen;
goString,*pgoString;

extern void print(char* msg);
extern void printGoPrint();
extern void printData(pgoString data,int intValue);
#endif
//test.c
#include "test.h"
void print(char* msg)
    printf("recv from go :%s",msg);


void printData(pgoString data,int intValue)
    char nData[data->datalen+1];
    nData[data->datalen] = 0;
    memcpy(nData,data->utf8Data,data->datalen);
    printf("recv from go :%s, intValue=%d",nData,intValue);


void printGoPrint()
    char nData[] = "这是来自于C的";
    goString cstr;
    cstr.utf8Data = &nData[0];
    cstr.datalen = strlen(nData);
    goPrint(&cstr);
    SayHello("SayHello from C");
//main.go
package main

/*
#include <stdlib.h>
#include "test.h"
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() 
    goString := C.CString("Hello from cgo \\n")
    C.print(goString)
    C.free(unsafe.Pointer(goString))
    temp := "this is from go"
    C.printData(C.pgoString(unsafe.Pointer(&temp)), C.int(231))
    C.printGoPrint()


//export goPrint
func goPrint(cmsg C.pgoString) 
    //这个cmsg就是一个pgoString,这里直接使用
    msg := *(*string)(unsafe.Pointer(cmsg))
    fmt.Println("cmsg from c=", msg)


//export SayHello
func SayHello(s *C.char) 
    fmt.Print(C.GoString(s))

然后此时再去编译,注意这个时候编译,需要按照目录编译,不能只制定main.go这个文件编译,否则会连接不到c语言文件中实现的函数了。

小结和注意要点

以上,就可以发现,拆分成多个文件,就可以不需要使用static修饰了,而也将代码分开更容易管理。

到此为止,基本上对于一些比较基本的CGO使用方式,也都讲解了一遍,实际上为了和其他语言进行交互,其实最主要的还是需要理解各个语言之间的数据类型是如何展现的,参数方式是如何传递的,只要理解了这些,其他的就是不变应万变了,以及在使用过程中一些魔法处置,可能需要一些经验,以及细致的去查看官方文档。最后,总结一下,可能最能碰到的一些需要注意的要点:

  1. go语言文件中,有//export导出函数的,无论本文件是否需要使用CGO写C的代码,必须要import "C",否则会编译出错,猜想主要应该是为了生成CABI接口,拆分出C代码,否则这个导出是无效的
  2. import "C"和C代码之间不能有空格,必须连接在一起写
  3. 交叉编译的时候生成动态库的时候,如果编译32位,GCC要选择32位的GCC,编译64位,要指定64位的GCC,否则不能编译成功
  4. go的export函数,导出函数调用方式都是cdecl的
  5. 下一期,将讲解Go,FFI之间如何使用回调函数互相调用,敬请期待。

以上是关于FFI实战之对接GO(CGO)的主要内容,如果未能解决你的问题,请参考以下文章

5.21-5.27博客精彩回顾

PHP FFI调用go,居然比go还快

cgo之简介

CGO实战-封装qsort函数

CGO实战-封装qsort函数

cgo之类型转换