Visual Basic 6 Business Objects
Many people store their object's data in a variable based on a UDT, because it's a very concise and convenient way to handle the values. While Visual Basic allows us to pass UDT variables as parameters, there are some serious drawbacks to this approach.
In particular, we need to make the UDT Public in order to pass it as a parameter. This feature cant be used if our objects are not part of an ActiveX server for instance from a Standard EXE project. Additionally, this approach makes it very easy for someone to write a client program that retrieves our objects data, manipulates it without our business logic and places it back in the object. Finally, VB passes UDT variables in a way not supported by other tools or languages. Using this technique prevents us from passing the data to routines written in other languages or through other tools such as Microsoft Message Queue.
The ideal would be if we could use a UDT to store our objects data, but also be able to retrieve that data as a single stream of data say in a String variable.
Luckily, there is a very nice solution that enables us to efficiently convert a UDT variable to a String variable so we can pass its data to another ActiveX component. We'll take a good look at this solution, but first let's put it in perspective.
Many languages have some equivalent to Visual Basic's UDT. For instance, C has the struct, and both FORTRAN and Pascal have similar constructs. Some languages natively support multiply-defined structures, where the programmer can define two different variable definitions for the same chunk of memory. FORTRAN implements COMMON memory blocks, while C uses a union keyword within a struct:
Multiply-defined structures are very powerful. They allow us to set up a user-defined type like this:
Private Type DetailType Name As String * 30 Age As Integer End Type
Then we can set up another user-defined type that is exactly as long as DetailType:
Private Type SummaryType Buffer As String * 31 End Type
Remember that in Win32 environments such as Windows 98 and Windows NT, strings are all Unicode; therefore, each character in a string will actually consume 2 bytes of memory. This is so Windows can support more complex character sets, such as those required by Arabic or languages in the Far East.
So DetailType is a total of 62 bytes in length: 60 for the Name field and 2 for the Age field. This means that SummaryType also needs to be 62 bytes in length. With Unicode, a 62 byte String is actually half that many characters. Therefore, dividing 62 by 2, we find that SummaryType needs to be 31 characters long.
It's worth noting that if DetailType were 61 bytes in length (say Age was a Byte) then SummaryType would still need to be 31 characters long, since we must round up. If we didn't round up, then SummaryType would be just 30 characters long and could only hold 60 of the 61 characters: we'd lose one byte at the end.
FORTAN, C, and some other languages would allow a single variable to be referenced as DetailType or SummaryType. In other words, they would let us get at the same set of bytes in memory in more than one way. This means that we could set the Name and Age values with DetailType, and also treat the memory as a simple String without having to copy any values in memory.
Since Visual Basic doesn't allow us to pass a UDT variable as a parameter to an object, we need some way to convert our UDT variables to a data-type that can be passed. The ideal situation would be one in which we could simply define the same chunk of memory as both a UDT and a simple String variable, as shown above.
Although Visual Basic doesn't allow us to do this, it does provide us with a very efficient technique that we can use to provide an excellent workaround.
Visual Basic Implementation
Visual Basic's approach does require a memory copy, but it's performed with the LSet command, which is very fast and efficient. Let's take a look at how LSet works.
Open a Standard EXE project and type in the DetailType and SummaryType UDT's that we just looked at. They need to be entered in the General Declarations section of our form. Then add the following code to the Form_Click event:
Private Sub Form_Click() Dim udtDetail As DetailType Dim udtSummary As SummaryType With udtDetail .Name = "Fred Jones" .Age = 23 End With End Sub
This code simply defines a variable, using each UDT, and then loads some data into the udtDetail variable, which is based on the DetailType type. So far, this is pretty simple - so here comes the trick. We'll add a line using the LSet command:
Private Sub Form_Click() Dim udtDetail As DetailType Dim udtSummary As SummaryType With udtDetail .Name = "Fred Jones" .Age = 23 End With LSet udtSummary = udtDetail End Sub
This new line uses the LSet command to do a direct memory copy of the contents of udtDetail into udtSummary. Visual Basic doesn't perform any type checking here; in fact, it doesn't even look at the content of the variables; it just performs a memory copy. This is very fast and very efficient: substantially faster than trying to copy individual elements of data, for instance.
The result of this code is that the Name and Age values are stored in the udtSummary variable, and can be accessed as a string using udtSummary.Buffer. Of course, the values stored aren't all printable text, so if we try to print this value we'll get garbage. That's OK though: the objective was to get the data into a single variable. Now we can pass that string to another procedure using the following code:
Private Sub Form_Click() Dim udtDetail As DetailType Dim udtSummary As SummaryType With udtDetail .Name = "Fred Jones" .Age = 23 End With LSet udtSummary = udtDetail PrintValues udtSummary.Buffer End Sub
The PrintValues subroutine just accepts a simple String as a parameter. Of course, we're really passing a more complex set of data, but it's packaged into a simple String at this point so it's easy to deal with. Let's look at the PrintValues routine:
Private Sub PrintValues(Buffer As String) Dim udtDetail As DetailType Dim udtSummary As SummaryType udtSummary.Buffer = Buffer LSet udtDetail = udtSummary With udtDetail Debug.Print .Name Debug.Print .Age End With End Sub
Again, we declare variables using DetailType and SummaryType for use within the routine, and we copy the parameter value into udtSummary.Buffer. Both values are simple Strings, so this isn't a problem. We do need to get at the details of Name and Age, though, so we use the LSet command to perform a memory copy and get the data into the udtDetail variable:
LSet udtDetail = udtSummary
Once that's done, we can simply use udtDetail as normal: in this case, for just printing the values to the Immediate window in the Visual Basic IDE.
If you run this program, and click on the form, the appropriate output will appear in the Immediate window accordingly.
In most languages, including Visual Basic, the compiler will align certain user-defined data-types in memory so that they fall on longword boundaries. This means that the compiler inserts filler space if an element won't start on an even 4-byte boundary.
This slightly complicates how we determine the length of our string buffer UDT. While this memory alignment problem adds some complexity to our use of LSet to copy a UDT, it's not hard to overcome. Let's look at the details of the problem, and then we'll see how easy it is to solve.
Consider the following user-defined type:
Private Type TestType B1 As Byte L1 As Long End Type
If we assume that the compiler starts the type on a memory boundary, then B1 will be at the start of a 4-byte boundary. But B1 is only a single byte long, and it's required that L1 start on a longword boundary; so that leaves 3 bytes of space, between B1 and L1, which need to be taken up. This is where the compiler inserts the filler space.
One way to quickly check the actual length (in bytes) of a UDT is to declare a variable based on the UDT in question and use the LenB function. This function will return the length, in bytes, of any variable - including one based on a UDT. For instance, we could write the following code:
Private Sub TypeLength() Dim udtTest As TestType MsgBox LenB(udtTest) End Sub
If we run this subroutine, we get a message box showing the total number of bytes in the TestType UDT, which in this case is 8.
Not all data-types are longword-aligned. Some are word-aligned, meaning that theyll fall on 2-byte boundaries. To accurately determine where the compiler will be adding filler space, we need to know which data-types will be word-aligned and which will be longword-aligned. The following data-types are always aligned to a word (2 byte) boundary:
Byte Integer Boolean String
The following data-types are longword aligned by the compiler:
Long Single Double Date Currency Decimal Object Variant
The compiler will add space as needed in front of any of these data-types, so they always start on a longword boundary.
When the compiler adds these filler spaces, it makes our UDT that much longer. Of course, we're creating another UDT to copy our data into, so we need to know exactly how long to make that UDT so that it can fit all the data. Here's our problem: the length of the string needs to be inflated to include the filler spaces - otherwise it will be too short, and the last few bytes of data will be lost during the copy.
The following UDT will hold 6 bytes of data:
Private Type StringType Buffer As String * 3 End Type
At first glance, we might expect StringType to be able to contain the TestType elements shown above, since a Byte and Long combined are only 5 bytes in length. But because L1 (the Long) needs to be longword-aligned, the compiler inserts 3 filler bytes before L1; therefore, the total length of TestType is actually 8 bytes. This means that we need the following UDT to hold all the data from TestType:
Private Type StringType Buffer As String * 4 End Type
While the issue of longword alignment makes it more complicated to determine the length of the buffer UDT, it is a predictable behavior, and it isn't really very hard to ensure we get the length correct.
Page 4 of 13