[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
Last edited by DEXWERX; Mar 23rd, 2017 at 09:19 AM.
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.
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.
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?
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