论API调用中AsAny类型参数的使用
这是VB这门语言对于指针的使用上的一个话题。C语言和C++的 void*
类型的函数形参,允许任意类型的指针被当作实参进行传递,是一个被广泛使用的接口设计方式。
VB语言有类似的东西,典型例子就是VB6的 ByRef xxxx As Any
的API参数。虽然更常见的做法是使用 Long
类型,传递一个指针值的整数,来进行这类接口对接时的用法,但书写方面多少还是不方便的。
VB6时代:使用 As Any
类型实现 void*
参数的接口的设计
对于VB6这门语言,向API传递指针的方式通常靠使用As Any的方式,如下所示:
Declare Sub RtlCopyMemory Lib "kernel32.dll" (Destination As Any, Source As Any, ByVal Length As Long)
上述API声明的含义是:API函数的名字叫 RtlCopyMemory
,它没有返回值( 它是 Declare Sub
,而不是 Declare Function
),它是 kernel32.dll
的导出函数,调用约定是 _stdcall
(VB6的API调用都是这个调用约定) ,它有三个参数,一个是 Destination
,类型是 Any
,并且传递的方式是 ByRef
,意为“按引用传递”,相当于C++的引用,或者C语言传递变量的指针。(因为VB6默认的传递参数的方式就是 ByRef
,所以可以省略不写,而含义相反的 ByVal
就需要显式写出来);一个是 Source
,它也是一个 ByRef
方式传递的 Any
类型变量;最后一个是 Length
,它是个长整数(在VB6里,长整数指的是32位有符号整数),并且它使用 ByVal
方式传递,也就是直接传值。
从书写方式上,前两个参数,你可以把任何变量传递进去,实际的行为是VB6把该变量的地址传递进去。而第三个参数则是将其值传递进去。可以参照下例代码,当你像这样输入代码调用这个API的时候,它的行为可以做到把变量A的值复制进变量B中:
Dim A As Long
Dim B As Long
A = 123
RtlCopyMemory B, A, 4
'此时B的值也是123了
这样的用法在VB6里非常简单方便:你可以把任何类型的变量的地址直接传递进去,并且不需要调用 VarPtr()
函数,因为 VarPtr()
函数的调用是有类似于调用API那样的调用开支的。
而除了 VarPtr
函数调用的开支被节省了以外,代码本身看起来会更直观一些(当然不熟悉VB6的人看到这种不带取地址行为的 引用传递 还是会感到很头疼的),并且参数的类型上只要它的内容是对的就可以这样用,非常自由(虽说也很不清真,因为 类型检查 没了)。
但是,到了VB.NET,由于没有As Any可用,很多时候使用指针是一个很麻烦的行为。事实上,由于VB.NET和VB的差异并不仅仅是语法上的差异,更主要的是: VB.NET是一门虚拟机语言 。其JIT编译执行方式,内存的管理和GC等,都和VB6大不一样,这导致API的调用方式也发生了巨大的变化。
VB.NET时代:使用 Marshal
进行非托管内存和代码的交互
由于VB.NET的托管内存的变量、数组等的管理方式与 “编译型语言” 的变量、数组的管理方式大不相同,你不能直接从VB.NET的托管内存里直接取得你的变量或者数组的地址,然后直接拿来用。事实上,VB.NET由于其语言机制, 数据并不需要严格按照其声明的形式那样来存储 ,尤其是结构体。
然而,从语法上,为了兼容旧的VB6语法,API的声明里依然允许你使用 ByRef
的方式来实现按照 变量的引用 (或者更底层的说法: 变量的指针 )来作为API的参数实现通过参数来接收API的输出。
'VB.NET 将 Type 替换成了 Structure 用于描述结构体
Structure Point_API
Public x As Integer
Public y As Integer
End Structure
'这个 API 通过一个 Point_API 结构体来返回鼠标指针的位置
Public Declare Function GetCursorPos Lib "user32.dll" (lpPoint As Point_API) As Long
调用上述 API 的时候,其参数 lpPoint As Point_API
是一个 ByRef
按引用传递的结构体变量,调用的时候, VB.NET 会自动将结构体打包为按照其定义严格排列其成员的非托管内存数据块,然后将地址交给API。当API返回,VB.NET会从这块非托管的内存块里取回其中的数据,重新填回托管代码的结构体。
但是对于这样的VB.NET语言, As Any
的方式过于狂野,并不适合被允许使用。因为在VB6你可以用 As Any
传递小到一个 Integer
、大到一个数组或者一个结构体的参数,并且你传递数组的时候,如果不想传递 SAFEARRAY
,你一般会传递数组的第 0 个元素,相当于传递一个原始数组的指针,而这从语法层面上难以预测你到底是想传递这个数组第 0 个元素的引用,还是一整个数组的数据的指针。 所以VB.NET把 As Any
禁用了。
但 微软官方提供了一种方式 允许你使用旧的 As Any
的方式进行“任意结构体或者变量”的传递(注意它没有提到数组),那就是使用如下声明的参数类型:
<MarshalAsAttribute(UnmanagedType.AsAny)> ByVal xxxx As Object
微软说,这个可以用于 void*
类型的参数。讲道理,它是不是应该和VB6的 ByRef xxxx As Any
一样呢?
但,它是 ByVal
,而且类型是 Object
,其中微软说当你不想让变量的值被函数改变的时候,你应该使用 ByVal
传参,意味着你传递进去的参数是得不到修改的,对比VB6的 ByRef xxxx As Any
能取回数据这一点是大不一样的。
此外,在VB6的时候,对 xxxx As Any
类型的参数,我可以在传递的时候指定 ByVal yyy
来传递 yyy
的值而非地址(引用),或者 ByVal 0
来传递 NULL
,但在 VB.NET ,经过测试我发现:我不能使用 ByVal yyy
或者 ByVal 0
来直接传递数值。我必须传递一个现有的变量。这导致我无法传递 NULL
。
并且我注意到,微软提供的这种参数声明方式,它其实就是用 ByVal
方式传值的。那我是不是直接传递一个变量,它直接就能把变量当作指针值传递进去,还是会把变量的地址传递进去呢? 此处应当实践。 于是我建立了一个工程,然后编写了以下代码来测试:
Imports System.Runtime.InteropServices
Public Class Form1
Public Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlCopyMemory" _
(
<MarshalAsAttribute(UnmanagedType.AsAny)> ByVal Dst As Object,
<MarshalAsAttribute(UnmanagedType.AsAny)> ByVal Src As Object,
ByVal cb_Copy As IntPtr
)
Public Declare Sub CopyMemory2 Lib "kernel32.dll" Alias "RtlCopyMemory" (ByVal Dst As IntPtr, ByVal Src As IntPtr, ByVal cb_Copy As IntPtr)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'实验用数值
Dim Val As Long = 123
'非托管内存
Dim Buffer1 As Long, Buffer2 As Long
Buffer1 = Marshal.AllocHGlobal(8)
Buffer2 = Marshal.AllocHGlobal(8)
'先把两块内存都初始化为特定数值
Marshal.WriteInt64(Buffer1, Val)
Marshal.WriteInt64(Buffer2, &H123456789ABCDEF0)
'然后调用API试图,试图让它把缓冲区1里的数值复制到缓冲区2
CopyMemory(Buffer2, Buffer1, 8)
'取回缓冲区2的数值,看看是否复制成功
Val = Marshal.ReadInt64(Buffer2)
'弹出对话框显示执行的结果
MsgBox(Val & vbCrLf & Hex(Val) & vbCrLf & Hex(Buffer1) & vbCrLf & Hex$(Buffer2) & vbCrLf & Marshal.SizeOf(Val))
'释放分配的内存
Marshal.FreeHGlobal(Buffer1)
Marshal.FreeHGlobal(Buffer2)
End
End Sub
End Class
运行的时候,弹出的对话框,其内容证明:我这样调用 CopyMemory
,并不能让缓冲区2的数值变为缓冲区1的数值。也就是说, 内存没有被复制到正确的地址上。 对话框内容如下:
1311768467463790320
123456789ABCDEF0
1B2677E0
1B267880
8
为了观察它具体的行为,我开启了 对非托管内存的反汇编调试 ,然后在VS里观察其JIT编译的汇编代码,内容直接粘贴过来了:
'实验用数值
Dim Val As Long = 123
00007FFDB5F32304 mov ecx,7Bh
00007FFDB5F32309 movsxd rcx,ecx
00007FFDB5F3230C mov qword ptr [rbp+110h],rcx
'非托管内存
Dim Buffer1 As Long, Buffer2 As Long
Buffer1 = Marshal.AllocHGlobal(8)
00007FFDB5F32313 mov ecx,8
00007FFDB5F32318 call System.Runtime.InteropServices.Marshal.AllocHGlobal(Int32) (07FFE14B456C0h)
00007FFDB5F3231D mov qword ptr [rbp+0F8h],rax
00007FFDB5F32324 mov rcx,qword ptr [rbp+0F8h]
00007FFDB5F3232B call System.IntPtr.op_Explicit(IntPtr) (07FFE14405A40h)
00007FFDB5F32330 mov qword ptr [rbp+0F0h],rax
00007FFDB5F32337 mov rcx,qword ptr [rbp+0F0h]
00007FFDB5F3233E mov qword ptr [rbp+108h],rcx
Buffer2 = Marshal.AllocHGlobal(8)
00007FFDB5F32345 mov ecx,8
00007FFDB5F3234A call System.Runtime.InteropServices.Marshal.AllocHGlobal(Int32) (07FFE14B456C0h)
00007FFDB5F3234F mov qword ptr [rbp+0E8h],rax
00007FFDB5F32356 mov rcx,qword ptr [rbp+0E8h]
00007FFDB5F3235D call System.IntPtr.op_Explicit(IntPtr) (07FFE14405A40h)
00007FFDB5F32362 mov qword ptr [rbp+0E0h],rax
00007FFDB5F32369 mov rcx,qword ptr [rbp+0E0h]
00007FFDB5F32370 mov qword ptr [rbp+100h],rcx
'先把两块内存都初始化为特定数值
Marshal.WriteInt64(Buffer1, Val)
00007FFDB5F32377 mov rcx,qword ptr [rbp+108h]
00007FFDB5F3237E call System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
00007FFDB5F32383 mov qword ptr [rbp+0D8h],rax
00007FFDB5F3238A mov rcx,qword ptr [rbp+0D8h]
00007FFDB5F32391 mov rdx,qword ptr [rbp+110h]
00007FFDB5F32398 call System.Runtime.InteropServices.Marshal.WriteInt64(IntPtr, Int64) (07FFE14B45400h)
00007FFDB5F3239D nop
Marshal.WriteInt64(Buffer2, &H123456789ABCDEF0)
00007FFDB5F3239E mov rcx,qword ptr [rbp+100h]
00007FFDB5F323A5 call System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
00007FFDB5F323AA mov qword ptr [rbp+0D0h],rax
00007FFDB5F323B1 mov rcx,qword ptr [rbp+0D0h]
00007FFDB5F323B8 mov rdx,123456789ABCDEF0h
00007FFDB5F323C2 call System.Runtime.InteropServices.Marshal.WriteInt64(IntPtr, Int64) (07FFE14B45400h)
00007FFDB5F323C7 nop
'然后调用API试图,试图让它把缓冲区1里的数值复制到缓冲区2
CopyMemory(Buffer2, Buffer1, 8)
00007FFDB5F323C8 mov rcx,7FFE13DFAEA8h
00007FFDB5F323D2 call CORINFO_HELP_NEWSFAST (07FFE15492540h)
00007FFDB5F323D7 mov qword ptr [rbp+0C8h],rax
00007FFDB5F323DE mov rcx,qword ptr [rbp+0C8h]
00007FFDB5F323E5 mov rax,qword ptr [rbp+100h]
00007FFDB5F323EC mov qword ptr [rcx+8],rax
00007FFDB5F323F0 mov rcx,7FFE13DFAEA8h
00007FFDB5F323FA call CORINFO_HELP_NEWSFAST (07FFE15492540h)
00007FFDB5F323FF mov qword ptr [rbp+0C0h],rax
00007FFDB5F32406 mov rcx,qword ptr [rbp+0C0h]
00007FFDB5F3240D mov rax,qword ptr [rbp+108h]
00007FFDB5F32414 mov qword ptr [rcx+8],rax
00007FFDB5F32418 mov rcx,qword ptr [rbp+0C8h]
00007FFDB5F3241F mov qword ptr [rbp+0B8h],rcx
00007FFDB5F32426 mov rcx,qword ptr [rbp+0C0h]
00007FFDB5F3242D mov qword ptr [rbp+0B0h],rcx
00007FFDB5F32434 mov ecx,8
00007FFDB5F32439 call System.IntPtr.op_Explicit(Int32) (07FFE14387880h)
00007FFDB5F3243E mov qword ptr [rbp+0A8h],rax
00007FFDB5F32445 mov rcx,qword ptr [rbp+0B8h]
00007FFDB5F3244C mov rdx,qword ptr [rbp+0B0h]
00007FFDB5F32453 mov r8,qword ptr [rbp+0A8h]
00007FFDB5F3245A call TestProj.Form1.CopyMemory(System.Object, System.Object, IntPtr) (07FFDB5F30868h)
00007FFDB5F3245F nop
'取回缓冲区2的数值,看看是否复制成功
Val = Marshal.ReadInt64(Buffer2)
00007FFDB5F32460 mov rcx,qword ptr [rbp+100h]
00007FFDB5F32467 call System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
00007FFDB5F3246C mov qword ptr [rbp+0A0h],rax
00007FFDB5F32473 mov rcx,qword ptr [rbp+0A0h]
00007FFDB5F3247A call System.Runtime.InteropServices.Marshal.ReadInt64(IntPtr) (07FFE14B45160h)
00007FFDB5F3247F mov qword ptr [rbp+98h],rax
00007FFDB5F32486 mov rcx,qword ptr [rbp+98h]
00007FFDB5F3248D mov qword ptr [rbp+110h],rcx
注意看,其中的 CopyMemory(Buffer2, Buffer1, 8)
那一块的反汇编,可以看到有几个对 CORINFO_HELP_NEWSFAST
的调用,我在单步调试的时候,发现它返回了新的临时地址。
在 call TestProj.Form1.CopyMemory(System.Object, System.Object, IntPtr)
的地方,我发现传递进去的两个参数,分别就是两个由 CORINFO_HELP_NEWSFAST
返回的临时地址。并且在调用结束后,它直接丢弃了这个临时地址里的数据,从而进入 Val = Marshal.ReadInt64(Buffer2)
这一块了。
我认为VB.NET把 <MarshalAsAttribute(UnmanagedType.AsAny)> ByVal xxxx As Object
这个参数对应的传入的变量打包到了临时内存后,将临时内存的地址传递进去了。并且因为是 ByVal
, API对临时变量的修改被丢弃了 。
经过一些测试,我发现传入的变量类型可以是 Byte
、 Short
、 Integer
、 Long
这些,也可以是一个结构体。这方面的行为倒确实和VB6的 ByRef xxx As Any
很像:你可以传递各种各样的类型或者结构体进去,然后实际上是传递了地址进去。
为了保证能取回临时变量的数据,我尝试将 <MarshalAsAttribute(UnmanagedType.AsAny)> ByVal Dst As Object
中的 ByVal
改为 ByRef
,然后编译执行,可以看到,在 call TestProj.Form1.CopyMemory(System.Object, System.Object, IntPtr)
后, 确实增加了更多的指令 :
'然后调用API试图,试图让它把缓冲区1里的数值复制到缓冲区2
CopyMemory(Buffer2, Buffer1, 8)
00007FFDB5F523C8 mov rcx,7FFE13DFAEA8h
00007FFDB5F523D2 call CORINFO_HELP_NEWSFAST (07FFE15492540h)
00007FFDB5F523D7 mov qword ptr [rbp+0D8h],rax
00007FFDB5F523DE mov rcx,qword ptr [rbp+0D8h]
00007FFDB5F523E5 mov rax,qword ptr [rbp+120h]
00007FFDB5F523EC mov qword ptr [rcx+8],rax
00007FFDB5F523F0 mov rcx,qword ptr [rbp+0D8h]
00007FFDB5F523F7 mov qword ptr [rbp+118h],rcx
00007FFDB5F523FE mov rcx,7FFE13DFAEA8h
00007FFDB5F52408 call CORINFO_HELP_NEWSFAST (07FFE15492540h)
00007FFDB5F5240D mov qword ptr [rbp+0D8h],rax
00007FFDB5F52414 mov rcx,qword ptr [rbp+0D8h]
00007FFDB5F5241B mov rax,qword ptr [rbp+128h]
00007FFDB5F52422 mov qword ptr [rcx+8],rax
00007FFDB5F52426 mov rcx,qword ptr [rbp+0D8h]
00007FFDB5F5242D mov qword ptr [rbp+110h],rcx
00007FFDB5F52434 lea rcx,[rbp+118h]
00007FFDB5F5243B mov qword ptr [rbp+0D0h],rcx
00007FFDB5F52442 lea rcx,[rbp+110h]
00007FFDB5F52449 mov qword ptr [rbp+0C8h],rcx
00007FFDB5F52450 mov ecx,8
00007FFDB5F52455 call System.IntPtr.op_Explicit(Int32) (07FFE14387880h)
00007FFDB5F5245A mov qword ptr [rbp+0C0h],rax
00007FFDB5F52461 mov rcx,qword ptr [rbp+0D0h]
00007FFDB5F52468 mov rdx,qword ptr [rbp+0C8h]
00007FFDB5F5246F mov r8,qword ptr [rbp+0C0h]
00007FFDB5F52476 call TestProj.Form1.CopyMemory(System.Object ByRef, System.Object ByRef, IntPtr) (07FFDB5F50868h)
00007FFDB5F5247B nop
00007FFDB5F5247C mov rcx,qword ptr [rbp+110h]
00007FFDB5F52483 call qword ptr [指针指向: Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(System.Object) (07FFDDE81A710h)]
00007FFDB5F52489 mov qword ptr [rbp+0B8h],rax
00007FFDB5F52490 mov rcx,qword ptr [rbp+0B8h]
00007FFDB5F52497 mov qword ptr [rbp+128h],rcx
00007FFDB5F5249E mov rcx,qword ptr [rbp+118h]
00007FFDB5F524A5 call qword ptr [指针指向: Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(System.Object) (07FFDDE81A710h)]
00007FFDB5F524AB mov qword ptr [rbp+0B0h],rax
00007FFDB5F524B2 mov rcx,qword ptr [rbp+0B0h]
00007FFDB5F524B9 mov qword ptr [rbp+120h],rcx
但是,虽然看起来,函数调用后返回的数据是可以被取回的,实际上这份代码是不能被执行的,因为 <MarshalAsAttribute(UnmanagedType.AsAny)>
修饰的参数必须不能是 ByRef
。
System.Runtime.InteropServices.MarshalDirectiveException:“无法封送处理“parameter #1”: 不能对返回类型、ByRef 参数、ArrayWithOffset 或从非托管传入托管的参数使用 AsAny。”
也就是说,微软所说的 <MarshalAsAttribute(UnmanagedType.AsAny)> ByVal xxxx As Object
,并不和VB6的 As Any
相似,事实上比VB6的 As Any
坑多了。
对于这样的情况,正确的用法应当是使用 ByVal xxx As IntPtr
的方式进行指针的传递,然后使用 Marshal
处理所有的非托管内存相关的操作。
总结
微软官方提出的 <MarshalAsAttribute(UnmanagedType.AsAny)> ByVal xxxx As Object
的用法并不和VB6自己的 As Any
相似。事实上,它的底层操作是 将你传入的变量或结构体打包到一个临时的非托管内存里,再将这个临时内存的地址传递进API 。并且,你无法像VB6使用 ByVal 0
那样直接传 NULL
值或者传其它值, 你只能传递一个地址 ,并且 API对这个地址的写入的数据会被丢弃 。
我打赌:微软自己也不知道自己设计的VB6的 As Any
能被人玩出花。