Loading simple RTF containing an image does not release memory on subsequent loads

roy nelson 25 Reputation points
2025-08-05T09:42:18.5566667+00:00

Consider this code it uses the MSFTEdit Richedit 50 control, every time you click the caption it will load the oom.rtf which contains an image, it displays the private bytes used by the process, (you can Task manager or Process explorer to the see the memory increase). There appear to be no way to stem the leak.

#include <windows.h>
#include <richedit.h>
#include <psapi.h>
static const TCHAR RTFFilename[] = TEXT("oom.rtf");
HWND RichEditHandle, MainWindowHandle;
DWORD CALLBACK MyRead(DWORD dwCookie, LPBYTE pbBuffer, LONG cb, LONG *pcb)
{
	HFILE	hf = (HFILE) dwCookie;
	if(hf == HFILE_ERROR)
		return (DWORD) E_FAIL;
	*pcb = _lread(hf, pbBuffer, cb);
	return (DWORD) (*pcb >= 0 ? NOERROR : (*pcb = 0, E_FAIL));
}
SIZE_T GetCurrentProcessMemoryInfo()
{
    PROCESS_MEMORY_COUNTERS_EX pmc;
	pmc.cb = sizeof(pmc);
	GetProcessMemoryInfo(GetCurrentProcess(), (PPROCESS_MEMORY_COUNTERS)&pmc,sizeof(pmc));
	return (pmc.PrivateUsage / 1024);
}
void ReadRTFFile(HWND hwndRE,  LPCSTR szFile)
{
	EDITSTREAM es;
    DWORD dwFormat;
	es.dwCookie = (DWORD) _lopen(szFile, OF_READ);
	if(es.dwCookie == (DWORD) HFILE_ERROR) return;
    dwFormat = SF_RTF;
	es.dwError = 0;
	es.pfnCallback = MyRead;
	SendMessage(hwndRE,EM_STREAMIN,dwFormat, (LPARAM) &es);
	_lclose((HFILE) es.dwCookie);
	SendMessage(hwndRE, EM_SETMODIFY, (WPARAM) FALSE, 0);
	InvalidateRect(hwndRE, NULL, TRUE);
	UpdateWindow(hwndRE);
}
void ShowMemoryUser(HWND WindowH)
{
	TCHAR szT[256];
    wsprintf(szT, TEXT("Loaded... %s memory used: %dkb"), RTFFilename,GetCurrentProcessMemoryInfo());
	SetWindowText(WindowH, szT);
}
void ShowLoading(HWND WindowH)
{
	TCHAR szT[256];
    wsprintf(szT, TEXT("Loading... %s memory used: %dkb"),RTFFilename, GetCurrentProcessMemoryInfo());
	SetWindowText(WindowH, szT);
}
#ifdef MSFTEDIT_CLASS
#undef MSFTEDIT_CLASS
#define MSFTEDIT_CLASS		"RICHEDIT50W"
#endif
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    switch (msg) {
    case WM_CREATE: {
        LoadLibrary(TEXT("Msftedit.dll")); // Ensure the RichEdit DLL is loaded
        RichEditHandle = CreateWindowEx(0, MSFTEDIT_CLASS, TEXT(""),
            WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL,
            10, 10, 750, 550, hwnd, NULL, GetModuleHandle(NULL), NULL);
        ReadRTFFile(RichEditHandle,RTFFilename);
    }
       break;
    case WM_NCLBUTTONDOWN:
    {
       if (wp == HTCAPTION)
       {
        ShowLoading(MainWindowHandle);
       	ReadRTFFile(RichEditHandle, RTFFilename);
       	ShowMemoryUser(MainWindowHandle);
       } else
        return DefWindowProc(hwnd, msg, wp, lp);
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wp, lp);
    }
    return 0;
}
int PASCAL
WinMain(HINSTANCE hinstCurr, HINSTANCE hinstPrev, LPSTR szCmdLine, int nCmdShow)
{
	WNDCLASS wc = { 0 };
	wc.lpfnWndProc = WndProc;
	wc.hInstance = hinstCurr;
	wc.lpszClassName = TEXT("MyLeaktestWindowClass");
	RegisterClass(&wc);
	MainWindowHandle = CreateWindow(wc.lpszClassName, TEXT("Click here.."),
		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
		800, 600, NULL, NULL, hinstCurr, NULL);
	ShowWindow(MainWindowHandle, nCmdShow);
	UpdateWindow(MainWindowHandle);
    ShowMemoryUser(MainWindowHandle);
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}
Windows development | Windows API - Win32
{count} vote

Answer accepted by question author
  1. RLWA32 51,361 Reputation points
    2025-11-27T11:05:49.59+00:00

    @roy nelson You can control the memory usage when an rtf file contains images by implementing the IRichEditOleCallback interface. I haven't tested every type of image, but in a minimal test that loaded and reloaded an rtf file containing two images the memory leak appeared to be eliminated.

    I added some modifications to your posted code, including using the right mouse button to initiate the reload of the rtf file.

    Give this a try -

    #include <windows.h>
    #include <richedit.h>
    #include <RichOle.h>
    #include <psapi.h>
    #include <crtdbg.h>
    
    class RichCallback : public IRichEditOleCallback
    {
    public:
        RichCallback();
        // *** IUnknown methods ***
        STDMETHOD(QueryInterface) (REFIID riid, LPVOID FAR* ppvObj)
        {
            HRESULT	hr = E_NOINTERFACE;
            if (ppvObj == NULL)
                hr = E_POINTER;
            else
            {
                *ppvObj = NULL;
                if (riid == IID_IRichEditOleCallback || riid == IID_IUnknown)
                {
                    *ppvObj = (LPVOID)this;
                    AddRef();
                    hr = S_OK;
                }
            }
    
            return hr;
        }
    
        STDMETHOD_(ULONG, AddRef) ()
        {
            m_ref++;
            return m_ref;
        }
    
        STDMETHOD_(ULONG, Release) ()
        {
            ULONG l = --m_ref;
            if (m_ref == 0)
                delete this;
            return l;
        }
    
        // *** IRichEditOleCallback methods ***
        STDMETHOD(GetNewStorage) (LPSTORAGE FAR* ppstg);
        STDMETHOD(GetInPlaceContext) (LPOLEINPLACEFRAME FAR* lplpFrame,
            LPOLEINPLACEUIWINDOW FAR* lplpDoc,
            LPOLEINPLACEFRAMEINFO lpFrameInfo) { return E_NOTIMPL;};
        STDMETHOD(ShowContainerUI) (BOOL fShow) { return E_NOTIMPL; };
        STDMETHOD(QueryInsertObject) (LPCLSID lpclsid, LPSTORAGE lpstg, LONG cp) { return S_OK; };
        STDMETHOD(DeleteObject) (LPOLEOBJECT lpoleobj) { return S_OK; };
        STDMETHOD(QueryAcceptData) (LPDATAOBJECT lpdataobj,
            CLIPFORMAT FAR* lpcfFormat, DWORD reco,
            BOOL fReally, HGLOBAL hMetaPict) { return E_NOTIMPL; };
        STDMETHOD(ContextSensitiveHelp) (BOOL fEnterMode) { return E_NOTIMPL; };
        STDMETHOD(GetClipboardData) (CHARRANGE FAR* lpchrg, DWORD reco,
            LPDATAOBJECT FAR* lplpdataobj) { return E_NOTIMPL; };
        STDMETHOD(GetDragDropEffect) (BOOL fDrag, DWORD grfKeyState, LPDWORD pdwEffect) { return E_NOTIMPL; };
        STDMETHOD(GetContextMenu) (WORD seltype, LPOLEOBJECT lpoleobj, CHARRANGE FAR* lpchrg, HMENU FAR* lphmenu) { return E_NOTIMPL; };
    
        //RichCallback Methods
        HRESULT ClearAndReallocate();
    private:
        ~RichCallback();
        ULONG	m_ref, m_stgNum;
        IStorage* m_pStorage{};
        ILockBytes* m_pLockBytes{};
    };
    
    RichCallback::RichCallback() : m_ref(1), m_stgNum(0)
    {
        HRESULT hr = ClearAndReallocate();
        _ASSERTE(SUCCEEDED(hr));
    
    }
    
    RichCallback::~RichCallback()
    {
        m_pStorage->Release();
        m_pLockBytes->Release();
    }
    
    STDMETHODIMP RichCallback::GetNewStorage(LPSTORAGE FAR* ppstg)
    {
        HRESULT hr{ E_FAIL };
        WCHAR	stgName[31]{}, wstgNum[4]{};
    
        wcscpy_s(stgName, _countof(stgName), L"rcbStg");
        _ultow_s(++m_stgNum, wstgNum, _countof(wstgNum), 10);
        wcscat_s(stgName, _countof(stgName), wstgNum);
    
        hr = m_pStorage->CreateStorage(stgName, STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE | STGM_DIRECT,
            0, 0, ppstg);
    
        _ASSERTE(SUCCEEDED(hr));
    
        return hr;
    }
    
    HRESULT RichCallback::ClearAndReallocate()
    {
        HRESULT hr;
        if (m_pStorage)
        {
            m_pStorage->Release();
            m_pStorage = nullptr;
        }
    
        if (m_pLockBytes)
        {
            m_pLockBytes->Release();
            m_pLockBytes = nullptr;
        }
    
        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, 1048576 * 20);
    
        if (hMem)
        {
            hr = CreateILockBytesOnHGlobal(hMem, TRUE, &m_pLockBytes);
    
            if (SUCCEEDED(hr) && m_pLockBytes)
                hr = StgCreateDocfileOnILockBytes(m_pLockBytes, STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE | STGM_DIRECT,
                    0, &m_pStorage);
        }
        else
            hr = HRESULT_FROM_WIN32(GetLastError());
    
        _ASSERTE(SUCCEEDED(hr));
    
        return hr;
    }
    
    
    static const CHAR RTFFilename[] = "C:\\Users\\RLWA32\\Documents\\PicDoc.rtf";
    static const TCHAR RTFFilename_t[] = TEXT("C:\\Users\\RLWA32\\Documents\\PicDoc.rtf");
    HWND RichEditHandle, MainWindowHandle;
    DWORD CALLBACK MyRead(DWORD_PTR dwCookie, LPBYTE pbBuffer, LONG cb, LONG *pcb)
    {
        HFILE	hf = (HFILE) dwCookie;
        if(hf == HFILE_ERROR)
            return (DWORD) E_FAIL;
        *pcb = _lread(hf, pbBuffer, cb);
        return (DWORD) (*pcb >= 0 ? NOERROR : (*pcb = 0, E_FAIL));
    }
    SIZE_T GetCurrentProcessMemoryInfo()
    {
        PROCESS_MEMORY_COUNTERS_EX pmc;
        pmc.cb = sizeof(pmc);
        GetProcessMemoryInfo(GetCurrentProcess(), (PPROCESS_MEMORY_COUNTERS)&pmc,sizeof(pmc));
        return (pmc.PrivateUsage / 1024);
    }
    void ReadRTFFile(HWND hwndRE,  LPCSTR szFile)
    {
        EDITSTREAM es;
        DWORD dwFormat;
        es.dwCookie = (DWORD_PTR) _lopen(szFile, OF_READ);
        if(es.dwCookie == (DWORD_PTR) HFILE_ERROR) return;
        dwFormat = SF_RTF;
        es.dwError = 0;
        es.pfnCallback = MyRead;
        SendMessage(hwndRE,EM_STREAMIN,dwFormat, (LPARAM) &es);
        _lclose((HFILE) es.dwCookie);
        SendMessage(hwndRE, EM_SETMODIFY, (WPARAM) FALSE, 0);
        InvalidateRect(hwndRE, NULL, TRUE);
        UpdateWindow(hwndRE);
    }
    void ShowMemoryUser(HWND WindowH)
    {
        TCHAR szT[256];
        wsprintf(szT, TEXT("Loaded... %s memory used: %dkb"), RTFFilename_t,GetCurrentProcessMemoryInfo());
        SetWindowText(WindowH, szT);
    }
    void ShowLoading(HWND WindowH)
    {
        TCHAR szT[256];
        wsprintf(szT, TEXT("Loading... %s memory used: %dkb"), RTFFilename_t, GetCurrentProcessMemoryInfo());
        SetWindowText(WindowH, szT);
    }
    #ifndef UNICODE
    #undef MSFTEDIT_CLASS
    #define MSFTEDIT_CLASS		"RICHEDIT50W"
    #endif
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
        static RichCallback *pRichCallback = new RichCallback;
       
        switch (msg) {
        case WM_CREATE: {
            LoadLibrary(TEXT("Msftedit.dll")); // Ensure the RichEdit DLL is loaded
            RichEditHandle = CreateWindowEx(0, MSFTEDIT_CLASS, TEXT(""),
                WS_VSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL,
                10, 10, 750, 550, hwnd, NULL, GetModuleHandle(NULL), NULL);
            SendMessage(RichEditHandle, EM_SETOLECALLBACK, 0, (LPARAM)pRichCallback);
            ReadRTFFile(RichEditHandle,RTFFilename);
        }
           break;
        case WM_NCRBUTTONDOWN:
        {
           if (wp == HTCAPTION)
           {
                pRichCallback->ClearAndReallocate();
                ShowLoading(MainWindowHandle);
                ReadRTFFile(RichEditHandle, RTFFilename);
                ShowMemoryUser(MainWindowHandle);
           }
           else
            return DefWindowProc(hwnd, msg, wp, lp);
        }
        break;
        case WM_DESTROY:
            pRichCallback->Release();
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hwnd, msg, wp, lp);
        }
        return 0;
    }
    int PASCAL
    WinMain(HINSTANCE hinstCurr, HINSTANCE hinstPrev, LPSTR szCmdLine, int nCmdShow)
    {
        WNDCLASS wc = { 0 };
        wc.lpfnWndProc = WndProc;
        wc.hInstance = hinstCurr;
        wc.lpszClassName = TEXT("MyLeaktestWindowClass");
        RegisterClass(&wc);
    
        OleInitialize(NULL);
    
        MainWindowHandle = CreateWindow(wc.lpszClassName, TEXT("Click here.."),
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
            800, 600, NULL, NULL, hinstCurr, NULL);
        ShowWindow(MainWindowHandle, nCmdShow);
        UpdateWindow(MainWindowHandle);
        ShowMemoryUser(MainWindowHandle);
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    
        OleUninitialize();
    
        return 0;
    }
    
    
    1 person found this answer helpful.

2 additional answers

Sort by: Most helpful
  1. Michael Le (WICLOUD CORPORATION) 6,020 Reputation points Microsoft External Staff Moderator
    2025-11-25T10:45:34.1433333+00:00

    Hello @roy nelson ,

    Sorry for the delayed response.

    I have investigated this and can confirm the memory leak is reproducible. It appears this is due to internal caching mechanisms within the control that do not free memory even after the control is destroyed.

    I attempted to resolve this doing these between loads:

    Unfortunately, none of these steps released the memory. As you can see from the picture, the memory usage continues to grow with each load (20 clicks).

    User's image

    Since the leak is contained within the current process, I found that creating the RichEdit control in a separate process and then terminating it, which seems to be a viable workaround to avoid running out of memory in this specific case.

    User's image

    But frankly, this is not an ideal solution. Having to spawn a separate process just to host a control is quite heavy-handed and may introduce additional complexity in terms of inter-process communication.

    I doubt there is much that can be done to fix this, as it seems to be an inherent issue with the control itself. This control hasn't received any major updates for years. It is better to look for other alternatives.


  2. roy nelson 25 Reputation points
    2025-11-27T13:27:29.3466667+00:00

    Yes, unfortunately I pasted it in... the GetNewStorage only seems to be called when I paste from the clipboard.

    My document contains PNGs, I think it will use GDI+ to display those.


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.