博观而约取,厚积而薄发。这篇文章主要讲述FFI实战之对接GO(CGO)相关的知识,希望能为你提供帮助。
简要说明Go语言发行到现在已经超过了10个年头了,虽然已经过了那么久了,也已经很稳定了,生态也很强大了。但是从编程世界来说,也依然是个儿童,前辈们也依然活力满满,所以为了使用前辈们留下来的武器和库,咱们必须要和前辈们进行必要的交互,在咱们编程世界中,称之为FFI,也就是外部函数交互接口,Go中用来做这一块的,保不齐的会需要用到CGO,本文不会涉及太多的深度的CGO方面的内容,不过是记录一些实用的技巧,以及日常实用需要掌握的常规的转换,关于CGo比较详细的教程请参考《Go语言高级编程》。
明确目标咱们用CGO的目的是什么,前面也说了,就是FFI,用来和其他的语言进行交互,那么交互的主要规则就是在双方都能识别出对方,主要包括:
- 调用方式,CGO使用GCC编译,默认的调用方式就是cdecl,stdcall是win下独有的调用方式,主要区别就是cdecl是由调用者去清理堆栈,而windows是由于系统本身很多时候需要在堆栈上操作,所以其设计的stdcall是被调用的函数执行完毕之后自己清理堆栈,他们两个的传参方式都是由堆栈传参,从右向左传递,其他并无不同,至于比较详细解释,可以网络上查看相关的资料
- 参数类型的话主要就是表现在入栈的参数的内容长度一致则可以。
初入门槛无论如何,咱们还是先从一个最简单的例子入手,第一步先写一个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使用方式,也都讲解了一遍,实际上为了和其他语言进行交互,其实最主要的还是需要理解各个语言之间的数据类型是如何展现的,参数方式是如何传递的,只要理解了这些,其他的就是不变应万变了,以及在使用过程中一些魔法处置,可能需要一些经验,以及细致的去查看官方文档。最后,总结一下,可能最能碰到的一些需要注意的要点:
- go语言文件中,有//export导出函数的,无论本文件是否需要使用CGO写C的代码,必须要import " C" ,否则会编译出错,猜想主要应该是为了生成CABI接口,拆分出C代码,否则这个导出是无效的
- import " C" 和C代码之间不能有空格,必须连接在一起写
- 交叉编译的时候生成动态库的时候,如果编译32位,GCC要选择32位的GCC,编译64位,要指定64位的GCC,否则不能编译成功
- go的export函数,导出函数调用方式都是cdecl的
- 【FFI实战之对接GO(CGO)】下一期,将讲解Go,FFI之间如何使用回调函数互相调用,敬请期待。
推荐阅读
- docker入门到进阶一
- spring+mybatis启动NoClassDefFoundError异常分析三部曲之二(定位错误)
- Spark,一个奇迹的诞生
- 微服务化最佳实践
- mkw优秀职场人必修课-职场心理学, 助你走出内耗陷阱分享
- Nginx和Tomcat的安装
- 抖音快手机房搭建市场怎么样()
- find
- 小胖学Linux day21~22(find文件查找)