0

I am using some code in VBA that relies on CoTaskMemAlloc to create a COM object which won't have its memory released unexpectedly when VBA clears its variables. However I've noticed that if I use End then the IUnknown::Release method of the lightweight COM object that calls CoTaskMemFree never runs. (Basically the code in this post will have a memory leak I want to fix https://stackoverflow.com/a/52261687/6609896)

To avoid the memory leak, I thought at least I could save the pointers to the allocated memory in the AppDomain, and then next time VBA is run, if any pointers are left behind they get cleaned up. I came up with the following:

'@Folder("Implementation")
'@PredeclaredID
Option Explicit

Private Declare PtrSafe Function CoTaskMemAlloc Lib "ole32" (ByVal byteCount As LongPtr) As LongPtr
Private Declare PtrSafe Sub CoTaskMemFree Lib "ole32" (ByVal pMemory As LongPtr)

Private localCacheInstance As Collection
Private Const name As String = "d5167d32-602c-4375-8eed-6ed642cad409" 'use ps [guid]::NewGuid() to avoid name clashes

Private Property Get defaultAppDomain() As AppDomain
    Static host As New mscoree.CorRuntimeHost
    Static result As mscorlib.AppDomain
    If result Is Nothing Then
        host.Start
        host.GetDefaultDomain result
    End If
    Set defaultAppDomain = result
End Property

Private Property Get openMemoryAddresses() As Collection
    ' References:
    '  mscorlib.dll
    '  Common Language Runtime Execution Engine
    If localCacheInstance Is Nothing Then
        With defaultAppDomain
            'if collection not in cache then regenerate it
            If IsObject(.GetData(name)) Then
                'save it to a local copy for faster access (so we don't keep going through appDomain)
                Set localCacheInstance = .GetData(name)
            Else
                Set localCacheInstance = New Collection
                .SetData name, localCacheInstance
            End If
        End With
    End If

    Set openMemoryAddresses = localCacheInstance
End Property

Public Function MemAlloc(ByVal cb As LongPtr) As LongPtr
    MemAlloc = CoTaskMemAlloc(cb)
    Debug.Print "Alloc "; MemAlloc
    openMemoryAddresses.Add MemAlloc
End Function

Public Sub FreeAll()
    'This is idempotent so can be called twice in a row without breaking anything
    Dim addr As Variant
    For Each addr In openMemoryAddresses
        Debug.Print "Free "; addr
        CoTaskMemFree addr
    Next addr
    
    'to avoid double releasing memory next time we're called, we must clear the reference
    resetCache
End Sub

Private Sub resetCache()
    defaultAppDomain.SetData name, Empty
    Set localCacheInstance = Nothing
End Sub

Private Sub Class_Initialize()
    If Not Me Is CoTaskAllocator Then Err.Raise vbObjectError + 1, , "You cannot instantiate a new " & TypeName(Me) & ", use the predeclared instance"
    FreeAll
End Sub

Private Sub Class_Terminate()
    FreeAll
End Sub

I do not know how to debug memory leaks like this, does my approach seem sound? Is there a simpler approach? Am I understanding the semantics of CoTaskMemAlloc, that while Excel.exe is running, the appdomain and the memory allocated will remain live.


N.b. The code is used like this


Dim pMemory1 As LongPtr = CoTaskAllocator.MemAlloc(18)

'... Stop Button

Dim pMemory2 As LongPtr = CoTaskAllocator.MemAlloc(34) 'will free pMemory1 if still around
Greedo
  • 4,967
  • 2
  • 30
  • 78
  • If you do alloc/free by yourself and cannot catch all free cases, you will have memory leaks. What makes you thing what you do is better/more lightweight than using COM objects using VBA? Not sure why .NET is in the picture either. – Simon Mourier Aug 10 '23 at 13:44
  • @SimonMourier (1) This is VBA meaning you cannot create certain COM objects - specifically anything that is not derived from IDispatch. I'm trying to implement IEnumVARIANT like this https://stackoverflow.com/a/52261687/6609896. (2) Lightweight is a term I stole from elsewhere. In this context it doesn't mean better, it is just used to refer to creating COM objects on-the-fly from minimal basics; e.g. a vtable, some instance memory and nothing else... – Greedo Aug 10 '23 at 14:26
  • (3) So .Net comes in with `AppDomain`, used to persist data between VBA sessions; vba variables are all cleared when the stop button is pressed but AppDomain isn't, so it can be used to store some data over a "hard stop". It's a bit of a hack and I'm worried if the lifetimes will match up between CoTaskMemAlloc and AppDomain, otherwise I could have a bug when I think a pointer hasn't been freed but it has, I think – Greedo Aug 10 '23 at 14:27
  • Trying to do complex interop scenario in VB/VBA sure doesn't make your life easy, it's just the wrong tool for that. Since you have .NET in the game, why don't you move parts of your interop VBA code in there and communicate back with VBA, .NET is much more powerful in terms of interop. – Simon Mourier Aug 10 '23 at 18:10
  • Well... I have mscorlib which is a COM library for grabbing some stuff from .NET. But ultimately I want all user code to be VBA with copy-paste deployment. – Greedo Aug 10 '23 at 20:51
  • You can use reg-free COM, and .NET supports it too. That gives you copy-paste deployment for COM objects. – Simon Mourier Aug 10 '23 at 21:28

0 Answers0