pythonwinapictypes

Window procedure overwrite lead to crashes without error when too much instances are created


I'm making an App that uses SDL / Pygame for displaying graphics. I've overwritten the window procedure in case of resizing to trigger a function and makes the app run smoother (cf this answer).

However I've noticed when reaching a certain amount of instances of a class, it crashes the app without any error in the logs. I've checked Windows logs but could not find anything. I've found te culprit is the the window procedure overwritte.

Find below a simulation of the issue :

  • The function windows_resize_procedure (inspired from this answer trigger App.resize and App.draw when the user resizes the window
  • [Element() for _ in range(1000)] creates artificially high number of instances of Element. In my real program I have ~50 of them only, but I consider them "bigger" (more attributes and methods). A 1000 is enough to crash the program in a few seconds on my computer, but you might want to increase this number depending on yours.
import pygame,sys,platform

def windows_resize_procedure(hwnd,draw_func,resize_func,screen):
    try:
        import ctypes
        from ctypes import wintypes

        user32 = ctypes.windll.user32

        WNDPROC = ctypes.WINFUNCTYPE(
            ctypes.c_long, 
            wintypes.HWND, 
            ctypes.c_uint, 
            ctypes.POINTER(wintypes.WPARAM),
            ctypes.POINTER(wintypes.LPARAM))
        WM_SIZE = 0x0005
        RDW_INVALIDATE = 0x0001
        RDW_ERASE = 0x0004
        GWL_WNDPROC = -4

        old_window_proc = user32.GetWindowLongPtrA(
            user32.GetForegroundWindow(),
            GWL_WNDPROC
        )

        def new_window_proc(hwnd, msg, wparam, lparam):
            if msg == WM_SIZE:
                resize_func(screen.get_size())
                draw_func()
                user32.RedrawWindow(hwnd, None, None, RDW_INVALIDATE | RDW_ERASE)
            return user32.CallWindowProcA(old_window_proc, hwnd, msg, wparam, lparam)


        new_window_proc_cb = WNDPROC(new_window_proc)

        user32.SetWindowLongPtrA(
            user32.GetForegroundWindow(), 
            GWL_WNDPROC, 
            ctypes.cast(new_window_proc_cb, ctypes.POINTER(ctypes.c_long))
        )
    except Exception as e:
        print(e)

class Element:
    def __init__(self):
        self.foo = 'foo'

class App:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((200,200),pygame.RESIZABLE )
        self.clock = pygame.time.Clock()
        self.hwnd = pygame.display.get_wm_info()['window']

        # Commenting the two following lines "fix" the issue
        # Meaning when not overwritting the window procedure it works well
        if platform.system() == 'Windows':
            windows_resize_procedure(self.hwnd,self.draw,self.resize,self.screen)       
        
        self.elements = []
    def inputs(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        # Simulating high volume of instances
        [Element() for _ in range(1000)]
    
    def resize(self,size):
        print(f'resizing to {size}')
    
    def draw(self):
        self.screen.fill('white')

    def run(self):
        while True:
            self.inputs()
            pygame.display.update()
            self.clock.tick()

if __name__ == "__main__":
    app = App()
    app.run()

Solution

  • You don’t appear to be retaining a reference to new_window_proc_cb. Without a reference, Python may garbage collect the callback object, while it is still bound to the Windows API. The result will be a crash sometime later when the reclaimed memory is reused for some unrelated purpose.

    To fix it, you’ll probably want to just make a global variable where you store all your registered WNDPROCs.

    This is documented on the ctypes documentation:

    Note: Make sure you keep references to CFUNCTYPE() objects as long as they are used from C code. ctypes doesn’t, and if you don’t, they may be garbage collected, crashing your program when a callback is made.