golang学习笔记-golang调用c实现的dll接口细节(二) - Go语言中文社区

golang学习笔记-golang调用c实现的dll接口细节(二)


  各种原因需要与c或者c++打交道,之前对cgo有一点的了解,曾经了在了解的过程中记录了学习的过程。仅在使用的角度上讲,但是好多东西确实是模棱两可。一个契机,需要在go的框架下用到c++语言的sdk,顺便就记录一下cgo的学习过程,然后再给自己挖个坑,再深入了解一下cgo的机理和更加广泛的使用。

  本篇文章主要从主调的角度入手,介绍如何在go中使用c的代码,面对工程级的如何模块化,对于小的c代码如何在一个文件中实现;介绍如何在c中使用go的导出函数,作为c函数的回调函数使用。

1. go调用c

1.1 快速入门

1.1.1 cgo初体验

  构造一个超级简单的cgo程序,在这个程序中我们只引入cgo的包。虽然在代码中没有用到cgo相关的代码,但是在编译和链接的阶段启动了gcc编译器。算是一个完整的cgo程序。

package main

import "C"
import "fmt"

func main() {
	fmt.Println("hello world")
}

1.1.2 基于c标准库输出字符

  在go程序中引用c的标准库函数,打印字符串。在下面代码中C.CString("hello world")申请了c的内存,需要手动释放掉,不释放的话会导致内存泄露。但是这个示例不影响,程序退出后系统会自动回收资源。

package main

//#include <stdio.h>
import "C"

func main() {
	C.puts(C.CString("hello world"))
}

1.1.3 使用自己的函数

  在1.1.2中我们使用了c的标准库函数puts输出了一个字符串,在这定义一个函数,打印go输入的字符串到终端上。这个例子调用c的free函数,释放了内存,就不会存在内存泄露的问题(需要引入stdlib.h标准库)。

package main

/*
#include <stdio.h>
#include <stdlib.h>
static void sayHello(const char* s)
{
	puts(s);
}
*/
import "C"
import "unsafe"

func main() {
	cs := C.CString("hello world")
	defer C.free(unsafe.Pointer(cs))
	C.sayHello(cs)
}

1.1.4 模块化自定义的函数

  在1.1.3示例中定义了一个sayHello的函数,实现了打印字符串的功能,但是看起来很乱,没有模块化。我们将c函数剥离为一个.c的函数,放在与main.go同级的目录下,相应的修改main.go函数的部分代码。

//sayHello.c
#ifndef _HELLO_H
#define _HELLO_H
#include <stdio.h>
void sayHello(const char *s)
{
    puts(s);
}
#endif
package main

/*
#include <stdlib.h>
#include "sayHello.c"
*/
import "C"
import "unsafe"

func main() {
	cs := C.CString("hello world")
	defer C.free(unsafe.Pointer(cs))
	C.sayHello(cs)
}

1.1.5 声明和实现分离

  将sayHello模块头文件和实现文件分离,main.go文件只需要引入.h文件即可。

  需要注意的是,这里的编译不能用go build main.go的指定文件方式编译,这种编译会下面的报错。

# command-line-arguments
/tmp/go-build799889451/b001/_x002.o:在函数‘_cgo_3e94971ce40c_Cfunc_sayHello’中:
/tmp/go-build/cgo-gcc-prolog:61:对‘sayHello’未定义的引用
collect2: 错误:ld 返回 1

  在main.go文件的目录下执行go build编译文件,执行在终端上打印hello world字样。

//sayHello.h
#ifndef _HELLO_H
#define _HELLO_H
void sayHello(const char *s);
#endif
//sayHello.c
#include <stdio.h>
#include "sayHello.h"
void sayHello(const char *s)
{
    puts(s);
}
package main

/*
#include "sayHello.h"
#include <stdlib.h>
*/
import "C"
import "unsafe"

func main() {
	cs := C.CString("hello world")
	defer C.free(unsafe.Pointer(cs))
	C.sayHello(cs)
}

1.2 go调用c++的库

1.2.1 c++代码

  和 go 模块化调用 c 代码类似,调用 c++时同样将头文件和实现文件分离,只不过为了满足 go 调用的 c 的
函数范式,需要在 c++的实现文件中以 c 的风格引入头文件。

1.头文件的代码
#define _HELLO_H_
void sayHello(const char* s);
#endif // !_HELLO_H_
2.实现文件的代码

在这个文件中使用 c++的标准输出流输入字符串到终端上。

#include  <iostream>
extern "C"{
    #include "hello.h"
}
void sayHello(const char* s)
{
    std::cout << s << std::endl;
}
3.go 调用

调用部分和之前的一样。

package main

/*
#include"hello.h"
#include<stdlib.h>
*/
import "C"
import "unsafe"

func main() {
   cs := C.CString("hello world")
   defer C.free(unsafe.Pointer(cs))
   C.sayHello(cs)
}
4.编译和运行

编译时不能指定文件编译,执行执行go build即可。

文件结构:

.
├── hello.cpp
├── hello.h
├── main.go
└── README.md

0 directories, 4 files

1.3 go调用c的实例

待续。。。

2.c调用go

  上面小结介绍了go调用c和c++函数的方式和过程,这小结我们看一下如何将go的函数导出,给c语言的函数使用。

2.1 go函数导出入门

  在1.1.3章节实现了一个c函数,用于在终端打印一个字符串。现在我们想用go函数来打印这个字符串,并且将这个函数导出,然后再用go的主程序来调用这个导出函数,实现打印的目的。以下用 go 语言实现hello.h的函数功能,在 go 的函数中实现 c 语言的调用。

2.1.1 go的导出函数

  定一个名为hello.go的文件,在这个文件中实现一个打印字符串到终端的函数,并且使用关键字将这个函数导出。

package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
   fmt.Println(C.GoString(s))
}

上面函数的//export SayHello表示的含义和必须的要求:

  • //export 为导出的关键子,斜杠后面不能用空格,必须挨着
  • SayHello 为导出的函数名,必须功能函数名字一样,且函数的参数需要转换为 c 包中定义的变量
  • 上面两个之间用空格隔开

上面的函数在编译时就导出了一个 go 函数,对应的是hello.h文件中的SayHello函数。

2.1.2 c的头文件

定一个hello.h的头文件,在这个文件中声明一个 c 的函数。

void SayHello(/*const*/ char* s);

2.1.3 go调用函数

  在main.go文件中,直接使用导出的函数SayHello,需要引入hello.h的头文件。我们只是在hello.h文件中声明了SayHello函数,程序在编译和链接时使用的是我们在文件hello.go文件中实现并导出的SayHello函数。

package main

/*
#include<hello.h>
*/
import "C"

func main() {
	C.SayHello(C.CString("hello world"))
}

2.1.4 编译和运行

1. 文件结构
.
├── hello.go
├── hello.h
├── main.go
└── README.md

0 directories, 4 files
2.编译运行

main.go文件下执行go build不指定文件编译即可。

2.2 在一个文件中实现go函数的导出和调用(一)

  在上面 1.1 中使用文件分离的方式实现了 go 函数的导出并且调用。这个示例中将在一个文件中导出 go 的函数并且在 go 的main 函数中调用。

2.2.1 代码和解释

package main

/*
#include<stdlib.h>
void SayHello(char * s);
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	cs := C.CString("hello world")
	defer C.free(unsafe.Pointer(cs))
	C.SayHello(cs)
}

//export SayHello
func SayHello(s *C.char) {
	fmt.Println(C.GoString(s))
}

上面的代码分为 3 个部分。

1. c包中函数的声明

  这部分声明了一个 c 风格的函数SayHello,由于在调用时需要释放申请的内存,因此引入了 c 的标准库stdlib.h文件。

*注意*:第一个/*import "C"之间不能有空行。

/*
#include<stdlib.h>
void SayHello(char * s);
*/
import "C"
2. go对应c函数的实现和导出

导出名为SayHello的 go 函数,参数使用 c 包中定义的参数类型。

//export SayHello
func SayHello(s *C.char) {
	fmt.Println(C.GoString(s))
}
3. 对 go 的导出函数的调用

调用导出函数和之前的一样,用 free 释放内存。

func main() {
	cs := C.CString("hello world")
	defer C.free(unsafe.Pointer(cs))
	C.SayHello(cs)
}

2.2.2 编译和运行

1. 文件结构
.
└── main.go

0 directories, 1 file
2. 编译和运行

在 main.go 目录下执行go build直接编译。

2.3 在一个文件中实现go函数的导出和调用(二)

  在 1.2 章节中的导出函数参数仍然使用的是 c 包中的参数,但是我们能不能使用 go 的参数类型呢?毕竟这是在 go 的函数中呀,我们看如何用 go 的参数实现。

2.3.1 代码和解释

  通过分析可以发现 SayHello 函数的参数如果可以直接使用 Go 字符串是最直接的。在 Go1.10 中 CGO 新增加了一个GoString预定义的 C 语言类型,用来表示 Go 语言字符串。下面是改进后的代码:

//build in go1.10
package main

/*
void SayHello(_GoString_ s);
*/
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello("Hello World")
}

//export SayHello
func SayHello(s string) {
	fmt.Println(s)
}

2.4 go导出函数实例

在这个例子中总结了两个方面的知识点:

  • go调用c的函数
  • go函数导出且传递给c的函数作为c的回调函数

2.4.1 文件结构

一共有四个文件,hello.hhello.c分别是c函数的声明和实现,hello.go为go的导出函数的声明和实现文件,main.go调用函数。

.
├── hello.c
├── hello.go
├── hello.h
└── main.go

0 directories, 4 files

2.4.2 go导出函数的声明

在这个名为hello.go的文件中,定义了两个导出函数SayHelloexport_flow

SayHello函数接受一个char的入参,并且将入参打印到终端。

export_flow函数没有入参和返回值,这个函数被调用后会在终端上打印一个字符串。

package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
	fmt.Println(C.GoString(s))
}

//export export_flow
func export_flow() {
	// 这个是测试的go的回调函数,这个函数注入到c的代码中,可以理解为在这个函数中实现了数据的处理
	fmt.Println("this is flow func in go")
}

2.4.3 c函数的声明

  这个文件中声明了在文件hello.go中导出的函数SayHello,只是在这个位置声明,实现是在go文件中实现的。

另一个声明callFolw是c语言的函数。这个还是接受了flow类型的参数fn(指针函数)

//hello.h
#ifndef _HELLO_H_
#define _HELLO_H_
//声明一个回调函数的类型,这个类型名为flow,没有入参,返回值为void
typedef void (*flow)();
//go导出函数的声明
void SayHello(char * s);

// c语言函数的声明
void callFlow(flow fn);
#endif

2.4.4 c的实现函数

根据2.4.2和2.4.3,在这个文件中实现了c语言的函数callFlow,只是调用了一个函数指针fn

#include "hello.h"
#include <stdlib.h>
#include <stdio.h>

void callFlow(flow fn)
{
    fn();
}

2.4.5 main调用函数

1. 调用的代码
package main

/*
#include "hello.h"
#include <stdlib.h>
extern void export_flow();
*/
import "C"
import "unsafe"

func main() {
	cs := C.CString("Hello World")
	defer C.free(unsafe.Pointer(cs))
	C.SayHello(cs)
	C.callFlow(C.flow(C.export_flow))
}

2.解释和说明

上面的主程调用中大致可以分为2个部分,依次说明:

  • c包引入和声明
/*
#include "hello.h"
#include <stdlib.h>
extern void export_flow();
*/
import "C"

第2行:引入了.h头文件声明,声明go的导出函数SayHello、c语言指针函数flow和c语言函数callFolw

第3行:引入c语言标准库free来释放内存

第4行:声明一个c语言的导出函数export_flow,这个函数无入参,返回值为void,对应的是hello.go文件中的export_flow函数

第5、6行:必须无空行,且不能合并import "C"

  • 函数的调用
cs := C.CString("Hello World")
defer C.free(unsafe.Pointer(cs))
C.SayHello(cs)
C.callFlow(C.flow(C.export_flow))

第1、2行:定义一个字符串,并申请空间。延迟释放申请到的空间

第3行:调用go导出的函数SayHello,在终端打印hello world字样

第4行:

C.callFlow调用c语言的函数callFlow

C.export_flow是go的导出函数export_flow

callFlow函数有一个类型为flow的指针函数作为入参,我们把go的导出函数export_flow作为callFlow的入参,此时需要转换一下类型,合并之后就是C.callFlow(C.flow(C.export_flow))

2.4.6 编译和运行

在main函数的目录下运行go build编译,运行结果:

Hello World
this is flow func in go

2.5 go导出函数回调c的普通类型

  将go的函数导出,作为参数传递给c的函数,作为回调函数使用。将c函数中的数据回调到go的代码中做业务逻辑。在这个例子中回调了常见的数据读取方式,一个指针和长度,用于取c函数中的一段内存数据。

2.5.1 文件结构

.
├── hello.c
├── hello.go
├── hello.h
└── main.go

0 directories, 4 files

hello.hhello.c文件实现c函数的逻辑和部分变量的定义,hello.go文件定义go的导出函数,main.go是调用的主函数。

2.5.2 c函数的逻辑

1 hello.h声明

 在这个文件中,定义了一个函数指针类型export_fetchDatas,作为c语言函数的回调参数。引用了go的导出函数export_fetchDatasextern关键字说明函数已经在别的文件中(hello.go)实现,此处只是引用。同时定义了两个c语言的函数loginlogout作为调用入口。

// hello.h
#ifndef _HELLO_H_
#define _HELLO_H_

//定义用户信息的结构体
typedef struct tagUserInfo
{
    unsigned char* username;
    unsigned char* password;
}USERINFO,*LUNSERINFO;

//引用go的导出函数
extern int export_fetchDatas(unsigned char *pBuf, unsigned int RevLen);

//函数指针(用于回调)
typedef int (*fetchDatas)(unsigned char*,unsigned int);

// c语言函数的声明
void login(LUNSERINFO userinfo, fetchDatas fn);
void logout();
#endif

实现文件hello.c, 在实现中使用死循环来一直向入参回调变量中写入数据。

#include "hello.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

bool _bExit = false;

void login(LUNSERINFO userinfo, fetchDatas fn)
{
    printf("<< login@:login msgn");
    if (0 == strcmp(userinfo->username, "admin"))
        printf("<< login@: username=%s, password=%s login success.n:", userinfo->username, userinfo->password);
    else
        printf("<< login@: username=%sn", userinfo->username);

    char buffer[512];
    int count = 0;
    while (1)
    {
        if (_bExit == true)
        {
            printf("<< login@:fetch exitn");
            break;
        }
        int n = sprintf(buffer, "count=%d", count++);
        fn(buffer, strlen(buffer));
        memset(buffer, 0, sizeof(buffer));
        sleep(5);
    }
    printf("<< login@:promt data end.n");
}

void logout()
{
    printf("<< logout@:rcv logout signaln");
    _bExit = true;
}

2.5.3 go导出函数

go的导出函数文件中定义了导出函数export_fetchDatas,参数直接使用了"C"包中的定义,这里的参数类型需要与hello.h文件中export_fetchDatasfetchDatas的类型一致,要不然编译报错。

// hello.go
package main

import "C"

import (
	"reflect"
	"unsafe"
)

//export export_fetchDatas
func export_fetchDatas(pBuf *C.uchar, RevLen C.uint) int {
	var buf []byte
	data := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
	data.Data = uintptr(unsafe.Pointer(pBuf))
	data.Len = int(RevLen)
	data.Cap = int(RevLen)

	logger.Printf("## export_fetchWithErr@:this is callback result:%v", string(buf))
	return 0
}

2.5.4 调用和编译

以下是摘录的部分主程序调用代码。

var userinfo = C.struct_tagUserInfo{}
userinfo.username = (*C.uchar)(unsafe.Pointer(username))
userinfo.password = (*C.uchar)(unsafe.Pointer(password))
logger.Printf(">> 调用c的函数login,模拟登陆的操作,并传递一个go的到处函数作为login函数的回调,打印c函数中回调得到信息")
C.login((*C.struct_tagUserInfo)(unsafe.Pointer(&userinfo)), C.fetchDatas(C.export_fetchDatas))

编译时,直接在main.go文件的同级目录下运行go build即可,运行后hello.go的导出函数会一致打印收到的c函数中的数据。
在这里插入图片描述

2.6 go导出函数回调c的结构体

  在这个例子中,go的导出函数接受一个结构体变量,并且将结构体变量打印到终端上。

2.6.1 文件结构

这个示例中同样是4个文件,略有不同的是这次使用的是cpp的文件,用c的方式引入定义,cpp中可以用c++实现。

.
├── hello.cpp
├── hello.go
├── hello.h
└── main.go

0 directories, 4 files

2.6.2 c函数实现

这个c的头文件中,定义了一个结构体tagStudent,这个结构体又包含一个结构体。里面需要注意的一些问题后面集中整理

// hello.h
#ifndef _HELLO_H_
#define _HELLO_H_

//定义用户信息的结构体
typedef struct tagHomeInfo
{
    char addr[255]; //住址
    char code[255]; //邮编
} HOMEINFO, *LHOMEINFO;

typedef struct tagStudent
{
    HOMEINFO homeInfo; //家庭信息
    char name[255];    //姓名
    char gender;       //性别
    int age;           //年龄
} STUDENT, *LSTUDENT;

//函数指针(用于回调)
typedef void (*fetch_student)(STUDENT *);

//go导出函数的声明
extern void export_fetch_student(STUDENT *stu);

// c语言函数的声明
void queryStudent(fetch_student fn);
#endif

2.6.3 go导出函数

这个导出函数和2.5中的导出函数不同的是,在go的文件中又定义了一份结构体。这个结构体和在c函数中定义的结构体在结构和字段的类型上一致。c结构体中的子结构在什么位置定义,go中的结构体必须在同样的位置上定义。
go的导出函数必须用c中函数的真实而定结构体名,在这个里面体现为tagStudent(访问c的结构体需要加struct_前缀)。导出函数收到c的结构体后指针,将结构体指针强制转换成go的结构体指针。然后就可以使用go的结构体对象了。
这个文件中因为使用到了c函数的定义中结构体,因此也引入了C包和hello.h头文件。

package main

/*
#include "hello.h"
*/
import "C"
import (
	"unsafe"
)

type Student struct {
	HomeInfo HomeInfo
	Name     [255]byte
	Gender    byte
	Age      int32
}
type HomeInfo struct {
	Addr [255]byte
	Code [255]byte
}

//export export_fetch_student
func export_fetch_student(st *C.struct_tagStudent) {
	p := (*Student)(unsafe.Pointer(st))
	name := p.Name
	gender := p.Gender
	age := p.Age
	addr := p.HomeInfo.Addr
	code := p.HomeInfo.Code

	logger.Printf("name=%s", string(name[:]))
	logger.Printf("gender=%c", gender)
	logger.Printf("age=%d", age)
	logger.Printf("addr=%s", string(addr[:]))
	logger.Printf("code=%s", string
                            
                            版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_30549833/article/details/108581489
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢