简章入题
上一篇,咱们讲过了GO与FFI的基本知识,包括如何让GO和C语言互相调用,以及常规的一些变量如何通过构造相同的结构类型来进行转换,以此来达到咱们的目的,然后在很多时候,咱们不仅仅是调用函数,还需要一些更能打通双方的操作
比如说,我们用GO写了一个动态库,去给C语言用,而这个动态库中会不断的去接收一些状态,然后状态变更了,就通知到C语言,让C语言去执行,当然这里的方法比较多,比如说用Socket,用消息等等等,但是这些用在进程内通讯就有点大材小用了,所以我们首要考量的就是咱们需要在合适的时候去获取C语言的某一个函数去执行
很多人可能就说,那简单啊,上一篇讲过了,咱们在C语言中写一个函数导出来,然后GO语言不就能用了吗,确实是的啊,但是,如果说此时这个GO的库是别人写的一个共有库,是给你用的,此时,别人根本不知道你的导出函数是啥子,所以,这个时候,咱们的回调函数就要上线了,就是双方协商一个函数声明原型,但是咱们不去实现他,只要要使用的人,实现一个跟这个函数申明一样的函数,就能将这个函数传递给需要调用的地方,然后需要的时候,就直接使用这个函数指针来进行调用,这就是函数回调了,函数的真实实现在C中,调用这个函数在GO中,实际上原理和上一篇的一样,只是上一篇,咱们是直接使用的导出函数去执行,现在是咱们这个函数不会导出去,因为不同的人可能实现不同的函数功能,咱们用别人的库,传递过去的是函数的指针。
初步分析
首先,我们先明确两个目标,要传递一个函数指针,那么首先,我们需要搞一个相同的函数声明,如何来声明这个回调的函数类型呢,在哪里声明。
抓到问题了,咱们就专门针对性的去思考就行,那么首先,我们直接将函数原型类型声明在GO代码中行不行呢,思考一下:在GO语言中,函数本身是一等公民和其他类型一样,并无不同,是可以作为变量传递的,且先不说GO函数原型的ABI是否和C的一样,另外一个最主要的是GO的函数传参方式,Go1.17之后,X86平台下传参方式是使用fastcall方式,也就是寄存器先用,和咱们的stdcall,cdecl都不一样,具体的,可以去看看GO反射调用函数的里面的函数调用相关的代码(也就是Value.Call的代码),里面有明确的标记当前函数的调用方式(如abiStepStack等)。
从以上猜想上面我们就可以知道GO函数原型并不是一个CABI函数原型,而在Go语言中,要将一个Go的函数类型转到一个和其他语言通用的函数指针原型类型就需要使用syscall.NewCallback函数或者NewCallbackCDecl函数,这两个函数主要就是表示调用方式不同,一个是stdcall,还有一个是cdecl,可以查看这两个函数,实际上调用的都是compileCallback,而compileCallbac的第一个参数是Go函数,第二个参数,就是之前我们在上一篇讲过的了
stdcall是在函数自身清理堆栈,咱们在Windows中最常见,也就是基本上,如果函数有几个参数的话,函数调用结束之后,会跟上一个 ret XXX 这类指令来平堆栈,而Cdecl的函数在函数执行完了之后,函数本身并不会有一个 ret XXX 的函数,反而会是在 调用这个函数的函数中也就是 call XX这个函数之后会产生一个 Add ESP,XXX 的指令来平栈,至于为啥要设计成这样,主要原因就是cdecl需要支持那种不知道需要传递多少个参数的函数(比如printf函数),所以函数自身无法知道要平的堆栈的大小,只有调用方知道有多少个参数,所以只能由调用方来平栈,其实基本上就是之前上一篇讲过的,具体的可以自行查看相关的反汇编代码,这里不再详诉了,哎,关于这方面一下子又说多了,这块如果没有相关的知识,可能比较糊涂,没关系,咱们记住就好。
从上面的分析,所以如果要在Go中定义一个回调函数,给外面的语言使用,如果要直接使用Go语言自身的方式的话,就必须使用syscall.NewCallback或者NewCallbackCDecl来将函数声明包一包,然后才能是通用的CABI,比如
type goCallBackNotify func(string2 C.pgoString) int
var mb goCallBackNotify
syscall.NewCallback(mb)
类似于这种,包过之后才能是一个有效的CABI,而这个是没有啥意义的,就搞一个原型,也没必要搞那么复杂,所以我们直接将回调函数的原型声明在CGO区。这样就保障了ABI通用,是最简单的,也最明了的了。比如上面声明为
/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _goString{
char* utf8Data;
int datalen;
}goString,*pgoString;
typedef int _stdcall (*notifyCallBack)(pgoString string);
/*
import "C"
这个时候就有了一个回调函数原型,并且指定了调用方式为_stdcall
将C函数指针传递到go
回调函数原型咱们已经定义好了,现在咱们要做的如何将一个外部的函数传递到Go中来,然后让Go可以调用这个外部函数,怎么传呢,函数指针,咱们直接使用uintptr作为参数来表示函数指针就行。那么来了
var notifyCallBack uintptr
//export registerCallBack
func registerCallBack(callBackFunc uintptr) {
notifyCallBack = callBackFunc
}
上面搞了一个全局变量notifyCallBack,使用这个来保存注册的回调函数,以便于后续咱们在go中来调用这个全局函数;到这里,注册写好了,那么下面我们需要在C语言中写一个函数,用这个C函数作为回调函数,然后在Go中来调用这个回调函数,也就是上面的notifyCallBack
/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _goString{
char* utf8Data;
int datalen;
}goString,*pgoString;
typedef int _stdcall (*notifyCallBack)(pgoString string);
//外部语言定义的回调函数NotifyMsg
static int _stdcall NotifyMsg(pgoString data){
char nData[data->datalen+1];
nData[data->datalen] = 0;
memcpy(nData,data->utf8Data,data->datalen);
printf("recv from go :%s, End\n",nData);
return 0;
}
//这里用来调用回调函数
static void callNotify(void* notifyFunc,pgoString string){
((notifyCallBack)notifyFunc)(string);
}
static void init(){
registerCallBack(NotifyMsg);
}
/*
import "C"
好,这里基本上,我们就算是定义模拟了一个外部函数的调用环境。
然后现在咱们做一个在Go中模拟调用这个环境的
import (
"fmt"
"unsafe"
)
//这个就是在GO中模拟调用上面的回调函数
func notify(msg string) {
C.callNotify(unsafe.Pointer(notifyCallBack), C.pgoString(unsafe.Pointer(&msg)))
}
func main() {
C.init()
notify("this is from Go")
}
此时,go中的notify函数就是对于C语言中的回调函数NotifyMsg的包装了。这个写法算是比较明白的,是在CGO内部写了一个回调函数的调用跳板函数C.callNotify来实现了和回调函数的对接。
那么如果不使用跳板函数的话,能不能实现调用这个回调函数呢?大家可以思考一下,咱们下回分解