Results 1 to 9 of 9

Thread: [VB6] VB6.tlb - Passing a ParamArray without Copying

  1. #1

    Thread Starter
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    [VB6] VB6.tlb - Passing a ParamArray without Copying

    The Issue: How do you access a ParamArray, and pass it to another function,
    without copying it.
    The Answer: Pointer magic. ParamArrays are really just a
    Variant Array - managed by the runtime. For whatever reason though - the
    runtime / ide doesn't allow you to get at it's address easily, or even pass it
    to another function without making a copy

    This particular technique I believe was first pioneered by Matt Curland.
    Originally Curland used assembly to get the stack pointer,
    add a hard coded offset (depending on the function parameters passed before
    the ParamArray) to calculate where the SAFEARRAY** is on the stack, then
    return the dereferenced pointer. Then you can safely swap the SAFEARRAY
    reference with another SAFEARRAY.

    edit: After a a good amount of fumbling, I used Curland's proven methods, with my own self modfying code.
    I usually avoid assembly, and dislike mixing it with VB - but here's the cleaned up routines.

    just include MParamArray.bas in a project, and you can use the GetParams function.
    GetParams takes a single Parameter Offset, and returns a Variant Array, that you can use to pass to other functions.
    GetParams steals the Array reference off the stack, without copying the original Array.

    Code:
    ' SomeForm.frm or Class1.cls
    Private Sub Func1(ParamArray Params())
        ' NOTE: offset calculation from Frame Pointer in a Class Module (.cls) or Form (.frm)
        ' saved ebp (4) + return address (4) + this ptr (4) + No Params before ParamArray (0)
        OtherFunc GetParams(12)
    End Sub
    Code:
    ' StandardModule.bas
    Private Sub Func2(ParamArray Params())
        ' NOTE: offset calculation from Frame Pointer (ebp) in a Standard Module (.bas)
        ' saved ebp (4) + return address (4) + no other params on stack before ParamArray (0)
        Dim ParamOffset As Long: ParamOffset = 4 + 4 + 0
        If InIDE Then ParamOffset = ParamOffset + 4 ' Standard Module Functions need an extra 4 bytes when debugging
    
        'If you need to pass the array as a ParamArray, to allow modification of ByRef paramaters, Use CallPtr
        CallPtr AddressOf Module1.OtherFunc, vbEmpty, RefAry(GetParams(ParamOffset))
    End Sub
    Attached Files Attached Files
    Last edited by DEXWERX; Mar 23rd, 2017 at 09:19 AM.

  2. #2

    Thread Starter
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    Updated MParamArray.bas

    Code:
    Option Explicit
    
    Private Declare Function VirtualProtect Lib "kernel32" ( _
        ByVal lpAddress As LongPtr, _
        ByVal dwSize As Long, _
        ByVal flNewProtect As Long, _
        ByRef lpflOldProtect As Long) As Long
     
    Private Const PAGE_EXECUTE_READWRITE = &H40
    
    ' InIDE Helper... from TheTrick
    Private Function MakeTrue(ByRef bVar As Boolean) As Boolean: bVar = True: MakeTrue = True: End Function
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Public Function GetParams(ByVal ParamArrayOffset As Long) As Variant()
    '   Steals a SAFEARRAY(VARIANT)* Reference from the caller's stack
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Dim InIDE As Boolean: Debug.Assert MakeTrue(InIDE)
        Dim Addr As LongPtr
        Dim Protection As Long
        
        ' Patch FramePtr function to return ebp
        Addr = FncPtr(AddressOf MParamArray.FramePtr)
        If InIDE Then Addr = DeRef(PtrAdd(Addr, &H16))  ' skip debug stub
    
        VirtualProtect Addr, 4, PAGE_EXECUTE_READWRITE, Protection
        DLng(Addr) = &H90C3C58B             ' mov eax,ebp / ret / nop
        VirtualProtect Addr, 4, Protection, Protection
        
        ' Get Caller FramePtr
        ' NOTE: typical function prologue points frame pointer to caller frame pointer
        Dim CallerFramePtr As LongPtr
        CallerFramePtr = DeRef(FramePtr())
    
        Dim ParamArrayPtr As LongPtr
        ParamArrayPtr = DeRef(PtrAdd(CallerFramePtr, ParamArrayOffset)) ' Calculate SAFEARRAY**
        GetParams = PtrVarAry(ParamArrayPtr)                            ' Convert Ptr to Variant Array
        'AryRef(GetParams) = DeRef(ParamArrayPtr)
        DeRef(ParamArrayPtr) = vbNullPtr                                ' Zero original
    
    'Exit Function
    
        ' self modify.
        Addr = FncPtr(AddressOf MParamArray.GetParams)
        If InIDE Then Addr = DeRef(PtrAdd(Addr, &H16))
    
        VirtualProtect Addr, 18, PAGE_EXECUTE_READWRITE, Protection
        DLng(PtrAdd(Addr, 0)) = &H4244C8B   ' mov ecx, [esp + 4]     ; FrameOffset -> ecx
        DLng(PtrAdd(Addr, 4)) = &HD4C8B     ' mov ecx, [ebp + ecx]   ; [FramePtr + ecx] -> SAFEARRAY**
        DInt(PtrAdd(Addr, 8)) = &H18B       ' mov eax, [ecx]         ; Deref SAFEARRAY** -> SAFEARRAY* (returned in eax)
        DInt(PtrAdd(Addr, 10)) = &HD233     ' xor edx, edx           ; Zero the original SAFEARRAY* to keep runtime happy
        DInt(PtrAdd(Addr, 12)) = &H1189     ' mov [ecx], edx         ; prevents crash on second de-allocation
        DLng(PtrAdd(Addr, 14)) = &H900004C2 ' ret 4 / nop
        VirtualProtect Addr, 18, Protection, Protection
    End Function
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Function FramePtr() As LongPtr
    ' Returns 32bit Frame Pointer (ebp)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Err.Raise E_CANT
    End Function
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Function PtrAdd(ByVal Pointer As LongPtr, ByVal Offset As Long) As LongPtr
    ' Unsigned Pointer addition avoids overflow when incrementing/decrementing accross 2G boundary
    ' NOTE: required when using /LARGEADDRESSAWARE on 64bit Windows
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        PtrAdd = (Pointer Xor &H80000000) + Offset Xor &H80000000
    End Function
    Last edited by DEXWERX; Mar 23rd, 2017 at 09:31 AM.

  3. #3
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,412

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    Hi Dexwerx. I have a need for a way to pass a ParamArray from one function to another in my open source vbFcgi project available here: https://github.com/jpbro/VbFcgi

    Your work here looks very promising and I'd like to give it a try, and I would like to request permission to use it in my project. Before I do I need to check in about the licensing. My project is licensed under the MIT license, and I don't know if it is compatible with your code or not. If so, and assuming you give me permission to us it, I don't know how you'd like your code to be licensed & attributed when I bring it over to vbFcgi.

    There are some further complications since you mention the technique was pioneered by Matt Curland, and I see a reference to the Trick in the comments. Lastly, I don't know if you are the author of the vb6.tlb library or not. I'd appreciate it if those points could be clarified.

    Thanks a lot!

  4. #4

    Thread Starter
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    VB6.tlb - I am the author, although I consider it an evolution or simplification of VBVM6 Lib. Unlike oleexp, this lib was written from scratch using IDL/midl. The APIs exposed are also more thought out and consistently named. (benefiting from the long history of VB6) also keep in mind that this is a 32bit tlb so the LongPtr is just an alias to Long. Feel free to decompile the typelib to see the source, but I can't distribute the source as is, because it contains MS code. The typelib's subclassing interfaces are compatible with the bulletproof subclassing DLL. Someday I'll document the APIs exposed.

    As for the GetParams function, there is no issue with you using it with attribution per an MIT license.
    The self modifying code is my own implementation, and there is no legal obligation to Curland.
    The only similarity or commonality is the assembly to return the base pointer. (mov eax,ebp / ret / nop)
    We also both pad instruction bytes with NOP (0xC0)

    As for InIDE(), I have a version somewhere that is not from The Trick, I was just being lazy...


    more notes on it's use:
    Passing a ParamArray without copying it is efficient, but you may run into issues with ByRef parameters that are packaged in a Variant. the VB runtime may not treat ByRef parameters as you expect - so you'll have to treat them special. If you try to assign a value to a Variant Array Item, it will just set that value byval in the array - and not write a value back to original ByRef parameter.

    You can check this using the VarVT function and vbByRef.

    Code:
    If VarVT(MyParam(0)) = (vbByRef Or vbLong) Then
        DLng(VarPtr(MyParam(0)) = 1234 ' assign 1234 to original variable
    End If
    So unless you call the next function using a thunk or DispCallFunc, and it's expecting a ParamArray, you're almost always better off just copying the whole ParamArray (which VB converts ByRef to ByVal automatically per VariantCopyInd).


    At the end of the day... I've never seen a real world scenario to use the code in this thread. I just don't recommend it. The use case is extremely narrow, and not worth opening yourself up to DEP issues with the self modifying code.

    I also recommend re-writing the portion that writes the thunk to something more generic
    I'll go ahead and make a routine to do it...
    Last edited by DEXWERX; Dec 22nd, 2017 at 01:44 PM.

  5. #5
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,412

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    Hi Dexwerx - thanks for the clarifications - I have some InIde code myself, so I can always swap that out. Just to confirm then that I have permission to use the code in VbFcgi? If so, how exactly would you like the attribution to appear in the source files?

    Thanks a lot.

  6. #6

    Thread Starter
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    updated version.

    Code:
    ' Copyright © 2017 Dexter Freivald. All Rights Reserved. DEXWERX.COM
    Option Explicit
    
    Private Declare Function VirtualProtect Lib "kernel32" ( _
        ByVal lpAddress As LongPtr, _
        ByVal dwSize As Long, _
        ByVal flNewProtect As Long, _
        ByRef lpflOldProtect As Long) As Long
     
    Private Const PAGE_EXECUTE_READWRITE = &H40
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Public Function StealParams(ByVal ParamArrayOffset As Long) As Variant()
    ' Steals a SAFEARRAY(VARIANT)* Reference from the caller's stack
    ' NOTE: You can only steal them once...
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        ' Patch FramePtr function to return ebp
        WriteThunk AddressOf MParamArray.FramePtr, &H90C3C58B   ' mov eax,ebp / ret / nop
        
        ' Get Caller FramePtr
        ' NOTE: typical function prologue points frame pointer to caller frame pointer
        Dim CallerFramePtr As LongPtr
        CallerFramePtr = DeRef(FramePtr())
    
        Dim ParamArrayPtr As LongPtr
        ParamArrayPtr = DeRef(PtrAdd(CallerFramePtr, ParamArrayOffset)) ' Calculate SAFEARRAY**
        StealParams = PtrVarAry(ParamArrayPtr)                          ' Cast Ptr to Variant Array
        'AryRef(StealParams) = DeRef(ParamArrayPtr)
        DeRef(ParamArrayPtr) = vbNullPtr                                ' Zero original
    
        'Exit Function
    
        ' self modify
        WriteThunk AddressOf MParamArray.StealParams, _
                   &H4244C8B, &HD4C8B, &H18B, &HD233, &H1189, &H900004C2
        '&H4244C8B  ' mov ecx, [esp + 4]     ; FrameOffset -> ecx
        '&HD4C8B    ' mov ecx, [ebp + ecx]   ; [FramePtr + ecx] -> SAFEARRAY**
        '&H18B      ' mov eax, [ecx]         ; Deref SAFEARRAY** -> SAFEARRAY* (returned in eax)
        '&HD233     ' xor edx, edx           ; Zero the original SAFEARRAY* to keep runtime happy
        '&H1189     ' mov [ecx], edx         ; prevents crash on second de-allocation
        '&H900004C2 ' ret 4 / nop
    End Function
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Function FramePtr() As LongPtr
    ' Returns 32bit Frame Pointer (ebp)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Err.Raise E_CANT
    End Function
    
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Function PtrAdd(ByVal Pointer As LongPtr, ByVal Offset As Long) As LongPtr
    ' Unsigned Pointer addition avoids overflow when incrementing/decrementing accross 2G boundary
    ' NOTE: required when using /LARGEADDRESSAWARE on 64bit Windows
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        PtrAdd = (Pointer Xor &H80000000) + Offset Xor &H80000000
    End Function
    
    Private Function InIDE() As Boolean
        InIDE = (App.LogMode = 0&)
    End Function
    
    Public Function WriteThunk(ByVal Address As LongPtr, ParamArray Ops() As Variant) As Long
    
        Dim Op As Long
        For Op = 0 To UBound(Ops)
            Select Case VarType(Ops(Op))
                Case vbLong:    WriteThunk = WriteThunk + 4&
                Case vbInteger: WriteThunk = WriteThunk + 2&
                Case Else:      Err.Raise 5
            End Select
        Next
        
        Dim Protection As Long
        Dim OpAddress As Long
        
        If InIDE Then Address = DeRef(PtrAdd(Address, &H16))  ' skip debug stub
        
        VirtualProtect Address, WriteThunk, PAGE_EXECUTE_READWRITE, Protection
        
        OpAddress = Address
        For Op = 0 To UBound(Ops)
            Select Case VarType(Ops(Op))
                Case vbLong
                    DLng(OpAddress) = Ops(Op)
                    OpAddress = PtrAdd(OpAddress, 4)
                Case vbInteger
                    DInt(OpAddress) = Ops(Op)
                    OpAddress = PtrAdd(OpAddress, 2)
            End Select
        Next
        
        VirtualProtect Address, WriteThunk, Protection, Protection
    End Function
    I typically put at the top of my code files.
    Code:
    ' Copyright © 2017 Dexter Freivald. All Rights Reserved. DEXWERX.COM

  7. #7
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,412

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    Great, thanks a lot Dexwerx!

  8. #8
    Lively Member
    Join Date
    Jul 2015
    Location
    Poland (moved away from Belarus)
    Posts
    110

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    Hi, DEXWERX.

    Is there a code without TLB to steal paramarray into variant variable?

    Regards.

  9. #9
    PowerPoster
    Join Date
    Jan 2020
    Posts
    3,746

    Re: [VB6] VB6.tlb - Passing a ParamArray without Copying

    how to run this?


    Code:
    Private Sub Command1_Click()
    Dim a As Long, b As Long
    a = 33
    b = 44
    MsgBox CallAdd(a, b)
    End Sub
    Code:
    Private Function CallAdd(ParamArray Params()) As Long
        ' NOTE: offset calculation from Frame Pointer in a Class Module (.cls) or Form (.frm)
        ' saved ebp (4) + return address (4) + this ptr (4) + No Params before ParamArray (0)
        Dim ParamOffset As Long: ParamOffset = 4 + 4 + 4 + 0
        Dim StolenParams(): StolenParams = GetParams(ParamOffset)
    
        ' Use CallPtr to pass a Variant Array as a ParamArray
     CallAdd = CallPtr(AddressOf Add, vbLong, RefAry(StolenParams))
    End Function
    
    Function Add(a As Long, b As Long) As Long
        Add = a + b
    End Function

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width