您的位置:首页 > 其它

P/Invoke (平台调用)

2015-12-09 17:21 549 查看
说到在 C#
中通过 P/Invoke
调用Win32 DLL,的确有很多地方让人值得去好好探索一下,如果你对这个主题尚不熟悉,请参考微软官方站点的入门性介绍《
C# 中通过 P/Invoke
调用Win32 DLL
》。记得我以前曾写过一篇blog《How
to call c++ exported method and classes in c#
》(另外一个较详细的版本)对使用C#调用win32
DLL中的函数或者C++ DLL中的类,进行了一个小的总结。最近的blog中,还有一个小的topic就是《很有趣的一个小外挂》,给出了一个简单的例子来说明如何通过调用系统的API函数进行某些操作的方法。今天blog中要说到的主题是有关P/Invoke中另外一个非常值得我们关注但同时也是比较有难度的一个问题,就是在使用DllImport函数时,如何正确地转写同Win32
DLL中等同的函数,其中包括了参数以及返回值数据类型的对应,内存对齐等问题。算是对今天工作的一个小的总结吧,水平有限,错误在所难免,希望大家一起来帮我批评分析改正。

Win32函数所使用的数据类型非常让人眩晕(至少对我来说,我记不太住很多J),比如REGSAM,DWORD(这个还好点),让一开始接触Win32函数声明时的人容易犯晕糊,我也不例外,后来陆续看了一些东西,这才算些许明白了一点。对于如何将Win32中所使用的数据类型和C#中的数据类型比较完整地一一对应起来,有篇blog《Win32类型和.net类型的对应表》,给出了一份比较简单的对应表,如果你要对应的数据类型不够复杂的话,看完他就应该能够搞定了。MSDN里面也有一个常用Win32数据类型与.NET平台数据类型的对应表。摘录如下:

Figure 2
Non-Pointer Data Types
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
†In Win32 this type is an integer with a specially assigned meaning; in contrast, the CLR provides a specific type devoted to this meaning.
对于简单类型的对应,已经差不多就可以解决掉了。但是如果要将如下两个函数(从海量智能分词研究版中提供的接口函数中选择出来的,因为他提供了文档说明
J
适合做例子
)转成C#中的版本,该如何操作呢?这就需要涉及到数据封送处理了。数据封送处理是
P/Invoke 具有挑战性的方面。当在托管和非托管代码之间传递数据时,就需要用到Marshal这个很有用的东西了。

Function 1:
SHLSegWord*
HLGetFileKeyAt(HANDLE hHandle, int iIndex);

Function 2:
bool HLGetFingerM (HANDLE hHandle, LPBYTE &rpData, DWORD &rdwLen);

struct SHLSegWord
{

char *s_szWord;

DWORD s_dwPOS;

float s_fWeight ;

SHLSegWord()

{

Reset();

};

void Reset()

{

s_szWord = NULL ;

s_dwPOS = 0 ;

s_fWeight = 0;

};
};
对于Function 1,一开始,可能一看到他的返回值是一个struct,就会自然地想到使用C#写一个和SHLSegWord一摸一样的结构体,但是这样是不行的,因为Function
1
返回的是一个SHLSegWord*,是一个指向结构体的指针,因此就不能使用C#中为值类型的struct来接收这个函数的返回值,是错误的。而C#中的class却是引用类型的,因此使用class是可行的,于是我就写了一个对应的:

namespace PInvoke.By.Jizhou
{

[StructLayout(LayoutKind.Sequential, CharSet =
CharSet.Ansi)] //这个attribute是必须的,不然编译器会改变字段的顺序和大小,就没有办法做到内存对齐了

public class
SHLSegWord

{

public string word = "";
// StringBuilder ??

public int POS = 0;

public float weight = 0;

}
}

接下来,用Marshal.SizeOf得到其大小,进行最初步的测试,看看它和在C/C++程序中用sizeof得到的大小是否相等,测试结果显示,相等,于是我就继续开始往下做,把Function
1
写成:

[DllImport("HLSSplit.dll")]

public
static extern
SHLSegWord HLGetWordAt(IntPtr hHandle,
int nIndex);

编译,通过,OK!太爽了,没想到这么顺利,于是就开始测试。可是到函数一执行,就让我大跌眼镜。一下子崩出来如下exception:“尝试读取或写入受保护的内存。这通常指示其他内存已损坏”,更让人郁闷的是,仔细查看后,发现循环调用这个函数时,居然是在index=5时出错,于是加上try
catch,嘿,居然index=6,7,8,9就可以通过,但是index=10又不行,之后一直到24却又可以。观察了一下,出错的index之间没有任何联系,也找不出任何规律,郁闷ing。于是我将SHLSegWord
结构体中的public string word = "";
修改承了public
StringBuilder word = new
StringBuilder(),可是问题依然存在。实在有点太诡异了,于是请教我的好朋友夏桅(CSDN上的suma),他是P/Invoke方面的牛人。他建议让我先把public
string word = ""; 改成public
IntPtr word = new
IntPtr(); 原因在于非内嵌的string不能放在传出参数里面,毕竟传出参数和传入参数是不同的,传出的话需要预先分配缓冲区,string是固定了值的类型,所以一般用stringbuilder。但是这个又是在结构体里面的string,稍微复杂一点,所以使用IntPtr应该是最好的方法了,因为IntPtr类型可以由支持指针的语言使用,并可作为在支持与不支持指针的语言间引用数据的一种通用方式。修改过后,错误仍然存在。仔细看看,发现原函数也返回的是一个指针,于是就觉得是不是应该也将public
static extern
SHLSegWord HLGetWordAt(IntPtr hHandle,
int nIndex);的返回类型,也修改成IntPtr,然后再使用Marshal.PtrToStructure()来接收数据,应该就比较安全了。果然,修改后,问题立马消失,至此,搞定。下面是较完整的示例代码:

1.SHLSegWord定义
namespace PInvoke.By.Jizhou
{
[StructLayout(LayoutKind.Sequential, CharSet =
CharSet.Ansi)]
public
class SHLSegWord

{

public IntPtr
word = new IntPtr();

public int POS = 0;

public float weight = 0;

}
}
2.HLGetWordAt函数定义
[DllImport("HLSSplit.dll")]
public
static extern
IntPtr HLGetWordAt(IntPtr hHandle,
int nIndex);

3.调用代码:

IntPtr
wordPtr =
HLSegFunc.HLGetWordAt(hHandle, i);
SHLSegWord wordStruct = (SHLSegWord)Marshal.PtrToStructure(wordPtr,
typeof(SHLSegWord));

string word =
Marshal.PtrToStringAnsi(wordStruct.word);

至此,Function 1的示例叙述完毕,下面看看Function
2:


bool HLGetFingerM (HANDLE hHandle, LPBYTE &rpData, DWORD
&rdwLen);

这个家伙同样长的比较怪异,第一个参数比较好办,使用IntPtr即可,第三个参数,转化为int,然后使用C#的out修饰符即可。可是第二个参数是指向指针的指针,有点难办了,再看看海量给出来的demo的C++代码,rpData居然是一个数组,这下给接收数据带来了不小的麻烦,我尝试将他转成

bool HLGetFingerM (IntPtr
hHandle, out byte[]
rpData,
out
int rdwLen);

但是结果不正确。几经try,都得不到正确的结果,无奈中又想起了IntPtr这个救苦救难的兄弟,换成IntPtr一try,唉,还真好了。下面给出实现代码:

1.
函数定义:

bool HLGetFingerM (IntPtr
hHandle, out
IntPtr
rpData,
out
int rdwLen);

2.
调用代码示例

IntPtr data =
new IntPtr();
int dataLen = 0;
bool successOrNot =
HLSegFunc.HLGetFingerM(hHandle,
out data, out dataLen);
if (successOrNot)//获得语义指纹
{

for (int j = 0; j < dataLen; j++)

{

IntPtr dataSnippet =
Marshal.ReadIntPtr(data, j);

string strU = Marshal.ReadByte(data, j).ToString("x2");

}

}

关于这个主题,可以参考以下资料:Marshaling
Data with Platform Invoke
Platform Invoke
Data Types
An Introduction to P/Invoke and Marshaling
on the Microsoft .NET Compact Framework


至此,全部完毕,总一个小的总结:

1.
在使用DllImport将Win32函数导入成C#对应的函数时,一定要注意Win32函数中的参数是传入还是传出,是值类型还是引用类型;

2.
对于C/C++中的复杂类型如Struct,转成C#对应的数据结构时,最好使用C#的Class而不是值类型的Struct;

3.
对于C/C++中的指针类型,最好使用IntPtr类型来进行数据的封送处理;

4.
.NET的字符串在内存中总是以Unicode方式编码的,但对于API函数需要string的编码,有时取决于Windows版本,有时需要显式设定,有时又需要显式转换。

5. 使用与基础
API 函数类型不同但与之兼容的 CLR 类型是 P/Invoke 较难使用的一个方面,这就需要多多实践,积累经验。

6.
上面费劲搞了这么多,就源于我没有DLL的源代码,如果你有C/C++
DLL的源代码,那么问题就简单多了。这里有解决方法:How
do I mix C# and C++ code in a single assembly?
引用其方法如下:

How do I mix C# and C++ code in a single assembly?

If your C++ code is not compiled with /clr:safe (i.e. it is compiled with /clr or /clr:pure), do the following:

1) compile your C++ code into .obj files

2) compile your C# code into a .netmodule, using /AddModule to reference the C++ .obj files

3) link the C# netmodule directly with the C++ object files using the C++ linker to create a mixed language assembly

If your C++ code is compiled with /clr:safe, build your C++ code as a .netmodule.
You can use it just like you would use a .netmodule from any other language.

This applies to .Net framework beta2+.

6.
如果你对P/Invoke感兴趣的话,这本书上面有提到一些,可以参考一下。.Net
Compact Framework Programming with C#
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: