Results 1 to 1 of 1

Thread: VBAStack - Programmatically read call stack in VBA6/7

Threaded View

  1. #1

    Thread Starter
    New Member
    Join Date
    Jan 2026
    Posts
    1

    VBAStack - Programmatically read call stack in VBA6/7

    Update - big news! 30/01/2025

    Hi again!

    After the changes to VBAStack, I realised it was now so much simpler that it can actually run entirely inside VBA, no .NET or COM required. I realise this probably eclipses the original post, but I'll leave it below anyway.

    Note that this code is incredibly rough in all honesty - it could do with adding some more Win32 API calls to check that the addresses it reads with RtlMoveMemory are actually readable, otherwise if something goes wrong it will cause CTD's.

    Code:
    Option Explicit On
    
    'Tested on x86 Access 2003, x86 Access 2013, x86 Access 365, x64 Access 2013, and x64 Access 365.
    
    'Example use:
    
    '    Private Sub Example()
    '
    '    Dim StackFrames() As VBAStack.StackFrame
    '    StackFrames = VBAStack.GetCallstack()
    '
    '    Dim str As String
    '    Dim i As Integer
    '
    '    For i = 0 To UBound(StackFrames)
    '
    '        str = str & StackFrames(i).FrameNumber & ", " & StackFrames(i).ProjectName & "::" & StackFrames(i).ObjectName & "::" & StackFrames(i).ProcedureName & vbCrLf
    '
    '    Next
    '    MsgBox (str)
    '
    '    'Above outputs this:
    '    ' 1, MyMod::Example
    '    ' 2, MyMod::Sub2
    '    ' 3, Form_Form1::Command0_Click
    '
    '    Dim frame As VBAStack.StackFrame
    '    frame = VBAStack.GetCurrentProcedure
    '
    '    MsgBox (frame.ObjectName & "::" & frame.ProcedureName)
    '    'Outputs this:
    '    ' MyMod::Example
    '
    '    End Sub
    
    
    #If VBA7 = False Then
    Private Enum LongPtr
            [_]
        End Enum
        Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef lpDest As Any, ByVal lpSource As LongPtr, ByVal cbCopy As Long)
    #Else
        Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef lpDest As Any, ByVal lpSource As LongPtr, ByVal cbCopy As Long)
    #End If
    
    #If Win64 Then
        Const PtrSize As Integer = 8
    #Else
        Const PtrSize As Integer = 4
    #End If
    
    Public Type StackFrame
        ProjectName As String
        ObjectName As String
        ProcedureName As String
        realFrameNumber As Integer
        FrameNumber As Integer
        Errored As Boolean
    End Type
    
    Public Function FrameCount() As Integer
    
    On Error GoTo ErrorOccurred
    
        FrameCount = -1
    
        'Get ptr to VBA.Err
        Dim errObj As LongPtr
        errObj = ObjPtr(VBA.Err)
        
        
        'Get g_ebThread
        Dim g_ebThread As LongPtr
        CopyMemory g_ebThread, (errObj + PtrSize * 6), PtrSize
        If g_ebThread = 0 Then GoTo ErrorOccurred
        
        'Get g_ExFrameTOS
        Dim g_ExFrameTOS As LongPtr
        #If Win64 Then
            g_ExFrameTOS = g_ebThread + (&H10)
        #Else
            g_ExFrameTOS = g_ebThread + (&HC)
        #End If
        If g_ExFrameTOS = 0 Then GoTo ErrorOccurred
        
        
        'Get top ExFrame
        Dim pTopExFrame As LongPtr
        CopyMemory pTopExFrame, g_ExFrameTOS, PtrSize
        If pTopExFrame = 0 Then GoTo ErrorOccurred
    
        
        'Loop over frames to count
        Dim pExFrame As LongPtr: pExFrame = pTopExFrame
        Do
            CopyMemory pExFrame, pExFrame, PtrSize
            FrameCount = FrameCount + 1
            If pExFrame = 0 Then Exit Do
        Loop
    
    Exit Function
    
    ErrorOccurred:
        
    End Function
    
    Public Function GetCurrentProcedure() As StackFrame
        GetCurrentProcedure = VBAStack.GetStackFrame(2)
    End Function
    
    Public Function GetCallstack() As StackFrame()
        Dim stackCount As Integer: stackCount = VBAStack.FrameCount
        Dim index As Integer: index = 1
        Dim FrameArray() As StackFrame
        ReDim FrameArray(stackCount - 2)
        
        Do Until index = stackCount
                    
            FrameArray(index - 1) = VBAStack.GetStackFrame(index + 1)
            index = index + 1
            
        Loop
        
        GetCallstack = FrameArray
    End Function
    
    Public Function GetStackFrame(Optional ByVal FrameNumber As Integer = 1) As StackFrame
    
    On Error GoTo ErrorOccurred
        
        If FrameNumber < 1 Then GoTo ErrorOccurred
        
        Dim retVal As StackFrame
        retVal.realFrameNumber = FrameNumber
        retVal.FrameNumber = FrameNumber - 1
        
        'Get ptr to VBA.Err
        Dim errObj As LongPtr
        errObj = ObjPtr(VBA.Err)
        
        
        'Get g_ebThread
        Dim g_ebThread As LongPtr
        CopyMemory g_ebThread, (errObj + PtrSize * 6), PtrSize
        If g_ebThread = 0 Then GoTo ErrorOccurred
        
        
        'Get g_ExFrameTOS
        Dim g_ExFrameTOS As LongPtr
        #If Win64 Then
            g_ExFrameTOS = g_ebThread + (&H10)
        #Else
            g_ExFrameTOS = g_ebThread + (&HC)
        #End If
        If g_ExFrameTOS = 0 Then GoTo ErrorOccurred
        
        
        'Get top ExFrame
        Dim pTopExFrame As LongPtr
        CopyMemory pTopExFrame, g_ExFrameTOS, PtrSize
        If pTopExFrame = 0 Then GoTo ErrorOccurred
    
    
        'Get next ExFrame (do this minimum once, since top frame is this procedure)
        Dim pExFrame As LongPtr: pExFrame = pTopExFrame
        Do
            CopyMemory pExFrame, pExFrame, PtrSize
            If pExFrame = 0 Then GoTo ErrorOccurred
            FrameNumber = FrameNumber - 1
        Loop Until FrameNumber = 0
        
        
        'Get RTMI
        Dim pRTMI As LongPtr
        CopyMemory pRTMI, (pExFrame + PtrSize * 3), PtrSize
        If pRTMI = 0 Then GoTo ErrorOccurred
        
        
        'Get ObjectInfo
        Dim pObjectInfo As LongPtr
        CopyMemory pObjectInfo, pRTMI, PtrSize
        If pObjectInfo = 0 Then GoTo ErrorOccurred
        
        
        'Get Public Object Descriptor
        Dim pPublicObject As LongPtr
        CopyMemory pPublicObject, (pObjectInfo + PtrSize * 6), PtrSize
        If pPublicObject = 0 Then GoTo ErrorOccurred
        
        
        'Get pointer to module name string from Public Object Descriptor
        Dim pObjectName As LongPtr
        CopyMemory pObjectName, (pPublicObject + PtrSize * 6), PtrSize
        If pObjectName = 0 Then GoTo ErrorOccurred
        
        
        'Read the object name string
        Dim objName As String
        Dim readByteObjName As Byte
        Do
            CopyMemory readByteObjName, pObjectName, 1
            pObjectName = pObjectName + 1
            If readByteObjName = 0 Then Exit Do 'Read null char - end loop
            objName = objName & Chr(readByteObjName)
        Loop
        retVal.ObjectName = objName
        
        'Get pointer to methods array from ObjectInfo
        Dim pMethodsArr As LongPtr
        CopyMemory pMethodsArr, (pObjectInfo + PtrSize * 9), PtrSize
        If pMethodsArr = 0 Then GoTo ErrorOccurred
        
        
        'Get count of methods from Public Object Descriptor
        Dim methodCount As Long
        CopyMemory methodCount, (pPublicObject + PtrSize * 7), 4
        If methodCount = 0 Then GoTo ErrorOccurred
        
        
        'Search the method array to find our RTMI
        Dim methodIndex As Integer: methodIndex = -1
        Dim i As Integer
        Dim pMethodRTMI As LongPtr
        For i = methodCount - 1 To 0 Step -1
            CopyMemory pMethodRTMI, (pMethodsArr + PtrSize * i), PtrSize
            If pMethodRTMI = 0 Then GoTo ErrorOccurred
            If pMethodRTMI = pRTMI Then
                methodIndex = i
                Exit For
            End If
        Next
        
        If methodIndex = -1 Then GoTo ErrorOccurred
        
        
        'Get array of method names from Public Object Descriptor
        Dim pMethodNamesArr As LongPtr
        CopyMemory pMethodNamesArr, (pPublicObject + PtrSize * 8), PtrSize
        If pMethodNamesArr = 0 Then GoTo ErrorOccurred
        
        
        'Get pointer to our method name
        Dim pMethodName As LongPtr
        CopyMemory pMethodName, (pMethodNamesArr + PtrSize * methodIndex), PtrSize
        If pMethodName = 0 Then GoTo ErrorOccurred
        
        
        'Read the method name string
        Dim procName As String
        Dim readByteProcName As Byte
        Do
            CopyMemory readByteProcName, pMethodName, 1
            pMethodName = pMethodName + 1
            If readByteProcName = 0 Then Exit Do 'Read null char - end loop
            procName = procName & Chr(readByteProcName)
        Loop
        retVal.ProcedureName = procName
        
        
        'Get ObjectTable
        Dim pObjectTable As LongPtr
        CopyMemory pObjectTable, (pObjectInfo + PtrSize * 1), PtrSize
        If pObjectTable = 0 Then GoTo ErrorOccurred
        
        
        'Get project name from ObjectTable
        Dim pProjName As LongPtr
        #If Win64 Then
            CopyMemory pProjName, (pObjectTable + &H68), PtrSize
        #Else
            CopyMemory pProjName, (pObjectTable + &H40), PtrSize
        #End If
        If pProjName = 0 Then GoTo ErrorOccurred
        
        
        'Read the project name string
        Dim projName As String
        Dim readByteProjName As Byte
        Do
            CopyMemory readByteProjName, pProjName, 1
            pProjName = pProjName + 1
            If readByteProjName = 0 Then Exit Do 'Read null char - end loop
            projName = projName & Chr(readByteProjName)
        Loop
        
        
        retVal.ProjectName = projName
        GetStackFrame = retVal
        
    Exit Function
    
    ErrorOccurred:
        retVal.Errored = True
        GetStackFrame = retVal
    End Function

    Original post

    First post here after lurking for quite some time! Hopefully I'm doing this right.

    Downloadable from Nuget, source is available at the Github repo.

    Update 1.0.0.14 - 26/01/2025

    VBAStack can now retrieve the callstack when running compiled Access MDE files! Good god that took some work. Still a bit shakey on the VB structures used (especially the x86/x64 differences between them) but so far, seems to behave great.

    VBAStack

    A library for retrieving VBA callstack information at runtime from Office applications. This enables debugging and error reporting capabilities for VBA add-ins and COM add-ins targeting Microsoft Office (or anything else that can call it and pass a VBE COM object to it, I suppose!).

    Credit to "The Trick" - without finding his VbTrickTimer code, I would've just accepted that VBE7 doesn't export the functions to do this, and would never have found them.

    Note on AI use
    The vast majority of this code was written with my own two hands, but I will admit to prettifying things (mostly documentation, and the bones of this readme) with AI.

    I did discover that it is absolutely terrible at debugging stuff when it isn't well-covered ground, though.

    Overview

    VBAStack allows you to programmatically retrieve the current VBA call stack from a running VBA application (Excel, Word, Access, etc.).

    It does need to be loaded into the same process as the VBE, as it calls internal undocumented functions inside VBE7.dll. As such, it's best deployed as part of a COM add-in or VBA add-in and called from VBA error handlers.

    I am personally using it with a VSTO addin for MS Access, which makes deployment incredibly easy (not quite as easy as a certain other tool that can get the callstack, but I'm working on it).

    These functions are not publicly documented or even exported from the DLL, however Microsoft does include them in the symbol files available on the Microsoft Symbol Servers. As such, we can dynamically load them from the PDB files and call them at runtime.

    Due to the undocumented nature of these functions, and my lack of confidence in my own ability, this library is provided as-is without any guarantees. Use at your own risk.

    Requirements

    • VBE 7.0+ (Office 2010 and later)
    • . NET Framework 4.8


    Architecture

    The solution consists of several interconnected projects:

    Core Projects

    VBAStack (VB.NET)
    The main library that provides the high-level API for retrieving VBA callstacks.

    Key Components:
    • VBECallstackProvider - Public API for getting callstack information
    • VBESymbolResolver - Communicates with PdbEnum to resolve function addresses
    • VBENativeWrapper - Marshals calls to native VBE7.DLL functions
    • VBEWindowHook - Manages VBE window visibility during callstack capture
    • VBEEnums - Defines VBE execution modes and constants


    PdbEnum (C#)
    Base library for enumerating symbols from PDB (Program Database) files. I do not recommend using this outside of this project, as I haven't tested it at all outside of getting the necessary symbols from VBE7.DLL's PDB.

    Key Components:
    • SymbolEnumerator - Interfaces with DbgHelp.dll to load and enumerate symbols
    • ModuleHelper - Manages module loading and symbol resolution
    • OutputFormatter - Formats symbol information for consumption
    • ImageHlpStructures - P/Invoke structures for DbgHelp API


    PdbEnum_x64 / PdbEnum_x86 (C#)
    These are external platform-specific console executables that extract symbol addresses from VBE7.DLL's PDB files. This is somewhat of a hack - use of DbgHelp.dll can mess with debugging, so I figured it was best to isolate it in a separate process.

    NativePtrCaller (C#)
    Provides wrappers for calling native function pointers from managed code using C# 9.0 function pointers.

    Key Functions:
    • EbMode() - Gets current VBE execution mode
    • EbSetMode() - Sets VBE execution mode (Design/Break/Run)
    • EbGetCallstackCount() - Gets number of stack frames
    • ErrGetCallstackString() - Retrieves formatted callstack string for a frame


    Usage

    Basic Example

    VBA error handler in your Access, Excel, Word, etc. VBA module
    Code:
    Public Sub ExampleVbaProcedure()
        On Error GoTo ErrorHandler
        '... your code here ...
    ErrorHandler:
        Dim callstack As String
        callstack = Application.COMAddins("MyAddin").Object.VSTO_GetVbaCallstack()
        'or...
        callstack = Application.COMAddins("MyAddin").Object.Generic_GetVbaCallstack(Application.VBE)
        MsgBox "An error occurred. Callstack:" & vbCrLf & callstack
        Exit Sub
    End Sub
    VB.Net - within a VSTO or COM add-in with access to the VBE object
    Code:
    Imports VBAStack
    
    'Expose a public function for VBA to call...
    Public Function VSTO_GetVbaCallstack() As String
        Dim vbe As Object = Globals. ThisAddIn.Application.VBE ' "Globals.ThisAddin" is a VSTO thing
        Return VBECallstackProvider.GetCallstack(vbe)
    End Function
    
    Public Function Generic_GetVbaCallstack(vbe As Object) As String
        Return VBECallstackProvider.GetCallstack(vbe)
    End Function
    C# - same as above, but in C#
    Code:
    using VBAStack;
    
    //Expose a public function for VBA to call...
    public string VSTO_GetVbaCallstack()
    {
        Object vbe = Globals.ThisAddIn.Application.VBE; // "Globals. ThisAddin" is a VSTO thing
        return VBECallstackProvider.GetCallstack(vbe);
    }
    public string Generic_GetVbaCallstack(Object vbe)
    {
        return VBECallstackProvider.GetCallstack(vbe);
    }
    Example Output

    Code:
    Module1::ProcessData
    Module2::CalculateResults
    ThisWorkbook::Workbook_Open

    How It Works

    1. On first use, VBAStack calls PdbEnum to extract function addresses from VBE7.DLL's PDB symbols
    2. PdbEnum uses DbgHelp. dll to download symbols from Microsoft Symbol Servers if not already cached, then searches for our target functions in VBE7
    3. VBAStack then caches the function pointers for subsequent calls
    4. It then calls "EbMode", from VBE7 - this checks what state the editor is in.
    5. To make sure the VBE window doesn't flash up on screen during the next step, it sets up a window hook to intercept messages to the VBE, and hide it if necessary
    6. If the VBE is in "Run" mode, we switch it to "Break" mode using "EbSetMode" - this is necessary for the next 2 steps, as otherwise the callstack functions won't work
    7. We call "EbGetCallstackCount" to find out how many stack frames there are
    8. We loop through each stack frame, calling "ErrGetCallstackString" to get a formatted string for each frame (which differs from the old versions of these functions - for VBA6 it seems you used EbGetExecutingProj and iterated through some structures)
    9. Do a little formatting on the results, because I prefer "::" to "."
    10. Restores original VBE mode and window state, if we had to change them
    11. Returns formatted callstack string


    Limitations

    • VBE 7.0+ Only: Does not support currently Office 2007 or earlier (VBA6)
    • Windows Only: Relies on Windows-specific APIs (DbgHelp.dll, kernel32.dll)
    • PDB Dependency: Requires Microsoft symbol servers to be accessible for first-time symbol download. This WILL NOT WORK in offline environments unless symbols are pre-cached - and at that point, it's easier to just hardcode the offsets.
    • Performance: First call may be a touch slow due to symbol loading (usually under a second on my machine, if it needs to download symbols though its entirely dependent on internet speed. The symbols are only ~6MB though, so it shouldn't be too bad)


    Building

    Todo. This sucks at the minute.

    Deployment

    When distributing:

    Just add the Nuget package to your project. Or;

    1. Include
      Code:
      VBAStack.dll
      in your project references
    2. Ensure
      Code:
      PdbEnum_x64.exe
      and
      Code:
      PdbEnum_x86.exe
      are in the same directory or a subdirectory
    3. Include
      Code:
      NativePtrCaller.dll
      and
      Code:
      PdbEnum.dll
    4. Ensure the target machine has internet access for initial PDB symbol download


    Troubleshooting

    "VBE version is less than 7.0"
    Upgrade to Office 2010 or later, or try your hand at implementing this for VBA6 and remove that check.

    "Could not get pointers to necessary VBE7 functions"
    • Check that PdbEnum executables are present
    • Verify internet connectivity for symbol server access


    Empty callstack returned
    • VBA code is not currently executing
    • The VBE is in Design mode with no code on the stack
    • There's an issue with symbol resolution
    Last edited by Ariche2; Jan 31st, 2026 at 10:22 AM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width