找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3605|回复: 10

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

[复制链接]
发表于 2021-2-3 16:33:49 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

论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 这个参数对应的传入的变量打包到了临时内存后,将临时内存的地址传递进去了。并且因为是 ByValAPI对临时变量的修改被丢弃了

经过一些测试,我发现传入的变量类型可以是 ByteShortIntegerLong 这些,也可以是一个结构体。这方面的行为倒确实和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 能被人玩出花。

回复

使用道具 举报

 楼主| 发表于 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类里的很多方法都可以替代掉它了。
另外微软推荐用DllImport声明API来着

回复 赞! 靠!

使用道具 举报

发表于 2021-2-10 04:36:29 | 显示全部楼层
当年用VB.NET写WIN64AST的时候,非常蛋疼。

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

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

现在想想也是蛮好笑的。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2021-2-10 19:23:28 | 显示全部楼层
美俪女神 发表于 2021-2-10 04:36
当年用VB.NET写WIN64AST的时候,非常蛋疼。

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

第三方Marshal
回复 赞! 靠!

使用道具 举报

发表于 2021-2-11 09:30:39 | 显示全部楼层
其实根本不用管它怎么定义。比如这个:
  1. Declare Sub RtlMoveMemory Lib "kernel32.dll" (Destination As Any, Source As Any, ByVal Length As Long)
复制代码

调用的时候可以强制byval:
  1. RtlMoveMemory byval varptr(a), byval varptr(b), 4
复制代码
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2021-2-11 13:43:33 | 显示全部楼层
美俪女神 发表于 2021-2-11 09:30
其实根本不用管它怎么定义。比如这个:
调用的时候可以强制byval:

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

点评

我说的就是VB6。VB.NET里哪有VarPtr。  发表于 2021-2-11 15:35
回复 赞! 靠!

使用道具 举报

发表于 2021-2-21 06:38:40 | 显示全部楼层

我发现可以通过分配一个PinnedGCHandle把变量锁死在内存里,然后获取一个指针值传给函数。
具体看链接里的例子。例子里的

Dim gch As GCHandle = GCHandle.FromIntPtr(param)
Dim tw As TextWriter = CType(gch.Target, TextWriter)

相当于TextWriter* tw=(TextWriter*)param了。
警告:传给FromIntPtr的指针必须是通过GCHandle给的指针值。

回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-22 05:18 , Processed in 0.043822 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表