Window procedure overwrite lead to crashes without error when too much instances are created
Question:
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()
Answers:
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.
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 triggerApp.resize
andApp.draw
when the user resizes the window [Element() for _ in range(1000)]
creates artificially high number of instances ofElement
. 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()
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.