March 12, 2024

VBA ParamArray and the built-in Array() function

Last modified: Tue Mar 12 22:02:10 UTC+0100 2024 © Attila Tarpai

ParamArray and the built-in Array() function

ParamArray is a VBA language construct to define a Sub/Function with variable number of arguments. ParamArray is an Optional argument, it must be declared last as Array of Variant with the ParamArray keyword. Optional|ByVal|ByRef is not valid.

ParamArray is essentially an Array built by the caller from zero or more arguments:

 arg1    arg2    arg3    arg4    arg5   ...     call "1st", 100                     <-- without optional parameters
  |       |       |       |       |      |      call "2nd", 200, coll, Array(1,2,3) <-- 2 optional parameters
  |       |       |_______|_______|______|      call "3rd", 300, 9,8,7,6,5          <-- 5 optional parameters
  |       |       |caller construct
  |       |       |
  V       V       V

param1  param2  ParamArray                      Sub (s As String, i As Integer, ParamArray p())
                  |_ param3
                  |_ param4
                  |_ param5
                  |_ ...

The Sub expects 3 pointers from the caller (all declared implicite ByRef). ParamArray is passed as any other Array, an address of a PSA - Pointer to SAFEARRAY structure. It is Dynamic Array but has some compile-time restrictions:

  • cannot be passed further as ByRef Variant
  • cannot ReDim/Erase

ParamArray's PSA is always valid and points to an allocated SAFEARRAY structure. This can be an empty array, which shows up as Variant(0 to -1) in Debug Windows (because cElements=0 of the SAFARRAYBOUND structure).

The Sub/Function accesses ParamArray's elements similar to elements of any other array: p(0), p(1) or For..Next/For Each.

ParamArray is ByRef

The specialty of ParamArray is how arguments are converted to Variants by the caller. Being parameters it follows the principle of passing ByRef, the default mechanism in VBA.

Consider a Sub with ParamArray:

Sub fParamArray(ParamArray a())
    a(0) = 100
    a(2) = "H"
End Sub

and another one calling fParamArray with 4 arguments:

Sub passParamArray()
    Dim L As Long, s As String
    L = 1
    s = "h"
    Debug.Print L, s
    fParamArray L, 254, s, "hi"
    Debug.Print L, s
End Sub

It prints the following showing L and s were passed ByRef and has changed the caller's local variables:

 1            h
 100          H

64-bit VBA ParamArray ByRef BUG

It concerns only one variable data type: LongLong. When we try to change a LongLong variable passed in ParamArray there is this error message:

Variable uses an Automation type not supported in Visual Basic (Error 458)

The test code:

Sub fParamArray(ParamArray a())
    a(0) = -1
End Sub

Sub passParamArray()
    Dim LL As LongLong
    fParamArray LL   ' ERROR
    fParamArray 10^  ' OK
End Sub

All other data types are writable, as above. Passing LongLong literal in ParamArray raises no error - but it's also meaningless and has no effect to change a temp Variant/LongLong variable made by the caller. My only guess is that in this particular case they forgot to extend to accept the VTYPE = 4014h (VT_BYREF|vbLongLong) Variant type for 64-bit VBA. VBA uses a subset of the VTYPE enum values defined by COM and there is always a run-time check.

The built-in Array() function

Member of VBA._HiddenModule and declared as:

Function Array(ParamArray ArgList() As Variant) As Variant

Array() takes its arguments, converts them to Variants and puts them into a Variant Array. Then returns the array in a Variant.

The array returned is dynamic and can be ReDim-ed. The Variant is always of array type even when Array() has been called without arguments.

What's really gonna bake your noodle is that Array() is declared with one ParamArray. So caller constructs the ParamArray as above, passes the address of the result array to Array(), which will assign this array to Variant and that is the return value. Although parameters from variables are received ByRef, the array-assigment-to-Variant operation will go through every element and performs assigment-to-Variant for each one using (possible deep) value copy. This is normal operation of array assignment in VBA when the FADF_VARIANT Feature Flag is set in the SAFEARRAY structure.

ParamArray vs Array()

So there is a considerable difference passing arguments in ParamArray vs using Array() due to how the mechanism builds up the Variant array and fill in with values.

Both takes its arguments and converts them to Variant implicitely but

ParamArray:

  • for variables other than Variant it creates a VT_BYREF-Variant with a pointer to the variable - equivalent to pass-as-Variant
  • for literals it uses assigment-to-Variant

Array():

  • uses assigment-to-Variant for every argument passed. Assignment-to-Variant is value copy and for String and Array datatypes, that are value types in VBA, it makes a deep copy (note BSTR2).
fParamArray L, 254, s, "hi"                         Array(L, 254, s, "hi")


ParamArray                                          Array()
  |   ______________       _____                      |   ______________
  |_ |0340_____ptr__| --> |___1_|                     |_ |0300_______1__|
  |   VT_BYREF             L                          |
  |   ______________                                  |   ______________
  |_ |0200_____254__|                                 |_ |0200_____254__|
  |                                                   |
  |   ______________       __________                 |   ______________
  |_ |0840_____ptr__| --> |__->BSTR1_|                |_ |0800__->BSTR2_|
  |   VT_BYREF             s                          |
  |   ______________                                  |   ______________
  |_ |0800__->BSTR__|                                 |_ |0800__->BSTR__|

         Variants                                            Variants

VT_BYREF Variant for variables                      All Assigment-to-Variant
Assigment-to-Variant only for literals

Memory dump of these SAFEARRAY structures

Lets construct a mimic Array() function first based on the ParamArray-Variant assignment assumption and then compare the outcome:

Function Array2(ParamArray ArgList() As Variant) As Variant
    Array2 = ArgList
End Function

These outputs were made using vbadump.bas (GitHub). Comparison of SAFEARRAY memory dumps between ParamArray, Array() and UDF Array2():

Passing 4 parameters:                              Passing empty:

--------------------------------------------       --------------------------------------------
fParamArray L, 254, s, "hi"                        fParamArray

SAFEARRAY STRUCTURE:                               SAFEARRAY STRUCTURE:
Offs          Name          Value Hex              Offs          Name          Value Hex
-4            VbVarType     00000000               -4            VbVarType     00000000
 0            cDims         0001                    0            cDims         0001
 2            fFeatures     0880                    2            fFeatures     0880
 4            cbElements    00000018                4            cbElements    00000018
 8            cLocks        00000000                8            cLocks        00000000
 16           pvData        000001CCC23B9DA0        16           pvData        000001CCC95651C0
 24           cElements     00000004                24           cElements     00000000
 28           lLbound       00000000                28           lLbound       00000000
Flags: FADF_VARIANT|FADF_HAVEVARTYPE               Flags: FADF_VARIANT|FADF_HAVEVARTYPE

--------------------------------------------       --------------------------------------------
Array(L, 254, s, "hi")                             Array()

SAFEARRAY STRUCTURE:                               SAFEARRAY STRUCTURE:
Offs          Name          Value Hex              Offs          Name          Value Hex
-4            VbVarType     0000000C               -4            VbVarType     00000000
 0            cDims         0001                    0            cDims         0001
 2            fFeatures     0880                    2            fFeatures     0880
 4            cbElements    00000018                4            cbElements    00000018
 8            cLocks        00000000                8            cLocks        00000000
 16           pvData        000001CCC23B9E80        16           pvData        0000000000000000
 24           cElements     00000004                24           cElements     00000000
 28           lLbound       00000000                28           lLbound       00000000
Flags: FADF_VARIANT|FADF_HAVEVARTYPE               Flags: FADF_VARIANT|FADF_HAVEVARTYPE
--------------------------------------------       --------------------------------------------

UDF Array2(L, 254, s, "hi")                        UDF Array2()

SAFEARRAY STRUCTURE:                               SAFEARRAY STRUCTURE:
Offs          Name          Value Hex              Offs          Name          Value Hex
-4            VbVarType     0000000C               -4            VbVarType     00000000
 0            cDims         0001                    0            cDims         0001
 2            fFeatures     0880                    2            fFeatures     0880
 4            cbElements    00000018                4            cbElements    00000018
 8            cLocks        00000000                8            cLocks        00000000
 16           pvData        000001CCC8F7E830        16           pvData        0000000000000000
 24           cElements     00000004                24           cElements     00000000
 28           lLbound       00000000                28           lLbound       00000000
Flags: FADF_VARIANT|FADF_HAVEVARTYPE               Flags: FADF_VARIANT|FADF_HAVEVARTYPE
--------------------------------------------       --------------------------------------------

Some interesting observations:

  • The arrays returned by Array() and our Array2() function are identical: so we guessed just about right. Both are Dynamic and can be ReDim-ed.
  • Calling Array() without arguments is legal in VBA and will return a special SA: the element number is zero - it is a copy of the empty ParamArray.
    This appears as Variant(0 to -1) in Debug windows.
  • FADF_HAVEVARTYPE is set for all these arrays but VbVarType at -4 is not updated for all.
  • in array assigment, when rvalue has zero VbVarType but proper Flags, the lvalue array will have proper VbVarType value set (Array2 = ArgList)
  • Empty ParamArray pvData is set and points to..? No idea where.

No comments:

Post a Comment