From 141204e632c9df3cb538b1f8f68802cc8f32a25b Mon Sep 17 00:00:00 2001 From: alteralto <182909819+alteralto@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:25:29 +0300 Subject: [PATCH] ctypesgen: Leave c_void_p alone in the generated POINTER wrapper ctypes.POINTER(None) returns ctypes.c_void_p itself, so the wrapper in the generated preamble replaced from_param on the c_void_p type the whole process shares. The replacement returns integers unchanged, and ctypes then passes them as a C long. On Windows that is 32 bits, so any void pointer argument declared after grass.lib is imported fails for a 64-bit address, for example LoadStringW in dateutil, which breaks matplotlib date axes in g.gui.timeline and g.gui.tplot. c_void_p's own from_param already turns None into NULL, so the wrapper now returns it unchanged. Fixes #2109 --- .../tests/lib_gis_ctypes_void_pointer_test.py | 30 +++++++++++++++++++ .../ctypesgen/printer_python/printer.py | 5 ++++ 2 files changed, 35 insertions(+) create mode 100644 lib/gis/tests/lib_gis_ctypes_void_pointer_test.py diff --git a/lib/gis/tests/lib_gis_ctypes_void_pointer_test.py b/lib/gis/tests/lib_gis_ctypes_void_pointer_test.py new file mode 100644 index 00000000000..ed9afd9dde4 --- /dev/null +++ b/lib/gis/tests/lib_gis_ctypes_void_pointer_test.py @@ -0,0 +1,30 @@ +"""Tests of the ctypes preamble shared by the grass.lib modules + +POINTER(None) in the generated modules is ctypes.c_void_p itself, so the +preamble must not replace its from_param: that would change every void pointer +argument in the process, including those of other packages. +""" + +import ctypes +import sys + +import pytest + +import grass.lib.gis # noqa: F401 # the import is what is being tested + + +def test_void_pointer_converts_integers(): + """After importing grass.lib, c_void_p still converts an integer address""" + assert not isinstance(ctypes.c_void_p.from_param(2**40), int) + + +@pytest.mark.skipif(sys.platform != "win32", reason="C long is 32 bits on Windows") +def test_void_pointer_argument_takes_a_64_bit_address(): + """A void pointer argument declared after the import takes any address""" + memmove = ctypes.CDLL("msvcrt").memmove + memmove.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t) + memmove.restype = ctypes.c_void_p + source = ctypes.create_string_buffer(b"grass") + target = ctypes.create_string_buffer(len(source)) + memmove(ctypes.addressof(target), ctypes.addressof(source), len(source)) + assert target.value == b"grass" diff --git a/python/libgrass_interface_generator/ctypesgen/printer_python/printer.py b/python/libgrass_interface_generator/ctypesgen/printer_python/printer.py index 8fd4327e650..601ef66d3b0 100755 --- a/python/libgrass_interface_generator/ctypesgen/printer_python/printer.py +++ b/python/libgrass_interface_generator/ctypesgen/printer_python/printer.py @@ -166,6 +166,11 @@ def _copy_preamble_loader_files(self, path): pointer = """def POINTER(obj): p = ctypes.POINTER(obj) + # POINTER(None) is ctypes.c_void_p itself, which the whole process shares, + # and its own from_param already turns None into NULL + if p is ctypes.c_void_p: + return p + # Convert None to a real NULL pointer to work around bugs # in how ctypes handles None on 64-bit platforms if not isinstance(p.from_param, classmethod):