当前位置: 主页 > JAVA语言

java 函数指针-用Golang扩展C语言系统的回调函数以实现扩展

发布时间:2023-06-13 09:16   浏览次数:次   作者:佚名

30 Jul 2017, 16:16

技术

cgo/golang/development

在github上关于cgo的wiki中,有一专门介绍了如何利用cgo技术通过函数指针调用Golang的函数实现. 不过,仔细观察这个章节的代码示例可以发现,它所要解决的其实是以下的场景:

在Golang中想要调用一个已有的C语言函数,但是该C语言函数要求一个函数指针作为参数时应该怎么办?

如果将这个场景稍微改变一下,改成以下场景,对应的解法又该是什么?

在一个C语言实现的已有系统中,对于一个要求函数指针的函数,如何传入一个Golang实现的回调函数以实现“用Golang扩展C语言系统”的目的。

我基于wiki中已有的代码简单探索了一下方法,结果分享如下:

函数 参数 是函数指针_函数指针 指针函数_java 函数指针

试验代码的准备

首先,需要一个声明了函数指针类型的头文件(也就是C语言和Golang的接口)。这里流用了上述wiki中的示例:

/* clib.h */
#ifndef CLIBRARY_H
#define CLIBRARY_H
typedef int (*callback_fcn)(int);
#endif

接下来是C语言程序中调用上述函数指针的入口函数.这个文件也是从wiki中流用的.

/* clib.c */
#include 
#include "clib.h"
void some_c_func(callback_fcn callback)
{
    int arg = 2;
    printf("C.some_c_func(): calling callback with arg = %d\n", arg);
    int response = callback(2);
    printf("C.some_c_func(): callback responded with %d\n", response);
}

在这个程序中,没有定义callback_fcn这个函数指针的具体实现。这个实现将交给下面的Golang进行

在Golang中实现回调函数

函数 参数 是函数指针_函数指针 指针函数_java 函数指针

/* goprog.go */
package main        /* 包名必须是main */
/*
#cgo CFLAGS: -I {clib.h的路径(目录)}
#include "clib.h"
int callOnMeGo_cgo(int in); // Forward declaration.
*/
import "C"
import "fmt"
//export callOnMeGo
func callOnMeGo(in int) int {
    fmt.Printf("Go.callOnMeGo(): called with arg = %d\n", in)
    return in + 1
}
func main() {}        /* 必须定义一个空的main函数 */

这个文件基于Wiki中的示例稍微改了一点,把main函数的实现给去掉了,但保留了一个空的main函数。 此外,不论这个文件在哪里创建,它的package被定义为main。相关的理由如下:

注意: 这里有一个坑:如果将带main函数的.c文件和这些go文件放在一起,然后启动golang编译器编译器编译,也会报错,说main函数数量过多。不知golang编译器为什么要去识别C语言的main函数…

这样一来,回调函数的实现本体就已经完成了。但是如果仅仅如此,是无法实现C语言调用这个函数的,这是因为两种语言的类型不一致,因此实际上上述回调函数的接口与函数指针的声明仍然不一样,所以需要一个Adapter。在cgo中java 函数指针java 函数指针,这样的Adapter被称为”Gateway Function”. 直接搬用Wiki中的代码即可:

/* cfuncs.go */
package main
/*
#include 
// The gateway function
int callOnMeGo_cgo(int in)
{
    printf("C.callOnMeGo_cgo(): called with arg = %d\n", in);
    int callOnMeGo(int);
    return callOnMeGo(in);
}
*/
import "C"

有意思的是,这个Gateway Function实际上是一个实现在go源码注释中的C语言函数,它的声明与函数指针一致。但是它实际封装的却又是一个golang函数.

构建过程

java 函数指针_函数指针 指针函数_函数 参数 是函数指针

到这时为止,所需的代码就算是写完了,接下来需要把程序构建并运行起来:

用golang编译器构建共享库:

$go build -buildmode=c-shared -o libgoprog.so {所有参与编译的go源码}

值得注意的是,-buildmode=c-shared 是直到golang1.5开始才有的选项,且该选项到目前为止(golang1.8)只支持linux平台,不支持windows和Mac OS.

编译成功后,会生成两个文件: 一个是库文件(libgoprog.so), 另一个是该库文件对应的头文件(libgoprog.h).这个头文件的片段如下:

/* libgoprog.h */
#include "clib.h"
int callOnMeGo_cgo(int in);  /* <- 在go源码中定义的Gateway Function */
...(中略)...
extern GoInt callOnMeGo(GoInt p0);   
...(下略)...

这时如果用file命令看一下生成的.so文件,应该是类似以下的结果:

函数指针 指针函数_函数 参数 是函数指针_java 函数指针

libgoprog.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=f49bbe5d2d38c184574b65ed11f55e84c1ad19e3, not stripped

同时,如果用nm查看这个.so文件的导出符号,就可以看到callOnMeGo和callOnMeGo_cgo这两个符号了

由于上一步骤生成了这个头文件,所以此处还需要修改一下之前的 clib.c 文件,把这个头文件给 #include 进去,从而就可在clib.c中看见那个Gateway Function的声明了,而且此时就可以为clib.c文件补上 main() 函数的实现了:

/* clib.c 完整版 */
#include 
#include "clib.h"
#include "libgoprog.h"    /* 追加头文件引用 */
void some_c_func(callback_fcn callback)
{
    int arg = 2;
    printf("C.some_c_func(): calling callback with arg = %d\n", arg);
    int response = callback(2);
    printf("C.some_c_func(): callback responded with %d\n", response);
}
int main(void) {        /* 追加main()函数实现 */
    some_c_func(callOnMeGo_cgo);
}

编译这个C程序

$gcc clib.c -I{clib.h的目录路径} -I{生成的libgoprog.h的目录路径} -L{libgoprog.so的目录路径} -lgoprog -o clibmain

一切正常的话,就可以正常生成可执行文件clibmain了。之后再将先前生成的libgoprog.so 放置到链接器可找到的路径下,执行该程序就可得到下述结果了:

函数指针 指针函数_函数 参数 是函数指针_java 函数指针

通过函数指针调用golang函数的输出

总结

综上,使用golang自带的cgo技术,可以方便地打通C语言和Golang语言。但目前,Go语言编译动态库还只能在Linux平台上实现,需要注意。

另外,考虑到两种语言在数据类型上还是存在较多差异(事实上,编译生成共享库时附带生成的头文件中就定义了大量golang类型到C语言的映射),因此,如果真的要编写程序在C语言中调用Go,其实有相当一部分工作量应该会花在数据类型转换上。

知识共享许可协议

本文采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

一见如故的Go语言

主线程等待子线程结束的各语言实现