VS2015 編寫C++的DLL,並防止DLL導出的函數名出現亂碼(以串口通信為例,實現串口通信)


  參考鏈接:https://blog.csdn.net/songyi160/article/details/50754705

 

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,
    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) //校驗位
    {
    case 0:
        p.Parity = NOPARITY; //無校驗
        break;
    case 1:
        p.Parity = ODDPARITY; //奇校驗
        break;
    case 2:
        p.Parity = EVENPARITY; //偶校驗
        break;
    case 3:
        p.Parity = MARKPARITY; //標記校驗
        break;
    }

    switch (stopbit) //停止位
    {
    case 1:
        p.StopBits = ONESTOPBIT; //1位停止位
        break;
    case 2:
        p.StopBits = TWOSTOPBITS; //2位停止位
        break;
    case 3:
        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)
        {
            return 0;
        }
        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內存
                return 0;
            }
        }
        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)return 0; //如果輸入緩沖區字節數為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的內存
                return 0;
            }
        }
        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) return 0; 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”。

 

  使用 extern "C" 跟不使用 extern "C" 的形成的DLL函數名差異如下圖測試DLL函數名所示:

  

  

  由圖可以看出兩者之間最終形成的名字會有差異的,到這里編譯就已經可以建成串口通信的 DLL 了,不過形成的的DLL通過函數查看器還會有一些除函數名以外的符號,最后進行函數命名規范就好了。“ .h ” 頭文件的作用僅僅能導出動態庫、明確編譯鏈接方式及確定入口點約定,還一個重要作用是打包給開發者,使其了解動態庫導出的函數及對應的的參數,為了確保導出函數名及入口點函數不變,此時需添加.def文件。

 

3 添加 def 文件,保持函數名以及入口點函數不變

  在解決方案中找到【源文件】右擊選擇【添加】》【新建項】,在彈出的添加新項對話框中進行如下圖所示選擇:

  然后編寫 “ USER_COM.def ” 文件,使用def文件的意義:將編譯器生成的函數修飾去掉,用更加自然、更加容易理解、更加容易記憶的名字來命名函數,而不是一串人一看就嚇一跳的 修飾名字。

LIBRARY "USER_COM"

EXPORTS
 OpenCOM @ 1
 COM_Send @ 2
 COM_RX @ 3
 Close_COM @ 4

 

4 編譯形成DLL

   最后編譯就可以形成對應的DLL了,要是要編譯32位的DLL就選擇X86,要是要選擇64位的DLL就選擇X64即可。

  用函數查看器看,函數名都正常了:

 

 

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM