Search code examples
c++winapiwindowwin32gui

Win32: Application process remains in Task Manager after closing the window


Problem

  • I’ve recently started learning Win32 programming with OpenGL and was experimenting with creating a basic window and testing various functions provided. However, I noticed that when I close the window (either using the close button [X] or Alt+F4), the application does not terminate completely—it remains in the Task Manager's process list.
  • Even when I remove everything from my WndProc function and simply return DefWindowProc(), the issue persists. The program lingers in the background, and I have to manually kill it from the Task Manager.

Current Code

#include <Windows.h>
#include <stdio.h>
#include <gl/GL.h>
#include <iostream>

const char *GREEN = "\x1b[38;2;45;200;120m";

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void GeneratePixelFormat(HDC &dc);
void SettingUpConsole();
void SetupRC(HDC hDC, GLuint nFontList);
void RenderScene(GLuint nFontList);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdArg, int nCmd)
{
    // SettingUpConsole();
    WNDCLASS wc = {0};
    wc.hInstance = hInstance;
    wc.lpszClassName = "Window Class";
    // ? CS_OWNDC is required in OpenGL because some old drivers are not very stable without this (check page 656 to 660 in OpenGL SuperBbile)
    wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    wc.lpfnWndProc = WndProc;
    wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(248, 248, 255));
    wc.cbWndExtra = 0;
    wc.cbClsExtra = 0;

    if (!RegisterClass(&wc))
        return 0;

    HWND WindowHandle = CreateWindowExA(
        0, wc.lpszClassName, "Custom Window",
        // ! When making window for OpenGL it is required to set clipchildren and siblings
        // ? If not used then opengl may draw onto sibling or children window also
        WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 0, 0, 1000, 460,
        NULL, NULL, 0, NULL);

    if (!WindowHandle)
    {
        return 0;
    }
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        // printf("Running message loop.\n");
    }
    printf("\x1b[38;2;120mExiting Loop.");

    return 0;
}

LRESULT CALLBACK WndProc(HWND WindowHandle, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static HDC dc = NULL;
    static HGLRC rc = NULL;
    static GLuint nFontList;

    switch (uMsg)
    {
    case WM_CREATE:
    {
        printf("%sGetting window's handle\n", GREEN);
        dc = GetDC(WindowHandle);
        printf("%sSetting pixel format\n", GREEN);
        GeneratePixelFormat(dc);
        printf("%sCreating rendering context...\n", GREEN);
        rc = wglCreateContext(dc);
        printf("%sSetting current context.\n", GREEN);
        wglMakeCurrent(dc, rc);
        printf("%sSending message 30 times per second (30fps)\n", GREEN);
        SetTimer(WindowHandle, 101, 33, NULL);
        SetupRC(dc, nFontList);
        printf("\x1b[38;2;150;150;150m\n");
        return 0;
    }
    case WM_TIMER:
    {
        InvalidateRect(WindowHandle, NULL, FALSE);
        printf("MESSAGE SENT\n");
        return 0;
    }
    case WM_CLOSE:
    {
        printf("WM_CLOSE received. Destroying window...\n");
        ReleaseDC(WindowHandle, dc);
        wglMakeCurrent(dc, NULL);
        wglDeleteContext(rc);
        DestroyWindow(WindowHandle);
        return 0;
    }
    case WM_DESTROY:
    {
        printf("WM_DESTROY received. Posting quit message...\n");
        PostQuitMessage(0);
        KillTimer(WindowHandle, 101);
        // ! Do not remove ExitProcess T_T U_U
        // ? Don't know why but closing the window don't close the process in the task manager
        // ? Therefore using ExitProcess(0) to force process to stop when wm_destory message received.
        // ExitProcess(0);
        return 0;
    }
    case WM_SIZE:
    {
        RenderScene(nFontList);
        SwapBuffers(dc);
        ValidateRect(WindowHandle, NULL);
        return 0;
    }
    case WM_PAINT:
    {
        RenderScene(nFontList);
        SwapBuffers(dc); // Ensure the rendered scene is displayed
        ValidateRect(WindowHandle, NULL);
        return 0;
    }
    default:
        return DefWindowProc(WindowHandle, uMsg, wParam, lParam);
    }
}

void SettingUpConsole()
{
    AllocConsole();
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);

    // Set console to ASCII mode
    SetConsoleOutputCP(CP_ACP);

    // Enable virtual terminal processing
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD consoleMode;
    GetConsoleMode(hConsole, &consoleMode);
    consoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    SetConsoleMode(hConsole, consoleMode);
}

void GeneratePixelFormat(HDC &dc)
{
    static PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
        1,                             // Version of this structure
        PFD_DRAW_TO_WINDOW |           // Draw to window (not to bitmap)
            PFD_SUPPORT_OPENGL |       // Support OpenGL calls in window
            PFD_DOUBLEBUFFER,          // Double buffered mode
        PFD_TYPE_RGBA,                 // RGBA color mode
        32,                            // Want 32-bit color
        0, 0, 0, 0, 0, 0,              // Not used to select mode
        0, 0,                          // Not used to select mode
        0, 0, 0, 0, 0,                 // Not used to select mode
        16,                            // Size of depth buffer
        0,                             // No stencil
        0,                             // No auxiliary buffers
        0,                             // Obsolete or reserved
        0,                             // No overlay and underlay planes
        0,                             // Obsolete or reserved layer mask
        0,                             // No transparent color for underlay plane
        0};                            // Obsolete

    int nPixelFormat = ChoosePixelFormat(dc, &pfd);
    SetPixelFormat(dc, nPixelFormat, &pfd);
}

void SetupRC(HDC hDC, GLuint nFontList)
{
    // Set up the font characteristics
    HFONT hFont;
    GLYPHMETRICSFLOAT agmf[128]; // Throw away
    LOGFONT logfont;
    logfont.lfHeight = -10;
    logfont.lfWidth = 0;
    logfont.lfEscapement = 0;
    logfont.lfOrientation = 0;
    logfont.lfWeight = FW_BOLD;
    logfont.lfItalic = FALSE;
    logfont.lfUnderline = FALSE;
    logfont.lfStrikeOut = FALSE;
    logfont.lfCharSet = ANSI_CHARSET;
    logfont.lfOutPrecision = OUT_DEFAULT_PRECIS;
    logfont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
    logfont.lfQuality = DEFAULT_QUALITY;
    logfont.lfPitchAndFamily = DEFAULT_PITCH;
    strcpy(logfont.lfFaceName, "Arial");
    // Create the font and display list
    hFont = CreateFontIndirect(&logfont);
    SelectObject(hDC, hFont);
    // Create display lists for glyphs 0 through 128 with 0.1 extrusion
    // and default deviation. The display list numbering starts at 1000
    // (it could be any number).
    nFontList = glGenLists(128);
    wglUseFontOutlines(hDC, 0, 128, nFontList, 0.0f, 0.5f,
                       WGL_FONT_POLYGONS, agmf);
    DeleteObject(hFont);
}
void RenderScene(GLuint nFontList)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // Blue 3D text
    glColor3ub(0, 0, 255);
    glPushMatrix();
    glListBase(nFontList);
    glCallLists(6, GL_UNSIGNED_BYTE, "SDFSDF");
    glPopMatrix();
}

My Makefile

# My Folders
include_folder = ./include
lib_folder = ./lib

# Libraries to include
my_libraries = User32.lib Gdi32.lib ucrt.lib kernel32.lib vcruntime.lib msvcrt.lib opengl32.lib

# INFO(Include Folders)
# Windows Kit Include Folders
kit_include_1 = "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/winrt"
kit_include_2 = "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/ucrt"
kit_include_3 = "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um"
kit_include_4 = "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared"
kit_include_5 = "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/cppwinrt"

# Windows Kit Lib Folders
kit_lib_1 = "C:/Program Files (x86)/Windows Kits/10/Lib/10.0.22621.0/um/x64"
kit_lib_2 = "C:/Program Files (x86)/Windows Kits/10/Lib/10.0.22621.0/ucrt/x64"

# MSVC Include Folders
msvc_include = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.41.34120/include"
msvc_atlmfc_include = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.41.34120/atlmfc/include"

# MSVC Lib Folders
msvc_lib = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.41.34120/lib/x64"
msvc_atlmfc_lib = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.41.34120/atlmfc/lib/x64"

# INFO(Combined Include Folders)
include_folders = /I$(msvc_include) /I$(msvc_atlmfc_include) /I$(kit_include_1) /I$(kit_include_2) /I$(kit_include_3) /I$(kit_include_4) /I$(kit_include_5) /I$(include_folder) /I"C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared"

#INFO(Combined Lib Folders)
lib_folders = /LIBPATH:$(msvc_lib) /LIBPATH:$(msvc_atlmfc_lib) /LIBPATH:$(kit_lib_1) /LIBPATH:$(kit_lib_2) /LIBPATH:$(lib_folder) $(my_libraries)

# When using windows kit 10.0.19041.0 i.e. Win32
build:
    cl main.cpp ${include_folders} /EHsc /link ${lib_folders} /OUT:./bin/program.exe /SUBSYSTEM:WINDOWS /NODEFAULTLIB /ENTRY:WinMain
    del *.obj

# ? Testing if I can use my custom made Logger.dll if I use g++ rather than msvc, because I was not able to use Logger.dll with msvc
# building with g++ without -mwindows flag
# -mwindows flag is used to hide the console window
# mark the entry point as WinMain
buildGPP:
    g++ main.cpp -o ./bin/program.exe -I$(include_folder) -L$(lib_folder) -L./bin -lUser32 -lGdi32 -lucrt -lkernel32 -lLogger

Question

  1. Am I missing something in the cleanup process or creation process of the window that cause window/process to not terminate in process in task manager?
  2. I am currently manually killing the Process with KillProcess function. Should I keep that or does that create any problem? (Although I have not seen anyone using it.)

What I Tried:

  1. Using only DefWindowProc() in WndProc, but the process still remains.
  2. Ensuring WM_CLOSE calls DestroyWindow(), and WM_DESTROY calls PostQuitMessage(0).
  3. Checked if the message loop exits by printing a log message after it exits (printf("Exiting main loop.")), which does print, meaning the loop terminates but the process does not.

Additional Notes

  1. Are there any debugging steps I should follow to track down why the process persists?

Update 1 - 15 February 2025

  1. It is now clear that the problem is related /ENTRY flag, if I specify entry flag WinMain the program is not able to close properly, but not specifying the flag does it. So is the problem related to compiler, I am using MSVC 2022 version. Answer from @tommybee and hints from @Mippy, @Red.Wave, @Mantee.Pink suggested to use console as starting point, and that also does help, but then what is problem with WinMain?

Solution

  • The issue isn't in the code. It is a valid C++ Windows program that terminates when control leaves the WinMain function. The problem is that the linker command line opts out of C++ language rules by supplying a custom entry point.

    The problem can be reproduced with the following program:

    #include <Windows.h>
    
    DWORD WINAPI ThreadProc(LPVOID)
    {
        ::Sleep(INFINITE);
        return 0;
    }
    
    int APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
    {
        ::CreateThread(nullptr, 0, ThreadProc, nullptr, 0, nullptr);
        return 0;
    }
    

    The primary thread creates a thread that sleeps indefinitely and returns immediately. Per C and C++ language rules returning from the entry point has the effect of terminating the program.

    This happens when compiling the program using the following command line:

    cl main.cpp /EHsc /link kernel32.lib /OUT:./bin/program.exe /SUBSYSTEM:WINDOWS
    

    If we instead set a custom entry point via the /ENTRY linker option, the program will not terminate when the primary thread returns from WinMain:

    cl main.cpp /EHsc /link kernel32.lib /OUT:./bin/program.exe /SUBSYSTEM:WINDOWS /NODEFAULTLIB /ENTRY:WinMain
    

    Launching program.exe now keeps the process alive indefinitely. If you attach a debugger, you will see that the process has a single thread that is sleeping (ThreadProc).

    When setting a custom entry point rather than letting the linker choose the default WinMainCRTStartup the rules for process termination change: A process terminates when a thread calls ExitProcess explicitly or when all threads have terminated. Raymond Chen explains this in his blog entry If you return from the main thread, does the process exit? in more detail.

    While calling ExitProcess from the custom WinMain entry point would address the immediate issue, it is not a practical solution. The language support libraries don't just terminate a process for us. They are responsible for a lot of things (such as initializing objects with static storage duration). The real solution is to drop the /ENTRY linker option and let the linker choose the default entry point.