您的位置:首页 > 编程语言 > C#

CSharp Tips:调用Win32 API如何处理指针类型的参数 (转载)

2010-04-29 14:33 836 查看
CSharp Tips:调用Win32 API如何处理指针类型的参数

[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpFileName);

但是如果是传出类型的字符串参数,简单的这么写就不行了。我的理解是String变成LPSTR,是DotNET Framework的交互接口帮我们做了一次转换,创建了一个字符数组,将我们提供的String复制了一次,再传递给API,并非简单的指针传递,所以当我们要求在我们设定的一个地址区域去写数据时,就不能够直接申明为String,而应该是Byte或者Char数组,可以参考下面的例子:
函数声明:
[align=left][/align]

[DllImport("user32",CharSet=CharSet.Ansi)]
public static extern Int32 GetClassName(IntPtr hwnd, Byte[] lpClassName, Int32 nMaxCount);

[align=left] 调用事例:[/align]

String sClassName = null;
Byte[] abClassName = null;
Int32 dwRet = 0;

abClassName = new Byte[100];
dwRet = GetClassName(this.Handle, abClassName, 100);
sClassName = System.Text.ASCIIEncoding.ASCII.GetString(abClassName,0,dwRet);
MessageBox.Show(sClassName);

还需要注意一点的就是Ansi还是Unicode的字符集了,申明的是什么就用什么转换。

2.2、 句柄—Handle
句柄严格意义上来说不能归在指针这一类,句柄是本宏定义掩盖了的一种数据结构,不过行为上和指针有些类似。最常见的有窗口句柄、Socket句柄还有内核对象的句柄等。总之H开头的一些定义基本都是句柄。
对于句柄来说我们通常无法直接访问句柄所代表的那个数据结构,只要记录句柄值就可以了,而且我们并不关心句柄这个值的内容,只要他有效就行了,所以句柄最容易处理。一般Win32下,句柄就是一个32位的整型,所以用Int32/UInt32或者IntPtr申明即可。还是上面那个例子,HMODULE就是一个句柄。

2.3、 基本类型的指针
两种情况下会出现基本类型的指针:一种是基本类型的地址,表示返回类型的参数;一种是表示传递一个基本类型的数组,这两种情况需要分别对待。
返回类型,C#中有专门的修饰符ref,表示参数传递按地址传送。缺省情况下参数都是按值传递的,如果希望按照地址传递,只要在参数前添加ref的修饰符即可。例如:

[DllImport("kernel32.dll")]
public extern static Int32 ReadFile(IntPtr hFile, Byte[] buffer,Int32 nNumberOfBytesToRead, ref Int32 lpNumberOfBytesRead, ref OVERLAPPED lpOverlapped);

对于C/C++中数组参数,就是一块连续内存的首地址。在C#中的数组默认都是从Array派生的类,虽然结构复杂了,但是内存布局应该是相同的,所以只要把参数定义为基本类型的数组就可以了。例如:

[DllImport("gdi32")]
public static extern Int32 GetCharWidth(HDC hdc, Int32 wFirstChar, Int32 wLastChar, int32[] lpBuffer);

2.4、 结构
说到结构,先要解释一下C#中数据类型的分类。C#中的数据类型一般有两种,一种是值类型,就是Byte、Int32之流,出于反射的需要,值类型都是从ValueType派生而得;一种是引用类型,从Object派生出来的类都是引用类型。所谓值类型,就是赋值和传递了传的是数据本身,引用类型传递的是数据所对应实例的引用,C#中结构(以struct定义的)是值类型的,类(以class定义的)是引用类型的。
实际调用API时,API参数如果是一个自定义结构指针的话,通常把数据结构定义为struct,在申明时函数接口时用ref修饰。例如Guid就是DotNET类库中内建的一个结构,具体用法如下:

///<summary>
///原形:HRESULT WINAPI GetDeviceID(LPCGUID pGuidSrc, LPGUID pGuidDest);
///</summary>
///<param name="pGuidSrc"></param>
///<param name="pGuidDest"></param>
///<returns></returns>
[DllImport("Dsound.dll")]
private static extern Int32 GetDeviceID(ref Guid pGuidSrc, ref Guid pGuidDest);

如果自定义结构的话,结构在内存中占据的字节数务必要匹配,当结构中包含数组的时候需要用MarshalAsAttribute属性进行修饰,设定数组长度。具体可以参考下面的例子:

public struct PAINTSTRUCT

[DllImport("user32")]
public static extern IntPtr BeginPaint(IntPtr hwnd, ref PAINTSTRUCT lpPaint);

2.5、 结构数组
结构数组就比较复杂了,就我个人的经验,不同厂商提供的API,实现不同,需要采用的处理方式也不相同的。
一般情况下,参照基本类型数组的调用方式即可,例如:

///<summary>
///原形:
/// typedef struct tagACCEL {
/// BYTE fVirt;
/// WORD key;
/// WORD cmd;
/// } ACCEL, *LPACCEL;
///</summary>
public struct ACCEL
{
public Byte fVirt;
public UInt16 key;
public UInt16 cmd;
}
///<summary>
///原形:int CopyAcceleratorTable(HACCEL hAccelSrc,LPACCEL lpAccelDst,int cAccelEntries);
///</summary>
///<returns></returns>
[DllImport("user32")]
public static extern Int32 CopyAcceleratorTable(IntPtr hAccelSrc, ACCEL[] lpAccelDst, Int32 cAccelEntries);

但是也有特殊情况,对些厂商提供的API中,不知是否和内存复制的方式有关,类似的函数,如果采用上面相同的定义方法调用的话,调用正确,但是应该返回的数据没有被改写。这个时候就需要另一种方法来解决了。
众所周知,在逻辑上结构是一段连续的内存,数组也是一段连续内存,我们可以从堆中直接申请一段内存,调用API,然后将返回的数据再转换成结构即可。具体可以参看下面的例子。
结构定义以及API声明:

[StructLayout(LayoutKind.Sequential, Pack = 8)]
private struct CmBoxInfo
{
public static CmBoxInfo Empty = new CmBoxInfo();

public byte MajorVersion;
public byte MinorVersion;
public ushort BoxMask;
public uint SerialNumber;
public ushort BoxKeyId;
public ushort UserKeyId;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = CM_PUBLIC_KEY_LEN)] public byte[] BoxPublicKey;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = CM_PUBLIC_KEY_LEN)] public byte[] SerialPublicKey;
public uint Reserve;

public void Init()
{
BoxPublicKey = new byte[CM_PUBLIC_KEY_LEN];
Debug.Assert(BoxPublicKey != null);
SerialPublicKey = new byte[CM_PUBLIC_KEY_LEN];
Debug.Assert(SerialPublicKey != null);
}
}
///<summary>
///原型:int CMAPIENTRY CmGetBoxes(HCMSysEntry hcmse, unsigned long idPort, CMBOXINFO *pcmBoxInfo, unsigned int cbBoxInfo)
///</summary>
[DllImport("xyz.dll")]
private static extern Int32 CmGetBoxes(IntPtr hcmse, CmGetBoxesOption idPort,IntPtr pcmBoxInfo, Int32 cbBoxInfo);

调用示例

IntPtr hcmBoxes = IntPtr.Zero;
CmAccess cma = new CmAccess();
CmBoxInfo[] aBoxList = null;
Int32 dwBoxNum = 0, dwLoop = 0,dwBoxInfoSize = 0;
IntPtr pBoxInfo = IntPtr.Zero;

dwBoxNum = m_pCmGetBoxes(hcmBoxes, CmGetBoxesOption.AllPorts, IntPtr.Zero, 0);
if (dwBoxNum > 0)
{
aBoxList = new CmBoxInfo[dwBoxNum];
if (aBoxList != null)
{
dwBoxInfoSize = Marshal.SizeOf(aBoxList[0]);
pBoxInfo = Marshal.AllocHGlobal(dwBoxInfoSize * dwBoxNum);
if (pBoxInfo != IntPtr.Zero)
{
dwBoxNum = m_pCmGetBoxes(hcmBoxes, CmGetBoxesOption.AllPorts, pBoxInfo, dwBoxNum);
for (dwLoop = 0; dwLoop < dwBoxNum; dwLoop++)
{
aBoxList[dwLoop] = (CmBoxInfo)Marshal.PtrToStructure((IntPtr)((UInt32)pBoxInfo + dwBoxInfoSize * dwLoop), CmBoxInfo.Empty.GetType());
}
Marshal.FreeHGlobal(pBoxInfo);
pBoxInfo = IntPtr.Zero;
}
else
{
aBoxList = null;
}
}
}

最后提一句,Marshal类非常有用,其中包括了大量内存申请、复制和类型转换的函数,灵活运用的话,基本上可以避免unsafe code。

2.6、 函数指针(回调函数)
C#中采用委托(delegate)和函数指针等同的功能,当API函数的参数为回调函数时,我们通常使用委托来替代。与C和C++ 中的函数指针相比,委托实际上是具体一个Delegate派生类的实例,它还包括了对参数和返回值,类型安全的检查。
先看一下下面的例子:

///<summary>
///原形:typedef BOOL (CALLBACK *LPDSENUMCALLBACKA)(LPGUID, LPCSTR, LPCSTR, LPVOID);
///</summary>
public delegate Boolean LPDSENUMCALLBACK(IntPtr guid, String sDesc, String sDevName, ref Int32 dwFlag);
///<summary>
///原形:HRESULT WINAPI DirectSoundCaptureEnumerateA(LPDSENUMCALLBACKA pDSEnumCallback, LPVOID pContext);
///</summary>
[DllImport("Dsound.dll")]
public static extern Int32 DirectSoundCaptureEnumerate(LPDSENUMCALLBACK pDSEnumCallBack, ref Int32 dwFlag);

具体调用方法如下:

dwRet = DirectSoundEnumerate(new LPDSENUMCALLBACK(DSoundEnumCallback),ref dwFlag);

这里需要特别注意的就是委托实际上是一个实例,和普通的类实例一样,是被DotNET Framework垃圾收集机制所管理,有生存周期的。上文例子的定义方式其实函数级别的局部变量,当函数结束时,将被释放,如果回调仍然在继续的话,就会产生诸如非法访问的错误。所以在使用回调函数的时候一定要比较清楚的了解,回调的作用周期是多大,如果回调是全局的,那么定义一个全局的委托变量作为参数。

2.7、 表示多种类型的指针—LPVOID以及其它
指针是C/C++的精髓所在,一个void能够应付所有的问题,我们遇到最多的可能就是LPVOID这样的参数。LPVOID最常用的有两种情况,一种就是表示一个内存块,另一种情况可能是根据其它参数的定义指向不同的数据结构。
第一种情况很好处理,如果是一个内存块,我们可以他当作一个Byte数组就可以了,例如:

[DllImport("kernel32.dll")]
public extern static Int32 ReadFile(IntPtr hFile, Byte[] buffer,Int32 nNumberOfBytesToRead, ref Int32 lpNumberOfBytesRead, ref OVERLAPPED lpOverlapped);

第二种情况比较复杂,C#中类型转换是有限制的,一个Int32是没法直接转换成为Point的,这个时候之能够根据不同的参数类型定义不同的重载函数了。例如GetProcAddress函数的lpProcName既可以是一个字符串表示函数名,又可以是一个高字为0的Int32类型,表示函数的序号,我们可以这样分别定义:

[DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
private extern static IntPtr GetProcAddress(IntPtr hModule, String sFuncName);
[DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
private extern static IntPtr GetProcAddressByIndex(IntPtr hModule, Int32 dwIndex);

在这里总结了调用API时有关指针的一些常见问题,你会发现大多数情况下C#依靠自身的能力就能解决问题,希望对大家有帮助。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: