11

I'm designing a dynamic buffer for outgoing messages. The data structure takes the form of a queue of nodes that have a Byte Array buffer as a member. Unfortunately in VBA, Arrays cannot be public members of a class.

For example, this is a no-no and will not compile:

'clsTest

Public Buffer() As Byte

You will get the following error: "Constants, fixed-length strings, arrays, user-defined types and Declare statements not allowed as Public members of object modules"

Well, that's fine, I'll just make it a private member with public Property accessors...

'clsTest

Private m_Buffer() As Byte

Public Property Let Buffer(buf() As Byte)
    m_Buffer = buf
End Property

Public Property Get Buffer() As Byte()
    Buffer = m_Buffer
End Property

...and then a few tests in a module to make sure it works:

'mdlMain

Public Sub Main()
    Dim buf() As Byte
    ReDim buf(0 To 4)

    buf(0) = 1
    buf(1) = 2
    buf(2) = 3
    buf(3) = 4


    Dim oBuffer As clsTest
    Set oBuffer = New clsTest

    'Test #1, the assignment
    oBuffer.Buffer = buf    'Success!

    'Test #2, get the value of an index in the array
'    Debug.Print oBuffer.Buffer(2)   'Fail
    Debug.Print oBuffer.Buffer()(2)    'Success!  This is from GSerg's comment

    'Test #3, change the value of an index in the array and verify that it is actually modified
    oBuffer.Buffer()(2) = 27
    Debug.Print oBuffer.Buffer()(2)  'Fail, diplays "3" in the immediate window
End Sub

Test #1 works fine, but Test #2 breaks, Buffer is highlighted, and the error message is "Wrong number of arguments or invalid property assignment"

Test #2 now works! GSerg points out that in order to call the Property Get Buffer() correctly and also refer to a specific index in the buffer, TWO sets of parenthesis are necessary: oBuffer.Buffer()(2)

Test #3 fails - the original value of 3 is printed to the Immediate window. GSerg pointed out in his comment that the Public Property Get Buffer() only returns a copy and not the actual class member array, so modifications are lost.

How can this third issue be resolved make the class member array work as expected?

(I should clarify that the general question is "VBA doesn't allow arrays to be public members of classes. How can I get around this to have an array member of a class that behaves as if it was for all practical purposes including: #1 assigning the array, #2 getting values from the array, #3 assigning values in the array and #4 using the array directly in a call to CopyMemory (#3 and #4 are nearly equivalent)?)"

Blackhawk
  • 5,984
  • 4
  • 27
  • 56
  • I ran accross this issue while building a data structure to buffer outgoing messages for some networking code. I was eventually able to resolve it, but since I didn't find the question on SO already, I figured I'd add it. If anyone knows the answer feel free to add it! If someone else gets it before I am able to post the answer, I'll accept. – Blackhawk Aug 15 '14 at 15:10
  • If you actually meant to copy the array each time, then you forgot [the parentheses](http://stackoverflow.com/q/5613564/11683), `oBuffer.Buffer()(2)`. Otherwise you might want to have a look e.g. [here](http://stackoverflow.com/a/16864377/11683). – GSerg Aug 15 '14 at 15:19
  • @GSerg That's the solution I realized - I could do something like changing the accessor to `Public Property Get Buffer(Index As Long) As Byte` if all I wanted to do was access individual items in the array, but in my case I need to use the buffer in a `CopyMemory` statement, so that wouldn't work. – Blackhawk Aug 15 '14 at 16:06
  • @GSerg I've actually been playing around with creating my own `clsByteArray` class using `HeapAlloc` and `HeapFree` to get around the fact that VBA copies arrays for assignments instead of doing it by reference. Do you know if there is a way of using Variants to pass around a reference of the same array in memory? (the `HeapAlloc` and `HeapFree` method leaks if the user clicks the Stop button in the debugger :'( so it's not my first choice for a solution) – Blackhawk Aug 15 '14 at 16:08
  • You can [construct an array descriptor](http://stackoverflow.com/q/11713408/11683) over existing (other array's) data. Or you can keep the accessor pattern, just add another property that would return the pointer (varptr) to the first member of the array, which you then can pass to `CopyMemory`. – GSerg Aug 15 '14 at 16:29
  • @GSerg funny thing - I went to upvote the answer and realized I had apparently already done so at some point in the past... I already have a SAFEARRAY Type I had used for a `getDims` function, so I will play around with this. I like the suggestion of using a Property to just get the address of the byte array in memory, and I think I might use it instead since there would be less risk of refering to deallocated memory. I appreciate all the input! If you want to copy/paste your comments into an answer I can accept. – Blackhawk Aug 15 '14 at 16:45
  • How about using a method to set values `SetValue(ByVal value as Byte, Byval index as Integer)` – John Alexiou Aug 15 '14 at 17:27
  • @ja72 That is worse than a [property let](http://stackoverflow.com/a/16864377/11683) which we have mentioned above. – GSerg Aug 15 '14 at 17:29

3 Answers3

3

So it turns out I needed a little help from OleAut32.dll, specifically the 'VariantCopy' function. This function faithfully makes an exact copy of one Variant to another, including when it is ByRef!

'clsTest

Private Declare Sub VariantCopy Lib "OleAut32" (pvarDest As Any, pvargSrc As Any)

Private m_Buffer() As Byte

Public Property Let Buffer(buf As Variant)
    m_Buffer = buf
End Property

Public Property Get Buffer() As Variant
    Buffer = GetByRefVariant(m_Buffer)
End Property

Private Function GetByRefVariant(ByRef var As Variant) As Variant
    VariantCopy GetByRefVariant, var
End Function

With this new definition, all the tests pass!

'mdlMain

Public Sub Main()
    Dim buf() As Byte
    ReDim buf(0 To 4)

    buf(0) = 1
    buf(1) = 2
    buf(2) = 3
    buf(3) = 4


    Dim oBuffer As clsTest
    Set oBuffer = New clsTest

    'Test #1, the assignment
    oBuffer.Buffer = buf    'Success!

    'Test #2, get the value of an index in the array
    Debug.Print oBuffer.Buffer()(2)    'Success!  This is from GSerg's comment on the question

    'Test #3, change the value of an index in the array and verify that it is actually modified
    oBuffer.Buffer()(2) = 27
    Debug.Print oBuffer.Buffer()(2)  'Success! Diplays "27" in the immediate window
End Sub
Blackhawk
  • 5,984
  • 4
  • 27
  • 56
2

@Blackhawk,

I know it is an old post, but thought I'd post it anyway.

Below is a code I used to add an array of points to a class, I used a subclass to define the individual points, it sounds your challenge is similar:

Mainclass tCurve

Private pMaxAmplitude As Double
Private pCurvePoints() As cCurvePoint

Public cDay As Date
Public MaxGrad As Double

Public GradChange As New intCollection
Public TideMax As New intCollection
Public TideMin As New intCollection
Public TideAmplitude As New intCollection
Public TideLow As New intCollection
Public TideHigh As New intCollection

Private Sub Class_Initialize()

    ReDim pCurvePoints(1 To 1500)
    ReDim curvePoints(1 To 1500) As cCurvePoint

    Dim i As Integer

    For i = 1 To 1500
        Set Me.curvePoint(i) = New cCurvePoint
    Next

End Sub

Public Property Get curvePoint(Index As Integer) As cCurvePoint

    Set curvePoint = pCurvePoints(Index)

End Property

Public Property Set curvePoint(Index As Integer, Value As cCurvePoint)

    Set pCurvePoints(Index) = Value

End Property

subclass cCurvePoint

Option Explicit

Private pSlope As Double
Private pCurvature As Double
Private pY As Variant
Private pdY As Double
Private pRadius As Double
Private pArcLen As Double
Private pChordLen As Double

Public Property Let Slope(Value As Double)
    pSlope = Value
End Property

Public Property Get Slope() As Double
    Slope = pSlope
End Property

Public Property Let Curvature(Value As Double)
    pCurvature = Value
End Property

Public Property Get Curvature() As Double
    Curvature = pCurvature
End Property

Public Property Let valY(Value As Double)
    pY = Value
End Property

Public Property Get valY() As Double
    valY = pY
End Property

Public Property Let Radius(Value As Double)
    pRadius = Value
End Property

Public Property Get Radius() As Double
    Radius = pRadius
End Property

Public Property Let ArcLen(Value As Double)
    pArcLen = Value
End Property

Public Property Get ArcLen() As Double
    ArcLen = pArcLen
End Property

Public Property Let ChordLen(Value As Double)
    pChordLen = Value
End Property

Public Property Get ChordLen() As Double
    ChordLen = pChordLen
End Property

Public Property Let dY(Value As Double)
    pdY = Value
End Property

Public Property Get dY() As Double
    dY = pdY
End Property

This will create a tCurve with 1500 tCurve.Curvepoints().dY (for example)

The trick is to get the index process correct in the main class !

Good luck !

mtholen
  • 1,631
  • 2
  • 15
  • 27
  • Thanks for this! The accessor functions you have for getting and setting values definitely fill requirements #2 and #3, but unfortunately it doesn't satisfy requirements #1 (be able to set the whole buffer directly from an existing one) and #4 (be able to get a direct reference to the buffer so I can CopyMemory()) – Blackhawk Mar 24 '20 at 21:59
0

Not the most elegant solution, but modeling from the code you provided...

In clsTest:

Option Explicit

Dim ArrayStore() As Byte

Public Sub AssignArray(vInput As Variant, Optional lItemNum As Long = -1)
    If Not lItemNum = -1 Then
        ArrayStore(lItemNum) = vInput
    Else
        ArrayStore() = vInput
    End If
End Sub

Public Function GetArrayValue(lItemNum As Long) As Byte
    GetArrayValue = ArrayStore(lItemNum)
End Function

Public Function GetWholeArray() As Byte()
    ReDim GetWholeArray(LBound(ArrayStore) To UBound(ArrayStore))
    GetWholeArray = ArrayStore
End Function

And in mdlMain:

Sub test()
Dim buf() As Byte
Dim bufnew() As Byte
Dim oBuffer As New clsTest

    ReDim buf(0 To 4)
    buf(0) = 1
    buf(1) = 2
    buf(2) = 3
    buf(3) = 4

    oBuffer.AssignArray vInput:=buf
    Debug.Print oBuffer.GetArrayValue(lItemNum:=2)

    oBuffer.AssignArray vInput:=27, lItemNum:=2
    Debug.Print oBuffer.GetArrayValue(lItemNum:=2)

    bufnew() = oBuffer.GetWholeArray
    Debug.Print bufnew(0)
    Debug.Print bufnew(1)
    Debug.Print bufnew(2)
    Debug.Print bufnew(3)

End Sub

I added code to pass the class array to another array to prove accessibility.

Even though VBA won't allow us to pass arrays as properties, we can still use Functions to pick up where properties fall short.

Dustin
  • 401
  • 3
  • 4
  • 1
    Sorry I've gone so long without replying! The problem with this method is that `bufnew() = oBuffer.GetWholeArray()` creates a copy of the entire array. This might not seem like a big issue, but suppose we're talking about a 2GB array... that would suck :( – Blackhawk Oct 29 '15 at 17:47