February 29, 2024

VBA String data type and memory layout

Last modified: Tue Mar 12 22:22:31 UTC+0100 2024 © Attila Tarpai

VBA Strings, memory layout, string functions

VBA Strings are Unicode BSTR structures based on underlying COM/Automation. BSTR is a composite data type:

  • length prefix - 32-bit integer, number of BYTES in the data string excluding the terminating zeroes
    values of 0, 2, 4, ..
  • data string - string of Unicode 2-byte characters
  • terminator - NULL (0x0000) WCHAR

A VBA String variable is a BSTR POINTER: it points to the first character of the data string:

                               +-----------+
                            -4 | Length=10 |
+-----------+                  +--+--+--+--+--+--+--+--+--+--+--+--+
|  pointer  |  ------------>   |     |     |     |     |     |  0  |
+-----------+                  +--+--+--+--+--+--+--+--+--+--+--+--+
Dim str As String                 BSTR unicode 2-byte characters
str = "hello"

At address -4 we find the Length field in memory.

The default value for a VBA String variable and for functions returning String is zero, a "null-pointer", i.e. no allocated string buffer is associated with the pointer. The assignment str = vbNullString will also write zero in the String variable after freeing up the BSTR buffer:

null-string:

+-----------+                  +-----------+
|     0     |                  |     0     |
+-----------+                  +-----------+
Dim str As String              Dim str As String
                               str = "hello"
                               str = VBA.vbNullString

An empty-string "" points to this 6-byte BSTR structure:

empty-string:
                               +-----------+
                            -4 |     0     |
+-----------+                  +--+--+-----+
|  pointer  |  ------------>   |  0  |
+-----------+                  +--+--+
Dim str As String
str = ""

VBA.vbNullChar is not really a constant: str = VBA.vbNullChar allocates this new BSTR each time of assignment:

                               +-----------+
                            -4 | Length=2  |
+-----------+                  +--+--+--+--+
|  pointer  |  ------------>   |  0  |  0  |
+-----------+                  +--+--+--+--+
Dim str As String
str = VBA.vbNullChar

After a String variable is assigned to hold an address of a particular BSTR string, these two are inseparable. Another assignment first frees the memory holding the previous string, allocates a new BSTR and resets the pointer. This happens extensively when VBA code is running and working with strings; all string functions, concatenation allocates a new BSTR (there is only one exception, similar to arrays, assignment from Functions - see later).

No two pointers are allowed to point to the same BSTR memory address.

On null- and empty-string

  • Both null- and the empty-string tests True for vbNullString.
  • Both appears as "" in Debug windows.
  • Len()/LenB() returns zero for both.
  • AscW() gives Run time error 5 for both.

For true null-string test use:
If StrPtr(s) Then ...

Variant/String

Variant stores the BSTR pointer:

Dim v
v = "hello"

0800-000000000000-C8A3DAD238020000-0000000000000000 (64-bit VBA Variant)

+--+--+-----+-----+-----+-----------------------+-----------------------+
|08 00|    reserved     |      pointer          |           0           |   64-bit VBA Variant
+--+--+-----+-----+-----+-----------------------+-----------------------+
                              |
                              |        +--+--+--+--+--+--+--+--+-
                              +------> |     |     |     |     |
                                       +--+--+--+--+--+--+--+--+-
                                          BSTR unicode 2-byte characters

08 is vbString. Type mismatch when passed to functions expecting String. All other operations uses implicite conversion.

String assignment

String conversion in VBA from other data types is quite liberal, VBA easily converts any expression to a temp-BSTR (except vbNull).

Only the String and Variant data type can store a BSTR pointer and be lvalue in String assignment. The two behaves exactly the same in assignment and conversion is implicite.

Dim v
Dim str As String
v = str
str = v

When the rvalue is another String- or Variant/String variable, VBA String data type is treated as value type: it involves deep-copy i.e. a new BSTR allocated and the string characters are copied.

String assignment from Function does NOT create a new BSTR copy i.e. VBA performs a simple pointer-assignment. The behaviour is the same for both String and Variant/String type Functions - similar to Mid()/Mid$():

lvalue        rvalue                     lvalue        rvalue

Variable      Variable                   Variable      Function
type          type                       type          type

  String     String                        String     String
         \ /                                      \ /
          =                                        =
         / \                                      / \
 Variant     Variant/String               Variant     Variant/String


    BSTR deep copy                            NO BSTR copy

  s2      =       s1                       s2      =       f1()
   |               |                        |               |
   +---> BSTR2     +---> BSTR1              +----->---------+---> BSTR1

Len()/LenB() for String

Len() returns the number of characters in string i.e Lenght field / 2.
LenB() returns the number of bytes in string i.e. the Lenght field at -4.

Mid()/Mid$()

Mid()/Mid$() functions out of range return an allocated empty string "" and never null-string.

In VBA the only way to manipulate characters directly in a string is the Mid() statement: it writes directly into the BSTR buffer i.e. no new BSTR is created:

Mid(str, 1, 1) = "x"

Asc()/AscW()

AscW returns 16-bit signed Integer (-32768 to 32767): the unicode character as it is stored in the buffer. No translation.
Asc returns values 0 to 255: behaves differently based on current locale.

StrPtr()

The StrPtr() function was added in Office 2010 and is located in VBA._HiddenModule:

Function StrPtr(Ptr As String) As LongPtr

It returns LongPtr, a memory address, which simply compiles to a Long or LongLong integer on 32/64-bit VBA.

For example:

Dim str As String
str = "hello"

Debugging gives this output (VBA32):

Hex(StrPtr(str)): 207F2A84
mem dump:
207F2A84: 68 00 65 00 6C 00 6C 00|6F 00 00 00 6E 00 67 00  | h.e.l.l.o...n.g."

The returned value is the content of the String variable, i.e. a pointer to the first character of the data string of the BSTR structure. Can be null-pointer.

NB: StrPtr(var) converts other data types to a temp BSTR, like numerics, expressions and even Fix-length strings; then returns the temp-BSTR address. vbNull gives error as usual.

Passing both String- and Variant/String arguments to StrPtr() returns the true BSTR pointer stored in the variable, even if the variable is the element of an Array (either in String- or Variant Array):

String        Variant/String       In String Array        In Variant Array
 _________     ________________     _________              _________
|_->BSTR__|   |0800____->BSTR__|   |___PSA___|            |___PSA___|
                                        |_______String         |________Variant/String
                                        |_______String         |________Variant/String
                                        |_______String         |________Variant/String

                          StrPtr(true-BSTR-Ptr)
                          VarPtr(true-Ptr)

PSA is Pointer to SAFEARRAY.

StrPtr() uses temp BSTR in Variant of array type

The only exception I've found is when the array are stored in Variant: there is some conversion going on and a temp BSTR is created and passed to StrPtr(). It is the same case for String- and Variant type Arrays in Variant. It's unclear why a String in a String Array returns true BSTR pointer yet String in a Variant of array does not. Tried to see if there is some connection to VarPtr(), but.. it got more confusing:

In Variant of array of String     In Variant of array of Variant
 ________________                  ________________
|0820______PSA___|                |0C20______PSA___|
            |_______String                    |________Variant/String
            |_______String                    |________Variant/String
            |_______String                    |________Variant/String

    StrPtr(temp-BSTR-Ptr)            StrPtr(temp-BSTR-Ptr)    <-- this I do not
    VarPtr(temp-Ptr)                 VarPtr(true-Ptr)         <-- this I understand from Sub(v)

VarPtr()
- In Variant/String(): VarPtr(v(0)) returns an address of a temp variable (as if a temp VT_BYREF Variant holding the address was passed by caller.. and VarPtr() returned this address. Vague guess only)
- In Variant/Variant(): here VarPtr(v(0)) returns true address!

Code example:

Dim v

ReDim v(3) As String
v(0) = "v0"
Debug.Print StrPtr(v(0))  ' temp BSTR address - not BSTR stored in v(0)

ReDim v(3)
v(0) = "v0"
Debug.Print StrPtr(v(0))  ' temp BSTR address - not BSTR stored in v(0)

VBA Fixed-length Strings

There is also the (legacy?) fixed-length String:

Dim str As String * 4

Allocates 4 x 2 = 8 bytes for unicode chars on "stack". No trailing zero. Filled with zeroes.
Every operation creates a new BSTR from it, on write back truncated/padded with spaces. Even when function returns null-ptr.
Ergo: the run-time takes care of fixed-strings, converts them back and forth. Just an emulation? Inefficient?

Useful to limit string lenght automatically.

February 16, 2024

VBA CVErr() and the Err.Raise() test

Last modified: Wed Mar 13 21:17:35 UTC+0100 2024 © Attila Tarpai

VBA CVErr() and the Err.Raise() test


About HRESULT

Win uses a 32-bit HRESULT value system to standardize error handling mechanism.

HRESULT: as a signed integer a negative value indicates failure and other values indicate success.

31        26                    15                              0
+-+-+-+-+-+-+---+---+---+---+---+---+---+---+---+---+---+---+---+
|S R C N r|       Facility      |             Code              |
+-+-+-+-+-+-+---+---+---+---+---+---+---+---+---+---+---+---+---+

S=0 success
S=1 failure

The R, C, N, and r bits are reserved in the HRESULT

Facility is a 11-bit field: for 2048 different sub-systems for error origin (ex. FACILITY_STORAGE=3, FACILITY_DISPATCH=2, FACILITY_WIN32=7)

Code is the unsigned 16-bit error code itself 0-65535.

Facility 0Ah is used by VBA to generate Legacy BASIC error codes (see below).

VBA vbObjectError = &H80040000. This is failure from FACILITY_ITF.


CVErr() makes HRESULT

The conversion function CVErr(Code):

  • returns Variant of type Variant/Error (vbError=10) that stores a 32-bit HRESULT value
  • Code is a Long value with valid range of 00000000-0000FFFF (16-bits). Any other value gives Run-time error 5, Invalid procedure call or argument.

CVErr(0) returns all zero Success HRESULT in the Variant, others generate "Automation error" S=1 failure with Facility=0A. Here is a memory dump:

CVErr(0)         --->  0A00-000000000000-0000000000000000-0000000000000000    64-bit Variant/Error (S=0 success)

CVErr(1..65535)  --->  0A00-000000000000-xxxx0A8000000000-0000000000000000    64-bit Variant/Error (S=1 failure)

When converting Variant/Error to Long we get only the lower 16-bits:

31                              15                              0
+-------------------------------+---+---+---+---+---+---+---+---+
|                               |             Code              |  Long
+-------------------------------+---+---+---+---+---+---+---+---+

                                                | CVErr(Code)
                                                |
                                                V
31        26                    15                              0
+-+-+-+-+-+-+---+---+---+---+---+---+---+---+---+---+---+---+---+
|S 0 0 0 0|           0A        |             Code              |  HRESULT in Variant/Error
+-+-+-+-+-+-+---+---+---+---+---+---+---+---+---+---+---+---+---+

                                                | CLng(Variant)
                                                |
                                                V
31                              15                              0
+-------------------------------+---+---+---+---+---+---+---+---+
|                               |             Code              |  Long
+-------------------------------+---+---+---+---+---+---+---+---+

Excel also uses some of the Variant/Error values. When a UDF function returns Variant/Error into a cell, f. ex. CVErr(2042), Excel will display it as #N/A.

Constant      Error number    Cell error value
xlErrDiv0     2007            #DIV/0!
xlErrNA       2042            #N/A
xlErrName     2029            #NAME?
xlErrNull     2000            #NULL!
xlErrNum      2036            #NUM!
xlErrRef      2023            #REF!
xlErrValue    2015            #VALUE!

The VBA Err() function

Err() is a Public function of Module Information. It returns an instance of ErrObject, which is a Class of VBA:

VBE7.DLL                                               Class ErrObject
VBA                                                  +-----------------+
|__  ..                                              |   Number        |
|___ Class ErrObject                                 |   Description   |
|___ Module Information                              |   Raise()       |
|__  ..   |___ ..                                    |   Clear()       |
          |___ Function Err() As ErrObject           |   ...           |
          |__  ..                                    +-----------------+

Err is a short-cut for Err(): Err.Raise(N) = Err().Raise(N)

Run-time errors or Err.Raise() fills out values in the ErrObject instance and VBA starts looking for an error handler along the call stack.

If Err is not cleared, only Number is updated when a subsequent Err.Raise() is called. This can happen in error handlers or after On Error Resume Next. Internal Run-time errors update all values in the ErrObject.


The big Err.Raise() test

This is an investigation to call Err.Raise(Number) in a VBA Module and observing the result in the ErrObject. Raise causes Run-time error that is trapped in the same procedure with On Error GoTo. The test code:

Sub errtrap()
  On Error GoTo label

      Err.Raise N
      ...
      Exit Sub

label:
      check Err.Number...
      check Err.Description...
      ...
      Resume Next
End Sub

Err.Raise(Number) expects a 32-bit Long argument for Number. Err.Number is also 32-bit Long value.

This is a flowchart what may happen internally:

Err.Raise(N)                                                             Err.Number
                                                                         Err.Description

+----------+      +----------+                    ?   +----------+
| 00000000 | ---> | S00A0005 |                  --->  | 0xxxxxxx |
+----------+      +----------+                 |      +----------+
+----------+      +----------+                 |      +----------+
| 0xxxxxxx | ---> | S00A0005 |                  --->  | S00A0000 |
+----------+      +----------+                 |      +----------+
+----------+      +----------+                 |         SUCCESS
| 0000xxxx | ---> | S00Axxxx |                 |
+----------+      +----------+                 |      +----------+      +----------+
                        |                       --->  | S00Axxxx | ---> | 0000xxxx |
                        |                      |      +----------+      +----------+
+----------+            |      +----------+    |           ^             Standard BASIC error or
| Sxxxxxxx | ----------- ----> |  HRESULT | ---            |             "Application-defined or object-defined error"
+----------+                   +----------+    |           |some
                                    ^          |      +----------+      +----------+
                                    |           --->  | Sxxxxxxx | ---> | Sxxxxxxx |
                                    |                 +----------+      +----------+
                                 Run-time                                "Automation error" + optional Description
                          Other Automation Objects

Err.Raise() in VBA code is raising an error: all Number values are converted to S=1 failure HRESULT codes. The run-time works with HRESULT internally and can also receive HRESULT success/failure codes from other Automation Objects. The HRESULT dispather then delivers codes to ErrObject with possible translations. S=0 success codes do not raise an error, execution continues with the next statement.

All these result were made using Err.Raise() only and in VBA64. Certain results are probably specific to an actual Win- and Office version.

Raising S=0

Valid only in the range of 00000001-0000FFFF, similar to CVErr(). All other values raise 5, "Invalid procedure call or argument":

Raise N      ----------->   Err.Number

00000000                    5, Invalid procedure call or argument
00000001 - 0000FFFF         Err.Number = N
00010000 - 07FFFFFF         5, Invalid procedure call or argument (sometimes 5, "Class not registered"?)

In the 0001-FFFF range many of them are Standard BASIC errors, like 3, "Return without GoSub".

Unused codes keep N and put "Application-defined or object-defined error" in Description.

I have not tested all above 65535..

Some of the Standard errors:

00000003	Return without GoSub
00000005	Invalid procedure call or argument
00000006	Overflow
00000007	Out of memory
00000009	Subscript out of range
0000000A	This array is fixed or temporarily locked
0000000B	Division by zero
0000000D	Type mismatch
0000000E	Out of string space
00000010	Expression too complex
00000011	Can't perform requested operation
00000012	User interrupt occurred
00000014	Resume without error
0000001C	Out of stack space
....
....
000002E8	Search text not found
000002EA	Replacements too long

Raising S=1

Generally, all negative S=1 failure HRESULT in VBA is "Automation error" with an optional error in Description. Most of HRESULT-s after Err.Raise() comes back untranslated in Err.Number. Facility code matters etc.

Raise N         ---->  Err.Number

Raise 8xxxxxxx  ---->  8xxxxxxx "Automation error"

Several HRESULT-s are translated to Standard errors and multiple HRESULT-s raised by Automation Objects can be translated to the same Legacy Error code. F. ex.:

Raise 80020001  ---->  000001B6 Object doesn't support this property or method
Raise 80020003  ---->  000001B6 Object doesn't support this property or method

This also means that getting an "Object doesn't support this property or method" in VBA we have no idea about the original Automation error.

Raising Facility 0Ah S=1 will be translated to Legacy VBA errors (therefore I think VBA works internally with HRESULT). F. ex.:

Raise 800A0000  ---->  SUCCESS, NOTHING HAPPENS, executes next statement
Raise 800A0001  ---->  1, "Application-defined or object-defined error"
Raise 800A0002  ---->  2, "Application-defined or object-defined error"
Raise 800A0003  ---->  3, "Return without GoSub"
...
...
Raise 800AFFFF  ---->  65535, "Application-defined or object-defined error"

An interesting test: Raise Sxxx0003 for all 2048 Facility where result is not merely "Automation error". A method to discover sub-systems and Facility codes:

80000003  Automation error One or more arguments are invalid
80010003  Automation error The caller is dispatching an intertask SendMessage call and cannot call out via PostMessage.
000001B6  Object doesn't support this property or method
0000004C  Path not found
80040003  Automation error This implementation doesn't take advises
80070003  Automation error The system cannot find the path specified.
80080003  Automation error RPC communication failed with OLE service
80090003  Automation error Bad Key.
00000003  Return without GoSub
800B0003  Automation error The form specified for the subject is not one supported or known by the specified trust provider.
800C0003  Automation error No Internet session has been established.
800F0003  Automation error The syntax of the INF is invalid.
80100003  Automation error The supplied handle was invalid.
80190003  Automation error Unexpected redirection status code (3xx).
801F0003  Automation error Asynchronous requests are not valid for this operation.
80270003  Automation error The entity ID conflicts with an already registered id.
80280003  Automation error TPM 1.2: One or more parameter is bad.
802A0003  Automation error This method cannot be called during this type of callback.
80310003  Automation error The BIOS did not correctly communicate with the master boot record (MBR). Contact the computer manufacturer for BIOS upgrade instructions.
80320003  Automation error The filter does not exist.
80390003  Automation error Some BCD entries were not synchronized correctly with the firmware.
803B0003  Automation error The network's underlying layer was not found.
803D0003  Automation error The operation is not allowed due to the current state of the object.
80400003  Automation error One or more fields in the input packet are invalid.
80550003  Automation error The EAS policy being evaluated cannot be enforced by the system.
80630003  Automation error The required service cannot be started.
80650003  Automation error The attribute cannot be written.
80660003  Automation error HD Audio widget does not support the connection list parameter.
80670003  Automation error The StateRepository configuration is not valid.
80830003  Automation error The specified storage tier could not be found on the volume. Confirm that the storage tier name is valid.
80860003  Automation error The application requesting authentication tokens is either disabled or incorrectly configured.
80B00003  Automation error Could not connect to dbgsrv server from ARM architecture device.
80E70003  Automation error The specified resiliency type is not valid.
83750003  Automation error Missing required element.
83760003  Automation error Protocol extensions are not supported.
87AF0003  Automation error Access permission denied

February 15, 2024

VBA run-time error handling mechanism

Last modified: Wed Mar 13 20:26:30 UTC+0100 2024 © Attila Tarpai

VBA error handling mechanism

Err.Raise() starts the same process as if a Run-time error has occurred:

  • On Run-time error or Err.Raise() VBA looks for an error handler along the call stack starting with the current procedure.
  • If none of the procedure has an error handler, the error bubbles up to the default error handler: a modal error message is displayed and execution stops.
  • If an error handler is defined in the current or closest caller, the error is trapped there.
  • Run-time errors or Err.Raise() occurring in the error handler bubbles up again i.e. error trapping is disabled when executing the error handler (to avoid infinite loop).
  • After execution transferred to another handler in another procedure, it never returns. Resume is local to the procedure.
   bubble
     ^      VBA Run-time
     |        | On Error.. error message is displayed
     |        |            and execution stops (default)
     |        |
              |__Sub1()
     ^            | On Error.. error handler in procedure
     |            |
                  |__Sub2()
     ^                |
     |                |
                      |__Sub3()
     ^                    | On Error.. error handler in procedure
     |                    |
                          |__Sub4()
     ^                        |
     |                        |
                              |

 Run-time error or Err.Raise in Sub4() is trapped in Sub3()
 Run-time error or Err.Raise in Sub3() is trapped in Sub3()
 Run-time error or Err.Raise in Sub3()'s error handler is trapped in Sub1()

About Line numbers vs Line labels

It seems like, at least in 64-bit Excel for Microsoft 365, there is not much difference any longer between Line numbers and Line labels.

Here "999" is Line number and "999:" is Line label:

Sub errtrap()                         Sub errtrap()
On Error GoTo 999                     On Error GoTo 999

      Err.Raise 3                           Err.Raise 3
      Exit Sub                              Exit Sub

999                                   999:
      Resume Next                           Resume Next
End Sub                               End Sub

These are not really true in VBA Docs either:

  • Line numbers should be unique within the module - NOT. Only within procedure.
  • Line labels should start with a letter - NOT. See the above compiles and runs.

At least using both "999" and "999:" within the same procedure gives Duplicate declaration error.

There is a little difference between Line numbers and Line labels when it comes to F8-debugging step-by-step:

  • Empty line with only a label is a F8-Step line.
  • Empty line with only a Line number is not: F8 steps on the next non-empty code line right away.

VBA Error handler Execution model

Custom run-time error handlers in VBA are implemented by jumping around within the procedure set up and controlled by Line numbers/labels, On Error and Resume statements. It's a bit like try...catch blocks but with more freedom.

Next is a pseudo-implementation model I have come up to understand what may happen internally.

Lets say every procedure has an Error Handling Mode internal variable. The On Error statement sets this variable to 3 possible values, which will go into effect on the next Run-time error or Err.Raise():

On Error GoTo Line    ---> sets to [Line]
On Error Resume Next  ---> sets to [Next]
On Error GoTo 0       ---> sets to [0]

By default On Error GoTo 0 is in effect i.e. error trapping is disabled. Run-time errors in the procedure will bubble and eventually handled above.

There is also a Code Execution State of the current line being executed: normal or trapped. Trapped mode [T] is entered only after On Error GoTo Line is in effect and a Run-time error occurs or the Err.Raise() statement issued. Execution jumps to Line and Trapped mode is activated.

  Code                                Error
 Execution                           Handling
  State                               Mode

----- Sub() ---------------------------------+
                              [0][Line][Next]|       bubble [0]
  [ ]                                        |         ^
  [ ]       ..                               |         |
  [ ]       Raise                            | jump ---+---> [Next]
  [ ]       next-statement                   |         |
  [ ]       ...                              |         V
  [ ]                                        |       [Line]
  [ ]                                        |
  [T] Line  +--[enter]---------------+       |
  [T]       |                        |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |       bubble
  [T]       |  ..                    |       |         ^
  [T]       |  ..                    |       |         |
  [T]       |  Raise (no return)     |       | jump ---+
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |                        |       |         +---> Re-try statement
  [T]       |  Resume                |       | jump ---+---> next-statement
  [T]       +--[exit][jump]----------+       |         +---> line
                                             |
                                             |
  [T]       |                        |       |
  [T]       |  On Error GoTo -1      |       |
  [T]       +--[exit]----------------+       |
  [ ]          ..                            |
  [ ]          ..                            |
  [ ]          ..                            |
---------------------------------------------+


  [T]         denotes lines executed in error trapped mode
  [ ]         are normal execution code lines
  [enter]     set [T] AND ignore Error Handling Mode on next error
  [exit]      clear [T] AND honour Error Handling Mode on next error

The main difference in trapped mode [T] is:

  • The Error Handling Mode is ignored, disabled. Run-time error or Err.Raise() will bubble. Even if an On Error statement appears in the error handler, which is legal and changes the Error Handling Mode.
  • Resume is a valid statement

There are 2 ways to exit trapped mode:

  • Resume, which always jumps
  • On Error GoTo -1

On Error GoTo -1

It does not change the Error Handling Mode and useful only in trapped mode (in normal execution mode it only clears the ErrObject):

  • clears the ErrObject
  • clears the [T] flag
  • continues execution on the next line in normal execution mode

BEWARE: errors now will cause endless loop if Error Handling Mode hasn't change from [Line].

On Error GoTo -1 is the same as Resume without jump - i.e. jump to the next line.

These are equivalent:

Sub errtrap()                           Sub errtrap()
  On Error GoTo label                     On Error GoTo label

      ...                                     ...
      Exit Sub                                Exit Sub

label:                                  label:
      ...                                     ...
      On Error GoTo -1                        Resume out
      ...                               out:  ...
      ...                                     ...
End Sub                                 End Sub

On Error Resume Next

When On Error Resume Next is set and an error occurs code doesn't enter trapped mode:

  • fill ErrObject
  • execute next statement
  • ErrObject retains its values until Clear

Resume

Resume
Resume Next
Resume Line

Resume can only be executed in trapped mode [T] (otherwise 20, Resume without error).

  • clears the ErrObject
  • clears the [T] flag
  • transfers execution back to the same, next or specified line in normal execution mode

Resume jump is always local to the procedure, it never jumps "back" to a nested procedure that caused the error.

Behaviour of the ErrObject

Every On Error and Resume statement clears the Err Object.

A Run-time error or Err.Raise() statement fills values in the ErrObject. The only difference I have found is:

  • Repeated Err.Raise() on a not cleared ErrObject sets only ErrNum.
  • Repeated internal Run-time errors update all values in the ErrObject.

Infinite loops with trapped code

It is a danger to end up in infinite loops using error handlers and issuing certain On Error/Resume statements.

InfiniteLoop1()
Resume in the error handler re-tries the Err.Raise 3 statement, which is trapped again...

Sub InfiniteLoop1()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    Resume
End Sub

InfiniteLoop2()
On Error GoTo -1 clears the [T] flag and the next Err.Raise() is trapped again...

Sub InfiniteLoop2()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    On Error GoTo -1
    Err.Raise 11
    Resume Next     ' never reached!
End Sub

InfiniteLoop3()
On Error GoTo -1 clears the [T] flag, which causes Resume an invalid statement, which is trapped again...

Sub InfiniteLoop3()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    On Error GoTo -1
    Resume Next     ' Run-time error 20, Resume without error
                    ' next line never reached!
End Sub




February 02, 2024

Memory layout of VBA variable types

Last modified: Fri Mar 15 09:58:23 UTC+0100 2024 © Attila Tarpai

Memory layout of VBA variable types

In VBA the Dim statement declares variables and allocates memory space. This is a quick overview of the main variable types and memory layout.

Some differences between 32- and 64-bit VBA:

  • In 64-bit VBA every pointer is 8 bytes - in 32-bit 4 bytes
  • LongLong is only valid in 64-bit VBA together with its type-character (^)
  • LongPtr is not a real datatype: compiles to Long or LongLong on 32/64-bit VBA
  • 64-bit Variant is larger and 64-bit SAFEARRAY structure is larger

Scalar variable types

  • The native or primitive datatypes for the CPU/FPU: integers and floating-point numbers IEEE 754.
  • Boolean is Integer with value restrictions.
  • Date and Currency are composed datatype structures based on underlying COM/Automation.
LongLong (signed 64-bit integer)   +--+--+--+--+--+--+--+--+
Double (64-bit floating-point)     |        8 bytes        |
Date and Currency                  +--+--+--+--+--+--+--+--+

                                   +--+--+--+--+
Long (signed 32-bit integer)       |  4 bytes  |
Single (32-bit floating-point)     +--+--+--+--+

                                   +--+--+
Integer (signed 16-bit integer)    |  2  |
Boolean (0 = False, -1 = True)     +--+--+

                                   +--+
Byte (unsigned 8-bit integer)      | 1|
                                   +--+

Declaring any of these datatypes with Dim allocates (aligned) n-bytes and initializes the memory with zeroes. So the default value zero = False for Boolean, #12:00:00 AM# for Date and 0 for Currency. It is also the default value for Functions of these types.

Pointer variable types

These VBA variable types store pointers:

  • String variable
  • Object types (all Object, Collection, Class, etc.)
  • Dynamic Array variable
0           4           8
+-----------+-----------+
|  pointer      64-bit  |
+-----------+-----------+

 Dim str as String        ---> to BSTR STRUCTURE

 Dim o as Object          ---> to IDispatch

 Dim varr()               ---> to SAFEARRAY STRUCTURE

After Dim and for functions returning these types the default value is zero, a null-ptr:

  • StrPtr() is zero for String
  • ObjPtr() is zero = Nothing for Object types
  • No such thing for Dynamic Arrays

VBA Variant

Speciality of VBA/COM is the Variant data type. Variant can store all variable types, including arrays. Conversion is implicite. It is a structure of tagVARIANT in Oaidl.h, which compiles to different sizes on 32- or 64-bit VBA and operated by COM routines:

0     2                 8                       16
+--+--+-----+-----+-----+-----------------------+
|TYPE |    reserved     |     8-byte data       |   32-bit VBA Variant: 16 bytes
+--+--+-----+-----+-----+-----------------------+

0     2                 8                       16                      24
+--+--+-----+-----+-----+-----------------------+-----------------------+
|TYPE |    reserved     |     8-byte data       |                       |   64-bit VBA Variant: 24 bytes
+--+--+-----+-----+-----+-----------------------+-----------------------+

Every Dim and each elements of Variant Arrays allocate 16- or 24 bytes.
The reason for larger size is this structure in tagVARIANT for UDT-s, when it stores two 64-bit pointers (for vbUserDefinedType Variant):

struct {
          PVOID       pvRecord;
          IRecordInfo *pRecInfo;
        } __VARIANT_NAME_4;

But Variant storing UDT-s is possible only for Types from external libraries, f. ex. mscorlib. User Types defined in a standard VBA Module or Class cannot be converted to Variant (VBA doesn't create an instance for IRecordInfo). This means that the last qword of all 64-bit Variant in normal VBA code is usually zero and unused.

Variant type-mapping and memory layout:

enum VARENUM      VbVarType       VarType()   |TYPE |    RESERVED     |        DATA           |    64-bit VBA only    |

VT_EMPTY=0        vbEmpty             0       |00 00|00 00 00 00 00 00|00 00 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
VT_NULL=1         vbNull              1       |01 00|00 00 00 00 00 00|00 00 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
VT_I2=2           vbInteger           2       |02 00|00 00 00 00 00 00|XX XX 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
VT_I4=3           vbLong              3       |03 00|00 00 00 00 00 00|XX XX XX XX 00 00 00 00|00 00 00 00 00 00 00 00|
VT_R4=4           vbSingle            4       |04 00|00 00 00 00 00 00|XX XX XX XX 00 00 00 00|00 00 00 00 00 00 00 00|
VT_R8=5           vbDouble            5       |05 00|00 00 00 00 00 00|XX XX XX XX XX XX XX XX|00 00 00 00 00 00 00 00|
VT_CY=6           vbCurrency          6       |06 00|00 00 00 00 00 00|XX XX XX XX XX XX XX XX|00 00 00 00 00 00 00 00|
VT_DATE=7         vbDate              7       |07 00|00 00 00 00 00 00|XX XX XX XX XX XX XX XX|00 00 00 00 00 00 00 00|
VT_BSTR=8         vbString            8       |08 00|00 00 00 00 00 00|XX XX XX XX 64 64 64 64|00 00 00 00 00 00 00 00|
VT_DISPATCH=9     vbObject            9       |09 00|00 00 00 00 00 00|XX XX XX XX 64 64 64 64|00 00 00 00 00 00 00 00|
VT_ERROR=10       vbError            10       |0A 00|00 00 00 00 00 00|XX XX XX XX 00 00 00 00|00 00 00 00 00 00 00 00|
VT_BOOL=11        vbBoolean          11       |0B 00|00 00 00 00 00 00|XX XX 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
VT_VARIANT=12     vbVariant          12
VT_UNKNOWN=13     vbDataObject       13
VT_DECIMAL=14     vbDecimal          14       |0E 00|XX XX XX XX XX XX|XX XX XX XX XX XX XX XX|00 00 00 00 00 00 00 00|
VT_UI1=17         vbByte             17       |11 00|00 00 00 00 00 00|XX 00 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
VT_I8=20          vbLongLong         20       |14 00|00 00 00 00 00 00|XX XX XX XX XX XX XX XX|00 00 00 00 00 00 00 00|
VT_RECORD=36      vbUserDefinedType  36       |24 00|00 00 00 00 00 00|XX XX XX XX XX XX XX XX|64 64 64 64 64 64 64 64|
VT_ARRAY=0x2000   vbArray            8192     |XX 20|00 00 00 00 00 00|XX XX XX XX 64 64 64 64|00 00 00 00 00 00 00 00|
VT_BYREF=0x4000   -                  -        |XX 40|00 00 00 00 00 00|XX XX XX XX 64 64 64 64|00 00 00 00 00 00 00 00|

The default value for Variant after Dim is all zero, vbEmpty.

  • VT_ARRAY = vbArray is a flag. These are Variants of array type and store a Pointer to SAFEARRAY structure (PSA).
  • VT_BYREF is a flag used by VBA to pass other data types to procedures as ByRef Variant. The conversion is implicite. VT_BYREF stores a pointer and its operation is invisible for the programmer: callee automatically dereferences the pointer each time of access (see details in Calling mechanisms).
  • vbVariant is used only together with the vbArray flag (Variant of array of Variant)
  • vbString, vbArray and vbObject Variant stores a pointer to BSTR structure, SAFEARRAY structure and IDispatch Interface respectively.

These data types live only inside a Variant by conversion and are handled by COM routines:

  • vbDecimal by CDec()
  • vbError by CVErr()

CDec(number) returns Variant/Decimal. DECIMAL maps to the Variant structure and uses the reserved bytes for a 14-byte structure:

+---+---+-------+-------+-------+-------------------------------+
|VARTYPE|      reserved         |        8-byte data            |  32-bit VBA Variant
+---+---+-------+-------+-------+-------------------------------+

+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|  res  |sc | S |     Hi32      |     Lo32      |     Mid32     |  DECIMAL
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

typedef struct tagDEC {
   WORD wReserved;
   BYTE scale;
   BYTE sign;
   ULONG Hi32;
   ULONGLONG Lo64;
 } DECIMAL;

CVErr(code) returns Variant/Error and store a 32-bit HRESULT value (see details in Error handling). The argument is 32-bit Long with acceptable range of 0-65535:

CVErr(0)         --->  0A00-000000000000-0000000000000000-0000000000000000    64-bit Variant/Error (S=0 success)

CVErr(1..65535)  --->  0A00-000000000000-xxxx0A8000000000-0000000000000000    64-bit Variant/Error (S=1 failure)

Passing variables and expressions as Byref Variant

Any variable, expression can be passed as Byref Variant and it is very common for several built-in functions as well.

Here Sub (v) receives one pointer, an address of a Variant and defers the actual value and type at run-time:

Sub (v)
                                                                                Pass:
                       _______________
               +--->  |_xxxx____xx____|  Variant variable                       Dim myVar As Variant
               |       _______________
               |      |_0800___->BSTR_|  Variant/String variable                Dim myStr As Variant
               |       _______________
               |      |_xx20___->SA___|  Variant/Array variable                 Dim myArr() As Variant
               |
               |
               |      or TEMP Variant made by caller:
               |       _______________
               +--->  |_0200____100___|  Variant/Integer                        100
               |       _______________
               |      |_0300___60000__|  Variant/Long                           60000
               |       _______________
 _________     |      |_0800___->BSTR_|  Variant/String to BSTR                 "hi"
|_________|->--+       _______________
     v         |      |_xx20___->SA___|  Variant/Array to SAFEARRAY             Array(1,2,3)
 address of    |
  Variant      |
               |      or for Non-Variant variable
               |      using TEMP VT_BYREF-Variant:
               |       _______________             ____
               +--->  |_xx40____ptr___|  ------>  |____|  Integer               Dim I As Integer
                       VT_BYREF-Variant            _________
                                                  |_________|  Long             Dim L As Long
                                                   _________
                                                  |__->BSTR_|  String           Dim str As String
                            ^                      _________
                            |                     |__->SA___|  Array            Dim varr()
                            |
                         VarPtr(v)

Note that VarPtr(v) will always return the address in v itself. Something to keep in mind when Non-Variant variables are passed.