`
weizhai12
  • 浏览: 144811 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

如何在C#中使用Win32和其他库之三

 
阅读更多

具有内嵌字符数组的结构

某些函数接受具有内嵌字符数组的结构。例如,GetTimeZoneInformation() 函数接受指向以下结构的指针:

typedef struct _TIME_ZONE_INFORMATION {     LONG       Bias;     WCHAR      StandardName[ 32 ];     SYSTEMTIME StandardDate;     LONG       StandardBias;     WCHAR      DaylightName[ 32 ];     SYSTEMTIME DaylightDate;     LONG       DaylightBias; } TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

在 C# 中使用它需要有两种结构。一种是 SYSTEMTIME,它的设置很简单:

   struct SystemTime   {      public short wYear;      public short wMonth;      public short wDayOfWeek;      public short wDay;      public short wHour;      public short wMinute;      public short wSecond;      public short wMilliseconds;   }

这里没有什么特别之处;另一种是 TimeZoneInformation,它的定义要复杂一些:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]struct TimeZoneInformation{    public int bias;   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]   public string standardName;   SystemTime standardDate;   public int standardBias;   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]   public string daylightName;   SystemTime daylightDate;   public int daylightBias;}

此定义有两个重要的细节。第一个是 MarshalAs 属性:

   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

查看 ByValTStr 的文档,我们发现该属性用于内嵌的字符数组;另一个是 SizeConst,它用于设置数组的大小。

我在第一次编写这段代码时,遇到了执行引擎错误。通常这意味着部分互操作覆盖了某些内存,表明结构的大小存在错误。我使用 Marshal.SizeOf() 来获取所使用的封送拆收器的大小,结果是 108 字节。我进一步进行了调查,很快回忆起用于互操作的默认字符类型是 Ansi 或单字节。而函数定义中的字符类型为 WCHAR,是双字节,因此导致了这一问题。

我通过添加 StructLayout 属性进行了更正。结构在默认情况下按顺序布局,这意味着所有字段都将以它们列出的顺序排列。CharSet 的值被设置为 Unicode,以便始终使用正确的字符类型。

经过这样处理后,该函数一切正常。您可能想知道我为什么不在此函数中使用 CharSet.Auto。这是因为,它也没有 A W 变体,而始终使用 Unicode 字符串,因此我采用了上述方法编码。

具有回调的函数

当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。

在 C# 中没有函数指针,而是使用“委托”,在调用 Win32 函数时使用委托来代替函数指针。

EnumDesktops() 函数就是这类函数的一个示例:

BOOL EnumDesktops(  HWINSTA hwinsta,            // 窗口实例的句柄  DESKTOPENUMPROC lpEnumFunc, // 回调函数  LPARAM lParam               // 用于回调函数的值);

HWINSTA 类型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC 所需的工作要多一些。下面是 MSDN 中的定义:

BOOL CALLBACK EnumDesktopProc(  LPTSTR lpszDesktop,  // 桌面名称  LPARAM lParam        // 用户定义的值);

我们可以将它转换为以下委托:

delegate bool EnumDesktopProc(   [MarshalAs(UnmanagedType.LPTStr)]   string desktopName,   int lParam);

完成该定义后,我们可以为 EnumDesktops() 编写以下定义:

[DllImport("user32.dll", CharSet = CharSet.Auto)]static extern bool EnumDesktops(   IntPtr windowStation,   EnumDesktopProc callback,   int lParam);

这样该函数就可以正常运行了。

在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给非托管函数。但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,因此它假定函数指针只需在调用该函数时有效即可。

结果是如果您调用诸如 SetConsoleCtrlHandler() 这样的函数,其中的函数指针将被保存以便将来使用,您就需要确保在您的代码中引用委托。如果不这样做,函数可能表面上能执行,但在将来的内存回收处理中会删除委托,并且会出现错误。

其他高级函数

迄今为止我列出的示例都比较简单,但是还有很多更复杂的 Win32 函数。下面是一个示例:

DWORD SetEntriesInAcl(  ULONG cCountOfExplicitEntries,           // 项数  PEXPLICIT_ACCESS pListOfExplicitEntries, // 缓冲区  PACL OldAcl,                             // 原始 ACL  PACL *NewAcl                             // 新 ACL);

前两个参数的处理比较简单:ulong 很简单,并且可以使用 UnmanagedType.LPArray 来封送缓冲区。

但第三和第四个参数有一些问题。问题在于定义 ACL 的方式。ACL 结构仅定义了 ACL 标头,而缓冲区的其余部分由 ACE 组成。ACE 可以具有多种不同类型,并且这些不同类型的 ACE 的长度也不同。

如果您愿意为所有缓冲区分配空间,并且愿意使用不太安全的代码,则可以用 C# 进行处理。但工作量很大,并且程序非常难调试。而使用 C++ 处理此 API 就容易得多。

属性的其他选项

DLLImport StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。下面列出了所有这些选项:

DLLImport

CallingConvention

您可以用它来告诉封送拆收器,函数使用了哪些调用约定。您可以将它设置为您的函数的调用约定。通常,如果此设置错误,代码将不能执行。但是,如果您的函数是 Cdecl 函数,并且使用 StdCall(默认)来调用该函数,那么函数能够执行,但函数参数不会从堆栈中删除,这会导致堆栈被填满。

CharSet

控制调用 A 变体还是调用 W 变体。

EntryPoint

此属性用于设置封送拆收器在 DLL 中查找的名称。设置此属性后,您可以将 C# 函数重新命名为任何名称。

ExactSpelling

将此属性设置为 true,封送拆收器将关闭 AW 的查找特性。

PreserveSig

COM 互操作使得具有最终输出参数的函数看起来是由它返回的该值。此属性用于关闭这一特性。

SetLastError

确保调用 Win32 API SetLastError(),以便您找出发生的错误。

StructLayout

LayoutKind

结构在默认情况下按顺序布局,并且在多数情况下都适用。如果需要完全控制结构成员所放置的位置,可以使用 LayoutKind.Explicit,然后为每个结构成员添加 FieldOffset 属性。当您需要创建 union 时,通常需要这样做。

CharSet

控制 ByValTStr 成员的默认字符类型。

Pack

设置结构的压缩大小。它控制结构的排列方式。如果 C 结构采用了其他压缩方式,您可能需要设置此属性。

Size

设置结构大小。不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。

从不同位置加载

您无法指定希望 DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一目的。

DllImport 调用 LoadLibrary() 来完成它的工作。如果进程中已经加载了特定的 DLL,那么即使指定的加载路径不同,LoadLibrary() 也会成功。

这意味着如果直接调用 LoadLibrary(),您就可以从任何位置加载 DLL,然后 DllImport LoadLibrary() 将使用该 DLL。

由于这种行为,我们可以提前调用 LoadLibrary(),从而将您的调用指向其他 DLL。如果您在编写库,可以通过调用 GetModuleHandle() 来防止出现这种情况,以确保在首次调用 P/Invoke 之前没有加载该库。

P/Invoke 疑难解答

如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:

  • long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。
  • 字符串类型设置不正确。

具有内嵌字符数组的结构

某些函数接受具有内嵌字符数组的结构。例如,GetTimeZoneInformation() 函数接受指向以下结构的指针:

typedef struct _TIME_ZONE_INFORMATION {     LONG       Bias;     WCHAR      StandardName[ 32 ];     SYSTEMTIME StandardDate;     LONG       StandardBias;     WCHAR      DaylightName[ 32 ];     SYSTEMTIME DaylightDate;     LONG       DaylightBias; } TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

在 C# 中使用它需要有两种结构。一种是 SYSTEMTIME,它的设置很简单:

   struct SystemTime   {      public short wYear;      public short wMonth;      public short wDayOfWeek;      public short wDay;      public short wHour;      public short wMinute;      public short wSecond;      public short wMilliseconds;   }

这里没有什么特别之处;另一种是 TimeZoneInformation,它的定义要复杂一些:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]struct TimeZoneInformation{    public int bias;   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]   public string standardName;   SystemTime standardDate;   public int standardBias;   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]   public string daylightName;   SystemTime daylightDate;   public int daylightBias;}

此定义有两个重要的细节。第一个是 MarshalAs 属性:

   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

查看 ByValTStr 的文档,我们发现该属性用于内嵌的字符数组;另一个是 SizeConst,它用于设置数组的大小。

我在第一次编写这段代码时,遇到了执行引擎错误。通常这意味着部分互操作覆盖了某些内存,表明结构的大小存在错误。我使用 Marshal.SizeOf() 来获取所使用的封送拆收器的大小,结果是 108 字节。我进一步进行了调查,很快回忆起用于互操作的默认字符类型是 Ansi 或单字节。而函数定义中的字符类型为 WCHAR,是双字节,因此导致了这一问题。

我通过添加 StructLayout 属性进行了更正。结构在默认情况下按顺序布局,这意味着所有字段都将以它们列出的顺序排列。CharSet 的值被设置为 Unicode,以便始终使用正确的字符类型。

经过这样处理后,该函数一切正常。您可能想知道我为什么不在此函数中使用 CharSet.Auto。这是因为,它也没有 A W 变体,而始终使用 Unicode 字符串,因此我采用了上述方法编码。

具有回调的函数

当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。

在 C# 中没有函数指针,而是使用“委托”,在调用 Win32 函数时使用委托来代替函数指针。

EnumDesktops() 函数就是这类函数的一个示例:

BOOL EnumDesktops(  HWINSTA hwinsta,            // 窗口实例的句柄  DESKTOPENUMPROC lpEnumFunc, // 回调函数  LPARAM lParam               // 用于回调函数的值);

HWINSTA 类型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC 所需的工作要多一些。下面是 MSDN 中的定义:

BOOL CALLBACK EnumDesktopProc(  LPTSTR lpszDesktop,  // 桌面名称  LPARAM lParam        // 用户定义的值);

我们可以将它转换为以下委托:

delegate bool EnumDesktopProc(   [MarshalAs(UnmanagedType.LPTStr)]   string desktopName,   int lParam);

完成该定义后,我们可以为 EnumDesktops() 编写以下定义:

[DllImport("user32.dll", CharSet = CharSet.Auto)]static extern bool EnumDesktops(   IntPtr windowStation,   EnumDesktopProc callback,   int lParam);

这样该函数就可以正常运行了。

在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给非托管函数。但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,因此它假定函数指针只需在调用该函数时有效即可。

结果是如果您调用诸如 SetConsoleCtrlHandler() 这样的函数,其中的函数指针将被保存以便将来使用,您就需要确保在您的代码中引用委托。如果不这样做,函数可能表面上能执行,但在将来的内存回收处理中会删除委托,并且会出现错误。

其他高级函数

迄今为止我列出的示例都比较简单,但是还有很多更复杂的 Win32 函数。下面是一个示例:

DWORD SetEntriesInAcl(  ULONG cCountOfExplicitEntries,           // 项数  PEXPLICIT_ACCESS pListOfExplicitEntries, // 缓冲区  PACL OldAcl,                             // 原始 ACL  PACL *NewAcl                             // 新 ACL);

前两个参数的处理比较简单:ulong 很简单,并且可以使用 UnmanagedType.LPArray 来封送缓冲区。

但第三和第四个参数有一些问题。问题在于定义 ACL 的方式。ACL 结构仅定义了 ACL 标头,而缓冲区的其余部分由 ACE 组成。ACE 可以具有多种不同类型,并且这些不同类型的 ACE 的长度也不同。

如果您愿意为所有缓冲区分配空间,并且愿意使用不太安全的代码,则可以用 C# 进行处理。但工作量很大,并且程序非常难调试。而使用 C++ 处理此 API 就容易得多。

属性的其他选项

DLLImport StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。下面列出了所有这些选项:

DLLImport

CallingConvention

您可以用它来告诉封送拆收器,函数使用了哪些调用约定。您可以将它设置为您的函数的调用约定。通常,如果此设置错误,代码将不能执行。但是,如果您的函数是 Cdecl 函数,并且使用 StdCall(默认)来调用该函数,那么函数能够执行,但函数参数不会从堆栈中删除,这会导致堆栈被填满。

CharSet

控制调用 A 变体还是调用 W 变体。

EntryPoint

此属性用于设置封送拆收器在 DLL 中查找的名称。设置此属性后,您可以将 C# 函数重新命名为任何名称。

ExactSpelling

将此属性设置为 true,封送拆收器将关闭 AW 的查找特性。

PreserveSig

COM 互操作使得具有最终输出参数的函数看起来是由它返回的该值。此属性用于关闭这一特性。

SetLastError

确保调用 Win32 API SetLastError(),以便您找出发生的错误。

StructLayout

LayoutKind

结构在默认情况下按顺序布局,并且在多数情况下都适用。如果需要完全控制结构成员所放置的位置,可以使用 LayoutKind.Explicit,然后为每个结构成员添加 FieldOffset 属性。当您需要创建 union 时,通常需要这样做。

CharSet

控制 ByValTStr 成员的默认字符类型。

Pack

设置结构的压缩大小。它控制结构的排列方式。如果 C 结构采用了其他压缩方式,您可能需要设置此属性。

Size

设置结构大小。不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。

从不同位置加载

您无法指定希望 DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一目的。

DllImport 调用 LoadLibrary() 来完成它的工作。如果进程中已经加载了特定的 DLL,那么即使指定的加载路径不同,LoadLibrary() 也会成功。

这意味着如果直接调用 LoadLibrary(),您就可以从任何位置加载 DLL,然后 DllImport LoadLibrary() 将使用该 DLL。

由于这种行为,我们可以提前调用 LoadLibrary(),从而将您的调用指向其他 DLL。如果您在编写库,可以通过调用 GetModuleHandle() 来防止出现这种情况,以确保在首次调用 P/Invoke 之前没有加载该库。

P/Invoke 疑难解答

如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:

  • long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。
  • 字符串类型设置不正确。
分享到:
评论

相关推荐

    如何在c#中使用win32和其他库_c#应用

    如何在c#中使用win32和其他库_c#应用

    C#封装好的Win32API.rar

    这是一个c# 调用win32api封装好的库代码,几乎封装了常用win32api。

    C#调用vlc播放器所用到的库(libvlc)

    win-x86 C#调用vlc播放器所用到的库(libvlc) C#调用vlc播放器所用到的库(libvlc) C#调用vlc播放器所用到的库(libvlc) C#调用vlc播放器所用到的库(libvlc) C#调用vlc播放器所用到的库(libvlc) C#...

    . net c# USB库用于WinUSB, LibUsb-Win32和libusb-1.0

    . net c# USB库用于WinUSB, LibUsb-Win32和libusb-1.0。通过使用通用设备类,应用程序可以在所有操作系统和驱动程序中工作,无需修改。大量示例代码。

    在Windows中用C#开发USB的接口库

    完全的通用USB开发库,不需要要任何的驱动(Windows自带),本人开发USB上位机软件包时,发现都是非托管的C++调用库,C#调用库非常少。所以收集了这个,供用.NET方式开发USB的朋友使用。(也包含VC++调用案例)

    C#调用PC主板蜂鸣器小喇叭speaker发声beep(Win7_64位亲测可用)

    不是拷贝XP的beep.sys实现,使用的是InpOut库,32位及64位均可使用。 我自己加了音符频率表,内附一首生日歌,类似于20年前DOS开发一样,使用蜂鸣器播放音乐,其它歌曲请自己编写。 另附InpOut32的调用源代码,不...

    [精]C#实现Win8窗体(SkinForm Demo)

    项目名称:[精]C#实现Win8窗体(SkinForm Demo) 界面库版本号:7.5 最新版本 下载内容: (C#)Win8窗体Demo源码一份, 可引用至工具箱最新版dll一份 实现功能: 1.发光标题。 2.直角边框和阴影。 3.扁平化系统按钮。 4...

    C#开发opcserver含源码

    经常使用的函数在工程中已经定义并使用.WTOPCSVRDLL使用说明中文.doc中,介绍的是VB中WtOPCSvr的使用方法.大家可以把VB的转变成C#的.我的工程中,已经给大家做了示例. 所有的文件:源码,dll文件,说明文件,都包含在...

    支持win10的C#可用以太网收发库文件,可代替Sharppcap

    在C#下实现MAC层以太网收发功能,互联网上多使用sharppcap,但这个库在win10下用不了(win7正常),至少在我电脑上不行,找了好久,发现了pcapdotnet,和sharppcap差不多,但能支持win10,而且功能还要更强大些,给...

    C#与C++之间DLL文件的创建与调用DEMO演示

    C#与C++之间DLL文件的创建与调用用VC创建DLL动态连接库 用C#跨语言DLL调用

    USB 通讯 libusb-win32

    USB读写采集vc源码, 数字界面,VC++ 6.0 MFC,libusb-win32应用

    WinIO3.0库(Win32)

    WinIo库允许32位和64位Windows应用程序直接访问I/O端口和物理内存。 3.0版本提供了以下特性: 支持32位和64位平台(不包括Itanium)。 WinIo现在可以被多个应用程序同时使用。 新的c#样本。 这个版本中的Bug修复: 修正...

    Api精灵 for c#

    欢迎使用精灵 FOR c#>这是我用C#写的第一个 程序,希望大家能够喜欢!(需要.NET环境支持) 主要功能: 简单快速对C#中使用的API函数进行查询,包括 1。提供在C#中能够直接运行的代码。 2。该函数的中文注释。 ...

    C#全局钩子屏蔽键盘按键Demo

    C#全局钩子屏蔽键盘按键Demo 可屏蔽键盘按键,组合键,Alt+F4等,亲测可用

    C#版DLL远程线程注入源代码

    C#没有自动调用WIN32的一些API函数,我们可以手动添加内核库到我们自己的代码中,从而实现调用win32API函数。 此方法不仅仅用于远程线程注入,还可以用于从MFC转C#过程中不知道如何用C#实现功能,但有知道如何用MFC...

    C#如何在后台捕捉按键

    C#如何在后台捕捉按键 [此问题的推荐答案] API别忘了 using System.Runtime.InteropServices; [DllImport("user32.dll")] public static extern UInt32 RegisterHotKey(IntPtr hWnd, UInt32 id, UInt32 ...

    C# SVN客户端动态库

    C#版SVN客户端动态库 该压缩包中包含了三个版本,SharpSvn v1.6、1.7、1.8, 包含.Net2.0和.Net4.0、64位和32位的发布版本,具体使用哪个版本可以根据开发需要进行选择,

    C#版SVN客户端动态库 SharpSvn v1.8

    C#版SVN客户端动态库 SharpSvn v1.8. 包含.Net2.0和.Net4.0的发布版本。 SharpSvn is a binding of the Subversion Client API for .Net 2.0-4.0+ applications contained within a set of xcopy-deployable dll's.

    c#开发opcserver例子(内附源代码)

    经常使用的函数在工程中已经定义并使用.WTOPCSVRDLL使用说明中文.doc中,介绍的是VB中WtOPCSvr的使用方法.大家可以把VB的转变成C#的.我的工程中,已经给大家做了示例. 所有的文件:源码,dll文件,说明文件,都包含在...

Global site tag (gtag.js) - Google Analytics