• Calling C++ DLLs from C++ Application Using VS2020


    DLLs in Visual C++


    Step by Step: Calling C++ DLLs from VC++ and VB - Part 1

     

    什么是静态连接库lib,什么是动态链接库dll?
             静态链接库与动态链接库都是共享代码的方式。两者区别:

    1.如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。

    2.静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接 库。静态链接库与静态链接库调用规则总体比较如下。

    对于静态链接库(比较简单):
    首先,静态链接库的使用需要库的开发者提供生成库的.h头文件和.lib文件。

    生成库的.h头文件中的声明格式如下:
    extern "C" 函数返回类型 函数名(参数表);
    在调用程序的.cpp源代码文件中如下:
    #include "../lib.h"
    #pragma comment(lib,"..//debug//libTest.lib") 
    //指定与静态库一起链接

    第二,因为静态链接库是将全部指令都包含入调用程序生成的EXE文件中。因此如果用的是静态链接库,那么也就不存在“导出某个函数提供给用户使用”的情况,要想用就得全要!要不就都别要!:)

    对于动态链接库:
    动态链接库的使用需要库的开发者提供生成的.lib文件和.dll文件。或者只提供dll文件。
    首先我们必须先注意到DLL内的函数分为两种: 
    (1)DLL 导出函数,可供应用程序调用; 
    (2)DLL 内部函数,只能在 DLL 程序使用,应用程序无法调用它们。
    因此调用程序若想调用DLL中的某个函数就要以某种形式或方式指明它到底想调用哪一个函数。


    VS2008环境下,C++生成Dll文件 C++调用Dll文件(非MFC)

    新建项目,选择Win32项目,工程名:09DllDemo 确定后,选择应用程序设置, 应用程序类型选择DLL(D),附加选项上选择 导出符号(X)(便于学习)。单击完成,完成工程创建。

    -----------------------------------------------------------------------------------------------------------------

    打开 09DllDemo.cpp文件,因为选择导出符号的缘故,VC++自动给我们生成了

    // 这是导出函数的一个示例。
    extern "C"
    {
    // 这是导出函数的一个示例。
    MY09DLLDEMO_API int fnMy09DllDemo()
    {
       return 42;
    }

    }

    打开 09DllDemo.h 我们可以看到声明的函数。

    //声明要导出的函数

    extern "C"
    {
    MY09DLLDEMO_API int fnMy09DllDemo();
    }

    ---------------------------------------------------------------------------------------------------------------------

    我们自己要添加入的函数按上述格式添加就可以了。

    选中项目,点生成,生成Dll文件的任务就完成了。最后09DllDemo工程产生的文件中有3个可以被其他工程所使用:09DllDemo.h 09DllDemo.dll 09DllDemo.lib。

    .dll文件就是动态链接库,.lib是供程序开发用的导入库,.h文件包含了导出函数的声明。

    调用Dll中的导出函数有两种方法:

    1.装载期间动态加载。

    模块可像调用本地函数一样调用从其他模板导出的函数(API函数就是这样调用的)。装载期间链结必须使用DLL的导入库(.lib)文件,它为系统提供了加载这个Dll和定位Dll中的导出函数所需的信息。

    应用程序启动时由载入器(加载应用程序的组件)载入09DllDemo.dll文件。载入器如何知道要载入哪些Dll呢?这些信息记录在执行文件(PE文件)的idata节中。使用这种方法不用自己写代码显式加载DLL。

    ---------------------------

    新建一个09ImportDemo的Win32控制台工程,将09DllDemo.h,09DllDemo.lib,09DllDemo.dll 3个文件拷贝到09ImportDemo目录下。

    下面给出了调用导出函数fnMy09DllDemo的代码

    #include "09DllDemo.h"
    #include <iostream>
    using namespace std;

    #pragma comment(lib,"09DllDemo")

    void main()
    {
    int a =fnMy09DllDemo();
    cout<<a<<endl;
    }

    发布软件时必须将该软件使用的Dll与主程序一起发布。09ImportDemo.exe和09DllDemo.dll放在同一个目录下。载入器加载Dll文件时,默认情况是在应用程序的当前目录下查找,如果找不到就到系统盘"\windows\system32"文件夹下查找,还找不到就按错误处理。

    --------------------------------------------------------------------------------------------------------------

    2.运行期间动态加载。(只需Dll文件即可)

    运行期间动态加载是在程序运行过程中显式得加载Dll库,从中导出需要的函数。

    为了能够在运行期间动态导出函数,一般需要在09DllDemo工程中建立一个DEF文件来指定要导出的函数。

    ----添加DEF文件

    打开09DllDemo工程,右键点击工程,选择添加,选择TextFile选项,输入文件名DllDemo.def

    新的DllDemo.def中添加如果内容

    =============================

    EXPORTS

            fnMy09DllDemo

    =============================

    重新生成下就完成了。

    回到09ImportDemo工程,将程序修改为

    ------------------------------------------------------------------------------------------

    #include <windows.h>
    #include <iostream>
    using namespace std;
    //定义FunctionFunc为指向一个返回值为int型 无参数的函数的指针
    typedef int (*FunctionFunc)();

    int main()
    {
       FunctionFunc _FunctionFunc;
       //加载目标Dll
       HMODULE hModule = ::LoadLibrary(TEXT("D:\\09DllDemo.dll"));
       if (hModule==NULL)//如果Dll加载失败,释放它占用的资源
       {
        ::FreeLibrary(hModule);
       }
       //取得目标Dll中导出函数的地址(提醒:函数名就是函数的入口地址)
       _FunctionFunc=(FunctionFunc)::GetProcAddress(hModule,"fnMy09DllDemo");
       if (_FunctionFunc==NULL)
       {
        ::FreeLibrary(hModule);
       }
       int a =_FunctionFunc();
       cout<< a<<endl;
       ::FreeLibrary(hModule);
       cin.get();
       return 1;

    }

    ------------------------------------------------------------------------------------------

    c#中调用Dll

    [DllImport("D:\\09DllDemo.dll")")]
            public static extern int fnMy09DllDemo();

    ------------------------------------------------------------------------------------------

    备注:

    在没加extern "C"生成的Dll的函数名会有变化,因此在运行期间动态链结过程中,调用GetProcAddress会返回空值。

    如不能修改Dll源码,可采用下面的方式解决(比较笨的方法,有好的请留言,谢谢):先用Depends.Exe(VS自带的工具)打开Dll文件,右键点击函数复制函数名。如获得的函数名为

    C++中 直接

    _FunctionFunc=(FunctionFunc)::GetProcAddress(hModule,);

    c#中

    [DllImport("D:\\09DllDemo.dll", EntryPoint = "")]

    public static extern int fnMy09DllDemo();



    创建DLL
    创建Win32 Console Application工程TheClasses 
    创建选项中选中 DLL, Export Symbols
    所有要导出的类都必须 class __declspec(dllexport) YourClassName {};
    创建App
    创建Win32 Console Application工程App
    创建完毕后在工程属性页, Configuration=Active(Debug)
    设置Linker->General->Additional Library Directories, 添加目录 Solution目录\Debug
    设置Linker->Input->Additional Dependencies, 添加TheClasses.lib(扩展名是lib)


    一、为什么需要dll

    代码复用是提高软件开发效率的重要途径。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用。比较常见的例子是各种应用程序框架,如ATL、MFC等,它们都以源代码的形式发布。由于这种复用是“源码级别”的,源代码完全暴露给了程序员,因而称之为“白盒复用”。“白盒复用”的缺点比较多,总结起来有4点。

    1. 暴露了源代码;
    2. 容易与程序员的“普通”代码发生命名冲突;
    3. 多份拷贝,造成存储浪费;
    4. 更新功能模块比较困难。

    实际上,以上4点概括起来就是“暴露的源代码”造成“代码严重耦合”。为了弥补这些不足,就提出了“二进制级别”的代码复用。使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为“黑盒复用”。

    在Windows操作系统中有两种可执行文件,其后缀名分别为.exe和.dll。它们的区别在于,.exe文件可被独立的装载于内存中运行;.dll文件却不能,它只能被其它进程调用。然而无论什么格式,它们都是二进制文件。上面说到的“二进制级别”的代码复用,可以使用.dll来实现。

    与白盒复用相比,.dll很大程度上弥补了上述4大缺陷。.dll是二进制文件,因此隐藏了源代码;如果采用“显式调用”(后边将会提到),一般不会发生命名冲突;由于.dll是动态链接到应用程序中去的,它并不会在链接生成程序时被原原本本拷贝进去;.dll文件相对独立的存在,因此更新功能模块是可行的。

    说明:实现“黑盒复用”的途径不只dll一种,静态链接库甚至更高级的COM组件都是。本文只对dll进行讨论。

    二、创建dll

    接下来用一个简单的例子来说明创建dll的方法。本例采用VS2010,使用C++编程语言,具体操作步骤如下。

    通过Start Page或者File菜单栏,新建一个Project,将会弹出新建项目对话框。选择Win32 Project向导,项目名为CreateDLL,解决方案名为DLLTEST(注意Create directories for solution是勾选上的),点击OK,接着点击Next,到Application Settings,选择应用程序类型为dll,并勾选“Export Symbols”,点击Finish。完成这一步之后,VS界面上左边的Solution Explorer中将会看到向导自动生成的文件列表,如图1所示。

    图1 wizard自动生成的文件列表

    在VS界面的编辑窗口中,展示了自动生成的CreateDLL.cpp的代码。

    1. // CreateDLL.cpp : Defines the exported functions for the DLL application.  
    2. //  
    3.   
    4. #include "stdafx.h"  
    5. #include "CreateDLL.h"  
    6.   
    7.   
    8. // This is an example of an exported variable  
    9. CREATEDLL_API int nCreateDLL = 0;  
    10.   
    11. // This is an example of an exported function.  
    12. CREATEDLL_API int fnCreateDLL(void)  
    13. {  
    14.     return 42;  
    15. }  
    16.   
    17. // This is the constructor of a class that has been exported.  
    18. // see CreateDLL.h for the class definition  
    19. CCreateDLL::CCreateDLL()  
    20. {  
    21.     return;  
    22. }  

    这里有3种类型的example,分别为导出变量nCreateDLL、导出函数fnCreateDLL以及导出类CCreateDLL。为了简化起见,本例只考虑导出函数。修改CreateDLL.h文件为:

    1. #ifdef CREATEDLL_EXPORTS  
    2. #define CREATEDLL_API __declspec(dllexport)  
    3. #else  
    4. #define CREATEDLL_API __declspec(dllimport)  
    5. #endif  
    6.   
    7. CREATEDLL_API void printMax(int&,int&);  
    8. CREATEDLL_API void printMax(int&,int&,int&);  

    修改CreateDLL.cpp文件为: 

    1. CREATEDLL_API void printMax(int& a,int& b)  
    2. {  
    3.     std::cout<<"Among ("<<a<<","<<b<<"), the Max Number is "<<(a>b?a:b)<<"\n";  
    4. }  
    5. CREATEDLL_API void printMax(int& a,int& b,int& c)  
    6. {  
    7.     std::cout<<"Among ("<<a<<","<<b<<","<<c<<"), the Max Number is "<<(((a>b?a:b)>c)?(a>b?a:b):c)<<"\n";  
    8. }  

    不难发现,printMax函数的作用就是打印出两个整数或三个整数中的最大值。需要说明的是,这里故意使用同名函数是为了引出导出函数的修饰名称,具体将在第四节中阐述。

    接下来,选择菜单Build->Build CreateDLL,Output窗口提示CreateDLL.dll文件生成成功,如图2所示。

     图2 CreateDLL.dll成功生成

    三、使用dll

    本例采用“显式调用”的方式使用CreateDLL.dll。显式调用方式相比于”隐式调用“有好有坏。显式调用只需要一个.dll文件就可以了,灵活性更好,更新模块方便;相对的,程序员需要做的事情更多,使用方法更为复杂。

    右键单击Solution Explorer中的Solution 'DLLTEST',在弹出的菜单中选择Add->New Project,选择Win32 Console Application,输入项目名为UseDLL,点击OK,接着点击Next,在Application Settings界面勾选EmptyProject并点击Finish。右键单击项目UseDLL,给它添加源文件UseDLL.cpp。这样操作之后,Solution Explorer的信息如图3所示。

    图3 向Solution'DLLTEST'添加项目UseDLL

    编写UseDLL.cpp的代码为:

    1. /*--UseDLL.cpp 
    2.  *Author: ume(李优米) 
    3.  *Use CreateDLL.dll explicitly 
    4.  */  
    5. #include<Windows.h>  
    6. #include<iostream>  
    7. typedef void(*FUNA)(int&,int&);  
    8. typedef void(*FUNB)(int&,int&,int&);  
    9. int main()  
    10. {  
    11.     const char* dllName = "CreateDLL.dll";  
    12.     const char* funName1 = "printMax";  
    13.     const char* funName2 = "printMax";  
    14.     int x(100), y(100), z(100);  
    15.     HMODULE hDLL = LoadLibrary(dllName);  
    16.     if(hDLL != NULL)  
    17.     {  
    18.         FUNA fp1 = FUNA(GetProcAddress(hDLL,funName1));  
    19.         if(fp1 != NULL)  
    20.         {  
    21.             std::cout<<"Input 2 Numbers:";  
    22.             std::cin>>x>>y;  
    23.             fp1(x,y);  
    24.         }  
    25.         else  
    26.         {  
    27.             std::cout<<"Cannot Find Function "<<funName1<<std::endl;  
    28.         }  
    29.         FUNB fp2 = FUNB(GetProcAddress(hDLL,funName2));  
    30.         if(fp2 != NULL)  
    31.         {  
    32.             std::cout<<"Input 3 Numbers:";  
    33.             std::cin>>x>>y>>z;  
    34.             fp2(x,y,z);  
    35.         }  
    36.         else  
    37.         {  
    38.             std::cout<<"Cannot Find Function "<<funName2<<std::endl;  
    39.         }  
    40.         FreeLibrary(hDLL);  
    41.     }  
    42.     else  
    43.     {  
    44.         std::cout<<"Cannot Find "<<dllName<<std::endl;  
    45.     }  
    46.     return 1;  
    47. }  

    代码比较长,但是并不难理解,这里仅说明代码中的一些要点。

    • 包含头文件Windows.h,原因在于程序中用到了LoadLibrary、FreeLibrary、GetProcAddress等Win32 API函数。
    • FUNA和FUNB是函数指针类型的声明。
    • 当程序不再使用dll时,应该调用FreeLibrary及时释放它占用的内存空间。
    • 如果在const char* dllName和funName底部出现红色波浪线提示,说明采用的字符集不匹配,需要修改项目UseDLL的属性CharaterSet为Not Set。
    • 为方便项目的调试,建议修改解决方案的Startup Project属性为Single startup project并以UseDLL为首选。

    然而,这个程序还有错误。编译并运行,结果如图4所示。

                    图4 UseDLL的运行结果

    这并不是期望中的结果。实际上,正如第二节提到的那样,造成这种错误的原因正是导出函数的修饰名称。虽然在CreateDLL.cpp中两个printMax函数有相同的名称,但在dll二进制文件中,经过编译器的“加工”,它们实际上各自有不同的名称了。这也是函数重载机制得以实现的一个技术支持。

    使用VS2010附带工具dumpbin,查看CreateDLL.dll的导出函数名,结果如图5所示。

     图5 查看CreateDLL.dll的导出函数名

    观察图5可以发现,CreateDLL.dll导出函数名为?printMax@@YAXAAH00@Z和?printMax@@YAXAAH0@Z。它们分别对应着三个整数的printMax和两个整数的printMax。因此,Use.DLL中funName应当相应修改为:

    1. const char* funName1 = "?printMax@@YAXAAH0@Z";  
    2. const char* funName2 = “?printMax@@YAXAAH00@Z”;  

    修改之后,再次编译运行,结果正确,如图6所示。

     图6 UseDLL正常运行

    四、dll导出函数名称规范化

    创建、使用dll并不复杂,走过前三节,相信读者肯定有这样的体会。然而,一个问题仍然值得思考:导出函数的修饰名称太“奇怪”,为dll的使用带来了不便,能不能让导出函数的修饰名称规范一些?

    答案是肯定的,而且方法至少有两种:一是运用extern "C"修饰printMax;二是运用模块定义文件.def。后者的效果更好,所以本节将使用.def来规范化导出函数的修饰名称。

    CreateDLL.dll导出的两个函数功能很简单,根据功能描述,理想的函数名称是pMaxA2和pMaxA3。在CreateDLL项目中添加CreateDLL.def文件:

    1. LIBRARY CreateDLL  
    2. EXPORTS  
    3. pMaxA2 = ?printMax@@YAXAAH0@Z  
    4. pMaxA3 = ?printMax@@YAXAAH00@Z  

    重新build项目CreateDLL,使用dumpbin再次查看CreateDLL.dll的导出函数名称,结果如图7所示。

    图7 规范化的函数名,奇怪的修饰名称还存在

    出现了期望的结果,但仍有小缺憾:奇怪的修饰名称仍然存在。能否去掉这些不太规范的修饰名称呢?当然是可以的。只需要将CreateDLL.h中#define CREATEDLL_API __declspec(dllexport) 修改为#define CREATEDLL_API即可。修改之后重新编译生成CreateDLL.dll,使用dumpbin查看导出函数名称,结果如图8所示。

    图8 规范化的函数名,去除了奇怪的修饰名称    

    回到UseDLL.cpp,修改funName:

    1. const char* funName1 = "pMaxA2";  
    2. const char* funName2 = "pMaxA3";  

    重新编译运行UseDLL,结果正确,与图6类似。
    五、dll的不足

    动态链接库虽然一定程度上实现了“黑盒复用”,但仍存在着诸多不足,笔者能够想到的有下面几点。

    1. dll节省了编译期的时间,但相应延长了运行期的时间,因为在使用dll的导出函数时,不但要加载dll,而且程序将会在模块间跳转,降低了cache的命中率。
    2. 若采用隐式调用,仍然需要.h、.lib、.dll文件(“三件套”),并不能有效支持模块的更新。
    3. 显式调用虽然很好地支持模块的更新,但却不能导出类和变量。
    4. dll不支持Template。

    二进制级别的代码复用相比源码级别的复用已经有了很大的进步,但在二进制级别的代码复用中,dll显得太古老。想真正完美实现跨平台、跨语言的黑盒复用,采用COM才是正确的选择。


    作者:Angelo Lee
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    Java学习第一周汇报
    Java暑期学习第八天日报
    Java暑期学习第十天日报
    Java暑期学习第十二天日报
    Java学习第二周汇报
    Java暑期学习第九天日报
    0006 列表(ul、ol、dl)
    0015 行高那些事:lineheight
    0016 CSS 背景:background
    HTTP中GET与POST的区别 99%的错误认识
  • 原文地址:https://www.cnblogs.com/yefengmeander/p/2887523.html
Copyright © 2020-2023  润新知