GO与C语言封装后的DLL交互

基于Windows实现C语言封装C++工程并导出DLL动态链接库,经过cgo响应http服务

Posted by 矫红岩 on November 24, 2018

环境

  1. gcc MinGW-W64 8.1.0
  2. go version go1.11 windows/amd64
  3. visual studio 2015 professional(可选) image
    关于go的环境参考博客《vscode配置go编译环境》

    C包装C++项目

    cgo很好的实现了c项目与go的交互,但是当项目需要引入C++时,比如STL(Standard Template Library)标准模板库,go不能读取C++的模板库。

go间接调用C++有两种方式:

  1. 通过swig包装C++项目生成go的包,参考我的博客:《通过swig实现go调用c++程序》
  2. 通过C语言包装C++项目

一个C包装的C++的例子

在整个工程里多加一个纯C的.h和.cpp文件
看图
image 图中我的工程项目是JP20,我在其中的一个类中增加两个用于测试的函数PrintHello和open。一个void类型,一个int类型,其中int类型具有返回值可以方便后续进行参数传递。

//jp20.cpp
void PrintHello() {
		printf("hello in c_idxjp\n");
}
int open(int param) {
		printf("open in c,para:%d\n",param);
		return param;
}

//jp20wrapper.h
#ifndef _JP20_WRAPPER_H
#define _JP20_WRAPPER_H
typedef struct jpWrapper jpWrapper;
#ifdef __cplusplus
//#ifndef _STDBOOL_H
//#define _STDBOOL_H
//#define _Bool bool
//#endif
extern"C" {
#endif
	jpWrapper *GetInstance(void);
	__declspec(dllexport) void PrintHello(jpWrapper* pjp);
	__declspec(dllexport) int open(jpWrapper* pjp,int para);
	void ReleseInstance(jpWrapper* pInstance);
#ifdef __cplusplus
};
#endif
#endif

在jp20wrapper.cpp文件中实现.h中的方法 image

#include "jp20Wrapper.h"
#include "jp20.h"
#ifdef __cplusplus
extern"C" {
#endif
struct jpWrapper
{
	CIdxJP idx_jp;
};
	jpWrapper *GetInstance(void) {
		return new jpWrapper;
	}
	void ReleseInstance(struct jpWrapper* pInstance) {
		delete pInstance;
		pInstance=0;
	}
	void PrintHello(jpWrapper* pjp) {
		pjp->idx_jp.PrintHello();
		printf("hello in c_wrapper_idxjp\n");
	}
	int open(jpWrapper* pjp,int para) {
		printf("open in c_wrapper,para:%d\n",para);
		return pjp->idx_jp.open(para);
	}
#ifdef __cplusplus
};
#endif

再来看一下jp20wrapper类,这个类是为了封装整个项目。

  1. 用结构体封装C++项目中的类,写在结构体中易于拓展,可以方便以后将其他类封装进去;
  2. 对结构体进行实例化GetInstance以及释放ReleseInstance;
  3. 封装两个函数,并导出dll。 dll的导出有两种方式:函数名前加上declspec(dllexport)和写入def文件,这里我用了第一种方式

    测试导出函数

    在vc自带的dumpbin.exe中进行测试

    vs\vc\bin\dumpbin.exe -exports "full-path\JPTree.dll"
    

    image 上图中可以看到两个导出函数。

Go调用DLL

image 开始测试的时候犯了一个经验主义错误,bug类型:exit status 3221225781
这是因为之前一直试图用go调用c的静态链接库,静态链接库是这样写的,但是动态链接库是完全不同的方式。 image

package main
import (
	"fmt"
	"syscall"
)
var D = syscall.NewLazyDLL("JPTree.dll")
func main() {
	fmt.Println("hello word")
		D := syscall.NewLazyDLL("JPTree.dll")
		DLL_PrintH := D.NewProc("PrintHello")
		DLL_PrintH.Call()
		DLL_Open := D.NewProc("open")
		ret,_,err := DLL_Open.Call(uintptr(10), uintptr(20))
		fmt.Println("ret:",ret)
		if err != nil {
			e := err.(syscall.Errno)
			println(err.Error(), "errno =", e)
		}
}

首先,动态链接库只需要将.dll文件与go的文件置于同一目录下即可,不需要整个C的工程。
引入动态链接库,官网上的说明有三种方式:

  1. LoadDLL loads the named DLL file into memory;
  2. MustLoadDLL is like LoadDLL but panics if load operation fails;
  3. LazyDLL is subject to the same DLL preloading attacks as documented on LoadDLL.( Use LazyDLL in golang.org/x/sys/windows for a secure way to load system DLLs.)

因为看到了secure,所以我选择用LazyDLL方式加载dll文件。
加载dll成功后通过NewProc获得每一个函数,并且通过Call函数实现参数传递。

实现http请求

Go语言有”net/http”等package,非常容易实现响应http请求。 image

package main
import (
	"fmt"
	"strconv"
	"syscall"
	"log"
	"net/http"
	"github.com/julienschmidt/httprouter"
)
var D = syscall.NewLazyDLL("JPTree.dll")
func main() {
	fmt.Println("hello word")
	router := httprouter.New()
	router.POST("/hello", Hello)
	router.POST("/open/:num", Open)
	log.Fatal(http.ListenAndServe(":8080", router))
}
func Hello(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	DLL_PrintH := D.NewProc("PrintHello")
	DLL_PrintH.Call()
	fmt.Fprint(w, "hello world")
}
func Open(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	DLL_Open := D.NewProc("open")
	str := p.ByName("num")
	fmt.Fprint(w, str)
	fmt.Fprint(w, "\n--------------\n")
	n, _ := strconv.Atoi(str)
	ret, _, err := DLL_Open.Call(uintptr(10), uintptr(n))
	if err != nil {
		e := err.(syscall.Errno)
		println(err.Error(), "errno =", e)
	}
	fmt.Fprint(w, uintptr(ret))
}

  1. 首先引用http服务相关包:”net/http”, “github.com/julienschmidt/httprouter”,该包可以在github上看到相关介绍;
  2. 将加载dll设置为全局变量。全局变量的声明在函数体外,并且全局变量的命名首字母应为大写;
  3. 设置两个处理器函数,分别调用dll里面的两个导出函数;
  4. 测试服务。这里我用了postman工具来测试服务。 image image