FFI实战之对接GO(CGO)

博观而约取,厚积而薄发。这篇文章主要讲述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 Lenint

其中Data实际上就是字符串的真实数据地址,而Go使用UTF8存放字符串,所以,这个Data就是一个指向Utf8数据串的指针,Len就是这个数据的长度。这样来看,是不是就比较明确了呢, 现在咱们将这个结构在C语言中声明一个相似的结构体。
typedef struct _goString char*utf8Data; size_tdatalen; 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=https://www.songbingjia.com/android/%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 = https://www.songbingjia.com/android/& 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_tdatalen; 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=https://www.songbingjia.com/android/%d",nData,intValue); void printGoPrint() char nData[] = "这是来自于C的"; goString cstr; cstr.utf8Data = https://www.songbingjia.com/android/& 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. 【FFI实战之对接GO(CGO)】下一期,将讲解Go,FFI之间如何使用回调函数互相调用,敬请期待。

    推荐阅读