关于C#调用托管DLL的研究

    做过.net的都知道,Asp.NET项目发布后,主程序目录中经常会存在很多第三方的托管Dll,显得很杂乱。因为.NET对使用DLL有一个机制叫做OutputFile机制,就是说项目主程序在调用DLL时,会通过OutputFile机制将DLL自动复制到主程序目录下。怎么才能将第三方DLL跟主程序EXE文件分开存放呢?或者说怎么才能让程序直接调用托管DLL中的函数呢?基于此问题,我几经查阅资料,并在领导和项目组长的指导下得出以下结论:目前阶段,只能使用反射机制实现C#调用托管DLL的目的。C#中,反射机制会审查DLL中的元数据并收集关于它的类型信息的能力。元数据就是一大堆的表,当编译程序集或者模块时,编译器会创建一个类定义表,一个字段定义表和一个方法定义表等。反射机制的System.reflection命名空间包含的几个类,允许你反射(解析)这些元数据表的代码。但是反射也有它的弊端,就是要以牺牲性能为代价,而且有些信息利用反射是无法得到的,在使用时要根据情况进行选择。

    研究过程几经周折,最初我认为课题的目的只是为了将DLLEXE主程序分开存放,以免主程序目录混乱。基于此目的,最初想到的是在WEB.CONFIG中设置属性,从而达到目标效果。经过查资料,发现标签可能会实现预期效果。在WEB.CONFIG中设置以下代码:


    
        
            
            //privatePath中存放的是程序集的位置
        
    

多次试验后发现如果不引用目标DLL,则程序显示找不到DLL,出现编译时错误。但是如果引用目标DLL,则NET体制总是会自动将目标DLL复制一份到主程序目录下,继续出现未解决的问题。默认情况下 CLR 会从当前目录、本地化资源目录、FirstAssemblies 等三个地方顺序搜索引用的 DLL,如果没有找到,会抛出 FileNotFoundException 。这可能就是此次试验失败的原因之一。

此时,就会想到是否可以把目标DLL加入本地化资源目录中,那么实现的方法只有将DLL放入电脑操作系统的System32目录下或者将DLL所在的绝对地址加入系统环境变量中。第二种情况需要将DLL的绝对地址复制到环境变量PATH的KEY中,PATH中的不同KEY值之间要用“;”分割。但是这种方法的弊端很明显:程序发布时会出现找不到DLL的问题。

在研究过程中,曾发现VS2010有嵌入互操作类型这个概念。什么是“嵌入互操作类型”呢?”嵌入互操作类型”中的嵌入就是引进、导入的意思,类似于c#中using,c中include的作用,目的是告诉编译器是否要把互操作类型引入。“互操作类型”实际是指一系列Com组件的程序集,是公共运行库中库文件,类似于编译好的类,接口等。“嵌入互操作类型”设定为true,实际上就是不引入互操作集(编译时候放弃Com程序集),仅编译用户代码的程序集。而设定为false的话,实际就是需要从互操作程序集中获取 COM 类型的类型信息。这就有些意思了,这更像是C#的编译器功能,而不像是C#的语言功能了。那么设置被引入DLL的“嵌入互操作类型”是否会有接近目标的效果呢,事实证明,“嵌入互操作类型”像是一个未成年人,不仅需要.NET FRAMEWORK版本的支持,还有待进一步的发展才能体现它更强大的功能。探索过程中难免有一些弯路,前路的伟人如此,更何况我这个半路出家的.NET菜鸟,我们就当这只是个小插曲,继续前进把!

接下来的我似乎深入了误区,为了实现第三方托管DLL跟主程序EXE的分离,我甚至想到了将DLL和EXE打包的“二货”方法,事后也是一阵郁闷,智商果然是硬伤啊!

好吧,既然“运行时程序”不可以解决问题,那么“运行后程序”是否可以呢?果然,还真让我找到了一个生成事件后命令行的东西。在“生成事件--&gt生成后事件”属性的命令行属性中设置“copy "$(TargetPath)" \TDM3000-DEV\Bin”,该命令表示在生成该工程后,把该工程生成的可执行文件或dll等拷贝到该工程所在磁盘的Bin目录下(即F:\TDM3000-DEV\Bin)。注意:该Bin目录首先要存在,否则链接会出错。通过这种方法将DLL放入特定文件夹下的目的是达到了,可是主程序目录下还是会存在第三方的托管DLL,这是因为生成后事件的copy语句只是将相关DLL整理到特定的文件夹下,方便寻找管理,却没有将目标DLL删除,当然删除了可能会出现FileNotFoundException的异常。所以,这个方法只能夭折了,这时候我原本跃跃欲试的激情早以消磨没了,是不是根本就没有C#调用托管DLL的方法呢?还是我知识浅显,不足以解决这个问题?

当然是后者,在项目组长的帮助下,我了解到有反射机制和DLLImport两种方法可以实现预期效果。C#中的反射机制很强大,也很复杂,项目组已经利用反射机制实现C#调用第三方托管DLL,但是它的弊端太明显了,反射机制在运行时会占用极大的性能。反射提供了封装程序集、模块和类型的对象( Type 类型),可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性,所在命名空间是System.Reflection。它在工作过程中需要调用很多命名空间中的类(System.Reflection.Assembly 、System.Reflection.MemberInfo、System.Reflection.EventInfo、 System.Reflection.FieldInfo、System.Reflection.MethodBase、System.Reflection.ConstructorInfo、System.Reflection.MethodInfo、System.Reflection.PropertyInfo、System.Type )对元数据进行解析,这就造成程序运行缓慢,性能占用极大的效果。

很多人知道C++和C#之间通过某种方式可以互相调用其编写的DLL,C#调用非托管DLL,也就是C++编写的DLL可以使用DLLImport语句。C#调用DLL中的非托管函数,首先应该在C#语言源程序中声明外部方法,其基本形式是:
[DLLImport(“DLL文件”)]
修饰符 extern 返回变量类型 方法名称 (参数列表)
其中:

DllImport只能放置在方法声明上。
DLL文件:包含定义外部方法的库文件。
修饰符: 访问修饰符,除了abstract以外,在声明方法时可以使用的修饰符。
返回变量类型:在DLL文件中你需调用方法的返回变量类型。
方法名称:在DLL文件中你需调用方法的名称。
参数列表:在DLL文件中你需调用方法的列表。
注意:需要在程序声明中使用System.Runtime.InteropServices命名空间。
DLL文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中Path所设置的路径)。
返回变量类型、方法名称、参数列表一定要与DLL文件中的定义相一致。
若要使用其它函数名,可以使用EntryPoint属性设置,EntryPoint设置的属性是函数的入口点,如果定义的方法和DLL中入口点方法名相同,EntryPoint可以省略。如:
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBox(int hWnd, string msg, string caption, int type);
其它可选的 DllImportAttribute 属性:
CharSet 参数指示用在入口点中的字符集。如果未指定 CharSet,则使用默认值 CharSet.Auto。

CallingConvention 参数指示入口点的调用约定。如果未指定 CallingConvention,则使用默认值 CallingConvention.Winapi。
SetLastError 参数指示方法是否保留 Win32"上一错误"。如果未指定 SetLastError,则使用默认值 false。
ExactSpelling 参数指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配。如果未指定 ExactSpelling,则使用默认值 false。
PPreserveSig 参数指示方法的签名应当被保留还是被转换。当签名被转换时,它被转换为一个具有 HRESULT 返回值和该返回值的一个名为 retval 的附加输出参数的签名。如果未指定 PreserveSig,则使用默认值 true。 

需要注意的是它是一次性属性类,用 DllImport 属性修饰的方法必须具有 extern 修饰符。在使用DLLImport过程中,经常会遇到Dll文件路径找不到问题,这时候用[DllImport(@"C:\OJ\Bin\Judge.dll")]这样指定DLL的绝对路径就可以正常装载。或者说使用 [DllImport("kernel32.dll")] 
  private extern static IntPtr LoadLibrary(String path); 
  [DllImport("kernel32.dll")] 
  private extern static IntPtr GetProcAddress(IntPtr lib, String funcName); 
  [DllImport("kernel32.dll")] 
  private extern static bool FreeLibrary(IntPtr lib);
  分别取得了LoadLibrary和GetProcAddress函数的地址,再通过这两个函数来取得我们的DLL里面的函数,具体方法不在这里阐述,有兴趣的可以参考网址:

http://www.cnblogs.com/shuaifeng/archive/2009/11/06/1597096.html

地址问题解决后,就可以继续测试C#调用C++的DLL是否可以运用到C#调用C#的DLL上,在测试过程中,使用的是自己编写的第三方托管DLL,可是无论EntryPoint怎么设置,都无法找到DLL中的托管函数,也就是说DLLIpmport无法找到DLL中的入口点。仔细思考后,自认为问题在于C#与C++语法的差异性。

致此,几乎所有可能的解决问题的办法都试用了一遍,也许是因为我刚入.NET的缘故,对C#的理解不够深入,暂时认为只有反射机制可以实现C#调用托管DLL。相信随着学习的深入,会发现更加高效率的方法实现C#调用第三方托管DLL,也希望有经验的前辈不吝赐教!拜谢!

 


 

 

 

 

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/30105891/viewspace-1445263/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/30105891/viewspace-1445263/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值