[DLL] Dynamic link library (dll) 的编写和使用教程

时间:2016-09-02 12:54:06   收藏:0   阅读:227

前一阵子,项目里需要导出一个DLL,但是导出之后输出一直不怎么对,改了半天才算改对。。。读了一些DLL教程,感觉之后要把现在的代码导出,应该还要花不少功夫。。。下面教程参照我读的3个教程写成,所以内容比较多:

http://www.tutorialspoint.com/dll/index.htm

http://www.tuicool.com/articles/ZVBnE3b

http://www.cnblogs.com/cswuyg/archive/2011/10/06/DLL2.html 

第三个链接,包含类导入和纯虚接口导入的例子,如果教程读完还觉得比较困惑,可以下载来看看。另外文章中代码以c++为主,之后可能会陆续将非c++代码用其他的sample code替换掉。

 

介绍

DLL是微软开发的共享库。DLL文件包括多个程序可同时共用的代码和数据,使代码具有重复利用性、模块化等特性。DLL在运行时链接应用与资源库,资源库不会被复制到可执行程序中。DLL也可以连接到其他DLL。

DLL的优点

重要的DLL文件

重要的DLL文件包括:

DLL文件编写

DLL的种类

两种链接DLL的方法为:

DLL入口

创建DLL时,可以选择性的声明进入点函数(The entry point function)。进入点函数在进程链接(attach)DLL或者离开(detach)DLL时自动调用。这个函数可以用于为DLL初始化或者释放空间。

如果应用是多线程的,可以用thread local storage (TLS)来分配每个线程的私有空间。以下代码是进入点函数的示例:

BOOL APIENTRY DllMain(
   HANDLE hModule,       // Handle to DLL module 
   DWORD ul_reason_for_call, 
   LPVOID lpReserved )     // Reserved
{
   switch ( ul_reason_for_call )
   {
      case DLL_PROCESS_ATTACHED:
      // A process is loading the DLL.
      break;
      
      case DLL_THREAD_ATTACHED:
      // A process is creating a new thread.
      break;
      
      case DLL_THREAD_DETACH:
      // A thread exits normally.
      break;
      
      case DLL_PROCESS_DETACH:
      // A process unloads the DLL.
      break;
   }
   return TRUE;
}

如果使用load-time dynamic linking时,进入点函数返回FALSE,应用程序不会开始。如果使用runtime dynamic linking,只有当前的DLL不会载入。

进入点函数只能进行简单的初始化,不能调用其他DLL的载入或结束函数。例如,在进入点函数中,不能直接或间接的调用LoadLibrary或LoadLibraryEx。另外,在进程结束时也不能调用FreeLibrary。

WARNING − 再多线程应用中,需要确保使用DLL全局数据时是同步的(进程安全)进而避免可能的数据冲突。为此,可以用TLS来为每个线程提供单独的空间。

导出DLL函数

可以用一下几种方法导出DLL函数:

关键字导出

def导出 

关键字导出

C语言可以实现应用二进制接口(ABI),这样使调用者和被调用着可以遵从统一的标准,但是C++语言没有这个特性,导致从一个编译器生成的binary不能被另一个编译器所识别。这样使得直接导出C++类就成了冒险。

我们假设有一个类DllClass,类里面有一个成员函数Foo。类DllClass的实现在DLL里,此DLL可以被不同的用户以下列方式所调用:

源代码包含两个工程:

DllClassLibrary以下列方式导出函数:

#ifdef DLLCLASSLIBRARY_EXPORT // inside DLL
  #define DLLCLASSAPI   __declspec(dllexport)
#else // outside DLL
  #define DLLCLASSAPI   __declspec(dllimport)
#endif  // DLLCLASSLIBRARY_EXPORT

DLLCLASSLIBRARY_EXPORT标签仅在DllClassLibrary工程中被定义,所以DLLCLASSAPI在工程DllClassLibrary代表__declspec(dllexport),而在客户工程(DllClassExecutable)代表

__declspec(dllimport)

C语言方法

传统C语言可以利用指针或者句柄实现面向对象语言的一些特性,比如调用一个函数来创建一个类对象,并用次对象作为参数来调用对象的不同函数操作。比如,DllClass对象可以用c接口导出:

typedef tagDLLCLASSHANDLE {} * DLLCLASSHANDLE;

// Factory function that creates instances of the DllClass object.
DLLCLASSAPI DLLCLASSHANDLE APIENTRY GetDllClass(VOID);
// Calls DllClass.Foo method.
DLLCLASSAPI INT APIENTRY DllClassFoo(DLLCLASSHANDLE handle, INT n);
// Releases DllClass instance and frees resources.
DLLCLASSAPI VOID APIENTRY DllClassRelease(DLLCLASSHANDLE handle);

// APIENTRY is defined as __stdcall in WinDef.h header.

客户端代码如下:

#include "DllClassLibrary.h"
...
/* Create DllClass instance. */
DLLCLASSHANDLE hDllClass = GetDllClass();
if(hDllClass)
{
  /* Call DllClass.Foo method. */
  DllClassFoo(hDllClass, 42);

  /* Destroy DllClass instance and release acquired resources. */
  DllClassRelease(hDllClass);

  /* Be defensive. */
  hDllClass = NULL;
}

此方法中,DLL必须提供创建对象和清理对象的函数接口。

/* void* GetSomeOtherObject(void) is declared elsewhere. */
DllClassHANDLE h = GetSomeOtherObject();

/* Oops! Error: Calling DllClass.Foo on wrong object intance. */
DllClassFoo(h, 42);

C++内在方法:导出类

Windows平台上几乎所有的C++编译器都支持从DLL导出类。导出类与导出函数一样:如果导出整个类的话,只需要在类前面加上标志 __declspec(dllexport/dllimport) ,如果导出类里面特定的成员函数,就在成员函数前面加上标志 __declspec(dllexport/dllimport) 。下面是实例代码:

// The whole CDllClass class is exported with all its methods and members.
class DLLCLASSAPI CDllClass
{
public:
  int Foo(int n);
};

// Only CDllClass::Foo method is exported.
class CDllClass
{
public:
  DLLCLASSAPI int Foo(int n);
};

没有必要显式的为类或者成员函数定义调用规则。C++编译器默认用 __thiscall

作为成员函数的调用规则。但是,不同的编译器对于函数的名称修饰方式不同,因此DLL和用户最好用相同版本的编译器。下图是一个visual c++编译器中的函数修饰名称。

 技术分享

可以看出编译器生成出来的名称修饰与源代码中的成员函数名称差别很大。下图是用dependency walker工具所描述的同一个DLL模块中的函数名称

技术分享

再次强调,只有MS C++能用这个DLL。为了使调用者和被调用者之间的名称修饰相同,DLL和用户还必须 用同一个版本的MS C++。下面是用户使用DllClass对象的代码。

#include "DllClassLibrary.h"
...
// Client uses DllClass object as a regular C++ class.
CDllClass dllclass;
dllclass.Foo(42);

NOTE:利用导出类的DLL和静态库没有什么区别。所有适用于静态库的规则也适用于动态库(DLL)。

NOTE:导出一个类,则意味着导出与类相关的一切:类成员变量,类成员函数(无论是显式声明还是隐式生成的),基类

class Base
{
  ...
};

class Data
{
  ...
};

// MS Visual C++ compiler emits C4275 warning about not exported base class.
class __declspec(dllexport) Derived :
  public Base
{
  ...
private:
  Data m_data;    // C4251 warning about not exported data member.
};

在上面的代码中,编译器警告没有导出基类和类成员变量。所以如果要成功的导出一个类,我们必须导出所有的基类和定义成员变量的类。这个滚雪球式的要求是一个明显的缺点。这也是为什么导出一个继承自STL模版或者含有STL模版成员函数的类是多么痛苦的一件事。导出一个STL map实例至少需要导出相关的10个以上的类。必须按照如下方法导出:

class  __declspec(dllexport) Base
{
  ...
};

class  __declspec(dllexport) Data
{
  ...
};

// MS Visual C++ compiler emits C4275 warning about not exported base class.
class __declspec(dllexport) Derived :
  public Base
{
  ...
private:
  Data m_data;    // C4251 warning about not exported data member.
};

C++成熟方法:利用纯虚接口 

C++纯虚接口是只只包含纯虚函数没有成员的类。它试图获得最佳的两个方面:独立于编译器的对象接口和方便的面向对象的函数调用。所有的一切只需要声明一个包含接口声明的头文件和实现一个返回一个对象的工厂函数。这个工厂函数需要一个标识符 __declspec(dllexport/dllimport)

。实例如下:

// The abstract interface for DllClass object.
// No extra specifiers required.
struct IDllClass
{
  virtual int Foo(int n) = 0;
  virtual void Release() = 0;
};

// Factory function that creates instances of the DllClass object.
extern "C" DLLCLASSAPI IDllClass* APIENTRY GetDllClass();

工厂函数 GetDllClass 被声明为 extern “C",是为了防止函数名字捆绑(name mangling),这样可以被任何C编译器所识别。据说,C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的是不同的,而且不同版本的编译器他们的Name-Mangling规则也是不同的。不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数。

C标准规定了C语言Name-Mangling的规范(林锐的书有这样说过)。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。

影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。如extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号,而extern “c” __cdecl则不会附加额外的符号。

dll中的函数在被调用时是以函数名或函数编号的方式被索引的。这就意味着采用某编译器的C++的Name-Mangling方式产生的dll文件可能不通用。因为它们的函数名重命名方式不同。为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即是对每一个导出函数声明为extern “C”,而且采用_stdcall调用约定,接着还需要对导出函数进行重命名,以便导出不加修饰的函数名

注意到extern “C”的作用是为了解决函数符号名的问题,这对于动态链接库的制造者和动态链接库的使用者都需要遵守的规则。 

动态链接库的显式装入就是通过GetProcAddress函数,依据动态链接库句柄和函数名,获取函数地址。因为GetProcAddress仅是操作系统相关,可能会操作各种各样的编译器产生的dll,它的参数里的函数名是原原本本的函数名,没有任何修饰,所以一般情况下需要确保dll’里的函数名是原始的函数名。分两步:

下面的是客户段代码,说明如何使用这个接口。

#include "DllClassLibrary.h"
...
IDllClass* pDllClass = ::GetDllClass();

if(pDllClass)
{
    pDllClass->Foo(42);

    pDllClass->Release();
    pDllClass = NULL;
}

定义一个不包含任何成员变量纯虚类,然后定义子类并实现接口成员函数。这样用户无需知道这个函数是如何实现的,只需知道它做了什么。

 技术分享

 

#include "DllClassLibrary.h"
#include <memory>
#include <functional>
...
typedef std::shared_ptr<IDllClass> IDllClassPtr;

IDllClassPtr ptrDllClass(::GetDllClass(), std::mem_fn(&IDllClass::Release));

if(ptrDllClass)
{
    ptrDllClass->Foo(42);
}

// No need to call ptrDllClass->Release(). std::shared_ptr class
// will call this method automatically in its destructor. 

NOTE: 关于STL模版类 

标准C++容器(vector,list,map)不是用来设计DLL的。C++标准对DLL保持沉默,因为DLL是跟平台相关的技术,在其他平台上并不是必须存在的。MS Visual C++可以导出或者导入STL类,只要我们在前面加上 __declspec(dllexport/dllimport) 标志。可以工作但是编译器会给出警告信息。我们应该知道,导出STL类与导出一般C++类一样,都会有前面(C++内在方法:导出类)所提及的缺点。

.def文件

def文件指定导出函数,并告知编译器不要以修饰后的函数名作为导出函数名,而以指定的函数名导出函数(比如有函数func,让编译器处理后函数名仍为func)。这样,就可以避免由于microsoft VC++编译器的独特处理方式而引起的链接错误。

也就是说,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不过,dll的制造者除了提供dll之外,还要提供头文件,需要在头文件里加上这extern”C”和调用约定,因为使用者需要跟制造者遵守同样的规则,除非使用者和制造者使用的是同样的编译器并对调用约定无特殊要求)。

在模块定义文件中,使用LIBRARY语句和EXPORTS语句来定义DLL。举例def文件格式:

LIBRARY  dll_name // dll name is not necessary, but must be the same as generated dll file
EXPORTS
  function_name @ function_index

例如

// SampleDLL.def
LIBRARY "sampleDLL"
EXPORTS
   HelloWorld

编写好之后加入到VC的项目中,就可以了。 

另外,要注意的是,如果要使用__stdcall,那么就必须在代码里使用上__stdcall,因为*.def文件只负责修改函数名称,不负责调用约定。也就是说,def文件只管函数名,不管函数平衡堆栈的方式。

如果把*.def文件加入到工程之后,链接的时候并没有自动把它加进去。那么可以这样做:

手动的在link添加:

注意到:即便是使用C的名称修饰方式,最终产生的函数名称也可能是会被修饰的。例如,在VC下,_stdcall的调用方式,就会对函数名称进行修饰,前面加‘_’,后面加上参数相关的其他东西。所以使用*.def文件对函数进行命名很有用,很重要。

也可以使用模块定义文件来声明需要导出的DLL函数。这种情况下,不需要加导出关键字 

示例DLL

可以选择Win32 Dynamic-Link Library project或MFC AppWizard (dll) project来创建DLL。以下是一个用Win32 Dynamic-Link Library project创建DLL的例子

// SampleDLL.cpp
#include "stdafx.h"
#define EXPORTING_DLL
#include "sampleDLL.h"

BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
   return TRUE;
}

void HelloWorld()
{
   MessageBox( NULL, TEXT("Hello World"), 
   TEXT("In a DLL"), MB_OK);
}

// File: SampleDLL.h
#ifndef INDLL_H
#define INDLL_H

   #ifdef EXPORTING_DLL
      extern __declspec(dllexport) void HelloWorld() ;
   #else
      extern __declspec(dllimport) void HelloWorld() ;
   #endif

#endif

调用DLL示例

// SampleApp.cpp 
#include "stdafx.h"
#include "sampleDLL.h"

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{     
   HelloWorld();
   return 0;
}

NOTE:

...
typedef VOID (*DLLPROC) (LPTSTR);
...
HINSTANCE hinstDLL;
DLLPROC HelloWorld;
BOOL fFreeDLL;

hinstDLL = LoadLibrary("sampleDLL.dll");

if (hinstDLL != NULL)
{
   HelloWorld = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld");
    
   if (HelloWorld != NULL)
      (HelloWorld);

   fFreeDLL = FreeLibrary(hinstDLL);
}
...

当你编译链接SampleDLL应用时,Windows操作系统按以下顺序搜索SampleDLL DLL:

注册DLL

为了使用DLL作为系统服务,必须注册DLL。有时引用发生冲突,DLL就不能继续使用。在“开始—>运行”中输入下列命令,可以从新注册DLL:

regsvr32 somefile.dll

这个命令假设somefile.dll在PATH指向的文件夹中。否则,必须用DLL的绝对地址。如下列命令所示,使用"/u”,可以取消注册DLL文件。

regsvr32 /u somefile.dll

这些命令可以用于打开和关闭某服务。

DLL工具

 Visual Studio提供了一些工具可以帮助解答DLL问题

Dependency Walker

Dependency Walker工具(depends.exe)可以扫描某一程序依赖的所有DLL。当你用Dependency Walker打开某个程序时,Dependency Walker进行如下检测:

通过Dependency Walker可以记录一个程序使用的所用DLLs。它可以帮助预防和改正未来可能发生的DLL问题。Dependency Walker的路径如下:

drive\Program Files\Microsoft Visual Studio\Common\Tools

DLL Universal Problem Solver

DLL Universal Problem Solver (DUPS) 工具是用来记录、比较和显示DLL信息的。下面列举了DUPS包含的一些工具

编写建议

 

原文:http://www.cnblogs.com/Xiaoyan-Li/p/5832778.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!