Results 1 to 11 of 11

Thread: Array Information

  1. #1

    Thread Starter
    Hyperactive Member
    Join Date
    Feb 2015
    Location
    Colorado USA
    Posts
    271

    Array Information

    Below is a follow-up for a discussion here on vbForums.

    Every array in VB is managed by VB through the use of a SFAEARRAY as defined here by Microsoft as part of Windows. Each array is assigned a SAFEARRAY structure when it is created. The SAFEARRAY contains information on the array itself, not the contents of the array. This structure contains everything needed for VB manage the array including the number of dimensions, byte size of the elements in the array, how many references have been made to the array, the lower bound of each dimension and the number of elements in the dimension and the location in memory of the data in the array.


    Once an array has been dimensioned with DIM, this SAFEARRAY also contains a flag that says it cannot be ReDim'd. On the other hand, if the array is declared in the DIM statement without array dimension and bounds, it can be ReDim'd as often as needed during the program’s life. An array with its bounds set by Dim statement are called static arrays and those that have their bounds set via ReDim are called dynamic arrays.

    Other than the flag telling VB that it is a static array and can’t be re-dimensioned, these types of arrays use the same SAFEARRAYs.

    Many types of arrays are possible: Byte, Integer, Long, Single, Double, Date, Currency, Boolean, String and Object. In VBA for office 2010+ there can also be LongLong arrays when running in a 64-bit host like Excel. In Office 2010+ arrays can be declared as LongPtr but during run-time those are set to 32-bit if the host is 32-bit or LongLong if the host is 64-bit.

    Other than Variants, all arrays are of fixed element size (a Variant can re-used as a single value or a different type of array or even arrays within arrays). An array can contain a large number of Longs, for example, but it cannot have Longs and Strings in the same array.

    A Variant doesn’t have to be an array but it can be. Strangely, any member of a Variant array itself can be an array of any type, including more Variants.

    VB provides almost all of the tools required to work with arrays in its native language so we normally don’t have to be concerned with the SAFEARRAY structure. You can access individual elements of an array by specifying the subscripts. You can make an array virtually any size you want and you can set the lower bound to start with anything you want (doesn’t have to be 0).

    However, there are a few limitations that are problematic. A dynamic array might have one or more dimensions assigned to it or it might be uninitialized (occurs initially during a run and after an Erase statement). There are no VB methods to find out this information. The standard way of dealing with this is to just try to use it and if it is uninitialized it will raise an error that you can catch and deal with.

    Also, you can’t determine at any point in time how many dimensions the dynamic array has, if any. Once you know the number of dimensions you can use LBound and UBound to determine the bounds of each array. Again, the only way to find out is to attempt to use it and catch the error VB raises.

    So for many years most of us have used a function that returns the number of dimensions. Under the hood, this function finds the location of the SAFEARRAY and it reads the first 2 bytes which contain the number of dimensions for the array (0 if uninitialized). So you are probably accessing the SAFEARRAYs of some of your arrays without even knowing it.

    There is a problem with the routines I have seen and used for this. They all use a passed parameter that is a Variant. Strangely enough, VB allows you to basically use any variable for the Variant parameter to a procedure and if it is not a Variant, VB makes a copy of it (such as a Long array) and wraps it up in a Variant structure for that procedure. So you can be wasting a lot of memory and time to make a copy of the non-Variant array just so VB can pass a Variant to the function you use to get the number of dimensions. So I wrote a set of functions that you can use for any type of VB array to find out how many dimensions it has and optionally, all of the rest of the information contained in its SAFEARRAY. It is much faster and efficient that the variant type (I have a function for Variants too) because it uses a reference to the array and does not have to copy it into a Variant.

    You would think that it would be possible to pass the name of any kind of array to a function and have it figure out all of the array parameters. But no, in every VB procedure you have to declare the Type of each parameter passed to/from the procedure. So if I have an array called “anArray” that is an array of Longs, I can send it only to a Sub or Function that has “xxx() as Long” as a parameter. That makes things somewhat more complex. All I really need to know about the array is the internal location of the array (a pointer, I know, VB doesn’t do pointers…). Even worse, although VB has functions for pointers to Strings, regular variables and objects, it does not have a function for the address of an array. So we play a game and re-use the VarPtr function (a function that returns the address of a regular variable) and I Alias it to something I call “VarPtrArray” and have that tell us the address of the array, not a regular variable.

    So each of the Functions I am providing just calls another routine I wrote that does the actual getting of the array information and passes the address of the array to it. So if you want to skip all of the individual type functions (one for a Byte array, one for a Long array, etc.) you can just call the main function (“GetArrayDims” and pass it the address of your array such as “VarPtrArray(anArray)”. Since that’s not normal VB language, I provided all of those other functions for you that use the more conventional name and type of the array. Either way works.

    The same set of functions work in VB6, 32-bit VBA and 64-bit VBA. Note that the function LongPtrNumDims is not available if you are running VB6 or Office pre-2010 because the LongPtr type did not exist in those. Also, LongLongNumDims is only available if you’re running 64-bit Office for the same reason.

    There is a function (VarNumDims) that determines if a variant is an array and if so, returns the exact same information as the other array info routines. Note that it is possible that the Variant doesn’t contain an array in which case it returns -1 instead of 0 or more dimensions.

    I wrote these for 2 reasons: 1) I wanted a more efficient way of getting the number of dimensions than using the one with passing a copy of my array through a Variant, and 2) I am working on a set of routines that lets you move whole variables and UDT’s between programs or to/from files and I needed to transfer a whole array at one time so I needed to know the memory address of the data block so I can copy it in one move. More on this later.

    Functions
    VarNumDims – for Variants
    ByteNumDims – for Byte arrays
    IntNumDims – for integer arrays
    LongNumDims –for Long arrays
    SingleNumDims – for Single arrays
    DoubleNumDims – for Double arrays
    DateNumDims – for Date arrays
    CurrNumDims – for Currency arrays
    BoolNumDims – for Boolean arrays
    StringNumDims – for String arrays
    LongPtrNumDims – for LongPtr arrays (only on VBA7, i.e., Office 2010+)
    LongLongNumDims – for LongLong arrays (only on VBA7 and running 64-bit)
    ObjNumDims – for Object arrays
    GetArrayDims - All of the above functions except StringNumDims call this one. It can also be called directly by all except Variants and String arrays. Instead of calling this with an array type you call it with VarPtrArray(array) which is what the above functions do anyway. Be careful not to give VarPtrArray a non-array variable; it will return the pointer (address) of any variable it is given). Also, do not send a Variant directly to this function since a Variant is laid out in memory differently than other variables including arrays. Use VarNumDims for Variants. There is a third parameter for this function which you should not set yourself. It is set False for all arrays except for a special Variant array (they call it a ByRef Variant array but it is not the same ByRef as in a procedure call).

    Return - The number of dimensions in the array.
    0 The array has no dimensions. It is uninitialized (start of run or Erase’d).
    >0 The number of dimensions in the array.
    -1 Only for a Variant. The Variant is not an array.

    Caution – The functions give a snapshot of the array at the time you execute the function. It isn’t dealing with the data in the array but only the structure of the array. This structure information is accurate until the variable goes out of scope and is deleted or the array is Redim’d or Erase’d. As the programmer, you are in charge of ReDim’s so you can re-run any of the functions as needed. Just know the data are not live but are a snapshot in time.

    Optional parameter “GetExtraInfo” – Defaults to False but if set True, generates more info about he specified array. There is a Public User Defined Type (UDT) called tArrayInfo and a Public variable called “ArrayInfo” of this type that is discussed below. Some data can always be found in this variable after one of these function calls (taken from the array’s SAFEARRAY) and there are a few more things you can obtain by calling one fo the functins with GetExtraInfo = True.

    Public Type tArrayInfo - see variable ArrayInfo just below this that uses this Type
    Size As Long ' Extra info, size of data ni the array
    NumElements As Long ' Extra Info, Number of elements in all array dimenstions
    Bounds() As Long – Extra Info, pairs of Lower/Upper bounds, # pairs = # Dims
    Example- Dim a(1 to 2, 0 to 4, 99 to 100) is put in pairs in this order 99,100,0,4,1,2
    cDims As Integer - The number of dimensions
    Features As Integer – Combination of the following possibilities
    0x0001 Array is allocated on the stack.
    0x0002 Array is statically allocated.
    0x0004 Array is embedded in a structure.
    0x0010 Array may not be resized or reallocated.
    0x0020 The SAFEARRAY MUST contain elements of a UDT.
    0x0040 The SAFEARRAY MUST contain MInterfacePointers elements.
    0x0080 An array that has a variant type.
    0x0100 An array of BSTRs.
    0x0200 An array of IUnknown*.
    0x0400 An array of IDispatch*.
    0x0800 An array of VARIANTs.
    0xF0E8 Bits reserved for future use.
    ElementSize As Long - The size of a single element
    cLocks As Long - Number of locks on the array
    pvData As Long/LongLong - Pointer to the array data.

    The Public variable ArrayInfo is of the above type. Obviously, the values in the variable only mean something if it’s an array (variant might not be) and the array has dimensions. It made more sense to me to re-use this public variable for each of the functions rather than having a bunch of variables of this type. If for some reason you need more than one of these at the same time you can easily declare another variable of the same type and copy ArrayInfo as needed.

    None of the code provided needs to be modified for your use. I have a master library of procedures I use all the time and I have incorporated this code into my library. You can do the same or you can leave it in its own .bas module as it is now.

    I have included sample programs for VB6 program as well as Excel file. Hopefully everything is clear. If not, please let me know.

    Update to v1.1.0 - See posts #5 and #6 below. Apparently VB handles String arrays differently so I modified my routine for getting array info for strings. Note that the old way works for static string arrays (one where the number of arrays and lower/upper bounds are set with a Dim statement instead of a ReDim statement).
    Attached Files Attached Files
    Last edited by MountainMan; Mar 18th, 2022 at 03:40 PM. Reason: Modified array info routine for String arrays

  2. #2
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    10,909

    Re: Array Information

    For everything but BSTR arrays and a Variant containing an array, I just use the following:

    Code:
    
    Option Explicit
    '
    Public Declare Function ArrPtr Lib "msvbvm60" Alias "VarPtr" (a() As Any) As Long
    Public Declare Function GetMem2 Lib "msvbvm60" (ByRef Source As Any, ByRef Dest As Any) As Long ' Always ignore the returned value, it's useless.
    Public Declare Function GetMem4 Lib "msvbvm60" (ByRef Source As Any, ByRef Dest As Any) As Long ' Always ignore the returned value, it's useless.
    '
    
    Public Function ArrayDims(pArray As Long) As Integer
        ' Won't work with BSTR arrays nor a Variant containing an array.
        ' Works with arrays of variants, of all other intrinsic types, of objects, and of fixed length strings.
        ' Example:  Debug.Print ArrayDims(ArrPtr(SomeArray()))
        ' Returns 0 if not dimensioned.
        '
        Dim pSA As Long
        If pArray = 0& Then Exit Function
        GetMem4 ByVal pArray, pSA                                       ' De-reference.
        If pSA = 0& Then Exit Function                                  ' Dynamic undimensioned array.
        GetMem2 ByVal pSA, ArrayDims
    End Function
    
    
    On BSTR arrays and a Variant containing an array, different approaches must be taken.

    -----

    Here's some test code that can be thrown into a Form1:
    Code:
    
    Option Explicit
    
    Private Sub Form_Load()
        Dim a() As Long
    
        Debug.Print "a()", ArrayDims(ArrPtr(a))
        ReDim a(1, 2, 3, 4, 5)
        Debug.Print "a()", ArrayDims(ArrPtr(a))
    
    End Sub
    
    
    Last edited by Elroy; Mar 18th, 2022 at 12:48 AM.
    Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.

  3. #3

    Thread Starter
    Hyperactive Member
    Join Date
    Feb 2015
    Location
    Colorado USA
    Posts
    271

    Re: Array Information

    Elroy,

    What you show is essentially how I handled everything except variant arrays (I fetch a bit more data from the SAFEARRAY structure than the 2 bytes for # of dimensions).

    BTW, you can get the array info for String arrays as well using your or my technique (getting the actual strings in a contiguous blob is a lot more arduous because the string data is not contiguous and it isn't part of the array "blob". But just getting number of dimensions etc. works for String arrays just like for other non-Variant arrays since they live in SAFEARRAYs as well.

    I also put in the code for LongLong's which I know is very similar to what you show except it has a an 8 byte pointer instead of a 4 byte one and it obviously doesn't matter for VB6. Same is true for an array of LongPtr's except you have to figure out whether it is an array of 64 or 32-bit values at run-time.

  4. #4
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    10,909

    Re: Array Information

    Quote Originally Posted by MountainMan View Post
    I also put in the code for LongLong's which I know is very similar to what you show except it has a an 8 byte pointer instead of a 4 byte one...
    Just as an FYI, you can do LongLongs in VB6 (in a Variant, similar to Decimals), so you probably want to be a bit more explicit about this, being 32-bit versus 64-bit, and not exactly about the LongLongs. Now, regarding LongPtrs, yeah, you're correct, as they're 32-bit or 64-bit depending on the bitness of how they're compiled in the VBA.
    Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.

  5. #5
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    10,909

    Re: Array Information

    Quote Originally Posted by MountainMan View Post
    BTW, you can get the array info for String arrays as well using your or my technique
    And, you can't use this method with dynamic string arrays (as I stated). To test, just change my Dim a() As Long to Dim a() As String, and watch it fail.

    There are two ways to do this: 1) is to use a TypeLib (as discussed in this thread), or 2) to temporarily shove the dynamic string array into a Variant (as shown here).
    Last edited by Elroy; Mar 18th, 2022 at 01:35 PM.
    Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.

  6. #6

    Thread Starter
    Hyperactive Member
    Join Date
    Feb 2015
    Location
    Colorado USA
    Posts
    271

    Re: Array Information

    That's strange. I looked all over the web for how to deal with SFEARRAYs and String arrays and I haven't found anything reasonable yet. In my sample I had a string array but it was a static array. When I added in the case for the dynamic array I get what you describe, basically not finding the SAFEARRAY. So I have modified my code for a string array to do it the old fashioned way, I check the LBound of each dimension until I get an error and then the # of Dimensions is 1 less. I haven't timed it yet but I think it is quicker and more efficient than copying the string array into a Variant although that technique works.

    There's got to be a SAFEARRAY out there that VB/Windows uses for dynamic string arrays but I haven't found it yet...

  7. #7
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    10,909

    Re: Array Information

    Here, I went ahead and put it together for the thread.

    Code:
    
    Option Explicit
    '
    Public Declare Function GetMem2 Lib "msvbvm60" (ByRef Source As Any, ByRef Dest As Any) As Long ' Always ignore the returned value, it's useless.
    Public Declare Function GetMem4 Lib "msvbvm60" (ByRef Source As Any, ByRef Dest As Any) As Long ' Always ignore the returned value, it's useless.
    '
    
    Public Function BstrArrayDims(vArray As Variant) As Integer
        ' Even though the argument is a variant,
        ' you should pass in a dynamic String array,
        ' as that's what this is for.
        '
        Const VT_ARRAY As Integer = &H2000                                  ' Variant type constants.
        Const VT_BYREF As Integer = &H4000
        Const VT_BSTR As Integer = &H8
        '
        Dim ppSA As Long
        GetMem4 ByVal VarPtr(vArray) + 8&, ppSA                             ' Dig array pointer out of Variant.
        If ppSA = 0& Then Err.Raise 13&, "An array wasn't passed to BstrArrayDims."
        Dim vt As Long                                                      ' Dig variant type out of Variant.
        GetMem2 vArray, vt
        If vt And VT_ARRAY = 0 Then Err.Raise 13&, "An array wasn't passed to BstrArrayDims."
        If vt And VT_BSTR = 0 Then Err.Raise 13&, "It wasn't a BSTR array passed to BstrArrayDims."
        '
        Dim pSA As Long
        If vt And VT_BYREF Then GetMem4 ByVal ppSA, pSA Else pSA = ppSA     ' Deals with Variant ByRef or ByVal.
        If pSA = 0& Then Exit Function                                      ' Not dimensioned (Erased).
        '
        GetMem2 ByVal pSA, BstrArrayDims                                    ' This is what we want.
    End Function
    
    

    And here's a bit of test code that can be thrown into a Form1:

    Code:
    
    Option Explicit
    
    Private Sub Form_Load()
    
        Dim bstr() As String
        Debug.Print BstrArrayDims(bstr)
        ReDim bstr(1, 2, 3, 4, 5)
        Debug.Print BstrArrayDims(bstr)
    
    End Sub
    
    -------------

    And as a note, either BstrArrayDims or ArrayDims could be used to see if an array is dimensioned. However, you've got to be careful with arrays that are 0 to -1 dimensioned. Most people like to consider those as undimensioned, but these functions will return 1 dimension for those.
    Last edited by Elroy; Mar 18th, 2022 at 03:56 PM.
    Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.

  8. #8
    PowerPoster
    Join Date
    Jul 2010
    Location
    NYC
    Posts
    7,653

    Re: Array Information

    Very helpful, just one note:

    Quote Originally Posted by MountainMan View Post
    Once an array has been dimensioned with DIM, this SAFEARRAY also contains a flag that says it cannot be ReDim'd. On the other hand, if the array is declared in the DIM statement without array dimension and bounds, it can be ReDim'd as often as needed during the program’s life. An array with its bounds set by Dim statement are called static arrays and those that have their bounds set via ReDim are called dynamic arrays.

    Other than the flag telling VB that it is a static array and can’t be re-dimensioned, these types of arrays use the same SAFEARRAYs.
    I know at least 1d fixed arrays in a UDT don't use a SAFEARRAY.


    This can be seen with 2 types, 1 with a fixed array, 1 with a variable length array:

    Code:
    Private Type fx
        a As Long
        b(0 To 3) As Long
    End Type
    Private Type vr
        a As Long
        b() As Long
    End Type
    When we run the following code:

    Code:
    Dim aa As fx
    Dim bb As vr
    
    ReDim bb.b(0 To 3)
    
    Debug.Print "VarPtr aa=" & VarPtr(aa)
    Debug.Print "VarPtr aa.b(0)=" & VarPtr(aa.b(0))
    
    Debug.Print "VarPtr bb=" & VarPtr(bb)
    Debug.Print "VarPtr bb.b(0)=" & VarPtr(bb.b(0))
    We get

    VarPtr aa=1701824
    VarPtr aa.b(0)=1701828
    VarPtr bb=1701816
    VarPtr bb.b(0)=86301424


    As you see, in aa, the fixed array, element 0 of array b is exactly where you would expect it in a regular, not safe, array, 4 bytes past the start of the structure.

    Meanwhile in bb, we find element 0 of array b is nowhere near the start of the structure, because the structure is actually a Long and a SAFEARRAY, and the data is stored elsewhere.

    So if your code was used to try to get info on a fixed/static array in a UDT, it will fail. Why? Well, let's look into what you normally see, like when you put a variable length array in...
    Last edited by fafalone; Apr 16th, 2022 at 09:56 AM.

  9. #9
    PowerPoster
    Join Date
    Jul 2010
    Location
    NYC
    Posts
    7,653

    Re: Array Information

    As a bonus investigation, let's break down how Type fx and bb work in the above code.

    "b() As Long" is actually a 4-byte pointer to a 1D SAFEARRAY; we declare the SAFEARRAY as follows:

    Code:
    Private Type SAFEARRAY1D
        cDims           As Integer
        fFeatures       As Integer
        cbElements      As Long
        cLocks          As Long
        pvData          As Long
        cElements       As Long
        lLbound         As Long
    End Type
    That's 24 bytes.

    So, if we copy the 4 bytes that start after the first 4 bytes of the structure to a Long,

    Code:
    Dim saptr As Long
    Dim sa1 As SAFEARRAY1D
    
    CopyMemory saptr, ByVal VarPtr(bb) + 4&, 4&
    We now have a pointer to our SAFEARRAY, so we can fill in sa1... and see where pvData points to

    Code:
    CopyMemory sa1, ByVal saptr, 24&
    Debug.Print sa1.pvData
    We run the code, and get
    VarPtr aa=1700168
    VarPtr aa.b(0)=1700172
    VarPtr bb=1700160
    VarPtr bb.b(0)=298402848
    pvData=298402848


    Well look, we found VarPtr(bb.b(0))

    But what about aa.b? If it was really a SAFEARRAY, pvData would show VarPtr(aa.b(0)), right?

    So, if we fill the sa1 using ArrPtr(aa.b) for saptr, what happens? Well, you either get 0 or VB crashes, because it's not a pointer to a SAFEARRAY, so ArrPtr chokes, because it attempts to treat it as a SAFEARRAY, and things go haywire. The address it returns is simply the pointer minus 20. Which is valid for a SAFEARRAY; here, no so much.
    Last edited by fafalone; Apr 16th, 2022 at 10:18 AM.

  10. #10

    Thread Starter
    Hyperactive Member
    Join Date
    Feb 2015
    Location
    Colorado USA
    Posts
    271

    Re: Array Information

    Interesting. It's sort of like how a fixed-length string is not really a BSTR but instead jsut a string of bytes. It looks like the VB designers made a fixed array in a Type behave totally differently than a normal array which uses SAFEARRAY.

  11. #11
    Fanatic Member
    Join Date
    Mar 2019
    Posts
    515

    Re: Array Information

    or it might be uninitialized (occurs initially during a run and after an Erase statement). There are no VB methods to find out this information. The standard way of dealing with this is to just try to use it and if it is uninitialized it will raise an error that you can catch and deal with.
    The below saves throwing and catching.

    If Not Not arrayName then
    Its dimensioned
    else
    it not dimensioned
    end if

Tags for this Thread

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