C#作为.NET 的主力开发语言,当然也是主要运行在 Windows 环境中了。而在日常的开发中,时不时会遇到项目需要调用环境 API 或者基于 Windows 开发的 C++程序的某个函数的情况。
本次我就来介绍下在 C#的程序中,如何调用系统 API 以及 C++程序的函数接口。
最近我在做的一个项目(C#语言开发)遇到了需要调用 C++的函数的问题,经过一番网上资料的汇总,利用前人造好的轮子亲身实践了一把,成功实现了需求。下面就把我收集的方法
做个总结,方便以后查找。
方法一:利用 DLLImport 特性导入
.NET Framework 类库(FCL)定义了几百个定制特性,可以将它们应用于自己的源代码中的各个元素。例如:
- 将 DllImport 特性应用于方法,告诉 CLR 该方法的实现位于制定 DLL 的非托管代码中。
- 将 Serializable 特性应用于类,告诉序列化格式化程序一个实例的字段可以序列化和反序列化。
- 将 AssemblyVersion 特性应用于程序集,设置程序集的版本号。
- 将 Flags 特性应用于枚举类型,枚举类型就成了位标志(bit flag)集合。
本文利用的特性就是:DLLImport。它在命名空间是System.Runtime.InteropServices
。要将该特性应用于方法,则必须至少提供包含入口点的 DLL 名称。
DLLImport 的定义如下,详细定义请参照官方文档:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[System.AttributeUsage(System.AttributeTargets.Method, Inherited=false)]
[System.Runtime.InteropServices.ComVisible(true)]
public sealed class DllImportAttribute : Attribute
{
public DllImportAttribute(string DllName){...} //构造函数,传入dll文件的位置(必须)
//部分常用字段
public CallingConvention CallingConvention; //指示入口点的调用约定(枚举类型:Cdecl/FastCall/StdCall/ThisCall/Winapi)
public CharSet CharSet; //指示如何向方法封送字符串参数,并控制名称重整
public string EntryPoint; //指示要调用的 DLL 入口点的名称或序号
public bool ExactSpelling; //是否必须与指示的入口点拼写完全一致,默认false
public bool PreserveSig; //方法的签名是被保留还是被转换
//etc...
}
|
在本次的项目开发中,需要调用既存的非托管 C++代码,该代码的作用是发送电文,并取得远端机器的特定返回数据。该类导出了一个接口函数int DataReq(DataInfo& info)
,
该函数的返回值为INT
类型,传入参数为一个结构体。我要做的就是在 C#工程中构造一个结构体,并在调用该函数的时候,将结构体传递过去。下面是代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class TestCallCPPFunc
{
//特别注意:m_strDllPath必须是const类型,因为DLLImport特性不支持动态dll路径,所以必须是常量。
private const string m_strDllPath = @"c:/interpub/wwwroot/somewhere/TestDataReq.dll";
[DLLImport(m_strDllpath, EntryPoint="DataReq")]
private extern static int DataReq(ref DataInfo info);
public void TestCall()
{
DataInfo info = new DataInfo();
//TODO:set some values to DataInfo's fields
int res = DataReq(ref info);
if(res == 0)
{
//do something: long reValue = info.somefield;
}
}
}
struct DataInfo
{
//some fields
}
|
到此,利用 DLLImport 导入非托管 dll 函数的方法就完成了。仔细看代码,你可能会发现,我在声明 dll 路径时用到了const
,这是因为 DLLImport 特性必须传入固定的 dll 路径,
或者只写 dll 文件的名称,例如:kernel32.dll
。只传入 dll 名称的场合,DLLImport 会按照以下顺序自动去寻找 dll 的存储路径:
- 项目所在路径
- System32 目录
- 环境变量中设置的目录
只要将需要调用的 dll 拷贝到这三个目录下面,就可以不用写全路径了。看到这里,可能会有个疑问,这个路径一定要是写死的吗?如果我的路径是个动态的,又该如何处理呢?
因为已知 DLLImport 并不支持传给它一个变量。这就引出了我们第二调用 DLL 方法,请继续往下看。
方法二:利用函数指针调用
还是上面的例子,我们换一个思路去解决这个问题。要利用函数指针,我们就要用到系统 dll(kernel32.dll
)提供的三个 API:
IntPtr LoadLibary(String path)
用来取得指定 dll 的指针(IntPtr 是一个指针类型),有了它就可以定位该 dll,这也就解决了动态 PATH 的问题
IntPtr GetProcAddress(IntPtr lib, String funcName)
用来取得指定 dll 中的某个函数的指针
bool FreeLibary(IntPtr lib)
用来释放掉指针
利用上面三个系统 API,我们来组织下调用代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
class DllInvokeClass
{
[DLLImport("kernel32.dll")]
private extern static IntPtr LoadLibary(string path);
[DLLImport("kernel32.dll")]
private extern static IntPtr GetProcAddress(IntPtr lib, string funcname);
[DLLImport("kernel32.dll")]
private extern static bool FreeLibary(IntPtr lib);
private IntPtr pLib; //dll的指针
public DllInvokeClass(string path)
{
plib = Loadlibary(path);
}
~DllInvokeClass()//显示的把dll指针释放
{
FreeLibary(path);
}
//声明一个返回类型是委托的函数,APIName是要调用的dll中函数的名字,Type t 是该函数的签名类型 => int DataReq(DataInfo& info)
//声明委托的原因是:利用函数指针取得的对象,是一个该函数的委托
public Delegate Invoke(string APIName, Type t)
{
IntPtr func = GetProcAddress(plib, APIName);
return Marshal.GetDelegateForFunctionPointer(func, t); //该函数通过,函数名和函数类型(签名),找到指定的函数,并返回该函数的委托
}
}
//下面是具体的调用实现
class TestCallCPPFunc
{
//声明了一个指向dll中要调用的函数的一个委托,它的类型就是要传给调用函数`invoke`的Type
public delegate int DataReqDelegate(ref DataInfo info);
public void TestCall(string dllPath)
{
DataInfo info = new DataInfo();
//TODO:set some values to DataInfo's fields
DllInvokeClass dll = new DllInvokeClass(dllPath);
//将系统API返回的委托,赋值给声明额委托变量,至此该变量就指向了dll中的指定函数
DataReqDelegate datareq = dll.invoke("DataReq", typeof(DataReqDelegate));
int res = dataReq(ref info);//这里就是调用函数的地方
if(res == 0)
{
//do something: long reValue = info.somefield;
}
}
}
struct DataInfo
{
//some fields
}
|
以上就是第二种调用指定 dll 中某个函数的方法了,值得注意的是,该方法利用到了 C#中委托的概念,其实委托也可以理解为 C#中的函数指针,
它规定了可以指向的函数的返回值以及参数列表。上面的例子将返回的委托赋值给了我们自己声明的委托变量,即指针的赋值,使得我们声明的委托
指向了该函数。
(全文完)
文章作者
Brein
上次更新
2019年1月14日 22时28分03秒
许可协议
转载本站文章请注明作者和出处 Brein's Blog,请勿用于任何商业用途