CLR+互操作完全介绍.doc_第1页
CLR+互操作完全介绍.doc_第2页
CLR+互操作完全介绍.doc_第3页
CLR+互操作完全介绍.doc_第4页
CLR+互操作完全介绍.doc_第5页
已阅读5页,还剩5页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

CLR 完全介绍 托管代码与非托管代码之间的封送处理 Yi Zhang and Xiaoying Guo 目录 InAttribute 和 OutAttribute 关键字 Out 和 Ref 以及通过引用传递 返回值 StringBuilder 和封送处理 复制和固定 内存所有权 反向 P Invoke 和委托生存期 P Invoke Interop Assistant 尝试一下 让我们面对现实吧 这个世界并不完美 几乎很少有公司在完全用托管代码开发程序 除此之外仍存在很多需要您处理的旧式非托管代码 您怎样将托管 和非托管项目集成起来呢 在形式上是采用从托管应用程序调用非托管代码 还是从非托管代码应用程序调用托管代码 幸运的是 Microsoft NET Framework 互操作在托管和非托管代码之间开辟了一条通道 而封送处理则在该连接中扮演着非常重要的角色 因为它 允许在两者之间进行数据交换 请参见图图 1 有很多因素会影响 CLR 在非托管和托管领域之间封送数据的方式 包括诸如 MarshalAs StructLayout InAttribute 和 OutAttribute 等属性 以及 C 中 out 和 ref 之类的语言关键字 Figure 1 Bridging the Gap between Managed and Unmanaged Code 单击该图像获得较大视图 因为这些因素很多 所以它可能是进行正确封送的一大难题 因为这项工作要求了解很多有关非托管和托管代码的详细情况 在本专栏中 我们会介绍您 在日常工作中尝试进行封送处理时将遇到的一些基本却又容易混淆的主题 我们不会介绍自定义封送处理 封送处理复杂的结构或其他高级主题 但是如 果真正理解了这些基本的概念 您就为处理这些问题做好准备了 InAttribute 和和 OutAttribute 我们要讨论的第一个封送处理主题是关于 InAttribute 和 OutAttribute 的使用 这是位于 System Runtime InteropServices 命名空间中的两种属 性类型 在将这些属性应用到您的代码中时 C 和 Visual Basic 允许使用缩写形式 In 和 Out 但是为了避免混淆我们坚持使用全名 当应用于方法参数和返回值时 这些属性会控制封送处理的方向 因此它们又被称为方向属性 InAttribute 告知 CLR 在调用开始的时候将数据从调 用方封送到被调用方 OutAttribute 则告知 CLR 在返回的时候将数据从被调用方封送回调用方 调用方和被调用方都可以是非托管或托管代码 例 如 在 P Invoke 调用中 是托管代码在调用非托管代码 但是在反向 P Invoke 调用中 就可能是非托管代码通过函数指针调用托管代码 InAttribute 和 OutAttribute 有四种可能的使用组合 只用 InAttribute 只用 OutAttribute 同时使用 InAttribute OutAttribute 以 及两者都不用 如果没有指定任何一个属性 那就是要 CLR 自己确定方向属性 默认情况下通常是使用 InAttribute 但是 如果是 StringBuilder 类 则在没有指定任何一个属性的情况下 会同时使用 InAttribute 和 OutAttribute 有关详细信息 请参阅后面有关 StringBuilder 的部分 另外 使用 C 中的 out 和 ref 关键字可能会更改已应用的属性 如图图 2 所示 请注意 如果没有为参数指定关键字 就意味着它是默认的输入参 数 Figure 2 Out and Ref and Their Associated Attributes C 关键字关键字属性属性 未指定 InAttribute out OutAttribute ref InAttribute OutAttribute 请看一下图图 3 中的代码 其中有三个本机 C 函数 并且它们都对 arg 进行相同的更改 此外 请注意对字符串操作使用 strcpy 仅仅是为了便于 说明 生产代码应改用这些函数的安全版本 它们可在 中找到 Figure 3 Trying Out Directional Attributes 复制代码 MARSHALLIB API void stdcall Func In Attribute char arg printf Inside Func In Attribute arg s n arg strcpy arg New MARSHALLIB API void stdcall Func Out Attribute char arg printf Inside Func Out Attribute arg s n arg strcpy arg New MARSHALLIB API void stdcall Func InOut Attribute char arg printf Inside Func InOut Attribute arg s n arg strcpy arg New 唯一的不同是我们在 P Invoke 签名中使用方向属性调用它们的方式 如下面的 C 代码所示 复制代码 DllImport MarshalLib dll public static extern void Func In Attribute In char arg DllImport MarshalLib dll public static extern void Func Out Attribute Out char arg DllImport MarshalLib dll public static extern void Func InOut Attribute In Out char arg 如果您通过 P Invoke 从托管代码调用这些函数 并将 Old 作为字符数组传递给这些函数 就会获得以下输出 出于演示目的而有所缩减 复制代码 Before Func In Attribute arg Old Inside Func In Attribute arg Old After Func In Attribute arg Old Before Func Out Attribute arg Old Inside Func Out Attribute arg After Func Out Attribute arg New Before Func InOut Attribute arg Old Inside Func InOut Attribute arg Old After Func InOut Attribute arg New 让我们进一步看一下结果 在 Func In Attribute 中 传入了原始值 但是在 Func In Attribute 内部发生的更改并没有传播回来 在 Func Out Attribute 中 没有传入原始值 而 Func Out Attribute 内部发生的更改已传播回来了 在 Func InOut Attribute 中 传入了原始值 并且 Func Out Attribute 内部发生的更改也已传播回来 然而 只要您稍做修改 情况就完全不同了 这一次让我们修改一下本机函数以使用 Unicode 如下所示 复制代码 MARSHALLIB API void stdcall Func Out Attribute Unicode wchar t arg wprintf L Inside Func Out Attribute Unicode arg s n arg printf Inside Func Out Attribute Unicode strcpy arg New n wcscpy arg L New 在此我们声明了 C 函数 仅应用 OutAttribute 并将 CharSet 更改为 CharSet Unicode 复制代码 DllImport MarshalLib dll CharSet CharSet Unicode public static extern void Func Out Attribute Unicode Out char arg 以下是输出 复制代码 Before Func Out Attribute Unicode arg Old Inside Func Out Attribute Unicode arg Old After Func Out Attribute Unicode arg New 有趣的是 尽管没有 InAttribute 也还是传递了原始值 DllImportAttribute 会告知 CLR 封送 Unicode 而且由于 CLR 中的字符类型也是 Unicode 所以 CLR 发现了一个优化封送处理的机会 即固定字符数组然后直接传递该字符的地址 稍后您将看到有关复制和固定的详细介绍 然 而 这并不意味着您应依赖这种行为 相反 在不依赖 CLR 默认封送行为的时候 应始终使用正确的封送方向属性 这种默认行为的典型例子是使用 int 参数的情况 不必指定 InAttribute int arg 某些情况下 OutAttribute 将被忽略 例如 由于 OutAttribute int 没有任何意义 所以 CLR 便忽略这个 OutAttribute 同样 OutAttribute 字符串也是如此 因为字符串是固定不变的 接口定义 IDL 文件也具有 in 和 out 属性 它们可视为与 CLR 中的 InAttribute 和 OutAttribute 相同 关键字关键字 Out 和和 Ref 以及通过引用传递以及通过引用传递 之前 我们已经介绍了 C 的 out 和 ref 关键字可以被直接映射到 InAttribute 和 OutAttribute 事实上 out 和 ref 还可以改变作为 CLR 封送对象或封送目标的数据类型 将数据作为 out 或 ref 传递与通过引用传递相同 如果使用 ILDASM 来检查中间语言 IL 中对应的函数签名 您 会看到该类型旁边有一个 以上签名会替代 ref 的默认方向行为 将它变成仅使用 InAttribute 在此特定的情况下 如果您执行 P Invoke 那么指向 ComplexStructure 它是一个值类型 的指针会从 CLR 端传递到本机端 但是被调用方无法使任何改动对 pStructure 指针所指向的 ComplexStructure 可见 图图 6 列举了一些方向属性和关键字组合的其他示例 Figure 6 More Attributes and Keywords C 签名非托管 IDL 签名MSIL 签名 Out InAttribute out int arg编译器错误 CS0036 out 参数不能有 In 属性 不适用 OutAttribute out int arg out int arg out int 会变成非托管签名 复制代码 HRESULT GetString in int id out retval char pszString 如果 PreserveSigAttribute 被设为 true P Invoke 的默认值 此转换就不会发生 请注意 对于 COM 函数而言 PreserveSigAttribute 通常默认设为 false 不过有很多方法可以改变此设置 有关详细信息 请查看有关 TlbExp exe 和 TlbImp exe 的 MSDN 文档 StringBuilder 和封送处理和封送处理 CLR 封送拆收器具有内置的 StringBuilder 类型知识 并且处理它的方式与处理其他类型不同 默认情况下 StringBuilder 作为 InAttribute OutAttribute 传递 StringBuilder 很特别 因为具有 Capacity 属性 该属性可以在运行时确定必需缓冲区的大小 并且它可被动态地更改 因 此 在封送过程中 CLR 可以固定 StringBuilder 直接传递在 StringBuilder 中使用的内部缓冲区的地址 并允许适当的本机代码更改该缓冲区的内 容 为了充分利用 StringBuilder 您将需要遵循下列所有规则 1 不要通过引用传递 StringBuilder 使用 out 或 ref 否则 CLR 会认为该参数的签名是 wchar t 而不是 wchar t 并且它将无 法固定 StringBuilder 的内部缓冲区 性能会大大降低 2 当非托管代码使用 Unicode 时使用 StringBuilder 否则 CLR 将不得不复制该字符串 并将它在 Unicode 和 ANSI 之间转换 这样会 降低性能 通常情况下 您应将 StringBuilder 作为 Unicode 字符的 LPARRAY 或作为 LPWSTR 封送 3 始终提前指定 StringBuilder 的容量 并确保该容量对存放缓冲区而言足够大 在非托管代码端的最佳做法是接受字符串缓冲区的大小作为参 数 以避免缓冲区溢出 在 COM 中 您还可以使用 IDL 中的 size is 来指定大小 复制和固定复制和固定 当 CLR 执行数据封送时 它有两个选择 复制和固定 请参阅 默认情况下 CLR 会创建一个将在封送过程中使用的副本 例如 如果托管代码要将某个字符串作为 ANSI C String 传递到非托管代码 CLR 会复制 该字符串 将其转换成 ANSI 然后将该临时对象的指针传递到非托管代码 该复制过程可能会相当慢 并可能造成性能问题 在某些情况下 CLR 可通过将托管对象直接固定到垃圾收集器 GC 堆来优化封送处理 这样在调用过程中就无法重定位它 指向托管对象 或指向托 管对象内部某个位置 的指针将被直接传递到非托管代码 当满足下列所有条件之后就可以执行固定 第一 托管代码必须调用本机代码 而不是本机代码调用托管代码 第二 该类型必须可直接复制或者必须可 以在某些情况下变得可直接复制 第三 您不是通过引用传递 使用 out 或 ref 第四 调用方和被调用方位于同一线程上下文或单元中 第二条规则需要进一步说明一下 可直接复制类型是指在托管和非托管内存中具有共同表示方法的类型 因此 在进行封送处理时可直接复制类型不需要 进行转换 不可直接复制但能够变成可直接复制的类型的典型例子是字符类型 默认情况下 它不可直接复制 因为它可被映射到 Unicode 或 ANSI 然而 由于字符在 CLR 中始终是 Unicode 所以当指定了 DllImportAttribute CharSet Unicode 或 MarshalAsAttribute UnmanagedType LPWSTR 时 它会变成可直接复制 在下面的示例中 arg 可被固定在 PassUnicodeString 中 但是无 法固定在 PassAnsiString 中 复制代码 DllImport MarshalLib dll CharSet CharSet Unicode public static extern string PassUnicodeString string arg DllImport MarshalLib dll CharSet CharSet Ansi public static extern string PassAnsiString string arg 内存所有权内存所有权 在函数调用期间 函数可对它的参数进行两种类型的更改 引用更改或就地更改 引用更改涉及更改指针指向的位置 如果指针已指向一块已分配的内存 那么就需要首先释放内存 否则指向它的指针会丢失 就地更改涉及更改引用所指向位置的内存 进行哪一种更改取决于参数的类型以及 最重要的是 被调用方和调用方之间的约定 但是 由于 CLR 无法自动了解合约 所以它不得不依赖有关类型 的常识 如图图 7 所示 Figure 7 CLR Type Knowledge IDL 签名签名更改类型更改类型 In Type不允许更改 In Type 不允许更改 Out Type 就地更改 In Out Type 就地更改 In Type 不允许更改 Out Type 引用更改 In Out Type 引用更改或就地更改 如前所述 在通过引用传递时只有引用类型有两层中间环节 但是也有一些例外 例如 MarshalAs UnmanagedType LPStruct ref Guid 所以 只有指向引用类型的指针或引用可以更改 如图图 8 所示 Figure 8 Type Change Rules C 签名签名更改类型更改类型 int arg不允许更改 out int arg就地更改 ref int arg就地更改 string arg不允许更改 out string arg引用更改 ref string arg引用更改或就地更改 InAttribute OutAttribute StringBuilder arg就地更改 OutAttribute StringBuilder arg就地更改 您不必担心就地更改所需的内存所有权 因为调用方已为被调用方分配了内存 而且调用方拥有这些内存 在此我们以 OutAttribute StringBuilder 为例 相应的本机类型为 char 假设是 ANSI 因为我们不是通过引用传递 数据被封送出去 而不是封送进来 内存由调用方 在本例中是 CLR 分配 内存的大小由 StringBuilder 对象的容量确定 被调用方不需要关注内存 为了更改字符串 被调用方会自己直接更改该内存 但是 在进行引用更改时 分清谁拥有哪个内存非常重要 否则可能会发生很多意外的后果 关于所 有权问题 CLR 遵循 COM 风格的约定 作为 in 传递的内存归调用方所有 应由调用方分配 由调用方释放 被调用方不应尝试释放或修改该内存 由被调用方分配并作为 out 传递或返回的内存归调用方所有 应由调用方释放 被调用方可释放作为 in out 传递自调用方的内存 为其分配新的内存 并覆盖原有的指针值 从而将其传递出去 新内存归调用方所有 这需要两层中间环节 例如 char 在互操作领域中 调用方 被调用方变成了 CLR 本机代码 上述规则意味着 在解除固定的情况下 如果在本机代码中接收到作为 out 传递自 CLR 的一个内存块的指针 您就需要释放它 另一方面 如果 CLR 接收到作为 out 传递自本机代码的指针 CLR 就需要释放它 显然 在第一种情况下 本机代码需要解除分配 而在第二种情况下 托管代码需要解除分配 由于这涉及到内存分配和解除分配 所以最大的问题是要使用什么函数 有很多选择 HeapAlloc HeapFree malloc free new delete 等等 但是 由于 CLR 在非 BSTR 情况下使用 CoTaskMemAlloc CoTaskMemFree 而在 BSTR 情况下使用 SysStringAlloc SysStringAllocByteLen SysStringFree 所以您就必须使用这些函数 否则就很可能会在某个版本的 Windows 下发生内存泄漏 或故障 我们已经看到过这样的情况 即在 Windows XP 中将经过 malloc 的内存传递给 CLR 之后程序没有发生故障 但在 Windows Vista 中却 发生了故障 除了这些函数以外 从 CoGetMalloc 返回的系统实现的 IMalloc 接口也很好用 因为在内部它们使用的是同一个堆 但是 最好始终坚持使用 CoTaskMemAlloc CoTaskMemFree 和 SysStringAlloc SysStringAllocByteLen SysStringFree 因为 CoGetMalloc 将来可能会发生变化 让我们看一个示例 GetAnsiStringFromNativeCode 采用 char 参数作为 in out 并返回 char 作为 out retval 对于 char 参数 它可以选择调用 CoTaskMemFree 来释放由 CLR 分配的内存 然后通过使用 CoTaskMemAlloc 来分配新内存 并用新内存的指针覆盖该指针 随 后 CLR 会释放该内存 并为托管字符串创建一个副本 对于返回值 它只需要通过使用 CoTaskMemAlloc 来分配新的内存块 然后将其返回给调用 方 返回后 新分配的内存即归 CLR 所有 CLR 会先使用它创建新的托管字符串 然后再调用 CoTaskMemFree 释放它 让我们看一下第一个选择 请参见图图 9 相应的 C 函数声明如下所示 Figure 9 Using Pointers 复制代码 MARSHALLIB API char stdcall GetAnsiStringFromNativeCode char arg char szRet char CoTaskMemAlloc 255 strcpy szRet Returned String From Native Code printf Inside GetAnsiStringFromNativeCode arg s n arg printf Inside GetAnsiStringFromNativeCode CoTaskMemFree arg arg CoTaskMemAlloc 100 strcpy arg Changed n CoTaskMemFree arg arg char CoTaskMemAlloc 100 strcpy arg Changed return szRet 复制代码 class Lib DllImport MarshalLib dll CharSet CharSet Ansi public static extern string GetAnsiStringFromNativeCode ref string inOutString 当下面的 C 代码调用 GetAnsiStringFromNativeCode 复制代码 string argStr Before Console WriteLine Before GetAnsiStringFromNativeCode argStr argStr string retStr Lib GetAnsiStringFromNativeCode ref argStr Console WriteLine AnsiStringFromNativeCode returns retStr Console WriteLine After GetAnsiStringFromNativeCode argStr argStr 输出是 复制代码 Before GetAnsiStringFromNativeCode argStr Before Inside GetAnsiStringFromNativeCode arg Before Inside GetAnsiStringFromNativeCode CoTaskMemFree arg arg CoTaskMemAlloc 100 strc py arg Changed AnsiStringFromNativeCode returns Returned String From Native Code After GetAnsiStringFromNativeCode argStr Changed 如果您准备调用的本机函数没有遵循这个约定 您就必须亲自封送 以避免内存损坏 这种情况很容易发生 因为非托管函数的功能可能会返回它需要的 任何内容 它可以每次返回同一块内存 也可以返回由 malloc new 分配的新内存块 等等 这些还是取决于约定 除了内存分配以外 传进或传出的内存大小也非常重要 正如在 StringBuilder 情形中所讨论的那样 修改 Capacity 属性也很重要 这样 CLR 便可 以分配足够大的内存来存放结果 此外 将字符串作为 InAttribute OutAttribute 不使用 out 或 ref 以及任何其他属性 封送并不是明智的办法 因为您不知道该字符串是否够大 您可以使用 MarshalAsAttribute 中的 SizeParamIndex 和 SizeConst 字段指定缓冲区的大小 但是 在通过引 用传递时 这些属性将无法使用 反向反向 P Invoke 和委托生存期和委托生存期 CLR 允许将委托传递到非托管领域 这样便可将该委托作为非托管函数指针调用 实际上 结果就是 CLR 创建了一个 thunk 后者将来自本机代码的 调用转发给实际委托 然后再转发给真正的函数 请参见图图 10 Figure 10 Using a Thunk 单击该图像获得较大视图 通常情况下 您不必担心委托的生存期 将委托传递到非托管代码时 CLR 就会确保委托在调用期间处于活动状态 但是 如果本机代码在超过调用时间跨度的情况下保留某个指针副本 而且准备在以后通过该指针回调 您可能需要使用 GCHandle 显式阻止垃圾收集 器收集该委托 必须提醒您的是 固定的 GCHandle 可能对程序性能有很大的负面影响 幸运的是 在本示例中 您不必分配固定的 GC 句柄 因为 该 thunk 已在非托管堆中分配 并且通过 GC 已知的引用间接引用该委托 因此 thunk 无法四处移动 并且当委托自身处于活动状态时本机代码应 始终能够通过非托管指针调用该委托 Marshal GetFunctionPointerForDelegate 可以将委托转换成函数指针 但是它对保证委托的生存期没有任何作用 请看一下下面的函数声明 复制代码 public delegate void PrintInteger int n DllImport MarshalLib dll EntryPoint CallDelegate public static extern void CallDelegateDirectly IntPtr printIntegerProc 如果您为其调用 Marshal GetFunctionPointerForDelegate 并存储返回的 IntPtr 则将 IntPtr 传递到您准备调用的函数 如下所示 复制代码 IntPtr printIntegerCallback Marshal GetFunctionPointerForDelegate new Lib PrintInteger MyPrintInteger GC Collect GC WaitForPendingFinalizers GC Collect CallDelegateDirectly printIntegerCallback 该委托可能会在您调用 CallDelegateDirectly 之前被收集 您会收到一个 MDA 错误 指出检测到 CallbackOnCollectedDelegate 要修复这个错 误 可以将对委托的引用存储在内存中或分配 GC 句柄 如果本机代码将非托管函数的指针返回给 CLR 本机代码将负责保留实际的函数代码 这通常不是问题 除非该代码位于动态加载的 DLL 中或者是动 态生成的 P Invoke Interop Assistant 了解并记住到目前为止已介绍的所有属性和规则可能会有一点困难 毕竟 大多数托管代码的开发人员只需要能够快速找到用于 Win32 API 函数的 P Invoke 签名 将其粘贴到他们的代码中就完成任务了 这正是 P Invoke Interop Assistant 可从 MSDN 杂志 网站获得 可以发挥作用的地 方 此工具可以有效地帮助从 C 转

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论