当前位置: 澳门新濠3559 > 编程 > 正文

1、 静态链接库和动态链接库,我们经常会调用一

时间:2019-12-08 22:39来源:编程
【C#1、 静态链接库和动态链接库,我们经常会调用一些标准的动态库或是C的类库。】 使用Gsof.Native 动态调用 C动态库 前言:这是一篇总结性的文章,需要有一点C++和dll基本知识的基础

【C#1、 静态链接库和动态链接库,我们经常会调用一些标准的动态库或是C的类库。】 使用Gsof.Native 动态调用 C动态库

前言:这是一篇总结性的文章,需要有一点C++和dll基本知识的基础,在网上查阅了很多资料感觉没有一篇详细、具体、全面的dll开发介绍,我这是根据最近项目和网上资料整理出来的,并附带实例的一个总结性的文章(由于篇幅较长故不附带源码解释)。另外,个人愚昧地认为以后C++的开发会更多地面向库的开发,所以学会库的开发必不可少。

 关于动态调用动态库方法说明 
一、  动态库概述 
1、  动态库的概念 
日常编程中,常有一些函数不需要进行编译或者可以在多个文件中使用(如数据库输入/输出操作或屏幕控制等标准任务函数)。可以事先对这些函数进行编译,然后将它们放置在一些特殊的目标代码文件中,这些目标代码文件就称为库。库文件中的函数可以通过连接程序与应用程序进行链接,这样就不必在每次开发程序时都对这些通用的函数进行编译了。

http://www.linux-cn.com/html/test/20070411/2287.html

一、背景

使用C# 开发客户端时候,我们经常会调用一些标准的动态库或是C的类库。
虽然C# 提供的PInvoke的方式,但因为使用的场景的多变,有些时候可能没办法,固定位置的调用,或是需要按需求调用不同的库。 设置当前目录的方式,有时会有加载不到的问题。
CLI的方式又需要用C++做再次的封装。

 

       动态库是一种在已经编译完毕的程序开始启动运行时,才被加载来调用其中函数的库。其加载方式与静态库截然不同。

摘要:动态链接库技术实现和设计程序常用的技术,在Windows和Linux系统中都有动态库的概念,采用动态库可以有效的减少程序大小,节省空间,提高效率,增加程序的可扩展性,便于模块化管理。但不同操作系统的动态库由于格式 不同,在需要不同操作系统调用时需要进行动态库程序移植。本文分析和比较了两种操作系统动态库技术,并给出了把Visual C++编制的动态库移植到Linux上的方法和经验。

二、说明

  1. 支持根据路径动态加载DLL类库
  2. 支持using 销毁加载的类库
  3. 调用方便

github:

nuget:

接口说明:

NativeFactory 创建INative调用对象:

/// <summary>
/// 创建INative 对象
/// </summary>
/// <param name="p_fileName">文件路径</param>
/// <returns></returns>
public static INative Create(string p_fileName);

/// <summary>
/// 创建INative 对象
/// </summary>
/// <param name="p_fileName">文件路径</param>
/// <param name="p_calling">调用转换方式(同PInvoke CallingConvention)/param>
/// <returns></returns>
public static INative Create(string p_fileName, CallingConvention _calling);

/// <summary>
/// 销毁INative, 也可以调用 Native的Dispose方法
/// </summary>
/// <param name="p_native"></param>
public static void Free(INative p_native);

INative:

public interface INative : IDisposable
{
    /// <summary>
    /// 获取函数委托
    /// </summary>
    /// <typeparam name="TDelegate"></typeparam>
    /// <returns></returns>
    TDelegate GetFunction<TDelegate>();
    /// <summary>
    /// 函数委托调用方式
    /// </summary>
    /// <typeparam name="TResult">返回值类型</typeparam>
    /// <typeparam name="TDelegate">函数对应的委托类型</typeparam>
    /// <param name="p_params">函数传参</param>
    /// <returns></returns>
    TResult Invoke<TResult, TDelegate>(params object[] p_params);
    /// <summary>
    /// 函数名调用
    /// </summary>
    /// <typeparam name="TResult">返回值类型</typeparam>
    /// <param name="p_funName">函数名</param>
    /// <param name="p_params">函数传参</param>
    /// <returns></returns>
    TResult Invoke<TResult>(string p_funName, params object[] p_params);
    /// <summary>
    /// 函数名调用
    /// </summary>
    /// <typeparam name="TResult">返回值类型</typeparam>
    /// <param name="p_funName">函数名</param>
    /// <param name="p_calling">调用转换方式(同PInvoke CallingConvention)</param>
    /// <param name="p_params">函数传参</param>
    /// <returns></returns>
    TResult Invoke<TResult>(string p_funName, CallingConvention p_calling, params object[] p_params);
    /// <summary>
    /// 函数名调用(非泛型)
    /// </summary>
    /// <param name="p_funName">函数名</param>
    /// <param name="p_retrunType">返回值类型</param>
    /// <param name="p_params">函数传参</param>
    /// <returns></returns>
    object Invoke(string p_funName, Type p_retrunType, params object[] p_params);
    /// <summary>
    /// 函数委托调用方式
    /// </summary>
    /// <typeparam name="TDelegate">函数对应的委托类型</typeparam>
    /// <param name="p_params">函数传参</param>
    void Call<TDelegate>(params object[] p_params);
    /// <summary>
    /// 函数名调用
    /// </summary>
    /// <param name="p_funName">函数名</param>
    /// <param name="p_params">函数传参</param>
    void Call(string p_funName, params object[] p_params);
    /// <summary>
    /// 函数名调用
    /// </summary>
    /// <param name="p_funName">函数名</param>
    /// <param name="p_calling">调用转换方式(同PInvoke CallingConvention)</param>
    /// <param name="p_params">函数传参</param>
    void Call(string p_funName, CallingConvention p_calling, params object[] p_params);
}

1、 静态链接库和动态链接库

2、  动态库的命名 
Linux下,动态库通常以.so(share object)结尾。(通常/lib和/usr/lib等目录下存在大量系统提供的以.so结尾的动态库文件)

1、引言

三、使用

libtest.dll 为 中包括一个test函数

 int test(int input)
 {
     return input;
 }

1.   静态链接库(LIB)只用在程序开发期间使用,而动态链接库(DLL)在执行期间使用。

Windows下,动态库常以.dll结尾。(通常C:windowsSystem32等目录下存在大量系统提供的以.dll结尾的动态库文件)

动态库(Dynamic Link Library abbr,DLL)技术是程序设计中经常采用的技术。其目的减少程序的大小,节省空间,提高效率,具有很高的灵活性。采用动态库技术对于升级软件版本更加容易。与静态库(Static Link Library)不同,动态库里面的函数不是执行程序本身的一部分,而是根据执行需要按需载入,其执行代码可以同时在多个程序中共享。

方法名调用

int input = 0;
int result = -1;
using (var native = NativeFactory.Create(@"../../libtest.dll"))
{
    result = native.Invoke<int>("test", input);
}

2.   静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

3、  动态库与静态库之间的区别 
静态库是指编译连接时,把库文件的代码全部加入到可执行文件中,所以生成的文件较大,但运行时,就不再需要库文件了。即,程序与静态库编译链接后,即使删除静态库文件,程序也可正常执行。

在Windows和Linux操作系统中,都可采用这种方式进行软件设计,但他们的调用方式以及程序编制方式不尽相同。本文首先分析了在这两种操作系统中通常采用的动态库调用方法以及程序编制方式,然后分析比较了这两种方式的不同之处,最后根据实际移植程序经验,介绍了把VC++编制的Windows动态库移植到Linux下的方法。

dynamic 方式调用

  • 优点:调用方便,简单类型调用时,不用做过多的定义。
  • 缺点:4.0下性能不理想,4.5+性能好很多,但相较于委托的方式,还差些。
int input = 0;
int result = -1;
using (dynamic native = NativeFactory.Create(@"../../libtest.dll"))
{
    result = native.test<int>(input);
}

3.   静态链接库,浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库libname.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于用户来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库正好相反,在编译链接时,没有把库文件的代码加入到可执行文件中,所以生成的文件较小,但运行时,仍需要加载库文件。即,程序只在执行启动时才加载动态库,如果删除动态库文件,程序将会因为无法读取动态库而产生异常。

2、动态库技术

委托方式调用

  • 优化:效率高,没有了第一次动态构造委托的消耗,可获取到函数委托增加 重复调用消耗
  • 缺点:如果函数较多,委托定义较为繁琐
[NativeFuncton("test")]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int Test(int p_sleep);

public void DelegateFunction()
{
    int input = 0;
    int result = -1;
    using (var native = NativeFactory.Create(@"../../libtest.dll"))
    {
        // 直接调用
        var result1 = native1.Invoke<int, Test>(input);

        // 获取函数委托调用 
        var test = native.GetFunction<Test>();
        result = test(input);
    }

    Assert.AreEqual(input, result);

}

4.   动态库则实现了增量更新,进程间可以共享动态库,节约了资源,当程序第一次调用动态库时,系统将该库加载到内存中,当另一个程序也调用这个库时,系统不再加载,而是将状态+1,当某个程序退出或释放该库时,状态则-1,直到当系统中没有程序调用该库时,系统自动将其清理并释放内存。

二、        Linux下动态调用动态库 
备注:以下linux实例说明都是在RedHat 5.1系统+ gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-46)上实现。

2.1 Windows动态库技术

 

1、  .so动态库的生成 
可使用gcc或者g++编译器生成动态库文件(此处以g++编译器为例)

动态链接库是实现Windows应用程序共享资源、节省内存空间、提高使用效率的一个重要技术手段。常见的动态库包含外部函数和资源,也有一些动态库只包含资源,如Windows字体资源文件,称之为资源动态链接库。通常动态库以.dll,.drv、.fon等作为后缀。相应的windows静态库通常以.lib结尾,Windows自己就把一些主要的系统功能以动态库模块的形式实现。

2、 知识点

g++ -shared -fPIC -c XXX.cpp

Windows动态库在运行时被系统加载到进程的虚拟空间中,使用从调用进程的虚拟地址空间分配的内存,成为调用进程的一部分。DLL也只能被该进程的线程所访问。DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。DLL模块中包含各种导出函数,用于向外界提供服务。DLL可以有自己的数据段,但没有自己的堆栈,使用与调用它的应用程序相同的堆栈模式;一个DLL在内存中只有一个实例;DLL实现了代码封装性;DLL的编制与具体的编程语言及编译器无关,可以通过DLL来实现混合语言编程。DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。

1.   在VS中创建的类库有2种类型,一种是直接选择VC++类库(是使用微软版本的C++创建的类库),一种是Win32项目或Win32控制台程序,然后选择对应的类库类型,也就是ANSI标准的C++类库,一般我们用这种方式创建的类库,而它又分三种:Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC ExtensionDLL(MFC扩展DLL)。

g++ -shared -fPIC -o XXX.so XXX.o

根据调用方式的不同,对动态库的调用可分为静态调用方式和动态调用方式。

2.   非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

2、  .so动态库的动态调用接口函数说明 
动态库的调用关系可以在需要调用动态库的程序编译时,通过g++的-L和-l命令来指定。例如:程序test启动时需要加载目录/root/src/lib中的libtest_so1.so动态库,编译命令可照如下编写执行:

(1)静态调用,也称为隐式调用,由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(Windows系统负责对DLL调用次数的计数),调用方式简单,能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须在源文件中声明一下。 LIB文件包含了每一个DLL导出函数的符号名和可选择的标识号以及DLL文件名,不含有实际的代码。Lib文件包含的信息进入到生成的应用程序中,被调用的DLL文件会在应用程序加载时同时加载在到内存中。

3.   当Windows要执行一个使用了动态链接库的程序而需要加载该链接库时,动态链接库文件必须储存在含有该.EXE程序的目录下、目前的目录下、Windows系统目录下、Windows目录下,或者是在通过MS-DOS环境中的PATH可以存取到的目录下(Windows会按顺序搜索这些目录)。

g++ -g -o test test.cpp –L/root/src/lib –ltest_so1

(2)动态调用,即显式调用方式,是由编程者用API函数加载和卸载DLL来达到调用DLL的目的,比较复杂,但能更加有效地使用内存,是编制大型应用程序时的重要方式。在Windows系统中,与动态库调用有关的函数包括:

4.   动态链接库模块可能有其它扩展名(如.EXE或.FON),但标准扩展名是.DLL。只有带.DLL扩展名的动态链接库才能被Windows自动加载。如果文件有其它扩展名,则程序必须另外使用LoadLibrary或者LoadLibraryEx函数加载该模块。

(此处,我们重点讲解动态库的动态调用的方法,关于静态的通过g++编译命令调用的方式不作详细讲解,具体相关内容可上网查询)

①LoadLibrary(或MFC 的AfxLoadLibrary),装载动态库。

5.   DLL 的编制与具体的编程语言及编译器无关。只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。

 

②GetProcAddress,获取要引入的函数,把符号名或标识号转换为DLL内部地址。

 

Linux下,提供专门的一组API用于完成打开动态库,查找符号,处理出错,关闭动态库等功能。

③FreeLibrary(或MFC的AfxFreeLibrary),释放动态链接库。

3、 关键字

下面对这些接口函数逐一介绍(调用这些接口时,需引用头文件#include <dlfcn.h>):

在windows中创建动态库也非常方便和简单。在Visual C++中,可以创建不用MFC而直接用C语言写的DLL程序,也可以创建基于MFC类库的DLL程序。每一个DLL必须有一个入口点,在VC++中,DllMain是一个缺省的入口函数。DllMain负责初始化(Initialization)和结束(Termination)工作。动态库输出函数也有两种约定,分别是基于调用约定和名字修饰约定。DLL程序定义的函数分为内部函数和导出函数,动态库导出的函数供其它程序模块调用。通常可以有下面几种方法导出函数:

1.   当建立一个DLL时,它应该包含处理字符和字符串的Unicode和非Unicode版的所有函数,比如实现ANSI版和宽字符版。

1)        dlopen

①采用模块定义文件的EXPORT部分指定要输入的函数或者变量。

#ifdef UNICODE
#define TextW  //定义宽字符版的函数
#else
#define TextA  //定义ANSI版的函数
#endif

函数原型:void *dlopen(const char *libname,int flag);

②使用MFC提供的修饰符号_declspec(dllexport)。

 

功能描述:dlopen必须在dlerror,dlsym和dlclose之前调用,表示要将库装载到内存,准备使用。如果要装载的库依赖于其它库,必须首先装载依赖库。如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。

③以命令行方式,采用/EXPORT命令行输出有关函数。

2.  __declspec(dllexport),该关键字位于类/函数的声明和定义中,表示该类/函数为DLL的导出类/函数。而DLL内的类/函数分两种,一种是DLL导出类/函数供外部程序调用,一种是DLL内部函数供DLL自己调用。

参数中的libname一般是库的全路径,这样dlopen会直接装载该文件;如果只是指定了库名称,在dlopen会按照下面的机制去搜寻:

在windows动态库中,有时需要编写模块定义文件(.DEF),它是用于描述DLL属性的模块语句组成的文本文件。

3.  __declspec(dllimport),该关键字说明类/函数为导入函数,与__declspec(dllexport)匹配对应,为了在应用程序中使用其声明的类/函数。

a.根据环境变量LD_LIBRARY_PATH查找

2.2 Linux共享对象技术

 

b.根据/etc/ld.so.cache查找

在Linux操作系统中,采用了很多共享对象技术(Shared Object),虽然它和Windows里的动态库相对应,但它并不称为动态库。相应的共享对象文件以.so作为后缀,为了方便,在本文中,对该概念不进行专门区分。Linux系统的/lib以及标准图形界面的/usr/X11R6/lib等目录里面,就有许多以so结尾的共享对象。同样,在Linux下,也有静态函数库这种调用方式,相应的后缀以.a结束。Linux采用该共享对象技术以方便程序间共享,节省程序占有空间,增加程序的可扩展性和灵活性。Linux还可以通过LD-PRELOAD变量让开发人员可以使用自己的程序库中的模块来替换系统模块。

4.  extern"C",是为了解决导出函数名的问题,因为C++编译器,为实现函数重载,在编译生成的汇编代码中要对函数名进行一些处理,而用 extern "C"声明的函数将使用函数名作符号名,这时C的处理方式。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。但是,冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。但这只解决了C/C++之间调用的问题,更可靠的方法是定义一个.def文件。

c.查找依次在/lib和/usr/lib目录查找。

同Windows系统一样,在Linux中创建和使用动态库是比较容易的事情,在编译函数库源程序时加上-shared选项即可,这样所生成的执行程序就是动态链接库。通常这样的程序以so为后缀,在Linux动态库程序设计过程中,通常流程是编写用户的接口文件,通常是.h文件,编写实际的函数文件,以.c或.cpp为后缀,再编写makefile文件。对于较小的动态库程序可以不用如此,但这样设计使程序更加合理。

5.  __cplusplus,是cpp中自定义宏,定义了这个宏表示它是一段cpp的代码。并且这是一个C++编译器保留的宏定义,意味着C++编译器认为这个宏已经定义了。

flag参数表示处理未定义函数的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数,先把库装载到内存,等用到没定义的函数再说;RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。

编译生成动态连接库后,进而可以在程序中进行调用。在Linux中,可以采用多种调用方式,同Windows的系统目录(..system32等)一样,可以把动态库文件拷贝到/lib目录或者在/lib目录里面建立符号连接,以便所有用户使用。下面介绍Linux调用动态库经常使用的函数,但在使用动态库时,源程序必须包含dlfcn.h头文件,该文件定义调用动态链接库的函数的原型。

如果函数这样定义:extern”C” int add(int a, int b);由于C编译器不能识别extern”C”指令,那么C调用C++程序就会出现问题,这时候__cplusplus就起作用了。如下:

2)        dlerror

(1)_打开动态链接库:dlopen,函数原型void *dlopen (const char *filename, int flag);

#ifdef __cplusplus
extern "C" {
#endif

//....声明代码

#ifdef __cplusplus
}
#endif

函数原型:char *dlerror(void);

dlopen用于打开指定名字(filename)的动态链接库,并返回操作句柄。

6.  #pragma comment( lib , "..//debug//LIBProject.lib" )的意思是指本文件(应用程序)生成的.obj文件应与LIBProject.lib一起连接。

功能描述:dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。dlerror在返回错误信息的同时,也会清除错误信息。

(2)取函数执行地址:dlsym,函数原型为: void *dlsym(void *handle, char *symbol);

7.  __stdcall,它是Standard Call的缩写,是C的标准函数调用方式:所有参数从右到左依次入栈,如果调用的是类成员的话,最后一个入栈的是this指针。堆栈中的参数由被调用的函数在返回后清除,函数在编译的时候就必须严格控制参数生成,否则返回后会出错。

3)        dlsym

dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。

8.  __fastcall,是编译器指定的快速调用方式。由于使用堆栈传递比较费时,因此__fastcall通常规定将前N(一般2个,不同的编译器规定使用的寄存器个数不同)个参数由CPU寄存器传递,其余的还是用内存的堆栈传递。返回方式和__stdcall相当。由于其涉及到编译器决定参数传递方式,故不能作为跨编译器的接口。

函数原型:void *dlsym(void *handle,const char *symbol);

(3)关闭动态链接库:dlclose,函数原型为: int dlclose (void *handle);

 

功能描述:在dlopen之后,库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数,则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数,

dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。

9.  __cdecl,它是C Declaration的缩写,表示C语言默认的函数调用方式:所有的参数从右到左依次入栈,参数由调用者清除(手动清除,调用者一般指编译器)。特点在于可以使用不定个数的参数。

4)        dlclose

(4)动态库错误函数:dlerror,函数原型为: const char *dlerror(void); 当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

 

函数原型:int dlclose(void *);

在取到函数执行地址后,就可以在动态库的使用程序里面根据动态库提供的函数接口声明调用动态库里面的函数。在编写调用动态库的程序的makefile文件时,需要加入编译选项-rdynamic和-ldl。

如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,而C/C++缺省的调用方式却为__cdecl(默认调用方式)。__stdcall与__cdecl的区别在于生成函数名最终符号的方式不同。若采用C编译方式(在C++中将函数声明为extern "C"),__stdcall约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl约定仅在输出函数名前面加下划线,形如_functionname。

功能描述:将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。如果存在析构函数,则在dlclose之后,析构函数会被调用。

除了采用这种方式编写和调用动态库之外,Linux操作系统也提供了一种更为方便的动态库调用方式,也方便了其它程序调用,这种方式与Windows系统的隐式链接类似。其动态库命名方式为“lib*.so.*”。在这个命名方式中,第一个*表示动态链接库的库名,第二个*通常表示该动态库的版本号,也可以没有版本号。在这种调用方式中,需要维护动态链接库的配置文件/etc/ld.so.conf来让动态链接库为系统所使用,通常把动态链接库所在目录名追加到动态链接库配置文件中。如具有X window窗口系统发行版该文件中都具有/usr/X11R6/lib,它指向X window窗口系统的动态链接库所在目录。为了使动态链接库能为系统所共享,还需运行动态链接库的管理命令./sbin/ldconfig。在编译所引用的动态库时,可以在gcc采用 –l或-L选项或直接引用所需的动态链接库方式进行编译。在Linux里面,可以采用ldd命令来检查程序依赖共享库。

注意,声明函数形式:extern “C” int __stdcall add(int x, int y);,应用程序中定义函数指针为:typedef int(__stdcall *lpAddFun)(int, int);

二、动态加载类

3、两种系统动态库比较分析

 

加载类有点困难,因为我们需要类的一个实例,而不仅仅是一个函数指针。我们无法通过new来创建类的实例,因为类是在动态库中定义的而不是在可执行程序中定义的,况且有时候我们连动态库中具体的类的名字都不知道。

Windows和Linux采用动态链接库技术目的是基本一致的,但由于操作系统的不同,他们在许多方面还是不尽相同,下面从以下几个方面进行阐述。

4、 静态库(lib/a)

解决方案是:利用多态性!我们在可执行文件中定义一个带虚成员函数的接口基类,而在模块中定义派生实现类。通常来说,接口类是抽象的(如果一个类含有虚函数,那它就是抽象的)。因为动态加载类往往用于实现插件,这意味着必须提供一个清晰定义的接口──我们将定义一个接口类和派生实现类。

(1)动态库程序编写,在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数作为初始化的人口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。Linux下的gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要到函数做特别声明,编写比较方便。

由于静态库是跟随着应用程序一起编译连接到源文件(exe)的,所以代码写法基本没有特殊的地方,需要注意的是extern"C"来确定是否使用C的方式编译,然后就是利用#ifndef来管理一类型的类/函数的调用。

接下来,在模块中,我们会定义两个附加的类工厂函数(class factory functions)(或称对象工厂函数)。其中一个函数创建一个类实例,并返回其指针;另一个函数则用以销毁该指针。这两个函数都以extern "C"来限定修饰。

(2)动态库编译,在windows系统下面,有方便的调试编译环境,通常不用自己去编写makefile文件,但在linux下面,需要自己动手去编写makefile文件,因此,必须掌握一定的makefile编写技巧,另外,通常Linux编译规则相对严格。

值得注意的是在应用程序中使用静态库时的声明#include"MathTool.h"、#pragma comment(lib,"LIBProject.lib"):直接将.h和.lib文件放在当前目录似乎是不行的(我测试时不行的),两种方法:1.使用相对路径或绝对路径包含.h和.lib文件。2.通过”属性”--”C/C++” --“常规”--”附加包含目录”,来指定.h的文件目录。通过”属性”--”链接器” -- “常规”--”附加库目录”,来添加.lib目录。这种方法动态库也适用。

实例如下:

(3)动态库调用方面,Windows和Linux对其下编制的动态库都可以采用显式调用或隐式调用,但具体的调用方式也不尽相同。

如果不想用#pragmacomment来指定lib,还可以在”属性”--”链接器”

“常规”--”附加依赖库”来指定库。也可以不设置库目录和依赖库名,而是直接“属性”--“链接器”--”命令行”,输入静态库的完整路径即可。但一般不推荐。

示例见:LIBProject。

 

5、 动态库(dll/so)

1.  动态库的lib文件和静态库的lib文件

静态库对应的lib文件叫静态库,动态库对应的lib文件叫导入库。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。

 

2.  声明导出函数(介绍.def)

DLL导出函数的声明有两种方式:一种是在声明中加__declspec(dllexport); 一种是采用模块定义(.def) 文件声明。.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

.def文件的规则为:

(1)LIBRARY语句说明.def文件相应的DLL;

 (2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);

(3).def 文件中的注释由每个注释行开始处的分号(;) 指定,且注释不能与语句共享一行。

GetInstance = GetInstance这样可以指定了DLL的函数导出后的名称仍然不变。

注:使用了.def文件记得在编译器:”属性”--”链接器”--”输入”--”模块定义文件”中设置改def文件。

如果使用__declspec(dllexport)的方式声明导出,在C++中使用当然没有问题,但是当其他语言(C#、VB)调用时,就会出现找不到函数名的情况,是因为C++中为实现重载机制将函数名改变了,这时我们为了能够让其他语言正确的调用,就要使用extern “C”+ __declspec(dllexport)+__stdcall的方式。而使用def文件这些都可以不要,这样更方便,但是用到类的地方还是要用第一种方式。

 

3.  调用方式

DLL的调用方式也有两种:一种是动态调用;一种是静态调用。

动态调用:它完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL文件。由LoadLibrary->GetProcAddress->FreeLibrary系统API提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式。

动态调用注意的地方,在函数前面加extern “C”或定义def文件。显式调用类库中的class是很危险和繁琐的,因此能隐式不显式,能静态不动态。

静态调用: 它的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。静态调用方式的当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就像调用程序内部的其他函数一样。静态调用方式简单实用,但不如动态调用方式灵活。

静态调用需要两步:

1.  告诉编译器与DLL相对应的.lib文件所在的路径及文件名, #pragma comment(lib,"dllTest.lib")。程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。具体可以这样做,将.h、.lib、.dll文件拷贝到客户端程序当前目录下,然后在程序中#include<*.h> + #pragmacomment(lib,"dllTest.lib");或者在客户端程序的工程属性里面增加对该lib文件的引入。

2.  声明导入函数, 用__declspec(dllimport)说明为导入函数。有时是不用声明的,在类库的编写过程中定义好了。

 

4.  DllMain函数

Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。当程序中没有写DllMain函数时,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不是DLL可以放弃DllMain函数。

根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用程序中引用DllMain函数,DllMain是自动被调用的。

DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。

 

BOOL APIENTRY DllMain( HMODULEhModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

 

 

解析:

APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;

ul_reason_for_call的四个参数DLL_PROCESS_ATTACH表示进程调用;DLL_THREAD_ATTACH表示线程调用;DLL_THREAD_DETACH线程释放;DLL_PROCESS_DETACH线程释放。

lpReserved是一个保留字,基本没有什么作用,无需了解。

进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。

GetProcAddress( hDll, MAKEINTRESOURCE ( 1 ) )要注意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):

#defineMAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#defineMAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#defineMAKEINTRESOURCE MAKEINTRESOURCEW
#else
#defineMAKEINTRESOURCE MAKEINTRESOURCEA

5.  DLL导出变量

DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据。步骤:

1.  库的头文件.h中声明,extern int dllGlobalVar;

2.  在.cpp文件中声明使用,声明:int dllGlobalVar;使用:dllGlobalVar = 100;

3.  .def文件中导出,”GlobalVar_a@[n]”的方式,有人说是dllGlobalVar CONSTANT(已过时)/ dllGlobalVar DATA这个方式,但是我用的不行。

4.  在应用程序中引用DLL中定义的全局变量,方式有两种:一种方法(使用def声明的变量):首先声明extern int dllGlobalVar;然后int a = *(int*)dllGlobalVar。 特别要注意的是用externint dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址。另一种方法:externint _declspec(dllimport) dllGlobalVar; 通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了。建议第二种方式,第一种方式我测试中遇到一些很奇怪的问题。希望,有人根据我的测试,帮我解决下。

示例见:DLLProjec

 

6.  DLL导出类

导出类,我们一般使用静态调用,这样我们可以将整个类导出,或者只将类中的指定成员导出。如果使用动态调用类,我们一般使用一个全局函数来返回一个类的对象。

最后,学习到这里,最大的领悟就是,类库的编写,主要理解extern”C”、_declspec(dllexport)、_declspec(dllimport)、__stdcall、__cdecl这几个基本关键字,类库是提供给别人用的,我需要做的就是指定将哪些类或函数指定为用户使用的。还有就是使用类库,知道静态调用和动态调用的方法。

示例见:DLLProjec

 附源码链接:

后记:本文很长,也是我花费了很多个工作日学习实践的成果,如果有错误或意见希望能给我留言,结合示例来看会更加好些,如果可以直接看懂示例,那么长的文字也就可以随便浏览了。接下来,我会介绍其他语言调用C++类库常用的方法(C#调用C++的库)。

 

       testBase.h中定义一个含有纯虚函数virtual void display() const = 0的基类。

(4)动态库输出函数查看,在Windows中,有许多工具和软件可以进行查看DLL中所输出的函数,例如命令行方式的dumpbin以及VC++工具中的DEPENDS程序。在Linux系统中通常采用nm来查看输出函数,也可以使用ldd查看程序隐式链接的共享对象文件。

       test1.cpp中定义继承类test1,并实现虚函数virtual void display() const的定义,并实现一个创建类函数和一个销毁类指针函数。

(5)对操作系统的依赖,这两种动态库运行依赖于各自的操作系统,不能跨平台使用。因此,对于实现相同功能的动态库,必须为两种不同的操作系统提供不同的动态库版本。

       test2.cpp中定义继承类test2,并实现虚函数virtual void display() const的定义,并实现一个创建类函数和一个销毁类指针函数。

4、动态库移植方法

       main.cpp中实现动态的调用不同库中的display()方法。

如果要编制在两个系统中都能使用的动态链接库,通常会先选择在Windows的VC++提供的调试环境中完成初始的开发,毕竟VC++提供的图形化编辑和调试界面比vi和gcc方便许多。完成测试之后,再进行动态库的程序移植。通常gcc默认的编译规则比VC++默认的编译规则严格,即使在VC++下面没有任何警告错误的程序在gcc调试中也会出现许多警告错误,可以在gcc中采用-w选项关闭警告错误。

testBase.h

下面给出程序移植需要遵循的规则以及经验。

#ifndef __TESTBASE_H__
#define __TESTBASE_H__

class testBase
{
public:
        testBase(){}
        virtual ~testBase(){}
        virtual void display() = 0;
};

typedef testBase* create_t();
typedef void destroy_t(testBase *p);
#endif

(1)尽量不要改变原有动态库头文件的顺序。通常在C/C++语言中,头文件的顺序有相当的关系。另外虽然C/C++语言区分大小写,但在包含头文件时,Linux必须与头文件的大小写相同,因为ext2文件系统对文件名是大小写敏感,否则不能正确编译,而在Windows下面,头文件大小写可以正确编译。

test1.cpp

(2)不同系统独有的头文件。在Windows系统中,通常会包括windows.h头文件,如果调用底层的通信函数,则会包含winsock..h头文件。因此在移植到Linux系统时,要注释掉这些Windows系统独有的头文件以及一些windows系统的常量定义说明,增加Linux都底层通信的支持的头文件等。

</pre><pre name="code" class="cpp">#include<stdio.h>
#include "testBase.h"

class test1:public testBase
{
public:
        void display()
        {
                printf("display in test1n");
        }
};

extern "C" testBase* create()
{
        return new test1;
} 

extern "C" void destroy(testBase *p)
{
        delete p;
}

(3)数据类型。VC++具有许多独有的数据类型,如__int16,__int32,TRUE,SOCKET等,gcc编译器不支持它们。通常做法是需要把windows.h和basetypes.h中对这些数据进行定义的语句复制到一个头文件中,再在Linux中包含这个头文件。例如把套接字的类型为SOCKET改为int。

test2.cpp

(4)关键字。VC++中具有许多标准C中所没有采用的关键字,如BOOL,BYTE,DWORD,__asm等,通常在为了移植方便,尽量不使用它们,如果实在无法避免可以采用#ifdef 和#endif为LINUX和WINDOWS编写两个版本。

#include<stdio.h>
#include "testBase.h"

class test2:public testBase
{
public:
        void display()
        {
                printf("display in test2n");
        }
};

extern "C" testBase* create()
{
        return new test2;
} 

extern "C" void destroy(testBase *p)
{
        delete p;
}

(5)函数原型的修改。通常如果采用标准的C/C++语言编写的动态库,基本上不用再重新编写函数,但对于系统调用函数,由于两种系统的区别,需要改变函数的调用方式等,如在Linux编制的网络通信动态库中,用close()函数代替windows操作系统下的closesocket()函数来关闭套接字。另外在Linux下没有文件句柄,要打开文件可用open和fopen函数,具体这两个函数的用法可参考文献[2]。

main.cpp

(6)makefile的编写。在windows下面通常由VC++编译器来负责调试,但gcc需要自己动手编写makefile文件,也可以参照VC++生成的makefile文件。对于动态库移植,编译动态库时需要加入-shared选项。对于采用数学函数,如幂级数的程序,在调用动态库是,需要加入-lm。

#include<stdio.h>
#include"testBase.h"
#include<dlfcn.h>

int main()
{
        void *handle;
        char *error;
        handle = dlopen("./libtest2.so", RTLD_LAZY);
        if (!handle)
        {
                printf("%sn", dlerror());
                return -1;
        }
        create_t* create = (create_t *)dlsym(handle, "create");
        if ( (error = dlerror()) != NULL)
        {
                printf("%sn", error);
                return -1;
        }
        destroy_t* destroy = (destroy_t *)dlsym(handle, "destroy");
        if ( (error = dlerror()) != NULL)
        {
                printf("%sn", error);
                return -1;
        }
        testBase *test = create();
        test->display();
        destroy(test);

        dlclose(handle);
        return 0;
}

(7)其它一些需要注意的地方

编译:

①程序设计结构分析,对于移植它人编写的动态库程序,程序结构分析是必不可少的步骤,通常在动态库程序中,不会包含界面等操作,所以相对容易一些。

g++ test1.cpp -fPIC -shared -olibtest1.so

②在Linux中,对文件或目录的权限分为拥有者、群组、其它。所以在存取文件时,要注意对文件是读还是写操作,如果是对文件进行写操作,要注意修改文件或目录的权限,否则无法对文件进行写。

g++ test2.cpp -fPIC -shared -olibtest2.so
g++ main.cpp -ldl -ltest1 -L./

③指针的使用,定义一个指针只给它分配四个字节的内存,如果要对指针所指向的变量赋值,必须用malloc函数为它分配内存或不把它定义为指针而定义为变量即可,这点在linux下面比windows编译严格。同样结构不能在函数中传值,如果要在函数中进行结构传值,必须把函数中的结构定义为结构指针。

三、总结:

④路径标识符,在Linux下是“/”,在Windows下是“”,注意windows和Linux的对动态库搜索路径的不同。

1、必须使用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。所以extern "C" 只是告诉编译器编和链接的时候都用c的方式的函数名字,函数里的内容可以为c的代码也可以为c++的。

⑤编程和调试技巧方面。对不同的调试环境有不同的调试技巧,在这里不多叙述。

2、链接时注意加动态连接参数-ldl

5、结束语

3、链接时要指定动态库路径(-L./)不然会会找不到动态库:cannot find -ltest1

本文系统分析了windows和Linux动态库实现和使用方式,从程序编写、编译、调用以及对操作系统依赖等方面综合分析比较了这两种调用方式的不同之处,根据实际程序移植经验,给出了把VC++编制的Windows动态库移植到Linux下的方法以及需要注意的问题,同时并给出了程序示例片断,实际在程序移植过程中,由于系统的设计等方面,可能移植起来需要注意的方面远比上面复杂,本文通过总结归纳进而为不同操作系统程序移植提供了有意的经验和技巧。

4、运行时程序会提示如下错误

 

 error while loading shared libraries: libtiger.so: cannot open shared object file: No such file or direct

这是因为程序运行时没有找到动态链接库造成的。程序编译时链接动态库和运行时使用动态链接库的概念是不同的,在运行时,程序链接的动态链接库需要在系统目录下才行。

使用以下方法可以解决此问题

a. 在linux下最方便的解决方案是拷贝libtest1.so到绝对目录 /lib 下(但是,要是超级用户才可以,因此要使用sudo哦,亲)。就可以生成可执行程序了

b.第二种方法是:将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量LD_LIBRARY_PATH中实现:

export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH

编辑:编程 本文来源:1、 静态链接库和动态链接库,我们经常会调用一

关键词:

  • 上一篇:没有了
  • 下一篇:没有了