5

With variant arrays where each element is a double array I am able to do the following:

Public Declare PtrSafe Sub CopyMemoryArray Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination() As Any, ByRef Source As Any, ByVal Length As Long)

Sub test()
    Dim vntArr() as Variant
    Dim A() as Double
    Dim B() as Double

    Redim vntArr(1 to 10)
    Redim A(1 to 100, 1 to 200)
    vntArr(1) = A
    CopyMemoryArray B, ByVal VarPtr(vntArr(1)) + 8, PTR_LENGTH '4 or 8
    'Do something
    ZeroMemoryArray B, PTR_LENGTH
End Sub

A and B will then point to the same block in memory. (Setting W = vntArr(1) creates a copy. With very large arrays, I want to avoid this.)

I'm trying to do the same, but with collections:

Sub test()
    Dim col as Collection
    Dim A() as Double
    Dim B() as Double

    Set col = New Collection
    col.Add A, "A"
    CopyMemoryArray B, ByVal VarPtr(col("A")) + 8, PTR_LENGTH '4 or 8
    'Do something
    ZeroMemoryArray B, PTR_LENGTH
End Sub

This sort of works, but for some reason the safe array structure (wrapped in Variant data type, similar to the variant array above) returned by col("A") only contains some exterior attributes like number of dimensions and dim boundaries, but the pointer to the pvData itself is empty, and so CopyMemoryArray call results in a crash. (Setting B = col("A") works fine.) Same situation with Scripting.Dictionary.

Does anyone know what's going on here? enter image description here


EDIT

#If Win64 Then
    Public Const PTR_LENGTH As Long = 8
#Else
    Public Const PTR_LENGTH As Long = 4
#End If

Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)

Private Const VT_BYREF As Long = &H4000&
Private Const S_OK As Long = &H0&

Private Function pArrPtr(ByRef arr As Variant) As LongPtr
    Dim vt As Integer

    CopyMemory vt, arr, 2
    If (vt And vbArray) <> vbArray Then
        Err.Raise 5, , "Variant must contain an array"
    End If
    If (vt And VT_BYREF) = VT_BYREF Then
        CopyMemory pArrPtr, ByVal VarPtr(arr) + 8, PTR_LENGTH
        CopyMemory pArrPtr, ByVal pArrPtr, PTR_LENGTH
    Else
        CopyMemory pArrPtr, ByVal VarPtr(arr) + 8, PTR_LENGTH
    End If
End Function

Private Function GetPointerToData(ByRef arr As Variant) As LongPtr
    Dim pvDataOffset As Long
    #If Win64 Then
        pvDataOffset = 16 '4 extra unused bytes on 64bit machines
    #Else
        pvDataOffset = 12
    #End If
    CopyMemory GetPointerToData, ByVal pArrPtr(arr) + pvDataOffset, PTR_LENGTH
End Function

Sub CollectionWorks()
    Dim A(1 To 100, 1 To 50) As Double

    A(3, 1) = 42

    Dim c As Collection
    Set c = New Collection

    c.Add A, "A"

    Dim ActualPointer As LongPtr
    ActualPointer = GetPointerToData(c("A"))

    Dim r As Double
    CopyMemory r, ByVal ActualPointer + (0 + 2) * 8, 8

    MsgBox r  'Displays 42
End Sub
drgs
  • 375
  • 2
  • 8
  • 1
    Not going to look deep into it, but it might be because one [has `VT_BYREF`](http://stackoverflow.com/a/32539884/11683) and the other does not. See e.g. http://stackoverflow.com/q/11713408/11683 for a more stable way to create arrays referring to the same data. – GSerg Apr 21 '17 at 21:28
  • Hmm, no, both vntArr(1) and col("A") have the same VarType = 8197 = 8192 (Array) + 5 (Double). With VT_BYREF it would have been 16384+8197 – drgs Apr 21 '17 at 21:44
  • Just to be sure, are you using VB's `VarType`? It hides `VT_BYREF` which is why I had to do it manually as above. – GSerg Apr 21 '17 at 21:56
  • I'm looking at type in Variant structure directly... *edited in main post* – drgs Apr 21 '17 at 22:02
  • This is one step further than you should look. You are trying to copy `VARIANT`'s member at offset 8 which is either `SAFEARRAY*` or `SAFEARRAY**`. You are not distinguishing them when [you should](http://stackoverflow.com/a/32539884/11683). – GSerg Apr 21 '17 at 22:12
  • Let me digest this – drgs Apr 21 '17 at 22:33
  • If I declare `arr(1 to 10) As Long`, I get ["true vartype"](http://stackoverflow.com/a/32539884/11683) of `0x6003`, which is `vbArray | vbLong | VT_BYREF`. If I then declare `v As Variant` and assign `arr` to it, I get true vartype of `0x2003` which is `vbArray | vbLong`. – GSerg Apr 21 '17 at 22:35
  • Items of a Collection are all variants by default. Can I add VT_BYREF to their type -- inside a Collection? – drgs Apr 21 '17 at 22:46
  • You cannot add anything to what Collection returns, but you can stop depending on whether or not there is `VT_BYREF` by studying the function I linked to several times. – GSerg Apr 22 '17 at 00:09
  • Anyway, it seems Collection object has some sort of "Item interface" if I can call it that, which only returns copies of what you specifically ask for. col("A")(1, 1) only returns that exact cell in the matrix... there are no values following behind it (as would normally be the case with pvData). – drgs Apr 22 '17 at 00:16
  • 2
    Do you have to store the array directly in the collection? I'd just create a class to hold the array and function to return the pointer, then just push the class into the collection. – Kelly Ethridge Apr 22 '17 at 17:18
  • @KellyEthridge That will certainly work if the goal is to only hold and pass the pointer around, however then you lose the ability to manipulate the array directly. It can't be made `public` in a VBA class, so access will need to be done with getters that will return a copy of either the entire array or an element. Depending on what happens to the array on the VBA side that might or might not be a problem. – GSerg Apr 22 '17 at 18:17

2 Answers2

7

VB is designed to hide complexity. Often that results in very simple and intuitive code, sometimes it does not.

A VARIANT can contain an array of non-VARIANT data no problem, such as an array of proper Doubles. But when you try to access this array from VB, you don't get a raw Double like it is actually stored is the blob, you get it wrapped in a temporary Variant, constructed at the time of access, specifically to not surprise you with the fact that an array declared As Variant suddenly produces a value As Double. You can see that in this example:

Sub NoRawDoubles()
  Dim A(1 To 100, 1 To 50) As Double
  Dim A_wrapper As Variant

  A_wrapper = A

  Debug.Print VarPtr(A(1, 1)), VarPtr(A_wrapper(1, 1))
  Debug.Print VarPtr(A(3, 3)), VarPtr(A_wrapper(3, 3))
  Debug.Print VarPtr(A(5, 5)), VarPtr(A_wrapper(5, 5))
End Sub

On my computer the result is:

88202488      1635820 
88204104      1635820 
88205720      1635820

Elements from A are in fact different and are located in memory where they should within the array and each one is 8 bytes in size, whereas "elements" of A_wrapper are in fact the same "element" - that number repeated three times is the address of the temporary Variant, 16 bytes in size, that is created to hold the array element and which the compiler decided to reuse.


That is why an array element returned in this way cannot be used for pointer arithmetic.

Collections themselves do not add anything to this problem. It's the fact that Collection has to wrap the data it stores in a Variant that messes it up. It would happen when storing an array in a Variant in any other place too.


To get the actual unwrapped data pointer suitable for pointer arithmetic, you need to query the SAFEARRAY* pointer from the Variant, where it can be stored with one or two levels of indirection, and take the data pointer from there.

Building on previous examples, the naive non-x64-compatible code for that would be:

Private Declare Function GetMem2 Lib "msvbvm60" (ByVal pSrc As Long, ByVal pDst As Long) As Long  ' Replace with CopyMemory if feel bad about it
Private Declare Function GetMem4 Lib "msvbvm60" (ByVal pSrc As Long, ByVal pDst As Long) As Long  ' Replace with CopyMemory if feel bad about it

Private Const VT_BYREF As Long = &H4000&

Private Function pArrPtr(ByRef arr As Variant) As Long  'Warning: returns *SAFEARRAY, not **SAFEARRAY
  'VarType lies to you, hiding important differences. Manual VarType here.
  Dim vt As Integer
  GetMem2 ByVal VarPtr(arr), ByVal VarPtr(vt)

  If (vt And vbArray) <> vbArray Then
    Err.Raise 5, , "Variant must contain an array"
  End If


  'see https://msdn.microsoft.com/en-us/library/windows/desktop/ms221627%28v=vs.85%29.aspx
  If (vt And VT_BYREF) = VT_BYREF Then
    'By-ref variant array. Contains **pparray at offset 8
    GetMem4 ByVal VarPtr(arr) + 8, ByVal VarPtr(pArrPtr)  'pArrPtr = arr->pparray;
    GetMem4 ByVal pArrPtr, ByVal VarPtr(pArrPtr)          'pArrPtr = *pArrPtr;
  Else
    'Non-by-ref variant array. Contains *parray at offset 8
    GetMem4 ByVal VarPtr(arr) + 8, ByVal VarPtr(pArrPtr)  'pArrPtr = arr->parray;
  End If

End Function

Private Function GetPointerToData(ByRef arr As Variant) As Long
  GetMem4 pArrPtr(arr) + 12, VarPtr(GetPointerToData)
End Function

Which then can be used in the following non-x64-compatible way:

Sub CollectionWorks()
  Dim A(1 To 100, 1 To 50) As Double

  A(3, 1) = 42

  Dim c As Collection
  Set c = New Collection

  c.Add A, "A"

  Dim ActualPointer As Long
  ActualPointer = GetPointerToData(c("A"))

  Dim r As Double
  GetMem4 ActualPointer + (0 + 2) * 8, VarPtr(r)
  GetMem4 ActualPointer + (0 + 2) * 8 + 4, VarPtr(r) + 4

  MsgBox r  'Displays 42
End Sub

Note that I am not sure that c("A") returns the same actual data every time as opposed to making copies as it pleases, so caching the pointer in this way may not be advised, and you might be better off first saving the result of c("A") into a variable and then calling GetPointerToData off that.

Obviously this should be rewritten to use LongPtr and CopyMemory, and I might do that tomorrow, but you get the idea.

Community
  • 1
  • 1
GSerg
  • 76,472
  • 17
  • 159
  • 346
  • I had to rewrite it to use RtlMoveMemory (msvbvm60 not found on my machine for some reason) -- see in my original post under EDIT at the bottom. It should work on either 32/64bits, but it still returns r with some gibberish numbers... – drgs Apr 22 '17 at 09:33
  • No there is something with Collections after all. Why does this work (32bit code): `A(1,1)=12 Dim B() As Double GetMem4 VarPtr(A_wrapper) + 8, VarPtr(B) MsgBox B(1,1)` – drgs Apr 22 '17 at 10:35
  • But not `GetMem4 VarPtr(c("A")) + 8, VarPtr(B)` ? Mechanics should be the same – drgs Apr 22 '17 at 14:36
  • 1
    @drgs This is because you are taking an address of a temporary (`c("A")` is a temporary), and trying to use that address in a different expression. That's like returning [address of a local variable](http://stackoverflow.com/a/6445794/11683) from a C++ function. If you first save the result of `c("A")` into a `Variant` it will work. In any case you probably don't realise just how much array copying is happening every time when you are preparing to clone the array, so it might be that you are actually making it worse. I would stop at using `GetPointerToData` for passing the blob address to C++. – GSerg Apr 22 '17 at 15:46
  • "Temporary variant"... Jesus. Ok, I made it work by saving c("A") into a Variant first, but you are right -- this creates a copy... I think the only way to get a direct pointer to A inside the collection is to [search through the collection by reading the first element and then to follow NextElement pointers etc](http://www.vbforums.com/showthread.php?338284-Resolved-How-To-Retrieve-Keys-from-Collection&p=2007969&viewfull=1#post2007969). – drgs Apr 22 '17 at 16:53
  • 1
    They are there somewhere... But the convenience of using key strings to lookup a variable is gone. I might end up with using collections to link key strings to LongPtr's, which point to arrays stored somewhere else. – drgs Apr 22 '17 at 16:54
1

It's easier if you treat both the base variables as Variant.

Option Explicit

#If Vba7 Then
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    Private Declare PtrSafe Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" (Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
#Else
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    Private Declare Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" (Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
#End If


Sub test()
    Dim col As Variant
    Dim B As Variant
    Dim A() As Double

    ReDim A(1 To 100, 1 To 200)
    A(1, 1) = 42
    Set col = New Collection
    col.Add A, "A"
    Debug.Print col("A")(1, 1)

    CopyMemory B, col, 16
    Debug.Print B("A")(1, 1)

    FillMemory B, 16, 0
End Sub

Also see these helpful links

Partial Arrays by reference

Copy an array reference in VBA

How do I slice an array in Excel VBA?

http://bytecomb.com/vba-reference/

Community
  • 1
  • 1
  • It's not `#If Win64`, it's `#If VBA7`. – GSerg Apr 21 '17 at 22:19
  • Yes, you're right. I originally copied from code using LongPtr which does need Win64. –  Apr 21 '17 at 22:27
  • No, it doesn't. The very point of `LongPtr` is that it exists on both Win32 and Win64 and means `Long` on the former and `LongLong` on the latter. The only compile-time check you ever need is `#If VBA7`. If it is, you use `LongPtr` regardless of bitness, if it isn't, you use `Long` regardless of bitness (because VBA prior to version 7 was exclusively 32-bit). – GSerg Apr 21 '17 at 22:30
  • Just a quick note, I need to keep B() as a double array. The first element of B is passed byref to some c++ dll declared functions which only accept a double pointer. (All in all, the idea is for these c++ calls to alter arrays stored inside a Collection object, and ideally as I mentioned without having to make copies of these arrays and then update them back into the Collection. Maybe I've over-complicated it...) – drgs Apr 21 '17 at 22:38
  • 1
    @drgs A `Variant` variable perfectly holds an array of doubles, and the pointer to the first element is a true pointer to doubles. You *can* take an extra effort to make sure a Variant variable contains an array of Variants each of which contains a Double, but you really don't need to. – GSerg Apr 21 '17 at 22:41
  • @GSerg - okay chill. I meant LongLong :) –  Apr 21 '17 at 22:42
  • Yes, it is, but how do i actually pass col("A")(1, 1) to a function which demands it to be explicitly a double by type? `Public Declare PtrSafe Sub dscal Lib "blas.dll" (ByRef n As Long, ByRef a As Double, ByRef x As Double, ByRef incx As Long)` + `dscal 100, 2, col("A")(1, 1), 1` will throw an error – drgs Apr 21 '17 at 22:52
  • If I change declaration `dscal Lib "blas.dll" (ByRef n As Long, ByRef a As Double, ByRef x As Variant, ByRef incx As Long)`, even if dll only accepts x as Double, would it actually work? – drgs Apr 21 '17 at 22:55
  • A dirty workaround assuming the data is always double is just `Debug.Print CDbl(B("A")(1, 1))`. I was assuming you wanted to copy the whole collection though? –  Apr 21 '17 at 23:03
  • Yes, but no longer the same pointer... I want to reference the double array inside a collection. No copying! :) – drgs Apr 21 '17 at 23:06
  • I think you need to update your question as it's not clear exactly what your attempting to do. –  Apr 21 '17 at 23:13
  • We got off track. The following works: B is a direct pointer to a double array stored in a variant array: `CopyMemoryArray B, ByVal VarPtr(vntArr(1)) + 8, PTR_LENGTH`... The following does not work, but only because of peculiarity of the Collection object: `CopyMemoryArray B, ByVal VarPtr(col("A")) + 8, PTR_LENGTH`... – drgs Apr 21 '17 at 23:23
  • It seems that col("A")(1, 1) is not a real representation of the array which is stored with key "A"... Ie. it's a dummy/copy which is invoked only on access... dbl = col("A")(1, 1) works fine, but to copy a double value `CopyMemory dbl, ByVal VarPtr(col("A")(1, 1)), 8` returns gibberish... Likewise to get the next double value in pvData `CopyMemory dbl, ByVal VarPtr(col("A")(1, 1))+8, 8` does not work either. I think it's done intentionally for performance. – drgs Apr 22 '17 at 00:14