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#中的函数指针, 它规定了可以指向的函数的返回值以及参数列表。上面的例子将返回的委托赋值给了我们自己声明的委托变量,即指针的赋值,使得我们声明的委托 指向了该函数。

(全文完)