由于C++没有同一的ABI接口,故各个编译器厂商生成的库接口不一样,需要转换。
假设现在有MSVC生成的DLL:add.dll
,其是由add.cpp与add.h生成:
// add.h
#ifndef ADD_H
#define ADD_H
__declspec(dllexport) int __stdcall add(int a, int b);
#endif // ADD_H
// add.cpp
#include "add.h"
__declspec(dllexport) int __stdcall add(int a, int b)
{
return a + b;
}
在cmd.exe下使用MSVC6的编译器生成add.dll
动态库与add.lib
导入库,并查看生成的DLL中的函数符号,命令如下:
cl.exe /LD add.cpp
dumpbin.exe /exports add.dll
dumpbin.exe /exports add.lib
- 第一条语句:生成DLL;
- 第二条语句:查看DLL中的被编译器修饰后的函数符号。
- 第三条语句:查看LIB导入库的函数名(导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息)
此时,从第二条语句的输出可以看到,生成的DLL中add
的函数符号为:?add@@YGHHH@Z
,关于符号的格式可以google编译器的符号修饰规则;
从第三条语句的输出可以看到,LIB导入库的函数名为:?add@@YAHHH@Z (int __stdcall add(int,int))
。
add.cpp
与add.h
中**__stdcall是Windows下的默认调用约定。若将其去掉,然后重新生成DLL**,再查看函数符号,则此时为:?add@@YAHHH@Z (int __cdecl add(int,int))
。
由于MSVC与Mingw对C++编译的库接口不一样(函数的符号不一样),不能正确链接,所以一般需要转换为C的ABI接口,因为C语言的ABI接口在各个编译器中都是相同的,不会修饰函数符号。
将C++的函数符号转换为C语言的函数符号也很简单(但这种无法将C++类成员函数转换),只需要将add.cpp
与add.h
中的函数定义与声明前加上extern "C"
:
// 当有extern "C"时,有无__declspec(dllexport)都一样
extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
即让其按照C语言编译器来编译add.cpp
,编译后生成的DLL的函数符号为:_add@8
__stdcall会使导出函数名字前面加一个下划线,后面加一个@再加上参数的字节数;若去掉**__stdcall或改为__cdecl**,则生成的DLL的函数符号为:add
有时候,在C++中既希望使用形如:
// 当有extern "C"时,有无__declspec(dllexport)都一样
extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
这样的函数声明与定义,又希望其生成的DLL的函数符号为函数的原始声明(即不对函数符号做修饰,使用**__cdecl调用约定),则需要使用def文件来生成具有指定函数符号的DLL**:
; 这是注释
LIBRARY add.dll
EXPORTS
add
在cmd.exe下使用MSVC编译器用以下命令生成具有函数原始声明的add.dll
:
cl.exe /c /I. add.cpp
link.exe /machine:i386 /def:add.def /out:add.dll add.obj
dumpbin.exe /exports add.dll
dumpbin.exe /exports add.lib
第一条语句:仅仅只编译(不链接)add.cpp
以生成add.obj
第二条语句:根据add.def
文件生成add.dll
第三条语句:查看DLL中add函数符号,为:add
第四条语句:查看LIB导入库中的函数名,为:_add@8
至此,就为编译后实际被修饰的函数_addd@8
生成了具有add
接口的DLL
i686-w64-mingw32-g++ -c -m32 -O0 -I. -o add.o add.cppp
dllwrap.exe --def add.def -o add.dll add.o --output-lib libadd.a
lib.exe /machine:i386 /def:add.def /out:add.lib
第一条语句:只编译不链接add.cpp
,生成add.o
第二条语句:根据add.def
文件生成add.dll
与导入库libadd.a
第三条语句:使用MSVC工具根据add.def
文件生成导入库add.lib
此处的add.dll
为上面使用MSVC编译器生成的,函数符号为_add@8
。
在bash.exe中使用以下命令:
pexports.exe add.dll > add.def
生成def
文件,内容如下:
LIBRARY add.dll
EXPORTS
_add@8
修改add.def
为:
LIBRARY add.dll
EXPORTS
add
然后再在bash.exe中执行:
dlltool.exe -D add.dll -d add.def -l libadd.a
以使用add.dll
根据add.def
文件生成libadd.a
注意,add.dll
是32bit系统生成的,需要指定使用i686版本的版本的pexports.exe
和dlltool.exe
工具
首先,在cmd.exe中使用以下命令分析GY8507上位机的VCI_CAN.dll
的函数符号与CANTest的ControlCAN.dll
的函数符号:
dumpbin.exe /exports VCI_CAN.dll
dumpbin.exe /exports ControlCAN.dll
对比后发现,ControlCAN.dll
中的函数符号,在VCI_CAN.dll
中都可以找到,其函数调用者所暴露的名称都一样,为未加修饰的函数名。
然后,再使用以下命令分析两个DLL的导入库VCI_CAN.lib
与ControlCAN.lib`:
dumpbin.exe /all VCI_CAN.lib
dumpbin.exe /all ControlCAN.lib
对比后发现相同的函数名有些不同,这里以VCI_Transmit
这个API为例:
VCI_CAN.lib
的VCI_Transmit
函数的信息:
Archive member name at 1D18: VCI_CAN.dll/
5E0B081F time/date Tue Dec 31 16:34:39 2019
uid
gid
0 mode
31 size
correct header end
Version : 0
Machine : 14C (i386)
TimeDateStamp: 5E0B081F Tue Dec 31 16:34:39 2019
SizeOfData : 0000001D
DLL name : VCI_CAN.dll
Symbol name : _VCI_Transmit@16
Type : code
Name type : ordinal
Ordinal : 15
ControlCAN.lib
中VCI_Transmit
函数的信息:
Archive member name at 11AC: ControlCAN.dll/
5C8B6E7A time/date Fri Mar 15 17:20:58 2019
uid
gid
0 mode
34 size
correct header end
Version : 0
Machine : 14C (i386)
TimeDateStamp: 5C8B6E7A Fri Mar 15 17:20:58 2019
SizeOfData : 00000020
DLL name : ControlCAN.dll
Symbol name : _VCI_Transmit@20
Type : code
Name type : undecorate
Hint : 13
Name : VCI_Transmit
从上面的VCI_Transmit
函数的信息例子可以发现,两个DLL中的VCI_Transmit
函数符号格式都是_VCI_Transmit@n
的格式,其中,@符号后跟的是函数参数所占字节数,因此,两个DLL中,API接口虽然函数名称相同,但参数个数不同。这说明其函数原型都遵循C语言的**__stdcall**调用约定,即函数声明形如:
extern "C" return_type __stdcall function_name(type1 arg1, type2, arg2)
另外,注意到上面的Name type信息行,VCI_CAN.dll
中为ordinal,ControlCAN.dll
中为undecorate。
最后,再对比两个DLL给出的头文件,VCI_CAN.h
与ControlCAN.h
(这里只看VCI_Transmit
函数的声明):
// VCI_CAN.h
extern "C" {
DWORD __stdcall VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend);
}
// ControlCAN.h
extern "C" ULONG __stdcall VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend, ULONG Len);
这和上面的分析也吻合,函数名相同,调用约定相同,但参数个数不同。
从上面分析发现,ControllCAN.dll
与VCI_CAN.dll
的函数同名,但参数个数不同,因此不能简单的将VCI_CAN.dll
重命名后直接替换CANTest软件中的ControllCAN.dll
。
因此,需要想一个方法,使CANTest软件通过ControllCAN.dll
去调用VCI_CAN.dll
中的函数才行,方案如下:
新的ControllCAN.dll ----> 中间转换静态库 ----> VCI_CAN.dll
此处的ControllCAN.dll
是新封装的DLL,不是CANTest软件下的ControllCAN.dll
,其内容不同,但其函数接口都一样,之后只需要用这个新的ControllCAN.dll
去替换CANTest下的原始ControllCAN.dll
即可;当然,还需要将VCI_CAN.dll
赋值到CANTest软件目录下。
新建GY8507_LIB_Wrap文件夹,将GY8507提供的二次开发包中的头文件ControllCAN.h
拷贝到此目录,并在此文件夹中新建warp.c与warp.h文件(下面仍然以VCI_transmit
函数为例):
原始GY8507提供的二次开发包的ControllCAN.h
:
#if !defined(AFX_CONTROLCAN_H__A09B0BED_2018_4536_89DA_E8A9BD2F74FB__INCLUDED_)
#define AFX_CONTROLCAN_H__A09B0BED_2018_4536_89DA_E8A9BD2F74FB__INCLUDED_
#include <afxtempl.h>
typedef CMap<DWORD ,DWORD,CString,CString> DEVICESN_MAP;
....
//专为打开NET_CAN设计
extern __declspec( dllexport ) DWORD VCI_OpenDevice(DWORD DeviceType,DWORD DeviceIP,DWORD DevicePort,DWORD HostPort);
extern "C"
{
DWORD __stdcall VCI_OpenDevice(DWORD DeviceType,DWORD DeviceInd,DWORD Reserved);
...
...
DWORD __stdcall VCI_Transmit(DWORD DeviceType,DWORD DeviceInd,DWORD CANInd,PVCI_CAN_OBJ pSend);
....
}
#endif
修改后的GY8507的ControllCAN.h
:
#ifndef CONTROLL_CAN_H
#deifne CONTROLL_CAN_H
#include <windows.h>
DWORD __stdcall VCI_OpenDevice(DWORD DeviceType,DWORD DeviceInd,DWORD Reserved);
...
DWORD __stdcall VCI_Transmit(DWORD DeviceType,DWORD DeviceInd,DWORD CANInd,PVCI_CAN_OBJ pSend);
....
#endif
// wrap.h
#ifndef WRAP_H
#define WRAP_H
#include "ControlCAN.h"
#ifdef __cplusplus
extern "C"{
#endif
typdef struct {
ULONG (*pfn_VCI_Transmit)(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend);
} GY8507_funcs_t;
extern GY8507_funcs_t* GY8507_funcs_table_get(void);
#ifdef __cplusplus
}
#endif
#endif /* WRAP_H */
// wrap.c
#include "wrap.h"
#if __cplusplus
extern "C" {
#endif
static GY8507_funcs_t g_GY8507_funcs_table = {
NULL
};
GY8507_funcs_t* GY8507_funcs_table_get(void)
{
HMODULE dll_module = LoadLibrary(TEXT("VCI_CAN.dll"));
if (dll_module == NULL)
{
return NULL;
}
g_GY8507_funcs_table.pfn_VCI_Transmit = (ULONG (*) (DWORD, DWORD, DWORD, PVCI_CAN_OBJ))GetProcAddress(dll_module, "VCI_Transmit");
return &g_GY8507_funcs_table;
}
#if __cplusplus
}
#endif
然后在bash.exe中使用以下命令生成中间静态库**libGY8507_LIB_WARP.a
**:
i686-w64-mingw32-gcc -c -m32 -g -O0 -I. -o ./warp.o ./warp.c
ar -cr ./libGY8507_LIB_WARP.a ./warp.o
新建GY8507_DLL_to_ZLG_DLL目录,将ZLG提供的二次开发包中的ControllCAN.h
拷贝到此目录下,然后新建ControlCAN.cpp
与ControlCAN.def
文件:
注意,在ControlCAN.h中开头添加一行#include <windows.h>
。
// ControlCAN.cpp
#include "ControlCAN.h"
// 有ZLG的函数接口转到调用GY8507的函数接口时需要使用
typedef struct _GY8507_VCI_INIT_CONFIG {
DWORD AccCode;
DWORD AccMask;
DWORD Reserved;
UCHAR Filter; // 过滤方式, 0: 单滤波, 1: 双滤波
UCHAR kCanBaud; // CAN波特率索引号
UCHAR Timing0;
UCHAR Timing1;
UCHAR Mode;
UCHAR CanRx_IER;
} GY8507_VCI_INIT_CONFIG, *GY8507_PVCI_INIT_CONFIG;
// 有ZLG的函数接口转到调用GY8507的函数接口时需要使用
typedef struct _GY8507_VCI_CAN_OBJ
{
BYTE ID[4];
UINT TimeStamp; //保留不用
BYTE TimeFlag; //保留不用
BYTE SendType; //保留不用
BYTE RemoteFlag; //是否是远程帧
BYTE ExternFlag; //是否是扩展帧
BYTE DataLen;
BYTE Data[8];
BYTE Reserved[3];
} GY8507_VCI_CAN_OBJ, *GY8507_PVCI_CAN_OBJ;
typedef struct {
...
ULONG (*pfn_VCI_Transmit)(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, GY8507_PVCI_CAN_OBJ pSend);
...
} GY8507_funcs_t;
extern "C" GY8507_funcs_t* GY8507_funcs_table_get(void);
static GY8507_funcs_t *g_GY8507_funcs = nullptr;
ULONG STDCALL VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend, ULONG Len)
{
ULONG rc = 0;
ULONG ret = 0;
GY8507_PVCI_CAN_OBJ pbuf;
VCI_CAN_OBJ send_buf = {0};
if (!g_GY8507_funcs)
{
g_GY8507_funcs = GY8507_funcs_table_get();
}
if (2 < DeviceType && DeviceType < 5)
{
DeviceType = 2;
}
for (ULONG i = 0; i < Len; ++i)
{
send_buf = pSend[i];
// ZLG CAN-ID ==> GY8507 CAN-ID
// ZLG 右对齐, GY8507 左对齐
if (pSend[i].ExternFlag == 1)
{
send_buf.ID = 0
| (((pSend[i].ID >> 0) & 0x1F) << 27) // GY8507 29bits CAN-ID [ 4: 0]bits
| (((pSend[i].ID >> 5) & 0xFF) << 16) // GY8507 29bits CAN-ID [12: 5]bits
| (((pSend[i].ID >> 13) & 0xFF) << 8) // GY8507 29bits CAN-ID [20:13]bits
| (((pSend[i].ID >> 21) & 0xFF) << 0); // GY8507 29bits CAN-ID [28:21]bits
}
else
{
send_buf.ID = 0
| (((pSend[i].ID >> 0) & 0x07) << 13) // GY8507 11bits CAN-ID [ 2:0]bits
| (((pSend[i].ID >> 3) & 0xFF) << 0); // GY8507 11bits CAN-ID [10:3]bits
}
pbuf = (GY8507_PVCI_CAN_OBJ)&send_buf;
ret = g_GY8507_funcs->pfn_VCI_Transmit(DeviceType, DeviceInd, CANInd, pbuf);
if (ret == 1)
{
rc++;
}
else if (ret == -1 || ret > 1)
{
break;
}
}
return rc;
}
// ControllCAN.def
LIBRARY "ControlCAN.dll"
EXPORTS
VCI_Transmit
注意,上面的ControlCAN.def文件中,只添加了一个函数VCI_Transmit
。
最后在bash.exe中使用以下命令生成新的ControlCAN.dll
:
i686-w64-mingw32-g++ -c -m32 -g -O0 -Isrc -o ./ControlCAN.o ./ControlCAN.cpp
dllwrap.exe --def ./ControlCAN.def -o ./ControlCAN.dll ./ControlCAN.o -LGY8507_LIB_WARP -lGY8507_LIB_WARP --output-lib ./ControlCAN.lib
至此,新的ControlCAN.dll
就生成了,然后将此ControlCAN.dll
与VCI_CAN.dll
,以及VCI_CAN.dll
的依赖SiUSBXp.dll
拷贝到CANTest软件的目录下,就可以使用CANTest连接GY8507了。