好久没写博客了,因为最近很忙,所以需要一些时间来整理下自己遇到的问题
最近在搞C#调用C++封装的DLL
由于是托管代码调用非托管代码,所以期间遇到了很多问题,也很扯淡
C#引用C++的API,无法像传统的方式一样,使用右键->引用来完成对程序集的添加。因此我们需要使用到System.Runtime.InteropServices中的DllImport特性,下面我们来了解下它。
DllImportAttribute的定义如下:
// 摘要: // 指示该特性化方法由非托管动态链接库 (DLL) 作为静态入口点公开。 [AttributeUsage(AttributeTargets.Method, Inherited = false)] [ComVisible(true)] public sealed class DllImportAttribute : Attribute { // 摘要: // 将 Unicode 字符转换为 ANSI 字符时,启用或禁用最佳映射行为。 public bool BestFitMapping; // // 摘要: // 指示入口点的调用约定。 public CallingConvention CallingConvention; // // 摘要: // 指示如何向方法封送字符串参数,并控制名称重整。 public CharSet CharSet; // // 摘要: // 指示要调用的 DLL 入口点的名称或序号。 public string EntryPoint; // // 摘要: // 控制 System.Runtime.InteropServices.DllImportAttribute.CharSet 字段是否使公共语言运行时在非托管 // DLL 中搜索入口点名称,而不使用指定的入口点名称。 public bool ExactSpelling; // // 摘要: // 指示是否直接转换具有 HRESULT 或 retval 返回值的非托管方法,或是否自动将 HRESULT 或 retval 返回值转换为异常。 public bool PreserveSig; // // 摘要: // 指示被调用方在从特性化方法返回之前是否调用 SetLastError Win32 API 函数。 public bool SetLastError; // // 摘要: // 启用或禁止在遇到被转换为 ANSI“?”字符的无法映射的 Unicode 字符时引发异常。字符。 public bool ThrowOnUnmappableChar; // 摘要: // 使用包含要导入的方法的 DLL 的名称初始化 System.Runtime.InteropServices.DllImportAttribute // 类的新实例。 // // 参数: // dllName: // 包含非托管方法的 DLL 的名称。如果 DLL 包含在某个程序集中,则可以包含程序集显示名称。 public DllImportAttribute(string dllName); // 摘要: // 获取包含入口点的 DLL 文件的名称。 // // 返回结果: // 包含入口点的 DLL 文件的名称。 public string Value { get; } }
从这个特性的定义中,我们可以看到以下几个特点
1、DllImport仅可作用于方法的声明 ——由AttributeTargets.Method决定
2、DllImport必须包含引入的DLL名称——由构造函数定义
3、DllImport声明的方法 必须使用extern关键字 表示这是一个外部实现的方法
另外这个特性的命名参数都有自己的默认值,在不进行特别声明的情况下,会使用默认值
1、CallingConvention ,默认为CallingConvention.Winapi
2、CharSet ,默认为CharSet.Auto
3、EntryPoint ,默认为方法本身的名称。
4、ExactSpelling ,默认值为False
5、PreserveSig ,默认值为True
6、SetLastError ,默认值为False
在使用DllImoprt是,我们除了要提供带有入口点的DLL名称,还经常会用到CallingConvention 、CharSet 、EntryPoint 这三个命名参数。并且DllImport寻找文件的方式为1、在程序运行目录中寻找。2、从System32中寻找。3、从环境变量的定义中寻找。因此无论放在这三个地方哪个地方,都可以保证正常的引入API。
下面我们用代码说话:
C++中的定义:
VIDEO_DEVIDE_LIBDLL int DeviceLogin(char* strDeviceIp, short m_siDevicePort, char* strUser, char* strPassword); VIDEO_DEVIDE_LIBDLL void DeviceLogout();
在引入函数的时候、我们要特别注意C++与C#的类型转换,具体的类型转换关系没有整理,网上有很多资料。如果类型对不上,会抛出异常。
C#中引入:
[DllImport(@"CPPSDKS\VideoDevSDK.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern int DeviceLogin(string strDeviceIp, short m_siDevicePort, string strUser, string strPassword); [DllImport(@"CPPSDKS\VideoDevSDK.dll")] public static extern void DeviceLogout();
大家仔细观察,会发现有参的方法需要对CallingConvention进行显式的设置。如果使用默认值 也就是Winapi定义的话,在调用这个方法的时候会抛出一个异常:对 PInvoke 函数“DemeterVideo.SDK!DemeterVideo.SDK.HKVersionSDK::DeviceLogin”的调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。请检查 PInvoke 签名的调用约定和参数与非托管的目标签名是否匹配。至于原因,由于是第一次接触,也不是很明了。而无参方法貌似就没有这么多的限制,这个问题头疼了很久,希望能为有需要的朋友提供一份便利。
一般的函数调用,在提供足够明确的标识之后,一般不会出现什么大的问题,但是我们不得不正视另一个问题,那就是回调。回调这个概念,在C#里体现形式就是“委托“,他们的目的都是一样的,传递方法。在C++里 应该就是传递一个方法的指针。
我们来看看代码:
C++
//结构
typedef struct{ long nWidth; long nHeight; long nStamp; long nType; long nFrameRate; DWORD dwFrameNum; }AMG_FRAME_INFO; //回调的函数类型的定义 typedef void(CALLBACK *fVideoHandle)(long nPort, void* pBuf,long nSize, AMG_FRAME_INFO * pFrameInfo, long nReserved1,long nReserved2); //设置回调 VIDEO_DEVIDE_LIBDLL void SetVideoHandleCallback(fVideoHandle fVideo);
这个回调的函数类型为fVideoHandle。这个措辞,我实在无法选择用一个准确的词汇进行描述,但是这个对象,在C#里它就是一个委托类型,它定义了可以接受的方法的定义。在这里,C++还定义了一个结构AMG_FRAME_INFO做为参数,这里可是一个赤裸裸的大坑。
我们来看C#代码吧,C++的真的太别扭了
/// <summary> /// 回调委托的参数之一 /// </summary> public struct AMG_FRAME_INFO { /// <summary> /// 宽度 /// </summary> public int nWidth; /// <summary> /// 高度 /// </summary> public int nHeight; /// <summary> /// /// </summary> public int nStamp; /// <summary> /// /// </summary> public int nType; /// <summary> /// /// </summary> public int nFrameRate; /// <summary> /// /// </summary> public uint dwFrameNum; } //定义委托 建议显式指定与非托管代码交互的约定 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void fVideoHandle(int nPort, IntPtr pBuf, int nSize, ref AMG_FRAME_INFO pFrameInfo, int nReserved1, int nReserved2); [DllImport(@"CPPSDKS\VideoDevSDK.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void SetVideoHandleCallback(fVideoHandle fVideo);
我们先来看设置回调的方法,在这个方法中,参数是我们定义好的委托fVideohandle。其它与一个普通的方法调用没有什么区别。
回过头来,我们看委托的定义。
在这个委托的定义中,我们使用了复杂类型,一个结构。由于我们的代码是托管的,因此无法直接访问非托管代码中定义的结构,我们需要创建自己的结构。并且在使用这个结构做为参数时,我们必须使用ref关键字,表示传递过去的不是这个结构的值得副本,而是一个引用的地址,这样C++才能对这个结构进行赋值操作。
说到这里,一些基本的调用方式就大概说完了。但是我们需要正视一个很严峻的问题,我们调用的代码是非托管代码,是UnSafe的,它们的内存需要自行处理、回收。但我们不行,我们的身后有位默默的清道夫——GC,垃圾回收机制,又称高潮兄。每当我们把内存占用推向一个又一个的高峰的时候,GC同志总是默默的回收那些没有被引用的对象。。。这是一件很伟大的事情,它让我们不用去考虑如何处理内存中的垃圾。然而在与非托管代码交互的时候,我们的蛋开始无休止的痛了。这种现象最常发生于传说中的”委托“中,刚我们也说了,委托的目的是用来传递方法,怎么传递呢?很明显,把方法的地址传过去呗。因此我们在对非托管代码设置回调的时候,实际是把我们的方法的内存地址传递给了非托管代码。非托管代码会通过这个地址,找到我们定义的方法,并且调用它。
我们知道,GC的运行是非常随机的,他的目的也很明确,收破烂咯。我们的委托,在把地址传递给非托管代码之后,就没有任何地方再引用它了,那么等待它的命运只有一个,被回收!这时候异常就产生了。下面我们来看看这个蛋疼的异常描述:
对“DemeterVideo.SDK!DemeterVideo.SDK.fVideoHandle::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。
这个异常产生的原因很简单,我偷懒了我在设置回调的时候直接写了下面这个代码
public void SetCallback() { HKVersionSDK.SetVideoHandleCallback(new fVideoHandle(callback)); }
当这个方法执行完毕后,这个委托就会面临一个很尴尬的问题 生命周期完毕,等待被回收。
那么这个问题怎么解决呢?全局变量。
设置一个与类型生命周期相同的全局变量,这样在这个类型生命周期走完最后一程之前,这个委托会坚强的活下来……
public class Sample { fVideoHanle videoCallback; publick void SetCallback() { callback = new fVideoHandle(OnStartPlaying); HKVersionSDK.SetVideoHandleCallback(Callback); } protected void Callback(...) { } }
这个问题相信也会有很多人无奈的碰到。因为我们把地址给过去了,但是处于当前地址的方法被回收了的话,C++那边就无法找到位于该地址的方法了,因此会抛出异常,这就是原因。
大概总结性的东西也就这么多了,其实没啥技术含量,都是马虎所致,但是碰到了,确实很恶心,并且答案还很不好找……