From 6b533874fec5814e9a853073c1b2eacb03b50d17 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Fri, 28 Aug 2026 17:57:35 +0200 Subject: [PATCH] [cpyrt] Exempt T* const& from the mutable pointer-reference ban TypeManip::compound() strips const, so T*& and T* const& both reach the converter factory as "*&" and were rejected alike. The callee cannot rebind the pointer through a T* const&, which is the ban's sole rationale; ask the type via a new interop::IsMutablePtrRefType (the referenced type is a pointer and not const-qualified) and route const ones to the regular instance-pointer converters. In the string-based factory a name that resolves to no type keeps the ban. --- src/cpyrt/Converters.cxx | 22 +++++++++++++--------- src/interop/cppjit_interop.h | 2 ++ src/interop/interop_wrapper.cxx | 10 ++++++++++ test/test_conversions.py | 22 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/cpyrt/Converters.cxx b/src/cpyrt/Converters.cxx index 8e1e7ec..45f1024 100644 --- a/src/cpyrt/Converters.cxx +++ b/src/cpyrt/Converters.cxx @@ -3382,15 +3382,19 @@ cppjit::cpyrt::CreateConverter(const std::string& fullType, cdims_t dims) { return (h->second)(dims); // mutable pointer references (T*&) are incompatible with Python's object - // model + // model; a string that resolves to no type keeps the ban if (!isConst && cpd == "*&") { - return new NotImplementedConverter{ - PyExc_TypeError, - "argument type '" + resolvedType + - "' is not supported: non-const references to pointers (T*&) allow a" - " function to replace the pointer itself. Python cannot represent " - "this safely. Consider changing the" - " C++ API to return the new pointer or use a wrapper"}; + interop::TCppType_t refType = + interop::GetType(resolvedType, /* enable_slow_lookup */ true); + if (!refType || interop::IsMutablePtrRefType(refType)) { + return new NotImplementedConverter{ + PyExc_TypeError, + "argument type '" + resolvedType + + "' is not supported: non-const references to pointers (T*&) " + "allow a function to replace the pointer itself. Python cannot " + "represent this safely. Consider changing the" + " C++ API to return the new pointer or use a wrapper"}; + } } // drop const, as that is mostly meaningless to python (with the exception @@ -3615,7 +3619,7 @@ cppjit::cpyrt::CreateConverter(interop::TCppType_t type, cdims_t dims) { // mutable pointer references (T*&) are incompatible with Python's object // model - if (!isConst && cpd == "*&") { + if (!isConst && cpd == "*&" && interop::IsMutablePtrRefType(resolvedType)) { return new NotImplementedConverter{ PyExc_TypeError, "argument type '" + resolvedTypeStr + diff --git a/src/interop/cppjit_interop.h b/src/interop/cppjit_interop.h index ca7c07e..9487a2a 100644 --- a/src/interop/cppjit_interop.h +++ b/src/interop/cppjit_interop.h @@ -71,6 +71,8 @@ std::string ResolveEnum(TCppScope_t enum_scope); RPY_EXPORTED bool IsLValueReferenceType(TCppType_t type); RPY_EXPORTED +bool IsMutablePtrRefType(TCppType_t type); +RPY_EXPORTED bool IsRValueReferenceType(TCppType_t type); RPY_EXPORTED bool IsClassType(TCppType_t type); diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index d29dada..28024c6 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -381,6 +381,16 @@ bool interop::IsLValueReferenceType(TCppType_t type) { return Cpp::GetValueKind(type) == Cpp::ValueKind::LValue; } +// Whether the callee can rebind a pointer through this reference (T*&); +// false for T* const& and for references to non-pointers. +bool interop::IsMutablePtrRefType(TCppType_t type) { + if (!Cpp::IsReferenceType(type)) + return false; + TCppType_t nonref = Cpp::GetNonReferenceType(type); + return Cpp::IsPointerType(nonref) && + !Cpp::HasTypeQualifier(nonref, Cpp::QualKind::Const); +} + bool interop::IsClassType(TCppType_t type) { return Cpp::IsRecordType(type); } bool interop::IsIntegerType(TCppType_t type, bool* is_signed /*= nullptr*/) { diff --git a/test/test_conversions.py b/test/test_conversions.py index 65b1600..669217b 100644 --- a/test/test_conversions.py +++ b/test/test_conversions.py @@ -139,6 +139,28 @@ def test05_bool_conversions(self): assert ns.Test2(True) assert not ns.Test2(False) + def test06_pointer_const_reference(self): + """A T* const& argument is accepted; only T*& is banned""" + + import cppjit + + cppjit.cppdef("""\ + namespace PtrConstRef { + struct A { int v = 3; }; + int by_const_ref(A* const& a) { return a->v; } + void by_mutable_ref(A*& a) { a = nullptr; } + }""") + + ns = cppjit.gbl.PtrConstRef + a = ns.A() + + # the callee cannot rebind the pointer, so this is safe to pass + assert ns.by_const_ref(a) == 3 + + # a mutable pointer reference stays unsupported + with raises(TypeError): + ns.by_mutable_ref(a) + def test07_mutable_voidp_reference(self): """An object can be passed through a non-const void*& argument"""