Showing posts with label Byte Array. Show all posts
Showing posts with label Byte Array. Show all posts

Tuesday, 28 April 2020

VBA, Named Pipes (& JavaScript) - binary serialization revisited

In this post I revisit the matter of the serialization of VBA Variant arrays to bytes and back again. This can be used VBA to VBA or interoperating with other technologies that can be sequence bytes, in this post I demonstrate JavaScript to VBA. I incorporate a better serialization technique code that uses Windows OS Named Pipes as written by VBForums.com contributor Olaf Schmidt.

Background

This is a revisit of an earlier article where I saved and loaded a Variant array to disk using VBA’s Open File For Binary, Put and Get statements. Whilst the code did what I wanted, I had complained that my code required a disk operation which carries a performance penalty.

I am indebted to a commenter (who gives no name) who tipped me off as to some code on VBForums written by Olaf Schmidt; Olaf’s code serializes using Windows OS Named Pipes and not a disk write. The Named Pipes are purely in memory and so this makes his code faster.

Moreover, the Named Pipes serialization yields a leading sequence of bytes that describes the number of dimensions in the array and the size and bounds of each dimension. This was something formally missing from disk write version my code and which I had had to implement manually, something of a nuisance.

I am thus doubly indebted to Olaf Schmidt’s code and to the commenter who tipped me off. Thank you. Do please keep the comments coming.

VBA Class Module - cPipedVariants

So with Olaf Schmidt's code as a starting point I have modified it to handle the use case of VBA variant arrays, i.e. a two dimensional array which is ready for pasting onto a worksheet. Olaf's original code demonstrated the serialization of user-defined types and these data structures are more prevalent in Visual Basic 6 (VB6) whereas Excel developers (I would argue) are more likely to deal with grids drawn from a worksheet or grids to be pasted onto a worksheet.

If you want the original version that deals with the serialization of UDTs it is on this link here to vb6forums.com.

So what follows in cPipedVariants, a modification on Olaf's original class cPipedUDTs. Much of the code is easy to follow but I will comment on the ‘secret sauce’ of the InitializePipe function.

The two key lines of code are the call to CreateNamedPipeW and then the Open "\\.\pipe\foo" For Binary statement. If I switch the order of these two around then the code fails. Internally, in its implementation the Open For Binary Statement must have a special case where it identifies the "\\.\pipe\ " prefix and then looks up in the list of created named pipes. This is not documented in any Microsoft documentation, or indeed StackOverflow. Only the VB6Forums.com users and specifically Olaf Schmidt understand this lore, it must be a throw back to the era of VB6. Anyway, it does work and I am grateful.

Add a class to your VBA project, call it cPipedVariants and then paste in the following code

Option Explicit

'* Pipe-based helper to serialize/deserialize VB-Variants InMemory ... [based on original code by Olaf Schmidt 2015]
'* Based on code by Olaf Schmidt 2015, http://www.vbforums.com/showthread.php?807205-VB6-pipe-based-UDT-serializing-deserializing-InMemory


'* https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea
Private Declare Function CreateNamedPipeW& Lib "kernel32" (ByVal lpName As Long, ByVal dwOpenMode&, ByVal dwPipeMode&, _
            ByVal nMaxInstances&, ByVal nOutBufferSize&, ByVal nInBufferSize&, _
            ByVal nDefaultTimeOut&, ByVal lpSecurityAttributes&)

'* https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
Private Declare Function WriteFile& Lib "kernel32" (ByVal hFile&, lpBuffer As Any, _
            ByVal nNumberOfBytesToWrite&, lpNumberOfBytesWritten&, ByVal lpOverlapped&)

'* https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
Private Declare Function ReadFile& Lib "kernel32" (ByVal hFile&, lpBuffer As Any, _
            ByVal nNumberOfBytesToRead&, lpNumberOfBytesRead&, ByVal lpOverlapped&)

'* https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
Private Declare Function PeekNamedPipe& Lib "kernel32" (ByVal hNamedPipe&, lpBuffer As Any, _
            ByVal nBufferSize&, lpBytesRead&, lpTotalBytesAvail&, lpBytesLeftThisMessage&)

'* https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-disconnectnamedpipe
Private Declare Function DisconnectNamedPipe& Lib "kernel32" (ByVal hPipe&)

Private Declare Function CloseHandle& Lib "kernel32" (ByVal hObject&)

Private mhPipe As Long
Private mlFileNumber As Long
Private mabytSerialized() As Byte

Private Enum eOpenMode
    PIPE_ACCESS_INBOUND = 1
    PIPE_ACCESS_OUTBOUND = 2
    PIPE_ACCESS_DUPLEX = 3
End Enum

Private Enum ePipeMode
    PIPE_TYPE_BYTE = 0
    PIPE_TYPE_MESSAGE = 4

    PIPE_READMODE_BYTE = 0
    PIPE_READMODE_MESSAGE = 2
   
    PIPE_WAIT = 0
    PIPE_NOWAIT = 1
End Enum

Private Enum ePipeInstances
    PIPE_UNLIMITED_INSTANCES = 255
End Enum

Public Function InitializePipe(Optional sPipeNameSuffix As String = "vbaPipedVariantArrays") As Boolean
    Const csPipeNamePrefix As String = "\\.\pipe\"
    CleanUp
   
    Dim sPipeName As String
    sPipeName = csPipeNamePrefix & sPipeNameSuffix
   
    '* Must call CreateNamedPipe first before calling Open <<pathname>> For Binary otherwise you get bad file number
    mhPipe = CreateNamedPipeW(StrPtr(sPipeName), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE + PIPE_READMODE_BYTE + PIPE_WAIT, _
            PIPE_UNLIMITED_INSTANCES, -1, -1, 0, 0)
           
    If mhPipe = -1 Then mhPipe = 0 'reset from InvalidHandleValue to "no Handle"
   
    If mhPipe Then
        '* only try to find a free VB-FileNumber when mhPipe is valid (i.e. pipe has been created)
        mlFileNumber = FreeFile
        If mlFileNumber Then
            Open sPipeName For Binary As mlFileNumber  'open only, when we got an mlFileNumber
        End If
    End If
   
    InitializePipe = mhPipe <> 0 And mlFileNumber <> 0
End Function

Public Function SerializeToBytes(ByRef vSrc As Variant, ByRef pabytSerialized() As Byte) As Long

    Dim lBytesAvail As Long

    Debug.Assert IsArray(vSrc)

    If mlFileNumber <> 0 Then
   
        '* this next line writes the Variant array to the pipe
        Put mlFileNumber, 1, vSrc
       
        '* we should now have some bytes to read out of the pipe, use PeekNamedPipe to verify there are bytes available
        PeekNamedPipe mhPipe, ByVal 0&, 0, ByVal 0&, lBytesAvail, 0
       
        If lBytesAvail > 0 Then
           
            '* so now we can dimension the byte array
            ReDim Preserve pabytSerialized(0 To lBytesAvail - 1)
           
            '* and now we can read the bytes out of the pipe and into the byte array
            ReadFile mhPipe, pabytSerialized(0), lBytesAvail, lBytesAvail, ByVal 0&
           
            '* return number of bytes as a courtesy (not actually required)
            SerializeToBytes = lBytesAvail
        End If
    End If
End Function

Public Function DeserializeFromBytes(ByRef abytSerialized() As Byte, ByRef pvDest As Variant) As Long
   
    Dim lBytesWritten As Long
   
    If mhPipe <> 0 And mlFileNumber <> 0 Then

        '* write the byte array to the pipe
        WriteFile mhPipe, abytSerialized(0), UBound(abytSerialized) + 1, lBytesWritten, 0
       
        If lBytesWritten = UBound(abytSerialized) + 1 Then
            '* the pipe contains a byte array serialization of a variant array
            '* we can use VBA's Get statement to read it directly into a variant array variable
            Get mlFileNumber, 1, pvDest
           
            '* report the amount of deserialized Bytes as a courtesy (not actually required)
            DeserializeFromBytes = Loc(mlFileNumber)
        End If
    End If
End Function

Private Sub CleanUp()
    If mlFileNumber Then Close mlFileNumber: mlFileNumber = 0
    If mhPipe Then DisconnectNamedPipe mhPipe
    If mhPipe Then CloseHandle mhPipe: mhPipe = 0
End Sub

Private Sub Class_Terminate()
    CleanUp
End Sub

VBA Standard Module - tstPipedVariants

So now we need some client code. Add a standard module to your VBA project and paste in the following code. I called this module tstPipedVariants.

Sub SamePipeForSerializeAndDeserialize()
    Dim oPipe As cPipedVariants
    Set oPipe = New cPipedVariants
   
    If oPipe.InitializePipe Then
        Dim vSource As Variant
        vSource = TestData

        Dim abytSerialized() As Byte

        Call oPipe.SerializeToBytes(vSource, abytSerialized)

        Stop '* at this point vSource is populated but vDestination is empty

        Dim vDestination As Variant
        oPipe.DeserializeFromBytes abytSerialized, vDestination
   
        Stop
    End If
End Sub

Function TestData() As Variant
    Dim vSource(1 To 2, 1 To 4) As Variant
    vSource(1, 1) = "Hello World"
    vSource(1, 2) = True
    vSource(1, 3) = False
    vSource(1, 4) = Null
    vSource(2, 1) = 65535
    vSource(2, 2) = 7.5
    vSource(2, 3) = CDate("12:00:00 16-Sep-1989") 'now()
    vSource(2, 4) = CVErr(xlErrNA)
    TestData = vSource
End Function

In the module tstPipedVariants run the test code subroutine SamePipeForSerializeAndDeserialize() by navigating and pressing F5 to reach the first Stop statement. On the first Stop statement vSource is populated but vDestination isn’t.

However, the byte array abytSerialized() is populated and we can go inspect this. The first twenty bytes are similar to SafeArray and SafeArrayBounds structures. The first two bytes represent a vbVarType of vbArray+vbVariant in low byte, high byte order. Next, two bytes gives the count of dimensions. Then for each dimension there are 8 bytes, giving a 4 byte dimension size and a 4 byte lower bound. This abridged SafeArray descriptor is most welcome. When VBA code writes a variant array to disk it omits such a descriptor which meant I had to augment the code to manually write in the dimensions. I am very much pleased that the Named Pipes implementation does this automatically for me.

After the first twenty bytes of abridged SafeArray descriptor the rest of the data follows. I wrote this up in the original blog post so I’ll refer you to that and skip the rest.

Returning to the test code, press F5 again to get the second Stop statement and behold in the Locals window the vDestination variable is now populated exactly the same as the vSource variable. Note how we did not need to dimension the vDestination variable before populating it, excellent!

This completes the VBA to VBA demo. We can move onto the JavaScript to VBA use case.

Revisiting the Javascript to VBA use case

JavaScript Changes

In the original article I gave some Javascript code to serialize a Javascript array to a byte array that can then be deserialized to a VBA variant array. This JavaScript code needs modifying to interoperate with the new Named Pipes VBA code given above. The change required is to give a correctly formatted abridged safe array descriptor. This is a simple change found at the top of the JavaScriptToVBAVariantArray.prototype.persistGrid function. All other code remains the same, so no further explanation is required. The JavaScript module remains something loadable into both browser and server JavaScript environments. The Node.js project given in the original blog post can still be used.

I have only included the function that has changed, JavaScriptToVBAVariantArray.prototype.persistGrid; for rest of the JavaScript module listing see the original blog post.

JavaScriptToVBAVariantArray.prototype.persistGrid = function persistGrid(grid, rows, columns) {
	try {
		/* Opening sequence of bytes is a reduced form of SAFEARRAY and SAFEARRAYBOUND
		 * SAFEARRAY       https://docs.microsoft.com/en-gb/windows/win32/api/oaidl/ns-oaidl-safearray
		 * SAFEARRAYBOUND  https://docs.microsoft.com/en-gb/windows/win32/api/oaidl/ns-oaidl-safearraybound
		 */

		var payloadEncoded = new Uint8Array(20);

		// vbArray + vbVariant, lo byte, hi byte
		payloadEncoded[0] = 12; payloadEncoded[1] = 32;

		// number of dimensions, lo byte, hi byte
		payloadEncoded[2] = 2; payloadEncoded[3] = 0;

		// number of columns, 4 bytes, least significant byte first
		payloadEncoded[4] = columns % 256; payloadEncoded[5] = Math.floor(columns / 256);
		payloadEncoded[6] = 0; payloadEncoded[7] = 0;

		// columns lower bound (safearray)
		payloadEncoded[8] = 1; payloadEncoded[9] = 0;
		payloadEncoded[10] = 0; payloadEncoded[11] = 0;

		// number of rows, 4 bytes, least significant byte first
		payloadEncoded[12] = rows % 256; payloadEncoded[13] = Math.floor(rows / 256);
		payloadEncoded[14] = 0; payloadEncoded[15] = 0;

		// rows lower bound (safearray)
		payloadEncoded[16] = 1; payloadEncoded[17] = 0;
		payloadEncoded[18] = 0; payloadEncoded[19] = 0;

		var elementBytes;
		for (var colIdx = 0; colIdx < columns; colIdx++) {
			for (var rowIdx = 0; rowIdx < rows; rowIdx++) {
				elementBytes = this.persistVar(grid[rowIdx][colIdx]);
				var arr = [payloadEncoded, elementBytes];

				payloadEncoded = this.concatArrays(arr); // Browser
			}
		}
		return payloadEncoded;
	}
	catch (err) {
		console.log(err.message);
	}
};

VBA web client code

Turning to the client VBA code we can greatly simplify the code now that the dimensioning is done for us. The resulting code is now trivial, here it is below. Add the following code to the tstPipedVariants module you added earlier. This code below requires you to add a Tools->Reference to Microsoft WinHTTP Services, version 5.1...
Sub TestByWinHTTP()
    '* this calls the Node.js project with the new JavaScript serialization
    Dim oWinHttp As WinHttp.WinHttpRequest '* Tools->References->Microsoft WinHTTP Services, version 5.1
    Set oWinHttp = New WinHttp.WinHttpRequest
    oWinHttp.Open "GET", "http://localhost:1337/", False
    oWinHttp.send
   
    If oWinHttp.Status = 200 Then
        If IsEmpty(oWinHttp.ResponseBody) Then Err.Raise vbObjectError, , "No bytes returned!"
        If UBound(oWinHttp.ResponseBody) = 0 Then Err.Raise vbObjectError, , "No bytes returned!"
       
        Dim oPipedVariants As cPipedVariants
        Set oPipedVariants = New cPipedVariants
        If oPipedVariants.InitializePipe Then
       
            Dim vGrid As Variant
            oPipedVariants.DeserializeFromBytes oWinHttp.ResponseBody, vGrid
           
            Stop '* Observe vGrid in the Locals window
           
            '* vGrid is now ready to paste on a worksheet
        End If
    End If
End Sub

So run this code with the Node.js project running and the vGrid should be populated. That's all folks. Enjoy!

Thanks again

Thanks once again to Olaf Schmidt and the anonymous tipper! Getting help to fix my code is very welcome.

Tuesday, 31 March 2020

Javascript - Binary representation of VBA's Variant Array

I am delighted to give a world first in this post where I give JavaScript code to convert a JavaScript array into a byte array loadable into a VBA variant array. This gives an efficient binary serialization format for web services and other network calls where the end client is VBA and especially Excel worksheet cells.

Following on from the previous post where I document an undocumented file serialization format used by VBA with the FreeFile, Open For Binary and Put statements. I can now go on to give some JavaScript to convert two dimensional JavaScript arrays containing JavaScript variables therein to a byte array directly loadable by VBA into a variant array.

This now means a VBA client need not consume Xml or JSON when calling a web service. This is a big win because both Xml and JSON are string based and require parsing into data structures which is expensive. The Xml needs further code to interrogate the nodes with XPath etc. If the end destination for the data is the worksheet then the variant array can be directly pasted into cells (some code is give below).

To get VBA client code to do less work requires the server-side code to do more work. A JavaScript module, JavaScriptToVBAVariantArray.js, is given below which converts JavaScript arrays and variables into a byte array directly loadable by VBA (save for an few initial bytes conveying number of rows and columns).

As well the key JavaScript module, JavaScriptToVBAVariantArray.js, I also give some Node.js server-side JavaScript code to implement a web service that demonstrates the byte array being communicated to a VBA client.

JavaScriptToVBAVariantArray.js - module common to browser and server

So the following JavaScript module is usable in both the browser client and a Node.js server. It encapsulates all the logic to convert a two dimensional (block not nested) JavaScript array containing primitives of strings, Booleans, dates and numbers to a byte array loadable by VBA into a Variant array ready to be pasted onto a worksheet. Also included is logic to take a JavaScript Error object and convert this to an Excel worksheet error code so one can transmit #REF!, #N/A! and #NAME? errors.

The code is implemented as a prototyped class, it includes two test methods JavaScriptToVBAVariantArray.testPersistVar and JavaScriptToVBAVariantArray.testPersistGrid to demonstrate the code's usage. The remaining methods contain production logic that utilize ArrayBuffers and collaborating classes Float64Array and Uint8Array.

So the following code to be saved to its own separate file called JavaScriptToVBAVariantArray.js which then should be added to a Node.js project. I am using Microsoft Visual Studio for a development environment.

'use strict';

// module exporting for node.js and browsers with thanks to
// https://www.matteoagosti.com/blog/2013/02/24/writing-javascript-modules-for-both-browser-and-node/

(function () {
    var JavaScriptToVBAVariantArray = (function () {
        var JavaScriptToVBAVariantArray = function (options) {
            var pass; //...
        };


        JavaScriptToVBAVariantArray.prototype.testPersistVar = function testPersistVar() {
            try {
                var payload;
                //payload = "Hello World";
                //payload = false;
                //payload = 655.35;
                payload = new Date(1989, 9, 16, 12, 0, 0);
                var payloadEncoded = persistVar(payload);
                return payloadEncoded;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.testPersistGrid = function testPersistGrid() {
            try {
                var rows = 2;
                var columns = 4;
                var arr = this.createGrid(rows, columns);
                arr[0][0] = "Hello World";
                arr[0][1] = true;
                arr[0][2] = false;
                arr[0][3] = null;

                arr[1][0] = 65535;
                arr[1][1] = 7.5;
                arr[1][2] = new Date(1989, 9, 16, 12, 0, 0);
                arr[1][3] = new Error(2042);

                var payloadEncoded = this.persistGrid(arr, rows, columns);
                return payloadEncoded;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistGrid = function persistGrid(grid, rows, columns) {
            try {

                var payloadEncoded = new Uint8Array(4);
                payloadEncoded[0] = rows % 256; payloadEncoded[1] = Math.floor(rows / 256);
                payloadEncoded[2] = columns % 256; payloadEncoded[3] = Math.floor(columns / 256);
                var elementBytes;
                for (var colIdx = 0; colIdx < columns; colIdx++) {
                    for (var rowIdx = 0; rowIdx < rows; rowIdx++) {
                        elementBytes = this.persistVar(grid[rowIdx][colIdx]);
                        var arr = [payloadEncoded, elementBytes];

                        payloadEncoded = this.concatArrays(arr); // Browser
                    }
                }
                return payloadEncoded;
            }
            catch (err) {
                console.log(err.message);
            }
        };


        JavaScriptToVBAVariantArray.prototype.concatArrays = function concatArrays(arrays) {
            // With thanks to https://javascript.info/arraybuffer-binary-arrays


            // sum of individual array lengths
            let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);

            if (!arrays.length) return null;

            let result = new Uint8Array(totalLength);

            // for each array - copy it over result
            // next array is copied right after the previous one
            let length = 0;
            for (let array of arrays) {
                result.set(array, length);
                length += array.length;
            }

            return result;
        };

        JavaScriptToVBAVariantArray.prototype.createGrid = function createGrid(rows, columns) {
            try {
                return Array.from(Array(rows), () => new Array(columns));
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistVar = function persistVar(v) {
            try {

                if (v === null) {
                    // return a Null
                    var nullVt = new Uint8Array(2);
                    nullVt[0] = 1;
                    return nullVt;

                } else if (v instanceof Error) {

                    return this.persistError(v);

                } else if (typeof v === 'undefined') {
                    return new Uint8Array(2); // return an Empty

                } else if (typeof v === "boolean") {
                    // variable is a boolean
                    return this.persistBool(v);
                } else if (typeof v.getMonth === "function") {
                    // variable is a Date
                    return this.persistDate(v);
                } else if (typeof v === "string") {
                    // variable is a boolean
                    return this.persistString(v);
                } else if (typeof v === "number") {
                    // variable is a number
                    return this.persistNumber(v);
                }

            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistError = function persistError(v) {
            try {
                var errorVt = new Uint8Array(6); // return a vtError
                errorVt[0] = 10; errorVt[4] = 10; errorVt[5] = 128;

                var errorNumber;
                try {
                    errorNumber = parseInt(v.message);
                }
                catch (err) {
                    errorNumber = 2000;
                    console.log(err.message);
                }
                errorVt[2] = errorNumber % 256; errorVt[3] = Math.floor(errorNumber / 256);

                return errorVt;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistNumber = function persistNumber(v) {
            try {
                var bytes;
                if (Number.isInteger(v)) {
                    bytes = new Uint8Array(6);
                    bytes[0] = 3; bytes[1] = 0;  // VarType 5 = Long
                    bytes[2] = v % 256; v = Math.floor(v / 256);
                    bytes[3] = v % 256; v = Math.floor(v / 256);
                    bytes[4] = v % 256; v = Math.floor(v / 256);
                    bytes[5] = v % 256;

                } else {
                    bytes = this.persistDouble(v, 5);
                }
                return bytes;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistDate = function persistDate(v) {
            try {
                // convert JavaScript 1970 base to VBA 1900 base
                // https://stackoverflow.com/questions/46200980/excel-convert-javascript-unix-timestamp-to-date/54153878#answer-54153878
                var xlDate = v / (1000 * 60 * 60 * 24) + 25569;
                return this.persistDouble(xlDate, 7);
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistDouble = function persistDouble(v, vt) {
            try {
                var bytes;
                bytes = new Uint8Array(10);
                bytes[0] = vt; bytes[1] = 0;  // VarType 5 = Double or 7 = Date
                var doubleAsBytes = this.doubleToByteArray(v);
                for (var idx = 0; idx < 8; idx++) {
                    bytes[2 + idx] = doubleAsBytes[idx];
                }
                return bytes;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.doubleToByteArray = function doubleToByteArray(number) {
            try {
                // https://stackoverflow.com/questions/25942516/double-to-byte-array-conversion-in-javascript/25943197#answer-39515587
                var buffer = new ArrayBuffer(8);         // JS numbers are 8 bytes long, or 64 bits
                var longNum = new Float64Array(buffer);  // so equivalent to Float64

                longNum[0] = number;

                return Array.from(new Int8Array(buffer));
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistString = function persistString(v) {
            try {
                var strlen = v.length;
                var bytes = new Uint8Array(strlen + 4);
                bytes[0] = 8; bytes[1] = 0;  // VarType 8 = String
                bytes[2] = strlen % 256; bytes[3] = Math.floor(strlen / 256);
                for (var idx = 0; idx < strlen; idx++) {
                    bytes[idx + 4] = v.charCodeAt(idx);
                }
                return bytes;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        JavaScriptToVBAVariantArray.prototype.persistBool = function persistBool(v) {
            try {
                var bytes = new Uint8Array(4);
                bytes[0] = 11; bytes[1] = 0;   // VarType 11 = Boolean
                if (v === true) {
                    bytes[2] = 255; bytes[3] = 255;
                } else {
                    bytes[2] = 0; bytes[3] = 0;
                }
                return bytes;
            }
            catch (err) {
                console.log(err.message);
            }
        };

        return JavaScriptToVBAVariantArray;
    })();

    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
        module.exports = JavaScriptToVBAVariantArray;
    else
        window.JavaScriptToVBAVariantArray = JavaScriptToVBAVariantArray;
})();

Node.js server code

The following code should is for running in Node.js. Start with a new Blank Node.js Web Application project in Microsoft Visual Studio, add the JavaScriptToVBAVariantArray.js module listed above. In the server.js file replace the code with the listing below. Running the project should spawn a new browser and then print something like the following, i.e. a mix of printable and non-printable characters; this is a browser attempting to display the generated byte array.

    Hello World ÿÿ ÿÿ  @  «ªªª® à@  ú  €
'use strict';

{
    try {
        var JavaScriptToVBAVariantArray = require('./JavaScriptToVBAVariantArray');
        var v = new JavaScriptToVBAVariantArray();
        var payloadEncoded = v.testPersistGrid();
    }
    catch (err) {
        console.log('Could not find JavaScriptToVBAVariantArray.js module:' + err.message);
    }
}

require('http').createServer(function (req, res) {
    try {
        var q = require('url').parse(req.url, true);
        if (q.pathname === '/') {
            /* it's the request for our serialized Variant array */
            res.writeHead(200, { 'Content-Type': 'text/html' });

            // https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer
            var buffer = Buffer.from(new Uint8Array(payloadEncoded));
            res.end(buffer);
        }
        else {
            var filename = "." + q.pathname;
            var fs = require('fs');
            fs.readFile(filename, function (err, data) {
                if (err) {
                    res.writeHead(404, { 'Content-Type': 'text/html' });
                    return res.end("404 Not Found");
                }
                res.writeHead(200, { 'Content-Type': 'text/html' });
                res.write(data);
                return res.end();
            });
        }
    }
    catch (err) {
        console.log(err.message);
    }
}).listen(process.env.PORT || 1337);

HTMLPage.html (optional)

I intend the end client to be VBA as we are demonstrating how to serialize to a byte array loadable by VBA. Nevertheless, I include an HTML page to prove the JavaScript works in the browser. Also, if one follows the instructions on the web page and one opens the Dev Tools and the Console then one can see the byte array is a more readily viewable form (or at least more viewable that the non-printable characters above). Once this is added to your Node.js project then in a browser you can type the url http://localhost:1337/HtmlPage.html to access the page.

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <p>
        <span style="font-family:Courier New, Courier, monospace">
            Look in the console!
            Do this by right-click menu and then Inspect to get the DevTools window
            then click on the Console tab
        </span>
    </p>
    <script src="https://raw.githubusercontent.com/arextar/browser-buffer/master/browser-buffer.min.js"></script>
    <script src="JavaScriptToVBAVariantArray.js"></script>
    <script>
        var v = new JavaScriptToVBAVariantArray();
        var payloadEncoded = v.testPersistGrid();
        console.log(payloadEncoded);
    </script>
</body>
</html>

Client VBA Code

Finally, we get to the VBA client code. Paste this into a standard module, you will need some code from the previous post. You will also need a Tools->Reference to Microsoft WinHTTP Services, version 5.1 to make the HTTP call.

So the listing below will call into the Javascript web service given above, it takes the HTTP response and then reads the rows and columns from the first four bytes, the remaining bytes it will write to a temporary file (this could be speeded with the use of a RAM disk). The temporary file is then read directly into a variant array new dimensioned with the correct rows and columns.

At this point any VBA code can work with this variant array like it was any other as the byte array has been converted to a VBA native data structure! The code below goes on to paste the variant array to a block of cells on a worksheet (please save your work as this overwrites the cells).

In the pasted cells (screenshot given) we can see a string, two booleans, a deliberately empty cell, an integer, a decimal number, a date, and a #N/A worksheet error code. This demonstrates the full range of variable types that can be transmitted via the byte array.

Sub TestByWinHTTP()
    Dim WinHttp As WinHttp.WinHttpRequest '* Tools->References->Microsoft WinHTTP Services, version 5.1
    Set WinHttp = New WinHttp.WinHttpRequest
    WinHttp.Open "GET", "http://localhost:1337/", False
    WinHttp.send
    
    If WinHttp.Status = 200 Then
        If IsEmpty(WinHttp.responseBody) Then Err.Raise vbObjectError, , "No bytes returned!"
        If UBound(WinHttp.responseBody) = 0 Then Err.Raise vbObjectError, , "No bytes returned!"
        
        '**** SAVE ****
        Dim lSaveFileNum As Long
        Dim sFullFilename As String
        sFullFilename = OpenCleanFileForBinary("JavascriptToVBABin.bin", lSaveFileNum)
        
        
        Dim lRows As Long, lColumns As Long
        lRows = WinHttp.responseBody(0) + WinHttp.responseBody(1) * 256
        lColumns = WinHttp.responseBody(2) + WinHttp.responseBody(3) * 256
        
        Dim lByteLoop As Long
        For lByteLoop = 4 To UBound(WinHttp.responseBody)
            Put lSaveFileNum, , CByte(WinHttp.responseBody(lByteLoop))
        Next lByteLoop
        Close lSaveFileNum
        
        '**** LOAD ****
        
        ReDim vGrid(0 To lRows - 1, 0 To lColumns - 1) As Variant
        Dim lLoadFileNum As Long
        lLoadFileNum = OpenFileForBinary(sFullFilename)
        Get lLoadFileNum, , vGrid
        Close lLoadFileNum
        
        '**** WRITE TO WORKSHEET CELLS ****
        
        Stop '* next line will overwrite cells!! please save your work!!
        Sheet1.Range("a11").Resize(lRows, lColumns).Value = vGrid
        Stop '* Observe the cells on the worksheet
    End If
End Sub

Final Thoughts

It is a shame that the serialization format does not natively include the dimensions of the array block/grid as I could shave even more lines of code on the VBA side. It is also a shame we have to write the file to disk instead of being able to load directly into memory; there is the option of using a RAM disk to speed the file operation. In the meantime I feel I can write JavaScript Chrome extensions that could now transmit blocks of cells to an Excel client in VBA's native serialization format. Cool!

Friday, 28 December 2018

VBA - Persistence - CopyMemory can be used to serialise state

First, a health warning, the code in this blog comes with no warranty. Microsoft would say that copying memory can have unpredictable results. Nevertheless, if you are fascinated by VBA then you might be interested in the guts of VBA.

In the previous post I showed how LSet can be used to copy the state from a user-defined type (UDT) into a byte array and vice versa. This can be used in distributed systems but also one could save the byte array to a file and then load into a subsequent Excel session. Thus it is possible to persist a (UDT) to file, and if you use classes then you can gather all your class level variables into a UDT. One can begin to see how a whole object graph (i.e. the state of a whole hierarchy of classes) might be persist-able.

At the end of the previous post I introduced the CopyMemory() function and I used it to replace the copying of a byte array.

The copied byte array was located in a companion UDT defined solely for LSet-ting to the real UDT. I wondered if I could cut out the middle step and use CopyMemory() to copy directly from the byte array to the real UDT. I have conducted some experiments and you can so long as you use Byte arrays for your strings. Details of my experimental code follow.

modTestCopyStringToBytes Standard Module

So we need to treat strings differently because they are not inherently contiguous, we need to convert them to a byte array but this is not too difficult. What is important is ensuring that no overwriting of other memory happens. In the procedure TestCopyStringToBytes() below you can see that we find the length of the array and the length of the string we're asked to copy and we truncate the string if the byte array is not large enough. We copy over only safe number of bytes. The code has plenty of comments.

  1. Option Explicit
  2.  
  3. '* No Warranty, use this code at your own risk!
  4.  
  5. Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)
  6.  
  7. '*
  8. '* Not used in the main code.
  9. '* Illustrates how strings have be treated carefully as an array of bytes
  10. '*
  11. Private Sub TestCopyStringToBytes()
  12.  
  13.     '* set up string
  14.     Dim sRHS As String
  15.     sRHS = "hello world"
  16.  
  17.     '* defining the string length here but it could passed in and thus unknown
  18.     Dim abStringAsBytes(0 To 9) As Byte
  19.  
  20.     '* standard array length determination, I know it is fixed above but ...
  21.     '* ... in other cases the array could be passed in and be of unknown length
  22.     Dim lMaxLen As Long
  23.     lMaxLen = (UBound(abStringAsBytes) - LBound(abStringAsBytes) + 1)
  24.  
  25.     '* we'd like to not overwrite the allocated memory so truncate the string
  26.     '* remember strings are two bytes for each character so we divide by 2
  27.     Dim sSafeRHS As String
  28.     sSafeRHS = Left$(sRHS, lMaxLen / 2)
  29.  
  30.     '* give a truncation warning to avoid shock
  31.     If lMaxLen < Len(sRHS) Then Debug.Print "Warning contents of sRHS will be truncated from '" & sRHS & "' to '" & sSafeRHS & "'"
  32.  
  33.     '* this next function will copy the memory from the string sSafeRHS to the first byte of the byte array, abStringAsBytes(0)
  34.     CopyMemory ByVal VarPtr(abStringAsBytes(0)), ByVal StrPtr(sSafeRHS), LenB(sSafeRHS)
  35.  
  36.     '* this next line works thanks to VBA being nice and interpreting a byte array as a string for us
  37.     '* (saves us having to reverse the memory copy operation)
  38.     Debug.Print abStringAsBytes
  39. End Sub

If you run the code you should get a truncation warning and the output of the copied truncated string like this

Warning contents of sRHS will be truncated from 'hello world' to 'hello'
hello

modCopyMemoryDirectToUdt Standard Module

Here is a a very full listing with plenty of debug code to illustrate what is going on. The code is heavily commented. The indented code in Main() is not strictly necessary but it serves as a guide as to what is going on.

  1. Option Explicit
  2.  
  3. '* No Warranty, use this code at your own risk!
  4.  
  5. Private Const mlPAD As Long = 28 '* just for formatting
  6.  
  7. Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)
  8.  
  9. '*
  10. '* See here we only need one type per entity/class
  11. '* DO NOT USE   "aFixedString As String * 20" as it is not contiguous!
  12. '* INSTEAD USE  "aFixedStringAsBytes(0 To 19) As Byte"
  13. '*
  14. Public Type udtContiguousType
  15.     aLong As Long
  16.     anInt As Integer
  17.     aDouble As Double
  18.     aFixedStringAsBytes(0 To 9) As Byte  '
  19.     aSecondLong As Long
  20.     aSecondDouble As Double
  21. End Type
  22.  
  23. '*
  24. '* The following property procedures could be adjusted to sit in a class
  25. '* the class could store the string as a byte array in the class's state UDT
  26. '* to the callers of the class the property could look like just a string
  27. '*
  28. Property Let MyFixedString(ByRef uType As udtContiguousType, ByVal sRHS As String)
  29.  
  30.     '* array length determination
  31.     Dim lMaxLen As Long
  32.     lMaxLen = ByteArrayLen(uType.aFixedStringAsBytes)
  33.  
  34.     '* we'd like to not overwrite the allocated memory so truncate the string
  35.     '* remember strings are two bytes for each character so we divide by 2
  36.     Dim sSafeRHS As String
  37.     sSafeRHS = Left$(sRHS, lMaxLen / 2)
  38.  
  39.     '* give a truncation warning to avoid shock
  40.     If lMaxLen < Len(sRHS) Then Debug.Print "Warning contents of sRHS will be truncated from '" & sRHS & "' to '" & sSafeRHS & "'"
  41.  
  42.     '* this next function will copy the memory from the string sSafeRHS ...
  43.     '* ... to the first byte of the byte array, uType.aFixedStringAsBytes(0)
  44.     CopyMemory ByVal VarPtr(uType.aFixedStringAsBytes(0)), ByVal StrPtr(sSafeRHS), LenB(sSafeRHS)
  45. End Property
  46.  
  47. Property Get MyFixedString(ByRef uType As udtContiguousType) As String
  48.     '* this line works thanks to VBA being nice and interpreting a byte array as a string for us
  49.     '* (saves us having to reverse the memory copy operation)
  50.     MyFixedString = uType.aFixedStringAsBytes
  51. End Property
  52.  
  53.  
  54. '*
  55. '* the main code, run this code
  56. '*
  57. Private Sub Main()
  58.     Dim bDebug As Boolean
  59.     bDebug = True
  60.  
  61.     Dim uTypeSrc As udtContiguousType
  62.     Dim uTypeDest As udtContiguousType
  63.  
  64.     '* set some values just like any other UDT, except for the strings
  65.     uTypeSrc.aLong = 1
  66.     uTypeSrc.anInt = 2
  67.     uTypeSrc.aDouble = 3.141
  68.     uTypeSrc.aSecondLong = -434634634
  69.     uTypeSrc.aSecondDouble = Sqr(10) * -1
  70.  
  71.     '* we have to treat strings specially
  72.     '* the string's special treatment is abstracted by the property procedures
  73.     MyFixedString(uTypeSrc) = "hello world"
  74.  
  75.     If bDebug Then
  76.         '* we have to be aware of truncation
  77.         Debug.Assert MyFixedString(uTypeSrc) = Left$("hello world", ByteArrayLen(uTypeSrc.aFixedStringAsBytes) / 2)
  78.  
  79.         '* for illustration we print the memory addresses so that one can see the udt's memory layout , not strictly necessary
  80.         PrintMemoryAddresses "uTypeSrc", uTypeSrc
  81.  
  82.         '* these next lines highlight the byte length of each member
  83.         '* not strictly necessary
  84.         Debug.Assert VarPtr(uTypeSrc) = VarPtr(uTypeSrc.aLong)
  85.         Debug.Assert VarPtr(uTypeSrc.anInt) - VarPtr(uTypeSrc.aLong) = 4
  86.         Debug.Assert VarPtr(uTypeSrc.aDouble) - VarPtr(uTypeSrc.anInt) = 4
  87.         Debug.Assert VarPtr(uTypeSrc.aFixedStringAsBytes(0)) - VarPtr(uTypeSrc.aDouble) = 8
  88.         Debug.Assert VarPtr(uTypeSrc.aSecondLong) - VarPtr(uTypeSrc.aFixedStringAsBytes(0)) =
  89.                                                             BytesAlignedLen(ByteArrayLen(uTypeSrc.aFixedStringAsBytes))
  90.         Debug.Assert VarPtr(uTypeSrc.aSecondDouble) - VarPtr(uTypeSrc.aSecondLong) = 4
  91.  
  92.         '* for illustration we print the memory addresses so that one can see the udt's memory layout , not strictly necessary
  93.         PrintMemoryAddresses "uTypeDest", uTypeDest
  94.  
  95.         '* more layout illustration
  96.         Debug.Assert VarPtr(uTypeDest) = VarPtr(uTypeDest.aLong)
  97.     End If
  98.  
  99.     CopyMemory ByVal (VarPtr(uTypeDest.aLong)), ByVal (VarPtr(uTypeSrc.aLong)), LenB(uTypeSrc)
  100.  
  101.     If bDebug Then
  102.         Debug.Assert MyFixedString(uTypeDest) = Left$("hello world", ByteArrayLen(uTypeSrc.aFixedStringAsBytes) / 2)
  103.         Debug.Assert uTypeDest.aLong = uTypeSrc.aLong
  104.         Debug.Assert uTypeDest.anInt = uTypeSrc.anInt
  105.         Debug.Assert uTypeDest.aDouble = uTypeSrc.aDouble
  106.         Debug.Assert MyFixedString(uTypeDest) = MyFixedString(uTypeSrc)
  107.         Debug.Assert uTypeDest.aSecondLong = uTypeSrc.aSecondLong
  108.         Debug.Assert uTypeDest.aSecondDouble = uTypeSrc.aSecondDouble
  109.     End If
  110.     Stop 'please go look at the Locals window
  111. End Sub
  112.  
  113. '*
  114. '* standard array length calculation
  115. '*
  116. Private Function ByteArrayLen(ByRef abBytes() As ByteAs Long
  117.     ByteArrayLen = (UBound(abBytes()) - LBound(abBytes()) + 1)
  118. End Function
  119.  
  120. '*
  121. '* each member of a user defined type is 32-bit aligned so this function calcs the aligned byte length
  122. '*
  123. Private Function BytesAlignedLen(ByVal cbByteCount As LongAs Long
  124.     If cbByteCount \ 4 = cbByteCount / 4 Then
  125.         BytesAlignedLen = cbByteCount
  126.     Else
  127.         BytesAlignedLen = ((cbByteCount \ 4) + 1) * 4
  128.     End If
  129. End Function
  130.  
  131. '*
  132. '* this prints the memory addresses so one can see the memory laid out
  133. '*
  134. Private Function PrintMemoryAddresses(ByVal sVarName As StringByRef uType As udtContiguousType)
  135.     Debug.Print()
  136.     Debug.Print Pad("&" & sVarName & "", mlPAD) & " = " & VarPtr(uType)
  137.     Debug.Print Pad("&" & sVarName & ".aLong", mlPAD) & " = " & VarPtr(uType.aLong) & " a Long has 4 bytes"
  138.     Debug.Print Pad("&" & sVarName & ".anInt", mlPAD) & " = " & VarPtr(uType.anInt) &
  139.         " an Int has 2 bytes but is 32-bit aligned so has 4 bytes"
  140.     Debug.Print Pad("&" & sVarName & ".aDouble", mlPAD) & " = " & VarPtr(uType.aDouble) & " a Double has 8 bytes"
  141.  
  142.     Dim lMaxLen As Long
  143.     lMaxLen = ByteArrayLen(uType.aFixedStringAsBytes)
  144.  
  145.     Dim lAlignedMaxLen As Long
  146.     lAlignedMaxLen = BytesAlignedLen(lMaxLen)
  147.  
  148.     Dim sByteLen As String
  149.     sByteLen = " this byte array has " & lMaxLen & " bytes" &
  150.         VBA.IIf(lAlignedMaxLen <> lMaxLen, " but is 32-bit aligned to " & lAlignedMaxLen & " bytes""")
  151.  
  152.     Debug.Print Pad("&" & sVarName & ".aFixedStringAsBytes", mlPAD) & " = " & VarPtr(uType.aFixedStringAsBytes(0)) & sByteLen
  153.     Debug.Print Pad("&" & sVarName & ".aSecondLong", mlPAD) & " = " & VarPtr(uType.aSecondLong) & " a Long has 4 bytes"
  154.     Debug.Print Pad("&" & sVarName & ".aSecondDouble", mlPAD) & " = " & VarPtr(uType.aSecondDouble) & " a Double has 8 bytes"
  155.     Debug.Print Pad("&" & sVarName & " total length", mlPAD) & " = " & LenB(uType)
  156.  
  157. End Function
  158.  
  159. '*
  160. '* this pads a string to help it looks columnar
  161. '*
  162. Private Function Pad(ByVal sText As StringByVal lLen As LongAs String
  163.     Pad = Left$(sText & String(lLen, " "), lLen)
  164. End Function

So when the Main() code runs we have two things to look at (1) the Immediate Window and (2) the Locals Window. Here are mine, firstly the Immediate Window

Warning contents of sRHS will be truncated from 'hello world' to 'hello'

&uTypeSrc                    = 5240168
&uTypeSrc.aLong              = 5240168 a Long has 4 bytes
&uTypeSrc.anInt              = 5240172 an Int has 2 bytes but is 32-bit aligned so has 4 bytes
&uTypeSrc.aDouble            = 5240176 a Double has 8 bytes
&uTypeSrc.aFixedStringAsByte = 5240184 this byte array has 10 bytes but is 32-bit aligned to 12 bytes
&uTypeSrc.aSecondLong        = 5240196 a Long has 4 bytes
&uTypeSrc.aSecondDouble      = 5240200 a Double has 8 bytes
&uTypeSrc total length       = 40

&uTypeDest                   = 5240128
&uTypeDest.aLong             = 5240128 a Long has 4 bytes
&uTypeDest.anInt             = 5240132 an Int has 2 bytes but is 32-bit aligned so has 4 bytes
&uTypeDest.aDouble           = 5240136 a Double has 8 bytes
&uTypeDest.aFixedStringAsByt = 5240144 this byte array has 10 bytes but is 32-bit aligned to 12 bytes
&uTypeDest.aSecondLong       = 5240156 a Long has 4 bytes
&uTypeDest.aSecondDouble     = 5240160 a Double has 8 bytes
&uTypeDest total length      = 40

Now here is my Locals Window screenshot with the variables expanded to show the members. It shows the contents successfully copied from uTypeSrc to uTypeDest.

Commentary

It is interesting that VBA could potentially participate in a persistence scheme. I am minded to investigate the persistence interfaces of the COM specification such as IPersist to see if VBA could indeed play in this league. One problem is that IPersist has non Automation types so one would need to define a proxy component and a proxy interface.

Final Warranty Warning

I can't give a warranty for this code (nor any code on this blog for that matter).

Thursday, 27 December 2018

VBA - Persistence - Use LSet to serialise user defined type to a byte array

First, a health warning; there is no warranty for this code in this blog post; use at your own risk.

Transmitting State In a Distributed System

Years ago, I encountered a technique from a great technical author called Rockford Lhotka who wrote a classic VB6 book called Professional Visual Basic 6 Distributed Objects; as its title suggests it's concerned with using VB6 to build systems that distributed objects across machines. VBA is a version of VB6 so the techniques found therein apply to Excel Developers. In this blog post (and some planned future posts), I comment upon those techniques; specifically the serialisation/persistence technique.

In a distributed system, at some point one inevitably passes an object as a parameter and this raises the potential problem of excessive network traffic. So, if an instance of Class1 residing on MachineA wants to call an instance of Class2 residing on MachineB and passes as a parameter a pointer to an instance of Class3 which was created on MachineA then MachineB will make network calls to query the state of Class3. If these network calls are excessive then you will want to redesign your system to instead serialise the state of Class3 to a byte stream (or other state vessel), pass the byte stream over the network as the parameter, instantiate a duplicate of Class3 on MachineB initialising its state from the passed parameter byte stream and make local calls to the local duplicate. If you change the state of Class3 you'll need to serialise the state and return it back to Machine1 back so its original instance of Class3 is synchronised.

As you can imagine designing distributed systems can be hard and actually this is not what this blog post is to be about. This blog post is meant to be about the persistence mechanism used by Rockford Lhotka, using the LSet keyword. But if you're interested in this problem, Rockford Lhotka, has gone to become a big name in the problem space of distributed systems, promoting his solution Component-based Scalable Logical Architecture (CSLA) an early version of which is found in the above referenced book.

The key takeaway from this section is that there are techniques to serialise a (VB6/VBA) class's state to a an array of bytes and then instantiate a duplicate. Just bear that in mind as a use case for the code below.

Microsoft seems to frown upon LSet (and by implication Rockford Lhotka's CSLA)

Above, I've written above quite a lot about Rockford Lhotka because his ideas seems to be a little at odds with the the official Microsoft documentation on LSet, here's an eye-catching quote ...

Using LSet to copy a variable of one user-defined type into a variable of a different user-defined type is not recommended.

But this is exactly how Rockford Lhotka achieves his trick of serialisation. We'll move onto an example before discussing the point at issue further.

Sample LSet to Byte Array Code

So how does Rockford Lhotka serialise state in VB6 and VBA? Here is some code which demonstrates an instance of a user-defined-type being serialised and the state then used in turn to create a second identical instance of the same user-defined-type.

modUdtToByteArrayByLSet Standard Module

  1. Option Explicit
  2.  
  3. '* No Warranty, use this code at your own risk!
  4.  
  5. 'Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)
  6.  
  7. Public Type udtMyType
  8.     aLong As Long
  9.     anInt As Integer
  10.     aDouble As Double
  11.     aFixedString As String * 10
  12.     aSecondLong As Long
  13.     aSecondDouble As Double
  14. End Type
  15.  
  16. Public Type udtByteArray48
  17.     value(0 To 47) As Byte
  18. End Type
  19.  
  20. Private Sub TestSaveToByteArray()
  21.     Dim uContiguousType As udtMyType
  22.     uContiguousType.aLong = 1
  23.     uContiguousType.anInt = 2
  24.     uContiguousType.aDouble = 3.141
  25.  
  26.     uContiguousType.aFixedString = "Hello"
  27.     uContiguousType.aSecondLong = -434634634
  28.     uContiguousType.aSecondDouble = Sqr(10) * -1
  29.  
  30.     Dim abSaved() As Byte
  31.     abSaved() = SaveToByteArray(uContiguousType, 48)
  32.  
  33.     Dim uContiguousType2 As udtMyType
  34.     LoadFromByteArray abSaved(), uContiguousType2, 48
  35.  
  36.     Debug.Assert uContiguousType2.aDouble = uContiguousType.aDouble
  37.     Debug.Assert uContiguousType2.aFixedString = uContiguousType.aFixedString
  38.     Debug.Assert uContiguousType2.aLong = uContiguousType.aLong
  39.     Debug.Assert uContiguousType2.anInt = uContiguousType.anInt
  40.     Debug.Assert uContiguousType2.aSecondLong = uContiguousType.aSecondLong
  41.     Debug.Assert uContiguousType2.aSecondDouble = uContiguousType.aSecondDouble
  42.     Stop
  43. End Sub
  44.  
  45. '******************************************************************************************************
  46. '*
  47. '* Persistence routines, you'll need these three for each type you intend to persist
  48. '* and customise the signatures
  49. '*
  50. '******************************************************************************************************
  51. Public Sub LoadFromByteArray(ByRef abBytes() As ByteByRef puContiguousType As udtMyType, ByVal lSizeOf As Long)
  52.  
  53.     CheckSizeOf puContiguousType, lSizeOf
  54.     Dim uByteArray48 As udtByteArray48
  55.  
  56.     'CopyMemory ByVal (VarPtr(uByteArray48.value(0))), ByVal (VarPtr(abBytes(0))), lSizeOf
  57.     Dim idx As Long
  58.     For idx = 0 To lSizeOf - 1
  59.         uByteArray48.value(idx) = abBytes(idx)
  60.     Next idx
  61.  
  62.     LSet puContiguousType = uByteArray48
  63. End Sub
  64.  
  65. Private Sub CheckSizeOf(ByRef puContiguousType As udtMyType, ByVal lSizeOf As Long)
  66.     Dim uByteArray48 As udtByteArray48
  67.     If Not LenB(puContiguousType) = LenB(uByteArray48) Then Err.Raise vbObjectError, , "#size of byte arrays do not match!"
  68.     If Not LenB(puContiguousType) = lSizeOf Then Err.Raise vbObjectError, , "#size of byte arrays do not match size_of!"
  69. End Sub
  70.  
  71. Public Function SaveToByteArray(ByRef puContiguousType As udtMyType, ByVal lSizeOf As LongAs Byte()
  72.  
  73.     CheckSizeOf puContiguousType, lSizeOf
  74.  
  75.     Dim uByteArray48 As udtByteArray48
  76.     LSet uByteArray48 = puContiguousType
  77.     SaveToByteArray = uByteArray48.value
  78.  
  79. End Function
  80.  

So to run the above code run TestSaveToByteArray() whose Stop statement conveniently allows inspection of the Locals Window which should look something like that below which shows two instances of the user-defined type myType with identical contents but the second has been populated from a byte array calculated from the first. Both reading and writing the byte array to the user-defined-typed is done with LSet...

Commentary

So whilst the above code works there is quite a lot with which I am unhappy. Primarily, if your UDT is 48 bytes long then you need to separately define another type, the serialisation type, to have one single member byte array of length 48 bytes. Now, I'm assuming that this technique implies that each class has a UDT to contain the state for the class (for I do not know any trick to access the state of a VBA class, if you know do please comment below). So imagine if you have twenty classes in your project of varying byte lengths then you'd need twenty separate UDTs for serialisation to cope with the varying lengths.

For each class, one would need the three persistence routines given in the code: LoadFromByteArray(), CheckSizeOf() and SaveToByteArray()

I just wonder if we can do better than this.

One immediate change I see is to optimise the copying of the array as given in LoadFromByteArray(). Iterating through each byte in the byte array in VBA is clearly sub-optimal, much better here to copy memory directly because an array of bytes is guaranteed to be contiguous. So there is a replacement for the array loop on lines 57-60, you need to uncomment line 56 and line 03; then comment out line 57-60.

But if you cross the Rubicon and start using CopyMemory() then aren't even more optimisations available? This will be the subject of the following posts.