VS2015编写C++的DLL,并防⽌DLL导出的函数名出现乱码(以串
⼝通信为例,实现串⼝通信)
1、新建项⽬
  建⽴好的项⽬界⾯如下:
  接着在解决⽅案中到【头⽂件】然后右击选择【添加】》【新建项】,在弹出的添加新项对话框中进⾏如下选择:
  继续按上⾯的⽅法在解决⽅案中到【源⽂件】然后右击选择【添加】》【新建项】,在弹出的添加新项对话框中进⾏如下选择:
  项⽬准备建好了,现在开始编程了。
2 编程实现串⼝通信
  我们先编写刚才建⽴好的 “ WzSerialPort.h ” ⽂件,该⽂件主要是做⼀些函数声明:
#pragma once
#ifndef _WZSERIALPORT_H
#define _WZSERIALPORT_H
class WzSerialPort
{
public:
WzSerialPort();
~WzSerialPort();
// 打开串⼝,成功返回true,失败返回false
// portname(串⼝名): 在Windows下是"COM1""COM2"等,在Linux下是"/dev/ttyS1"等
// baudrate(波特率): 9600、19200、38400、43000、56000、57600、115200
// parity(校验位): 0为⽆校验,1为奇校验,2为偶校验,3为标记校验(仅适⽤于windows)
/
/ databit(数据位): 4-8(windows),5-8(linux),通常为8位
// stopbit(停⽌位): 1为1位停⽌位,2为2位停⽌位,3为1.5位停⽌位
// synchronizeflag(同步、异步,仅适⽤与windows): 0为异步,1为同步
bool open(const char* portname, int baudrate, char parity, char databit, char stopbit, char synchronizeflag = 1);
//关闭串⼝,参数待定
void close();
//发送数据或写数据,成功返回发送数据长度,失败返回0
int send(const void *buf, int len);
//接受数据或读数据,成功返回读取实际数据的长度,失败返回0
int receive(void *buf, int maxlen);
private:
int pHandle[16];
char synchronizeflag;
};
#endif
  接着编写 “ WzSerialPort.cpp ” 的⽂件,该⽂件主要实现串⼝打开关闭以及数据传输函数:
#include "WzSerialPort.h"
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
#include <windows.h>
WzSerialPort::WzSerialPort()
{
}
WzSerialPort::~WzSerialPort()
{
}
bool WzSerialPort::open(const char* portname,
int baudrate,waitforsingleobject函数
char parity,
char databit,
char stopbit,
char synchronizeflag)
{
this->synchronizeflag = synchronizeflag;
HANDLE hCom = NULL;
if (this->synchronizeflag)
{
//同步⽅式
hCom = CreateFileA(portname, //串⼝名
GENERIC_READ | GENERIC_WRITE, //⽀持读写
0, //独占⽅式,串⼝不⽀持共享
NULL,//安全属性指针,默认值为NULL
OPEN_EXISTING, //打开现有的串⼝⽂件
0, //0:同步⽅式,FILE_FLAG_OVERLAPPED:异步⽅式
NULL);//⽤于复制⽂件句柄,默认值为NULL,对串⼝⽽⾔该参数必须置为NULL
}
else
{
//异步⽅式
hCom = CreateFileA(portname, //串⼝名
GENERIC_READ | GENERIC_WRITE, //⽀持读写
0, //独占⽅式,串⼝不⽀持共享
NULL,//安全属性指针,默认值为NULL
OPEN_EXISTING, //打开现有的串⼝⽂件
FILE_FLAG_OVERLAPPED, //0:同步⽅式,FILE_FLAG_OVERLAPPED:异步⽅式            NULL);//⽤于复制⽂件句柄,默认值为NULL,对串⼝⽽⾔该参数必须置为NULL
}
if (hCom == (HANDLE)-1)
{
return false;
}
//配置缓冲区⼤⼩
if (!SetupComm(hCom, 1024, 1024))
{
return false;
}
// 配置参数
DCB p;
memset(&p, 0, sizeof(p));
p.DCBlength = sizeof(p);
p.BaudRate = baudrate; // 波特率
p.ByteSize = databit; // 数据位
switch (parity) //校验位
{
case0:
p.Parity = NOPARITY; //⽆校验
break;
case1:
p.Parity = ODDPARITY; //奇校验
break;
case2:
p.Parity = EVENPARITY; //偶校验
break;
case3:
p.Parity = MARKPARITY; //标记校验
break;
}
switch (stopbit) //停⽌位
{
case1:
p.StopBits = ONESTOPBIT; //1位停⽌位
break;
case2:
p.StopBits = TWOSTOPBITS; //2位停⽌位
break;
case3:
p.StopBits = ONE5STOPBITS; //1.5位停⽌位
break;
}
if (!SetCommState(hCom, &p))
{
// 设置参数失败
return false;
}
//超时处理,单位:毫秒
//总超时=时间系数×读或写的字符数+时间常量
COMMTIMEOUTS TimeOuts;
TimeOuts.ReadIntervalTimeout = 1000; //读间隔超时,该时间为串⼝每次接收等待的时间间隔,数据不多可以把该时间改⼩,这⾥每次等待1000mS间隔    TimeOuts.ReadTotalTimeoutMultiplier = 500; //读时间系数
TimeOuts.ReadTotalTimeoutConstant = 5000; //读时间常量
TimeOuts.WriteTotalTimeoutMultiplier = 500; // 写时间系数
TimeOuts.WriteTotalTimeoutConstant = 2000; //写时间常量
SetCommTimeouts(hCom, &TimeOuts);
PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);//清空串⼝缓冲区
memcpy(pHandle, &hCom, sizeof(hCom));// 保存句柄
return true;
}
void WzSerialPort::close()
{
HANDLE hCom = *(HANDLE*)pHandle;
CloseHandle(hCom);
}
int WzSerialPort::send(const void *buf, int len)
{
HANDLE hCom = *(HANDLE*)pHandle;
if (this->synchronizeflag)
{
// 同步⽅式
DWORD dwBytesWrite = len; //成功写⼊的数据字节数
BOOL bWriteStat = WriteFile(hCom, //串⼝句柄
buf, //数据⾸地址
dwBytesWrite, //要发送的数据字节数
&dwBytesWrite, //DWORD*,⽤来接收返回成功发送的数据字节数
NULL); //NULL为同步发送,OVERLAPPED*为异步发送
if (!bWriteStat)
{
return0;
}
return dwBytesWrite;
}
else
{
//异步⽅式
DWORD dwBytesWrite = len; //成功写⼊的数据字节数
DWORD dwErrorFlags; //错误标志
COMSTAT comStat; //通讯状态
OVERLAPPED m_osWrite; //异步输⼊输出结构体
//创建⼀个⽤于OVERLAPPED的事件处理,不会真正⽤到,但系统要求这么做
memset(&m_osWrite, 0, sizeof(m_osWrite));
m_osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, L"WriteEvent");
ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误,获得设备当前状态
BOOL bWriteStat = WriteFile(hCom, //串⼝句柄
buf, //数据⾸地址
dwBytesWrite, //要发送的数据字节数
&dwBytesWrite, //DWORD*,⽤来接收返回成功发送的数据字节数
&m_osWrite); //NULL为同步发送,OVERLAPPED*为异步发送
if (!bWriteStat)
{
if (GetLastError() == ERROR_IO_PENDING) //如果串⼝正在写⼊
{
WaitForSingleObject(m_osWrite.hEvent, 1000); //等待写⼊事件1秒钟
}
else
{
ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误
CloseHandle(m_osWrite.hEvent); //关闭并释放hEvent内存
return0;
}
}
return dwBytesWrite;
}
}
int WzSerialPort::receive(void *buf, int maxlen)
{
HANDLE hCom = *(HANDLE*)pHandle;
//if (this->synchronizeflag)
/
/{
////同步⽅式,这⾥因为发送⽤了同步,接收想⽤异步,⼜没有重新初始化串⼝打开,就直接注释掉⽤串⼝异步接收了
//    DWORD wCount = maxlen; //成功读取的数据字节数
//    BOOL bReadStat = ReadFile(hCom, //串⼝句柄
//        buf, //数据⾸地址
//        wCount, //要读取的数据最⼤字节数
//        &wCount, //DWORD*,⽤来接收返回成功读取的数据字节数
//        NULL); //NULL为同步发送,OVERLAPPED*为异步发送
//    if (!bReadStat)
//    {
//        return 0;
//    }
//    return wCount;
//}
//else
{
//异步⽅式,⽤同步会阻塞
DWORD wCount = maxlen; //成功读取的数据字节数
DWORD dwErrorFlags; //错误标志
COMSTAT comStat; //通讯状态
OVERLAPPED m_osRead; //异步输⼊输出结构体
//创建⼀个⽤于OVERLAPPED的事件处理,不会真正⽤到,但系统要求这么做
memset(&m_osRead, 0, sizeof(m_osRead));
m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, L"ReadEvent");
ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误,获得设备当前状态
if (!comStat.cbInQue)return0; //如果输⼊缓冲区字节数为0,则返回false
BOOL bReadStat = ReadFile(hCom, //串⼝句柄
buf, //数据⾸地址
wCount, //要读取的数据最⼤字节数
&wCount, //DWORD*,⽤来接收返回成功读取的数据字节数
&m_osRead); //NULL为同步发送,OVERLAPPED*为异步发送
if (!bReadStat)
{
if (GetLastError() == ERROR_IO_PENDING) //如果串⼝正在读取中
{
//GetOverlappedResult函数的最后⼀个参数设为TRUE
//函数会⼀直等待,直到读操作完成或由于错误⽽返回
GetOverlappedResult(hCom, &m_osRead, &wCount, TRUE);
}
else
{
ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误
CloseHandle(m_osRead.hEvent); //关闭并释放hEvent的内存
return0;
}
}
return wCount;
}
}
  再接着,我们编写 “ USER_COM.cpp ” ⽂件,该⽂件主要是实现把 “ WzSerialPort.cpp ” ⽂件⾥的串⼝函数进⾏⼀次封装,封装声明成可供DLL外部调⽤的函数:
// USER_COM_DLL.cpp : 定义 DLL 应⽤程序的导出函数。
//
#include "stdafx.h"
#include "USER_COM.h"
#include <iostream>
#include "WzSerialPort.h"
#include "Windows.h"
using namespace std;
/*类重命名*/
WzSerialPort w;
/*************************************************
函数名:bool OpenCOM()
功能:打开串⼝
传⼊值:⽆
返回值:串⼝打开成功返回true,串⼝打开失败返回false
*************************************************/
bool OpenCOM()
{
return w.open("COM1", 115200, 0, 8, 1); //这⾥配置打开串⼝1,配置波特率为115200,数据位为8位,奇偶校验位为0,停⽌位为1,最后⼀位是同步异步选择位(隐藏)
}
/*************************************************
函数名:void COM_Send(unsigned char cmd, unsigned char data1, unsigned char data2)
功能:发送数据
传⼊值:num为要发送的数据量⼤⼩,Send_buff为发送数据的数组,数组⼤⼩要跟num⼤⼩⼀致
返回值:发送成功返回发送数据长度,发送失败返回0
*************************************************/
int COM_Send(int num ,uint8_t* Send_buff)
{
sen_len = w.send(Send_buff, num);
if (sen_len==0)
return0;
else
return sen_len ;
}
/*************************************************
函数名:void Close_COM()
功能:关闭串⼝
传⼊值:⽆
返回值:⽆
*************************************************/
void Close_COM()
{
w.close();
}
/*************************************************
函数名:void COM_RX(uint8_t* Rx_buff)
功能:接收数据数据
传⼊值:uint8_t* Rx_buff 接收的串⼝数据的缓存BUFF数组指针,
接收到数据后,直接把数据填到该Rx_buff,默认可接收的数据最⼤长度为255
返回值:int len 返回值为实际接收到的数据长度,返回0则代表没有接收到数据或者数据校验出错
*************************************************/
int COM_RX(uint8_t* Rx_buff)
{ //该函数编译X86的时候,调⽤时使⽤X64编译器⽆法调⽤,调⽤编译时要调⽤的话就要⽤X64编译
uint8_t buff[255];int i = 0;
memset(buff, 0, 255);
int len = w.receive(buff, 255); //参数:接收的数据buff;接收的最⼤数据长度;返回值为实际接收到的数据长度,其他APP使⽤该函数时使⽤新线程调⽤for (i = 0; i < len; i++)            Rx_buff[i] = buff[i];return len;
}
  最后我们在头⽂件那⾥,新建⼀个 “ USER_COM.h ” ⽂件实现把 “ USER_COM.cpp ” ⽂件⾥的函数声明为DLL的外部接⼝:
#ifdef USER_COM_EXPORTS
#define USER_COM_API __declspec(dllexport) //声明为DLL导出函数的宏定义
#else
#define USER_COM_API __declspec(dllimport)
#endif
#include "stdint.h"
extern"C" USER_COM_API bool OpenCOM();
extern"C" USER_COM_API int  COM_Send(unsigned char cmd, unsigned char data1, unsigned char data2);
extern"C" USER_COM_API int  COM_RX(uint8_t* Rx_buff);
extern"C" USER_COM_API void Close_COM();
注意:__stdcall定义导出函数⼊⼝点调⽤约定为_stdcall
extern "C" 说明导出函数使⽤C编译器,则函数名遵循C编译器的函数名修饰规则,不加extern "C"说明使⽤C++编译器的函数名修饰规则,两种规则区别如下:
(1)C编译器的函数名修饰规则
对于__stdcall调⽤约定,编译器和链接器会在输出函数名前加上⼀个下划线前缀,函数名后⾯加上⼀个“@”符号和其参数的字节数。
  例如 _functionname@number。__cdecl调⽤约定仅在输出函数名前加上⼀个下划线前缀,例如_functionname。__fastcall调⽤约定在输出函数名前加上⼀个“@”符号,后⾯也是⼀个“@”符号和其参数的字节数,例如@functionname@number
(2)C++编译器的函数名修饰规则
C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调⽤⽅式,返回值类型,甚⾄参数个数、参数类型。不管__cdecl,__fastcall还是__stdcall调⽤⽅式,函数修饰都是以⼀个“?”开始,后⾯紧跟函数的名字,再后⾯是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall⽅式,参数表的开始标识是“@@YG”,对于__cdecl⽅式则是“@@YA”,对于__fastcall⽅式则是“@@YI”。