C# .net 调用dll
2014-08-12 (2014-08-27更新)
C# .net dll简介
dll文件在windows上通常是指动态链接库文件,但是在.Net平台上dll为托管代码,虽然同样是为了共享代码,但不再是传统意义上的动态链接库了。
在.NET中,引入了一个程序集的概念,指经由编译器编译得到的,供CLR进一步编译执行的那个中间产物,在WINDOWS系统中,它一般表现为.dll,或者是.exe的格式。因此,在.Net中dll文件为程序集,也叫类库,是托管代码,.Net可以像动态链接库一样引用它,但是非托管代码,如C程序等无法直接调用该dll。
本文总结了.Net平台下调用.dll类库和动态链接库(本地dll,native code)文件的基本方法。
.Net程序集(DLL类库文件)调用
.Net程序集直接引用
在visual studio中引用类库非常方便,不仅可以直接浏览类库内的类和函数,还可以智能补全引用的函数。
- 解决方案资源管理器中找到“引用”=>右键=>添加引用=>浏览需要引用的类库
- 在代码前部用using语句引用类库的命名空间(可以不这样做,但会导致引用函数时,需要输入很长的名称)
- 然后在代码中直接调用类库内的函数
.NET利用反射的动态加载和卸载程序集(DLL类库文件)
反射提供了封装程序集、模块和类型的对象(Type 类型)。可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性。
需要引入System.Reflection命名空间,具体步骤如下:
Assembly dllx = Assembly.Load("file.dll");
object obj=dllx.CreateInstance("dllClass");
//调用没有参数的方法
object result=obj.GetType().GetMethod("dllmethod").Invoke(obj,null);
//调用有参数的方法
object result=obj.GetType().GetMethod("dllmethod").Invoke(obj,a,b);
object可以用var代替。使用object的效率低,在以上代码中建议用var。
在C#中,所有类型(预定义类型、用户定义类型、引用类型和值类型)都直接或间接从 Object 继承。可以将任何类型的值赋给 object 类型的变量。将值类型的变量转换为对象的过程称为“装箱”。将对象类型的变量转换为值类型的过程称为“拆箱”。
而Var则根据赋值的类型自动设置变量的类型,如:
var intinput=5;
等于int intinput =5
,var intinput ="5.5"
等于string intinput ="5.5"
var的使用情况
1. 必须在定义时初始化。也就是必须是var s = “abcd”
形式,而不能是:var s; s = “abcd”;
2. 一但初始化完成,就不能再给变量赋与初始化值类型不同的值了。
3. var要求是局部变量。
4. 使用var定义变量和object不同,它在效率上和使用强类型方式定义变量完全一样。
.NET调用c动态链接库(Native dll)
P/Invoke调用Native dll
需要引用InteropServices命名空间:
using System.Runtime.InteropServices;
简单P/Invoke调用
.NET调用P/Invoke是完成这一任务的最常用方法。另一种方法是使用 Managed Extensions to C++ 来包装函数(在此不作介绍)。
例如,我们要调用kernel32.dll中的beep函数,该函数在MSDN中的原型为:
BOOL Beep(DWORD dwFreq, DWORD dwDuration)
在.NET调用时需要编写这一原型,因此需要把数据类型转换为相应的.NET类型。
以C#为例,由于DWORD是4字节的整数,因此可以用int或uint作为C#对应类型。bool类型与Bool对应。因此可以用C#编写该函数原型如下:
public static extern bool Beep(int frequency, int duration);
使用extern指明该函数在别处。此原型将告诉运行时如何调用函数;现在我们在原型中添加DllImport属性告诉运行时在何处找到该函数。
[DllImport("kernel32.dll")]
public static extern bool Beep(int frequency, int duration);
需要在程序声明中使用System.Runtime.InteropServices命名空间
DllImport 允许您调用 Win32 中的任何代码。
在C#中,函数只支持__stdcall的调用方式。如果要调__cdecl的c/C++函数,要在DllImport里面指定 CallingConvention = CallConvention.Cdecl。
windows和.net数据类型对应表
Win32 Types | Specification | CLR Type |
---|---|---|
char, INT8, SBYTE, CHAR |
8-bit signed integer | System.SByte |
short, short int, INT16, SHORT |
16-bit signed integer | System.Int16 |
int, long, long int, INT32, LONG32, BOOL, INT |
32-bit signed integer | System.Int32 |
__int64, INT64, LONGLONG |
64-bit signed integer | System.Int64 |
unsigned char, UINT8, UCHAR, BYTE |
8-bit unsigned integer | System.Byte |
unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR, __wchar_t |
16-bit unsigned integer | System.UInt16 |
unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT |
32-bit unsigned integer | System.UInt32 |
unsigned __int64, UINT64, DWORDLONG, ULONGLONG |
64-bit unsigned integer | System.UInt64 |
float, FLOAT |
Single-precision floating point | System.Single |
double, long double, DOUBLE |
Double-precision floating point | System.Double |
DllImport属性使用方法
- DllImport只能放置在方法声明上
- 用DllImport 属性修饰的方法必须具有 extern 修饰符
- DllImport具有单个定位参数:指定包含被导入方法的 dll 名称的 dllName 参数
- DllImport具有五个命名参数:
- CallingConvention 参数指示入口点的调用约定。如果未指定 CallingConvention,则使用默认值 CallingConvention.Winapi,也就是
__stdcall
,这是C#的默认调用方式,不用显式声明。 - CharSet 参数指示用在入口点中的字符集。如果未指定 CharSet,则使用默认值 CharSet.Auto
- EntryPoint 参数给出 dll 中入口点的名称。如果未指定 EntryPoint,则使用方法本身的名称
- ExactSpelling 参数指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配。如果未指定 ExactSpelling,则使用默认值 false
- PreserveSig 参数指示方法的签名应当被保留还是被转换。当签名被转换时,它被转换为一个具有 HRESULT 返回值和该返回值的一个名为 retval 的附加输出参数的签名。如果未指定 PreserveSig,则使用默认值 true
- SetLastError 参数指示方法是否保留 Win32”上一错误”。如果未指定 SetLastError,则使用默认值 false
- CallingConvention 参数指示入口点的调用约定。如果未指定 CallingConvention,则使用默认值 CallingConvention.Winapi,也就是
复杂函数P/Invoke调用
如果要调用的函数参数是指针或是地址变量,怎么办?
对于这种情况可以使用C#提供的非安全代码来进行解决,但是,毕竟是非托管代码,垃圾资源处理不好的话对应用程序是很不利的。所以还是使用C#提供的ref以及out修饰字比较好。
例如:
int __stdcall FunctionName(unsigned char ¶m1, unsigned char *param2)
在C#中对其进行调用的方法是:
[DllImport("file.dll")]
public static extern int FunctionName(ref byte param1, ref byte param2);
看到这,可能有人会问,&是取地址,*是传送指针,为何都只用ref就可以了呢?一种可能的解释是ref是一个具有重载特性的修饰符,会自动识别是取地址还是传送指针。
在实际的情况中,我们利用参数传递地址更多还是用在传送数组首地址上。如:
byte[] param1 = new param1(6);
在这里我们声明了一个数组,现在要将其的首地址传送过去,只要将param1数组的第一个元素用ref修饰。具体如下:
[DllImport("file.dll")]
static extern int FunctionName(ref byte param1[1], ref byte param2);
通过api动态加载Native dll
因为C#中使用DllImport是不能像动态load/unload assembly那样,所以只能借助API函数了。在kernel32.dll中,与动态库调用有关的函数包括[3]:
- LoadLibrary(或MFC 的AfxLoadLibrary),装载动态库。
- GetProcAddress,获取要引入的函数,将符号名或标识号转换为DLL内部地址。
- FreeLibrary(或MFC的AfxFreeLibrary),释放动态链接库。
它们的原型分别是:
- HMODULE LoadLibrary(LPCTSTR lpFileName);
- FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);
- BOOL FreeLibrary(HMODULE hModule);
现在,我们可以用IntPtr hModule=LoadLibrary(“Count.dll”);来获得Dll的句柄,用IntPtr farProc=GetProcAddress(hModule,”_count@4”);来获得函数的入口地址。
但是,知道函数的入口地址后,怎样调用这个函数呢?因为在C#中是没有函数指针的,没有像C++那样的函数指针调用方式来调用函数,所以我们得借助其它方法。经过研究,发现我们可以通过结合使用System.Reflection.Emit及System.Reflection.Assembly里的类和函数达到我们的目的。
详见《C#程序实现动态调用DLL的研究》一文。
类库(托管dll)搜索路径
- 托管dll(类库)的搜索路径与native dll的搜索路径是不同的
- 搜索路径的方式是按照被调用的dll库(是类库还是native dll)决定的,与调用程序是否托管代码无关
.net CLR在运行时寻找正确的Assembly(类库dll也是Assembly),Net提供了搜索算法,可以根据.config文件添加自定义搜索路径。
搜索顺序如下:
- 在GAC(Global Assembly Cache,全局程序集缓存)中搜索相应版本的DLL.
- 配置文件(web.config或app.config)中配置codeBase(assemblyIdentity应该可以不用)
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="myAssembly"
publicKeyToken="32ab4ba45e0a69a1"
culture="neutral" />
<codeBase version="2.0.0.0"
href="om/myAssembly.dll"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
- 应用程序(.exe)当前目录下
- 配置文件(web.config或app.config)中配置privatePath
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="bin;bin2\subbin;bin3"/>
</assemblyBinding>
</runtime>
</configuration>
OK,CLR就是根据上面的顺序从1到4进行搜索Assembly的。如果没有搜索到指定版本的类库DLL,则程序会抛出异常,提示:DLL文件无法找到。