0xAA55 发表于 2021-2-3 16:33:49

【VB.NET】论API调用中AsAny类型参数的使用

# 论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` 禁用了。**

但 [**微软官方提供了一种方式**](https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/com-interop/walkthrough-calling-windows-apis) 允许你使用旧的 `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
    00007FFDB5F32304mov         ecx,7Bh
    00007FFDB5F32309movsxd      rcx,ecx
    00007FFDB5F3230Cmov         qword ptr ,rcx

            '非托管内存
            Dim Buffer1 As Long, Buffer2 As Long
            Buffer1 = Marshal.AllocHGlobal(8)
    00007FFDB5F32313mov         ecx,8
    00007FFDB5F32318call      System.Runtime.InteropServices.Marshal.AllocHGlobal(Int32) (07FFE14B456C0h)
    00007FFDB5F3231Dmov         qword ptr ,rax
    00007FFDB5F32324mov         rcx,qword ptr
    00007FFDB5F3232Bcall      System.IntPtr.op_Explicit(IntPtr) (07FFE14405A40h)
    00007FFDB5F32330mov         qword ptr ,rax
    00007FFDB5F32337mov         rcx,qword ptr
    00007FFDB5F3233Emov         qword ptr ,rcx
            Buffer2 = Marshal.AllocHGlobal(8)
    00007FFDB5F32345mov         ecx,8
    00007FFDB5F3234Acall      System.Runtime.InteropServices.Marshal.AllocHGlobal(Int32) (07FFE14B456C0h)
    00007FFDB5F3234Fmov         qword ptr ,rax
    00007FFDB5F32356mov         rcx,qword ptr
    00007FFDB5F3235Dcall      System.IntPtr.op_Explicit(IntPtr) (07FFE14405A40h)
    00007FFDB5F32362mov         qword ptr ,rax
    00007FFDB5F32369mov         rcx,qword ptr
    00007FFDB5F32370mov         qword ptr ,rcx

            '先把两块内存都初始化为特定数值
            Marshal.WriteInt64(Buffer1, Val)
    00007FFDB5F32377mov         rcx,qword ptr
    00007FFDB5F3237Ecall      System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
    00007FFDB5F32383mov         qword ptr ,rax
    00007FFDB5F3238Amov         rcx,qword ptr
    00007FFDB5F32391mov         rdx,qword ptr
    00007FFDB5F32398call      System.Runtime.InteropServices.Marshal.WriteInt64(IntPtr, Int64) (07FFE14B45400h)
    00007FFDB5F3239Dnop
            Marshal.WriteInt64(Buffer2, &H123456789ABCDEF0)
    00007FFDB5F3239Emov         rcx,qword ptr
    00007FFDB5F323A5call      System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
    00007FFDB5F323AAmov         qword ptr ,rax
    00007FFDB5F323B1mov         rcx,qword ptr
    00007FFDB5F323B8mov         rdx,123456789ABCDEF0h
    00007FFDB5F323C2call      System.Runtime.InteropServices.Marshal.WriteInt64(IntPtr, Int64) (07FFE14B45400h)
    00007FFDB5F323C7nop

            '然后调用API试图,试图让它把缓冲区1里的数值复制到缓冲区2
            CopyMemory(Buffer2, Buffer1, 8)
    00007FFDB5F323C8mov         rcx,7FFE13DFAEA8h
    00007FFDB5F323D2call      CORINFO_HELP_NEWSFAST (07FFE15492540h)
    00007FFDB5F323D7mov         qword ptr ,rax
    00007FFDB5F323DEmov         rcx,qword ptr
    00007FFDB5F323E5mov         rax,qword ptr
    00007FFDB5F323ECmov         qword ptr ,rax
    00007FFDB5F323F0mov         rcx,7FFE13DFAEA8h
    00007FFDB5F323FAcall      CORINFO_HELP_NEWSFAST (07FFE15492540h)
    00007FFDB5F323FFmov         qword ptr ,rax
    00007FFDB5F32406mov         rcx,qword ptr
    00007FFDB5F3240Dmov         rax,qword ptr
    00007FFDB5F32414mov         qword ptr ,rax
    00007FFDB5F32418mov         rcx,qword ptr
    00007FFDB5F3241Fmov         qword ptr ,rcx
    00007FFDB5F32426mov         rcx,qword ptr
    00007FFDB5F3242Dmov         qword ptr ,rcx
    00007FFDB5F32434mov         ecx,8
    00007FFDB5F32439call      System.IntPtr.op_Explicit(Int32) (07FFE14387880h)
    00007FFDB5F3243Emov         qword ptr ,rax
    00007FFDB5F32445mov         rcx,qword ptr
    00007FFDB5F3244Cmov         rdx,qword ptr
    00007FFDB5F32453mov         r8,qword ptr
    00007FFDB5F3245Acall      TestProj.Form1.CopyMemory(System.Object, System.Object, IntPtr) (07FFDB5F30868h)
    00007FFDB5F3245Fnop

            '取回缓冲区2的数值,看看是否复制成功
            Val = Marshal.ReadInt64(Buffer2)
    00007FFDB5F32460mov         rcx,qword ptr
    00007FFDB5F32467call      System.IntPtr.op_Explicit(Int64) (07FFE14BDB290h)
    00007FFDB5F3246Cmov         qword ptr ,rax
    00007FFDB5F32473mov         rcx,qword ptr
    00007FFDB5F3247Acall      System.Runtime.InteropServices.Marshal.ReadInt64(IntPtr) (07FFE14B45160h)
    00007FFDB5F3247Fmov         qword ptr ,rax
    00007FFDB5F32486mov         rcx,qword ptr
    00007FFDB5F3248Dmov         qword ptr ,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)
    00007FFDB5F523C8mov         rcx,7FFE13DFAEA8h
    00007FFDB5F523D2call      CORINFO_HELP_NEWSFAST (07FFE15492540h)
    00007FFDB5F523D7mov         qword ptr ,rax
    00007FFDB5F523DEmov         rcx,qword ptr
    00007FFDB5F523E5mov         rax,qword ptr
    00007FFDB5F523ECmov         qword ptr ,rax
    00007FFDB5F523F0mov         rcx,qword ptr
    00007FFDB5F523F7mov         qword ptr ,rcx
    00007FFDB5F523FEmov         rcx,7FFE13DFAEA8h
    00007FFDB5F52408call      CORINFO_HELP_NEWSFAST (07FFE15492540h)
    00007FFDB5F5240Dmov         qword ptr ,rax
    00007FFDB5F52414mov         rcx,qword ptr
    00007FFDB5F5241Bmov         rax,qword ptr
    00007FFDB5F52422mov         qword ptr ,rax
    00007FFDB5F52426mov         rcx,qword ptr
    00007FFDB5F5242Dmov         qword ptr ,rcx
    00007FFDB5F52434lea         rcx,
    00007FFDB5F5243Bmov         qword ptr ,rcx
    00007FFDB5F52442lea         rcx,
    00007FFDB5F52449mov         qword ptr ,rcx
    00007FFDB5F52450mov         ecx,8
    00007FFDB5F52455call      System.IntPtr.op_Explicit(Int32) (07FFE14387880h)
    00007FFDB5F5245Amov         qword ptr ,rax
    00007FFDB5F52461mov         rcx,qword ptr
    00007FFDB5F52468mov         rdx,qword ptr
    00007FFDB5F5246Fmov         r8,qword ptr
    00007FFDB5F52476call      TestProj.Form1.CopyMemory(System.Object ByRef, System.Object ByRef, IntPtr) (07FFDB5F50868h)
    00007FFDB5F5247Bnop
    00007FFDB5F5247Cmov         rcx,qword ptr
    00007FFDB5F52483call      qword ptr [指针指向: Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(System.Object) (07FFDDE81A710h)]
    00007FFDB5F52489mov         qword ptr ,rax
    00007FFDB5F52490mov         rcx,qword ptr
    00007FFDB5F52497mov         qword ptr ,rcx
    00007FFDB5F5249Emov         rcx,qword ptr
    00007FFDB5F524A5call      qword ptr [指针指向: Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(System.Object) (07FFDDE81A710h)]
    00007FFDB5F524ABmov         qword ptr ,rax
    00007FFDB5F524B2mov         rcx,qword ptr
    00007FFDB5F524B9mov         qword ptr ,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` 能被人玩出花。

0xAA55 发表于 2021-2-3 16:50:00

原来VB.NET的Debug模式的JIT汇编这么难看啊……

嗷嗷叫的老马 发表于 2021-2-3 20:11:42

多年不碰VB系了....

现在主力为C#.

不过VB系做些小工具还是不错!

唐凌 发表于 2021-2-4 08:30:33

其实我觉得要达到`As Any`的效果可以用重载声明的方法。不过就`CopyMemory`而言,我觉得`Marshal`类里的很多方法都可以替代掉它了。
另外微软推荐用(https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/com-interop/walkthrough-calling-windows-apis#api-calls-using-dllimport)声明API来着

Golden Blonde 发表于 2021-2-10 04:36:29

当年用VB.NET写WIN64AST的时候,非常蛋疼。

我甚至根本不知道怎样调用参数复杂的WINAPI,于是还用C写了个DLL。

后来所有涉及指针的操作(比如解析从驱动传回的结构体),我全部用VirtualAlloc和RtlMoveMemory来操作(地址用ULong来记录)。

现在想想也是蛮好笑的。

0xAA55 发表于 2021-2-10 19:23:28

美俪女神 发表于 2021-2-10 04:36
当年用VB.NET写WIN64AST的时候,非常蛋疼。

我甚至根本不知道怎样调用参数复杂的WINAPI,于是还用C写了个D ...

第三方Marshal

Golden Blonde 发表于 2021-2-11 09:30:39

其实根本不用管它怎么定义。比如这个:Declare Sub RtlMoveMemory Lib "kernel32.dll" (Destination As Any, Source As Any, ByVal Length As Long)
调用的时候可以强制byval:RtlMoveMemory byval varptr(a), byval varptr(b), 4

0xAA55 发表于 2021-2-11 13:43:33

美俪女神 发表于 2021-2-11 09:30
其实根本不用管它怎么定义。比如这个:
调用的时候可以强制byval:

仅限VB6。如果你在VB.NET里试图使用As Any或者在传参的时候使用ByVal,你不如认真看看本贴。

唐凌 发表于 2021-2-21 06:38:40

我发现可以通过分配一个`Pinned`的(https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandle?view=netframework-4.0)把变量锁死在内存里,然后获取一个指针值传给函数。
具体看链接里的例子。例子里的
```Visual Basic
Dim gch As GCHandle = GCHandle.FromIntPtr(param)
Dim tw As TextWriter = CType(gch.Target, TextWriter)
```
相当于`TextWriter* tw=(TextWriter*)param`了。
警告:传给`FromIntPtr`的指针必须是通过GCHandle给的指针值。
页: [1]
查看完整版本: 【VB.NET】论API调用中AsAny类型参数的使用