From 44cb3ff8252333c09e8a508d9a727e5146bc3cba Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Thu, 2 Jul 2026 20:23:17 +0200 Subject: [PATCH 1/2] [cppyy] syncup CPyCppyy and tests with latest compres forks CPyCppyy 80a6133 -> c300922: - 19a36fa Fix std::span iteration with libc++'s __wrap_iter - c300922 Return empty string instead of nullptr for unknown typecode cppyy eacef6b -> 8e025c8 (the Bazel build and the valgrind-suppression removal touch nothing under the mapped python/cppyy and test/ subtrees): - 39beb63 Free new[] arrays in test_lowlevel with ll.array_delete cppyy-backend unchanged at 93510c8. scripts/check_sync.py PASSES against the new anchors; test_lowlevel is valgrind-clean without the clang21 suppressions cppyy 491ec05 dropped. --- src/CPyCppyy/src/Pythonize.cxx | 21 +++++++++++ src/CPyCppyy/src/Utility.cxx | 2 +- test/test_lowlevel.py | 66 ++++++++++++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/CPyCppyy/src/Pythonize.cxx b/src/CPyCppyy/src/Pythonize.cxx index a4be700..7580fbd 100644 --- a/src/CPyCppyy/src/Pythonize.cxx +++ b/src/CPyCppyy/src/Pythonize.cxx @@ -958,6 +958,8 @@ static const ptrdiff_t PS_END_ADDR = 7; // non-aligned address, so no clash static const ptrdiff_t PS_FLAG_ADDR = 11; // id. static const ptrdiff_t PS_COLL_ADDR = 13; // id. +PyObject* STLIterNext(PyObject* self); // defined below; used by STLSequenceIter + PyObject* LLSequenceIter(PyObject* self) { // Implement python's __iter__ for low level views used through STL-type begin()/end() @@ -992,6 +994,25 @@ PyObject* STLSequenceIter(PyObject* self) PyObject* end = PyObject_CallMethodNoArgs(self, PyStrings::gEnd); if (end) { if (CPPInstance_Check(iter)) { + // Guarantee the returned iterator implements Python's iterator protocol. + // The deferred, name-based __next__ install can miss when an iterator's + // canonical return-type spelling and its scoped-final-name diverge + // (e.g libc++'s std::__1::__wrap_iter on macOS) leaving the type without + // __next__, so iter() rejects it. begin()/end() resolving here proves + // this is an STL forward iterator, so install the protocol now if it is still absent + PyTypeObject* itype = Py_TYPE(iter); + if (!PyIter_Check(iter)) { // no tp_iternext, or the + // _PyObject_NextNotImplemented sentinel + itype->tp_iternext = (iternextfunc)STLIterNext; + Utility::AddToClass((PyObject*)itype, CPPYY__next__, + (PyCFunction)STLIterNext, METH_NOARGS); + if (!itype->tp_iter) { + itype->tp_iter = (getiterfunc)PyObject_SelfIter; + Utility::AddToClass((PyObject*)itype, "__iter__", + (PyCFunction)PyObject_SelfIter, METH_NOARGS); + } + PyType_Modified(itype); + } // use the data member cache to store extra state on the iterator object, // without it being visible on the Python side auto& dmc = ((CPPInstance*)iter)->GetDatamemberCache(); diff --git a/src/CPyCppyy/src/Utility.cxx b/src/CPyCppyy/src/Utility.cxx index fbc8c17..bbe9d16 100644 --- a/src/CPyCppyy/src/Utility.cxx +++ b/src/CPyCppyy/src/Utility.cxx @@ -884,7 +884,7 @@ std::string CPyCppyy::Utility::CT2CppNameS(PyObject* pytc, bool allow_voidp) case 'd': name = "double"; break; case 'g': name = "long double"; break; case 'z': name = "const char*"; break; - default: name = (allow_voidp ? "void*" : nullptr); break; + default: if (allow_voidp) name = "void*"; break; } } diff --git a/test/test_lowlevel.py b/test/test_lowlevel.py index 256d140..202b4b4 100644 --- a/test/test_lowlevel.py +++ b/test/test_lowlevel.py @@ -432,8 +432,8 @@ def py2c(pyargs): voidpp = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_void_p)) for i in range(argc.value): - cppyy.ll.free(ctypes.cast(voidpp[i], ctypes.c_void_p)) - cppyy.ll.free(ptr) + cppyy.ll.array_delete(ctypes.cast(voidpp[i], ctypes.POINTER(ctypes.c_ubyte))) + cppyy.ll.array_delete['char*'](ctypes.cast(ptr, ctypes.POINTER(ctypes.c_char_p))) def test12_null_array(self): """Null low level view as empty list""" @@ -534,6 +534,68 @@ def test16_addressof_nullptr(self): assert cppyy.addressof(gbl.LLV.ptr_x) assert cppyy.addressof(gbl.LLV.ptr_null) == 0 + def test17_array_delete_multidim(self): + """Free a multidimensional (jagged) heap array with ll.array_delete""" + + import cppyy, ctypes + import cppyy.ll + + # C++ allocates a jagged 2D array (array of pointers to rows) + cppyy.cppdef("""\ + namespace ArrayDeleteMD { + void make2d(int& n, int& m, double**& a) { + n = 2; m = 3; + a = new double*[n]; + for (int i = 0; i < n; ++i) { + a[i] = new double[m]; + for (int j = 0; j < m; ++j) a[i][j] = 10.*i + j; + } + } }""") + + n = ctypes.c_int(0); m = ctypes.c_int(0) + ptr = ctypes.c_void_p() + cppyy.gbl.ArrayDeleteMD.make2d(n, m, ptr) + + assert n.value == 2 + assert m.value == 3 + rows = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_void_p)) + for i in range(n.value): + row = ctypes.cast(rows[i], ctypes.POINTER(ctypes.c_double)) + for j in range(m.value): + assert row[j] == 10.*i + j + + # delete[] each row, then the (outer) array of row pointers + for i in range(n.value): + cppyy.ll.array_delete(ctypes.cast(rows[i], ctypes.POINTER(ctypes.c_double))) + cppyy.ll.array_delete['void'](ptr) + + def test18_array_delete_fixed(self): + """Free a fixed-size contiguous multidimensional heap array""" + + import cppyy, ctypes + import cppyy.ll + + # C++ allocates a contiguous 2D array (single new int[3][4]) + cppyy.cppdef("""\ + namespace ArrayDeleteFixed { + intptr_t make(int& r, int& c) { + r = 3; c = 4; + int (*a)[4] = new int[3][4]; + for (int i = 0; i < r; ++i) + for (int j = 0; j < c; ++j) a[i][j] = c*i + j; + return (intptr_t)a; + } }""") + + r = ctypes.c_int(0); c = ctypes.c_int(0) + addr = cppyy.gbl.ArrayDeleteFixed.make(r, c) + + blk = ctypes.cast(addr, ctypes.POINTER(ctypes.c_int)) + for k in range(r.value * c.value): + assert blk[k] == k + + # a single contiguous allocation is released with a single delete[] + cppyy.ll.array_delete(ctypes.cast(ctypes.c_void_p(addr), ctypes.POINTER(ctypes.c_int))) + class TestMULTIDIMARRAYS: def setup_class(cls): import cppyy From 40fd39b8104bf223752f575907de3e1f557db7af Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Tue, 7 Jul 2026 00:00:28 +0200 Subject: [PATCH 2/2] [cppyy] syncup test_datatypes with latest compres cppyy Byte-copy test/test_datatypes.py from cppyy 41111ea (un-xfail test54 optional enum on Linux, scope the xfail to OS X, add int8_t enum coverage). The prerequisite CPyCppyy selectInstanceCnv int8/uint8 guard (b91ef22) is already synced. Sync anchors: cppyy 41111ea, CPyCppyy dead06d (vg CI guard, workflow only), cppyy-backend 95c06e2 (vg guard + Bazel infra, no mapped content). --- test/test_datatypes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test_datatypes.py b/test/test_datatypes.py index fc6d450..d6123c5 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -2403,7 +2403,7 @@ def test53_basic_nanoseconds_goodness(self): with raises(TypeError): ns53.f1(1000) - @mark.xfail(reason="optional on enum uint8_t is broken") + @mark.xfail(condition= IS_MAC, reason="std::optional::value_or rvalue conversion fails on OS X clang-repl (InstanceMoveConverter)") def test54_optional_use(self): import cppyy cppyy.cppdef(""" @@ -2418,6 +2418,9 @@ def test54_optional_use(self): enum class Tec : char {ZERO, ONE, TWO}; std::optional tec; + enum class Tei8 : int8_t {ZERO, ONE, TWO}; + std::optional tei8; + enum class Teu8 : uint8_t {ZERO, ONE, TWO}; std::optional teu8; } @@ -2439,6 +2442,10 @@ def test54_optional_use(self): ns54.tec = ns54.Tec.TWO assert ns54.tec == ns54.Tec.TWO + assert ns54.tei8.value_or(ns54.Tei8.TWO) == ns54.Tei8.TWO + ns54.tei8 = ns54.Tei8.TWO + assert ns54.tei8 == ns54.Tei8.TWO + assert ns54.teu8.value_or(ns54.Teu8.TWO) == ns54.Teu8.TWO ns54.teu8 = ns54.Teu8.TWO assert ns54.teu8 == ns54.Teu8.TWO