VC++编写DLL供C#使用

半年没有写东西了。也就出差了半年。半年没有用C#,写了半年VC,呵呵。有时候其实有挺多东西写的,不过一忙起来人就懒了。简单的东西不想写,复杂的又写不了,哈哈。最近不是太忙,大概看了下COM组件模型,然后对DLL工作,库文件有了一些兴趣。因为工作和自己写的程序可以延伸到这些知识上,所以对DLL进行了一定的了解,而且对于直接开始编写C#应用程序的人来说,也很少会去了解这些,所以决定写下来。

 

 

 一 库文件

 

 

库文件是我们在接触编程时经常听到的词。那么什么是库文件。其实库文件是一种重用的思想。比如我们自己编写C#的程序,可能会把公用的一些方法放在一个名为Common的项目中,然后编译成DLL,其他项目引用这个程序集,就可以调用这个里面的方法,而不用自己去实现。在说的大一点,.NET中提供给我们使用的类,也是存放在.NET库中的。而整个WINDOWS系统,都是建立在库文件基础上的。

实在最初的时候是没有库文件的概念,所有功能都是自己实现的。为了提高效率,减少重复工作,就必须对代码重用。重用方法有很对,比如对于一些通用的方法的实现代码让编译器自己生成,但这个问题是,一旦方法变更会添加,编译器也必须修改了。另一个方法是把常用的方法编译成.obj模块文件。这样在编译的时候,去链接这些模块文件,就能实现代码重用。但是所有方法放到一个obj文件中,文件会很庞大,占用内存;如果为每个方法提供一个obj文件,那又会相当的麻烦。

于是就出现了静态库(static library),静态库是一个或多个.obj文件的集合。使用到这些.obj文件对应的函数时,只需要在连接时提供该静态库,而不需要列举用到的.obj文件。而连接器进行连接的时候,只会将程序中用到的函数对应的.obj文件连接到程序中。静态库的形式就是我们WINDOWS上常见的.lib文件。但静态库的确定是,每个程序使用时都会吧需要的obj文件拷贝到内存中,100个程序就有100个拷贝,这样对内存的浪费很大,而且如果静态库文件更新的话,程序也需要重新编译,进行连接。于是共享库(shared library,也叫做动态连接库)就出现了。首先他是被加载到公用的内存空间中,是所有程序共享的,所以对节省了内存;另外他在编译时没有象静态库那样,把代码直接连接到了程序中,只是插入了动态库的一些信息,供调用时使用。这样,我们就有可能直接修改动态库,而不需要重新编译程序。

动态库当然也有他的问题,比如DLL HELL。DLL Hell 是指当多个应用程序共享一个DLL或COM组件时所引发的一系列问题。比如一个应用程序安装一个新版本DLL,而该DLL与机器上的现有版本不向后兼容。这样就可能导致以前的引用程序引发异常。而.NET的强命名程序集就解决了这个问题,它通过程序集(DLL)名称、语言、版本、KEY来区分DLL,而不仅仅是只通过名字。也就能保证对个版本的DLL同时工作。

 

二 使用库文件

 

 

其面说到了库文件有静态库和动态库以及.NET的程序集文件。我们在.NET中编写的DLL,在其他.NET程序中调用时很方便,引用DLL程序集,然后添加namespace,这样就能直接使用程序集中的方法了。这些都是因为.NET的程序集中的元数据,使得DLL文件有了自述性。所以也就取消了C++中的.h文件。

以下是在托管的VC++环境下使用:

对于非托管的VC++环境使用如下:

  • 使用静态库时:我们需要提供一个编译好的Lib文件。但是光有这个文件不行,因为我们不知道这个文件中包含了那些方法。所以我们还需要提供我们编写库文件时使用的.h头文件。在我们自己的VC项目中引入这个.lib库,然后吧.h头文件添加到项目中,就可以调用了。体使用方法参见:静态连接库的生成和使用
  •  使用动态库时:我们的库编译后生成DLL时,链接程序会自动生成一个与之对应的 LIB 导入文件。所以存在2种不同的调用方法,叫隐式加载和动态加载。所谓动态库静态加载,使用上和静态库类似,但是他必须提供DLL,而LIB文件也和静态库不同了,这里的LIB文件直接和DLL关联,实际的实现代码是在DLL中。而动态连接,就是使用LoadLibrary函数,加载DLL库;通过GetProcAddress 获得方法的指针,然后可以直接使用此方法,而不需要引入LIB文件和头文件。具体使用方法参见:DLL(Dynamic Link Libraries)专题

 

 

三 VC编写DLL

 

 

我使用VC++编写一个文件加密的DLL,并且在C#中调用。编写一个动态库很简单,以VS2010为列,我们在VC++下选择【WIN32】–【WIN32项目】。在应用程序向导中,选择应用程序类型为DLL,并且选择为空项目。然后添加一个.h文件和一个.cpp文件

// FileEncrypt.h

/*加密文件*/ 
extern "C" __declspec(dllexport) void EncryptMyFile(char *in_fname,char *out_file);
// Filencrypt.cpp

#include "FileEncrypt.h"
#include <stdexcept>
#include <iostream>
#include<stdio.h> 
#include<stdlib.h> 
#include<conio.h> 
#include<string.h>
#include <windows.h>


/*加密子函数开始*/ 
void EncryptMyFile(char *in_fname,char *out_file) 
{ 
  //实现省略
}

编译之后我们就能得到一个DLL和一个LIB文件。实现很简单,和我们写普通程序一样。唯一不同的是.h文件中声明函数时增加了extern “C” __declspec(dllexport) 这样一串在东西。其中extern “C” 网上解释非常多,简单的说是因为C++编译后,方法名字会变化,不是我们在程序中使用的EncryptMyFile。我们可以利用Depends工具来查看:

以上2个图就是使用了extern “C” 和没使用时的函数的名称。从第2个图可以看到,名字已经修改了。具体修改的参数意义可以参见DLL(Dynamic Link Libraries)专题 ,里面有详细介绍。所以当我们编写的DLL是给其他非VC程序调用,如C,C#等,就需要增加extern “C” ,来确保编译时不会被修改名字,以便程序使用。

另一个__declspec(dllexport)  标记使用来表示此函数为导出函数。。 一下内容是《WINDOWS核心编程》对这个的解释,具体可以参见书中原文。出此之外书中也对extern “C” 进行了解释。

当Micresoft的C/C++编译器看到变量、函数原型或C++类之前的这个修改符的时候,它就将某些附加信息嵌入产生的.OBJ文件中。当链接DLL的所有.OBJ文件时,链接程序将对这些信息进行分析。当DLL被链接时,链接程序要查找关于输出变量、函数或C++类的信息,并自动生成一个.LIB文件。该.LIB文件包含一个DLL输出的符号列表。当然,如果要链接引用该DLL的输出符号的任何可执行模块,该.LIB文件是必不可少的。除了创建.LIB文件外,链接程序还要将一个输出符号表嵌入产生的DLL文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母 顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址(RVA)放入DLL模块。

如果我们的程序缺少了__declspec(dllexport),那么其他应用程序调用DLL时就无法找到入口点:

鉴于上面的问题,VC还提供了一个DEF模块定义文件,来设置导出函数。我们可以在项目中添加一个DEF文件,配置如下,具体的DEF文件格式可以参见核心编程。这样我们就不需要在.h文件中添加额外的修饰符了

LIBRARY FileEncrypt
EXPORTS
	EncryptMyFile @1

 

四 C#中使用DLL

 

这里使用的DLL是VC下编译生成的DLL,而不是.NET下的DLL。两者是不同的。使用自己的DLL和我们使用系统API是一样的

[DllImport("FileEncrypt")]
public static extern void EncryptMyFile(string inFileName, string outFilename);

引入我们的DLL,并且声明这个方法就可以使用了。麻烦的仅仅是C#和C++直接数据类型的转换。其面提到,如果使用别人提哦那个的DLL,并且没有使用extern “C” 时,我们c#程序使用时会因为函数名不同而提示找不到函数入口点,我们可以通过设置DllImport属性中指定EntryPoint字段,指示要调用的DLL入口点名称和序号。

另外需要注意的地方就是VC运行库,默认编译运行库的参数是MD,这个时候,我们的DLL文件很小,需要目标机器上有VC运行库的支持,如果我们修改为MT则不在需要,而是吧需要的打包和DLL在一起了,此时的DLL会较大。这个我在编写的时候就遇到了。具体解释MSDN上有。

 

 

五 总结

 

其实对于有VC编程经验的人来说,上面都是挺简单的,不过对于一开始就写C#来说,不能说复杂,只能说不了解。所以还是有必要写下来。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

2 评论

Grace2009ff进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注