From eef3dbb3295dc29453f6d817e5c69059092a67f0 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Sat, 26 Sep 2026 18:02:22 +0300 Subject: [PATCH] [memory-ownership] Honour ownership attributes when taking and releasing memory Added an AllocType field on CPPMethod, cached from Cpp::IsAllocator, that now decides the ownership of a returned object. The overloads of a group share fMethodInfo, so the answer of the called overload stays in a local instead of going into the shared kIsCreator flag, which nothing ever clears. The analyzer is not called; this is the attribute-checker path only. AllocType was collapsed into kIsCreator and never reached op_dealloc_nofree, so malloc'd, new[]'d and raw operator-new'd memory were all freed with a scalar delete. It now rides on the instance as kIsArrayAlloc, kIsNoConstruct and kIsMalloc; op_dealloc_nofree branches on them to delete[], free, ::operator delete or ::operator delete[], with no destructor for the raw cases. All flags clear keeps the old delete path. bind_object carries those flags to the new proxy along with the ownership, as BindCppObjectNoCast() masks them out, so a cast object frees its memory the way it was allocated. Mirrored that with Cpp::IsDeallocator, cached the same way, and passed the call arguments to HandleReturn: handing a Python-owned instance to an ownership-taking function now gives ownership back to C++ instead of leaving it to be freed twice. IsDeallocator reports one DeallocType per parameter, so every argument of a call is matched to the parameter that the attribute labels. A positional argument finds its slot from its place in the args array, and a keyword argument from its name in the signature. Two shifts turn either number into a slot, because an implicit object parameter and the shape of the call each move it: GetDeallocArgShift() answers for the args array and GetDeallocParamShift() for the declaration. CPPMethod, CPPFunction and CPPClassMethod answer both for their own kind of callee, and the defaults on PyCallable keep every other callable out of this path. The interop layer carries what the above needs. interop::AllocType and interop::DeallocType name the CppInterOp enums, and IsAllocator() and IsDeallocator() join their neighbours in the wrapper, each holding the interpreter mutex. interop::Destruct() takes an object count, which it used to pin to 0; a count other than 0 makes the CppInterOp destructor wrapper run delete[] in place of delete, which is what the array path wants. CPPINTEROP_GIT_TAG moves to the CppInterOp commit that carries those two entry points, and test/Makefile builds the memory_analysis dictionary. Ownership is only released, never nullified: the instance keeps a stale pointer after the callee frees the memory. GetAllocBehaviour is restricted to raw-pointer returns. Exposed read-only __is_array_alloc__ / __is_no_construct__ / __is_malloc__ and added tests for the deallocation paths and for ownership release via free, via an ownership_takes function, and the negative case. An attribute reaches the tests three ways: a clang::annotate on the declaration, a redeclaration that carries it, and APINotes behind a module map. The release tests cover the bound, unbound and stored-bound forms of a member method, a static method and a free function, each in a positional and a keyword shape, a keyword shape with the names reversed, an argument that is no object at all, a cppDeallocNone parameter, a Python overload that carries no attribute, and a cast through bind_object. --- CMakeLists.txt | 2 +- src/cpyrt/CPPClassMethod.cxx | 18 + src/cpyrt/CPPClassMethod.h | 5 + src/cpyrt/CPPFunction.h | 5 + src/cpyrt/CPPInstance.cxx | 33 +- src/cpyrt/CPPInstance.h | 32 +- src/cpyrt/CPPMethod.cxx | 33 ++ src/cpyrt/CPPMethod.h | 14 + src/cpyrt/CPPOverload.cxx | 91 +++- src/cpyrt/PyCallable.h | 16 + src/cpyrt/cpyrtModule.cxx | 10 +- src/interop/cppjit_interop.h | 8 +- src/interop/interop_wrapper.cxx | 16 +- test/Makefile | 1 + test/cpp/MemoryOwnership/MemOwnrship.apinotes | 9 + .../MemoryOwnership/memory_analysis_redecl.h | 10 + test/cpp/MemoryOwnership/module.modulemap | 1 + test/cpp/memory_analysis.cxx | 74 +++ test/cpp/memory_analysis.h | 95 ++++ test/test_memoryanalysis.py | 503 ++++++++++++++++++ 20 files changed, 950 insertions(+), 26 deletions(-) create mode 100644 test/cpp/MemoryOwnership/MemOwnrship.apinotes create mode 100644 test/cpp/MemoryOwnership/memory_analysis_redecl.h create mode 100644 test/cpp/MemoryOwnership/module.modulemap create mode 100644 test/cpp/memory_analysis.cxx create mode 100644 test/cpp/memory_analysis.h create mode 100644 test/test_memoryanalysis.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 79828f6e..c3e857c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ include(GNUInstallDirs) # Perhaps this should permanently be OFF and users can build their own CppInterOp if they want to run the tests? option(CPPJIT_ENABLE_CPPINTEROP_TESTS "enable CppInterOp tests" OFF) set(CPPINTEROP_GIT_REPOSITORY "https://github.com/compiler-research/CppInterOp.git" CACHE STRING "") -set(CPPINTEROP_GIT_TAG "c8126249dd18363000b89333b34313ca2a83fb82" CACHE STRING "") +set(CPPINTEROP_GIT_TAG "c57d253e7cba94cc0976a9e36547a960f0fd2f83" CACHE STRING "") set(CPPINTEROP_SOURCE_DIR "" CACHE PATH "Override default CppInterOp built by ExternalProject_Add, with a path to local CppInterOp source") diff --git a/src/cpyrt/CPPClassMethod.cxx b/src/cpyrt/CPPClassMethod.cxx index bbdc1022..2ff21efe 100644 --- a/src/cpyrt/CPPClassMethod.cxx +++ b/src/cpyrt/CPPClassMethod.cxx @@ -50,6 +50,24 @@ PyObject* cpyrt::CPPClassMethod::Call(CPPInstance*& self, cpyrt_PyArgs_t args, return this->Execute(nullptr, 0, ctxt); } +//---------------------------------------------------------------------------- +Py_ssize_t cpyrt::CPPClassMethod::GetDeallocArgShift(bool isBound, + PyObject* const* args, + Py_ssize_t nargs) { + // a static method takes no object argument, so a bound call drops self + if (isBound) + return 0; + + // Call function above drops a leading instance that the call does not need, + // and then args[0] belongs to no parameter. The check below repeats the one + // in Call(), so a change to either one belongs in both. + if (nargs > 0 && CPPInstance_Check(args[0]) && fArgsRequired <= nargs - 1 && + interop::IsSubclass(((CPPInstance*)args[0])->ObjectIsA(), GetScope())) + return -1; + + return 0; +} + //---------------------------------------------------------------------------- PyObject* cpyrt::CPPClassMethod::GetTypeName() { PyObject* cppname = diff --git a/src/cpyrt/CPPClassMethod.h b/src/cpyrt/CPPClassMethod.h index 4ffe761c..bd79b043 100644 --- a/src/cpyrt/CPPClassMethod.h +++ b/src/cpyrt/CPPClassMethod.h @@ -14,6 +14,11 @@ class CPPClassMethod : public CPPMethod { PyObject* GetTypeName() override; public: + Py_ssize_t GetDeallocArgShift(bool isBound, PyObject* const* args, + Py_ssize_t nargs) override; + // a static method has no object argument. + Py_ssize_t GetDeallocParamShift() override { return 0; } + PyCallable* Clone() override { return new CPPClassMethod(*this); } PyObject* Call(CPPInstance*& self, cpyrt_PyArgs_t args, size_t nargsf, PyObject* kwds, CallContext* ctxt = nullptr) override; diff --git a/src/cpyrt/CPPFunction.h b/src/cpyrt/CPPFunction.h index 9e356c86..4e3b27e5 100644 --- a/src/cpyrt/CPPFunction.h +++ b/src/cpyrt/CPPFunction.h @@ -15,6 +15,11 @@ class CPPFunction : public CPPMethod { PyObject* GetTypeName() override; public: + // A free function takes its object as a plain first parameter, so it has no + // implicit this slot. The shift of a bound call stays the one of CPPMethod, + // as AdjustSelf() puts self in that first parameter. + Py_ssize_t GetDeallocParamShift() override { return 0; } + PyCallable* Clone() override { return new CPPFunction(*this); } PyObject* Call(CPPInstance*& self, cpyrt_PyArgs_t args, size_t nargsf, PyObject* kwds, CallContext* ctxt = nullptr) override; diff --git a/src/cpyrt/CPPInstance.cxx b/src/cpyrt/CPPInstance.cxx index ec882a53..fa5c2aa3 100644 --- a/src/cpyrt/CPPInstance.cxx +++ b/src/cpyrt/CPPInstance.cxx @@ -17,6 +17,7 @@ using namespace cppjit; // Standard #include +#include #include //- data _____________________________________________________________________ @@ -227,7 +228,17 @@ void cpyrt::op_dealloc_nofree(CPPInstance* pyobj) { if (pyobj->fFlags & CPPInstance::kIsValue) { interop::CallDestructor(klass, cppobj); interop::Deallocate(klass, cppobj); - } else + } else if (pyobj->fFlags & CPPInstance::kIsMalloc) + std::free(cppobj); + else if (pyobj->fFlags & CPPInstance::kIsNoConstruct) { + if (pyobj->fFlags & CPPInstance::kIsArrayAlloc) + ::operator delete[](cppobj); + else + ::operator delete(cppobj); + } else if (pyobj->fFlags & CPPInstance::kIsArrayAlloc) + interop::Destruct(klass, cppobj, 1); + // Default case: just kIsOwner set in all of memory-ownership flags + else interop::Destruct(klass, cppobj); } cppobj = nullptr; @@ -938,10 +949,30 @@ static int op_setownership(CPPInstance* pyobj, PyObject* value, void*) { return 0; } +// Added for testing purposes +//----------------------------------------------------------------------------- +static PyObject* op_get_array_alloc(CPPInstance* pyobj, void*) { + return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsArrayAlloc)); +} +//----------------------------------------------------------------------------- +static PyObject* op_get_no_construct(CPPInstance* pyobj, void*) { + return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsNoConstruct)); +} +//----------------------------------------------------------------------------- +static PyObject* op_get_malloc(CPPInstance* pyobj, void*) { + return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsMalloc)); +} //----------------------------------------------------------------------------- static PyGetSetDef op_getset[] = { {(char*)"__python_owns__", (getter)op_getownership, (setter)op_setownership, (char*)"If true, python manages the life time of this object", nullptr}, + {(char*)"__is_array_alloc__", (getter)op_get_array_alloc, nullptr, + (char*)"If true, the object was allocated with new[]/operator new[]", + nullptr}, + {(char*)"__is_no_construct__", (getter)op_get_no_construct, nullptr, + (char*)"If true, the memory is raw: no constructor was run", nullptr}, + {(char*)"__is_malloc__", (getter)op_get_malloc, nullptr, + (char*)"If true, the object was allocated with malloc", nullptr}, {(char*)nullptr, nullptr, nullptr, nullptr, nullptr}}; //= cpyrt type number stubs to allow dynamic overrides ===================== diff --git a/src/cpyrt/CPPInstance.h b/src/cpyrt/CPPInstance.h index c94ecfa1..00d6d7cc 100644 --- a/src/cpyrt/CPPInstance.h +++ b/src/cpyrt/CPPInstance.h @@ -29,20 +29,24 @@ class CPPInstance { public: enum EFlags { kDefault = 0x0000, - kNoWrapConv = 0x0001, // use type as-is (eg. no smart ptr wrap) - kIsOwner = 0x0002, // Python instance owns C++ object/memory - kIsExtended = 0x0004, // has extended data - kIsValue = 0x0008, // was created from a by-value return - kIsReference = 0x0010, // represents one indirection - kIsArray = 0x0020, // represents an array of objects - kIsSmartPtr = 0x0040, // is or embeds a smart pointer - kIsPtrPtr = 0x0080, // represents two indirections - kIsRValue = 0x0100, // can be used as an r-value - kIsLValue = 0x0200, // can be used as an l-value - kNoMemReg = 0x0400, // do not register with memory regulator - kIsRegulated = 0x0800, // is registered with memory regulator - kIsActual = 0x1000, // has been downcasted to actual type - kHasLifeLine = 0x2000, // has a life line set + kNoWrapConv = 0x0001, // use type as-is (eg. no smart ptr wrap) + kIsOwner = 0x0002, // Python instance owns C++ object/memory + kIsExtended = 0x0004, // has extended data + kIsValue = 0x0008, // was created from a by-value return + kIsReference = 0x0010, // represents one indirection + kIsArray = 0x0020, // represents an array of objects + kIsSmartPtr = 0x0040, // is or embeds a smart pointer + kIsPtrPtr = 0x0080, // represents two indirections + kIsRValue = 0x0100, // can be used as an r-value + kIsLValue = 0x0200, // can be used as an l-value + kNoMemReg = 0x0400, // do not register with memory regulator + kIsRegulated = 0x0800, // is registered with memory regulator + kIsActual = 0x1000, // has been downcasted to actual type + kHasLifeLine = 0x2000, // has a life line set + kIsArrayAlloc = 0x4000, // represents a heap allocated array of objects + kIsNoConstruct = + 0x8000, // represents constructor is not called in the allocation + kIsMalloc = 0x10000, // is allocated with malloc }; public: // public, as the python C-API works with C structs diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 0221f836..38aaebd2 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -85,6 +85,8 @@ inline void cpyrt::CPPMethod::Copy_(const CPPMethod& /* other */) { fExecutor = nullptr; fArgIndices = nullptr; fArgsRequired = -1; + fAllocType.reset(); + fDeallocType.reset(); } //---------------------------------------------------------------------------- @@ -703,6 +705,37 @@ PyObject* cpyrt::CPPMethod::GetArgDefault(int iarg, bool silent) { bool cpyrt::CPPMethod::IsConst() { return interop::IsConstMethod(GetMethod()); } +//---------------------------------------------------------------------------- +interop::AllocType cpyrt::CPPMethod::GetAllocBehaviour() { + if (fAllocType.has_value()) + return *fAllocType; + interop::TCppMethod_t method = GetMethod(); + if (!method) { + fAllocType = interop::AllocType::None; + return interop::AllocType::None; + } + if (!IsPointerType(interop::GetMethodReturnType(method))) { + fAllocType = interop::AllocType::None; + return interop::AllocType::None; + } + interop::AllocType attrResult = interop::IsAllocator(method); + fAllocType = attrResult; + return attrResult; +} + +//---------------------------------------------------------------------------- +interop::DeallocType cpyrt::CPPMethod::GetDeallocBehaviour(size_t index) { + if (!fDeallocType) { + fDeallocType.emplace(); + if (interop::TCppMethod_t method = GetMethod()) + interop::IsDeallocator(method, *fDeallocType); + } + if (index < fDeallocType->size()) { + return (*fDeallocType)[index]; + } + return interop::DeallocType::Opaque; +} + //---------------------------------------------------------------------------- PyObject* cpyrt::CPPMethod::GetScopeProxy() { // Get or build the scope of this method. diff --git a/src/cpyrt/CPPMethod.h b/src/cpyrt/CPPMethod.h index 4fdd792d..68c20228 100644 --- a/src/cpyrt/CPPMethod.h +++ b/src/cpyrt/CPPMethod.h @@ -5,6 +5,7 @@ #include "PyCallable.h" // Standard +#include #include #include #include @@ -62,6 +63,17 @@ class CPPMethod : public PyCallable { PyObject* GetCoVarNames() override; PyObject* GetArgDefault(int iarg, bool silent = true) override; bool IsConst() override; + interop::AllocType GetAllocBehaviour() override; + interop::DeallocType GetDeallocBehaviour(size_t index) override; + // A bound call passes self on its own, so args[0] lines up with the second + // slot. A direct call goes through the vector call, which skips the + // descriptor, and then self sits in args[0] itself. + Py_ssize_t GetDeallocArgShift(bool isBound, PyObject* const* /* args */, + Py_ssize_t /* nargs */) override { + return isBound ? 1 : 0; + } + // the first slot belongs to the implicit object argument of the method + Py_ssize_t GetDeallocParamShift() override { return 1; } PyObject* GetScopeProxy() override; interop::TCppFuncAddr_t GetFunctionAddress() override; @@ -121,6 +133,8 @@ class CPPMethod : public PyCallable { protected: // cached value that doubles as initialized flag (uninitialized if -1) int fArgsRequired; + std::optional fAllocType; + std::optional> fDeallocType; }; } // namespace cppjit::cpyrt diff --git a/src/cpyrt/CPPOverload.cxx b/src/cpyrt/CPPOverload.cxx index 96016205..13171c5f 100644 --- a/src/cpyrt/CPPOverload.cxx +++ b/src/cpyrt/CPPOverload.cxx @@ -142,14 +142,23 @@ static inline void ResetCallState(CPPInstance* descr_self, // helper to factor out return logic of mp_call / mp_vectorcall static inline PyObject* HandleReturn(CPPOverload* pymeth, CPPInstance* im_self, - PyObject* result) { + PyObject* result, PyCallable* method, + PyObject* const* args, size_t nargsf, + PyObject* kwds) { // special case for python exceptions, propagated through C++ layer if (result) { CPPInstance* cppres = (CPPInstance*)(CPPInstance_Check(result) ? result : nullptr); + interop::AllocType AT = method->GetAllocBehaviour(); + // the overloads of a group share fMethodInfo, so the answer of this one + // stays in a local instead of going into the shared flags + const bool allocates = AT != interop::AllocType::None && + AT != interop::AllocType::Null && + AT != interop::AllocType::Unknown; + // if this method creates new objects, always take ownership - if (IsCreator(pymeth->fMethodInfo->fFlags)) { + if (allocates || IsCreator(pymeth->fMethodInfo->fFlags)) { // either be a constructor with a fresh object proxy self ... if (IsConstructor(pymeth->fMethodInfo->fFlags)) { @@ -158,9 +167,76 @@ static inline PyObject* HandleReturn(CPPOverload* pymeth, CPPInstance* im_self, } // ... or be a regular method with an object proxy return value - else if (cppres) + else if (cppres) { cppres->PythonOwns(); + // After giving ownership, set proper flags to indicate allocation + // method/func + switch (AT) { + case interop::AllocType::Malloc: + cppres->fFlags |= CPPInstance::kIsMalloc; + break; + case interop::AllocType::NewArr: + cppres->fFlags |= CPPInstance::kIsArrayAlloc; + break; + case interop::AllocType::OperatorNew: + cppres->fFlags |= CPPInstance::kIsNoConstruct; + break; + case interop::AllocType::OperatorNewArr: + cppres->fFlags |= CPPInstance::kIsArrayAlloc; + cppres->fFlags |= CPPInstance::kIsNoConstruct; + break; + default: + break; + } + } + } + + // an argument that the called function releases belongs to C++ from now + // on, else Python frees it a second time + const Py_ssize_t nargs = cpyrt_PyArgs_GET_SIZE(args, nargsf); + const Py_ssize_t numKwds = + kwds && PyTuple_CheckExact(kwds) ? PyTuple_GET_SIZE(kwds) : 0; + // the keyword values follow the positional ones in call order, so their + // names are what tell which parameter each one belongs to + PyObject* argnames = numKwds ? method->GetSignatureNames() : nullptr; + + const bool isBound = pymeth->fSelf != nullptr; + // a place in the array becomes a slot with this shift + const Py_ssize_t argShift = + method->GetDeallocArgShift(isBound, args, nargs); + // a parameter index becomes a slot with this one + const Py_ssize_t paramShift = method->GetDeallocParamShift(); + // the array holds the positional values first, then the keyword ones + for (Py_ssize_t i = 0; i < nargs + numKwds; i++) { + // only a C++ object can hold memory that the callee releases + if (!CPPInstance_Check(args[i])) + continue; + + Py_ssize_t slot = -1; // no slot of its own + if (i < nargs) + slot = i + argShift; + else { + // kwds runs parallel to the keyword part of the array + // the place of the name in the signature is the parameter index + for (Py_ssize_t iarg = 0; iarg < PyTuple_GET_SIZE(argnames); iarg++) { + if (PyUnicode_Compare(PyTuple_GET_ITEM(argnames, iarg), + PyTuple_GET_ITEM(kwds, i - nargs)) == 0) { + slot = iarg + paramShift; + break; + } + } + } + // the callee drops this argument, or no name of it matches a parameter + if (slot < 0) + continue; + + const interop::DeallocType DT = method->GetDeallocBehaviour((size_t)slot); + if (DT == interop::DeallocType::None || + DT == interop::DeallocType::Opaque) + continue; + ((CPPInstance*)args[i])->CppOwns(); } + Py_XDECREF(argnames); // if this new object falls inside self, make sure its lifetime is proper if (!(pymeth->fMethodInfo->fFlags & CallContext::kNeverLifeLine)) { @@ -576,7 +652,8 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, if (!NoImplicit(&ctxt)) ctxt.fFlags |= CallContext::kAllowImplicit; // no two rounds needed PyObject* result = methods[0]->Call(im_self, args, nargsf, kwds, &ctxt); - return HandleReturn(pymeth, im_self, result); + return HandleReturn(pymeth, im_self, result, methods[0], args, nargsf, + kwds); } // otherwise, handle overloading @@ -599,7 +676,8 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, ctxt.fFlags |= CallContext::kAllowImplicit; PyObject* result = memoized_pc->Call(im_self, args, nargsf, kwds, &ctxt); if (result) - return HandleReturn(pymeth, im_self, result); + return HandleReturn(pymeth, im_self, result, memoized_pc, args, nargsf, + kwds); // fall through: python is dynamic, and so, the hashing isn't infallible ctxt.fFlags &= ~(CallContext::kAllowImplicit | CallContext::kPyException | @@ -656,7 +734,8 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, } } - return HandleReturn(pymeth, im_self, result); + return HandleReturn(pymeth, im_self, result, methods[i], args, nargsf, + kwds); } // else failure .. diff --git a/src/cpyrt/PyCallable.h b/src/cpyrt/PyCallable.h index 4b79ab00..3cc1e7ea 100644 --- a/src/cpyrt/PyCallable.h +++ b/src/cpyrt/PyCallable.h @@ -37,6 +37,22 @@ class PyCallable { virtual PyObject* GetCoVarNames() = 0; virtual PyObject* GetArgDefault(int /* iarg */, bool silent = true) = 0; virtual bool IsConst() { return false; } + virtual interop::AllocType GetAllocBehaviour() { + return interop::AllocType::None; + } + virtual interop::DeallocType GetDeallocBehaviour(size_t /* index */) { + return interop::DeallocType::Opaque; + } + // shift that turns a place in the args array into a GetDeallocBehaviour() + // slot, which the shape of the call decides + virtual Py_ssize_t GetDeallocArgShift(bool /* isBound */, + PyObject* const* /* args */, + Py_ssize_t /* nargs */) { + return 0; + } + // shift that turns a declared parameter index into a GetDeallocBehaviour() + // slot, which the declaration decides + virtual Py_ssize_t GetDeallocParamShift() { return 0; } virtual PyObject* GetScopeProxy() = 0; virtual interop::TCppFuncAddr_t GetFunctionAddress() = 0; diff --git a/src/cpyrt/cpyrtModule.cxx b/src/cpyrt/cpyrtModule.cxx index 915ceb74..7f2c07ef 100644 --- a/src/cpyrt/cpyrtModule.cxx +++ b/src/cpyrt/cpyrtModule.cxx @@ -585,14 +585,21 @@ static PyObject* BindObject(PyObject*, PyObject* args, PyObject* kwds) { // ownership is taken over as needed, again following the principle of // "least surprise" as most likely only the cast object will be retained bool owns = arg0_pyobj->fFlags & CPPInstance::kIsOwner; + // allocation flags travels with the ownership + const unsigned allocFlags = + arg0_pyobj->fFlags & + (CPPInstance::kIsMalloc | CPPInstance::kIsArrayAlloc | + CPPInstance::kIsNoConstruct); if (!isPython) { // ordinary C++ class PyObject* pyobj = BindCppObjectNoCast( interop::TCppObject_t((void*)((intptr_t)address.data + offset)), cast_type, owns ? CPPInstance::kIsOwner : 0); - if (owns && pyobj) + if (owns && pyobj) { arg0_pyobj->CppOwns(); + ((CPPInstance*)pyobj)->fFlags |= allocFlags; + } return pyobj; } else { @@ -624,6 +631,7 @@ static PyObject* BindObject(PyObject*, PyObject* args, PyObject* kwds) { if (pyobj && owns) { arg0_pyobj->CppOwns(); ((CPPInstance*)pyobj)->PythonOwns(); + ((CPPInstance*)pyobj)->fFlags |= allocFlags; } return pyobj; diff --git a/src/interop/cppjit_interop.h b/src/interop/cppjit_interop.h index 59c1a95b..5a10af0a 100644 --- a/src/interop/cppjit_interop.h +++ b/src/interop/cppjit_interop.h @@ -44,6 +44,8 @@ typedef Cpp::FuncRef TCppMethod_t; typedef Cpp::InterpRef TInterp_t; typedef size_t TCppIndex_t; typedef void* TCppFuncAddr_t; +typedef Cpp::AllocType AllocType; +typedef Cpp::DeallocType DeallocType; // direct interpreter access ------------------------------------------------- RPY_EXPORTED @@ -132,7 +134,7 @@ void Deallocate(TCppScope_t scope, TCppObject_t instance); RPY_EXPORTED TCppObject_t Construct(TCppScope_t scope, void* arena = nullptr); RPY_EXPORTED -void Destruct(TCppScope_t scope, TCppObject_t instance); +void Destruct(TCppScope_t scope, TCppObject_t instance, size_t count = 0); // method/function dispatching ----------------------------------------------- RPY_EXPORTED @@ -297,6 +299,10 @@ RPY_EXPORTED std::string GetDoxygenComment(TCppScope_t scope, bool strip_markers = true); RPY_EXPORTED bool IsConstMethod(TCppMethod_t); +RPY_EXPORTED +AllocType IsAllocator(TCppMethod_t); +RPY_EXPORTED +bool IsDeallocator(TCppMethod_t, std::vector& VPP); // Templated method/function reflection information // ------------------------------------ RPY_EXPORTED diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index d26697c8..e4b6c118 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -845,10 +845,11 @@ interop::TCppObject_t interop::Construct(TCppScope_t scope, return Cpp::Construct(scope, arena, /*count=*/1); } -void interop::Destruct(TCppScope_t scope, TCppObject_t instance) { +void interop::Destruct(TCppScope_t scope, TCppObject_t instance, + size_t count /*=0*/) { std::lock_guard Lock( InterOpMutex); // TODO: this shouldn't locks the JIT call - Cpp::Destruct(instance, scope, true, /*count=*/0); + Cpp::Destruct(instance, scope, true, count); } static inline bool copy_args(Parameter* args, size_t nargs, void** vargs) { @@ -1250,6 +1251,17 @@ interop::TCppType_t interop::GetMethodReturnType(TCppMethod_t method) { return Cpp::GetFunctionReturnType(method); } +bool interop::IsDeallocator(TCppMethod_t method, + std::vector& VPP) { + std::lock_guard Lock(InterOpMutex); + return Cpp::IsDeallocator(method, VPP); +} + +interop::AllocType interop::IsAllocator(TCppMethod_t method) { + std::lock_guard Lock(InterOpMutex); + return Cpp::IsAllocator(method); +} + std::string interop::GetMethodReturnTypeAsString(TCppMethod_t method) { std::lock_guard Lock(InterOpMutex); return Cpp::GetTypeAsString( diff --git a/test/Makefile b/test/Makefile index 7c891304..60070aea 100644 --- a/test/Makefile +++ b/test/Makefile @@ -15,6 +15,7 @@ dictnames = advancedcpp \ doc_helper \ example01 \ fragile \ + memory_analysis \ operators \ overloads \ pythonizables \ diff --git a/test/cpp/MemoryOwnership/MemOwnrship.apinotes b/test/cpp/MemoryOwnership/MemOwnrship.apinotes new file mode 100644 index 00000000..397d4f94 --- /dev/null +++ b/test/cpp/MemoryOwnership/MemOwnrship.apinotes @@ -0,0 +1,9 @@ +Name: MemOwnrship +Functions: + - Name: memOwnOperatorNew + SwiftReturnOwnership: cppAllocOperatorNew +Tags: + - Name: memOwn + Methods: + - Name: memOwnAllocator + SwiftReturnOwnership: cppAllocNew diff --git a/test/cpp/MemoryOwnership/memory_analysis_redecl.h b/test/cpp/MemoryOwnership/memory_analysis_redecl.h new file mode 100644 index 00000000..d8e9e636 --- /dev/null +++ b/test/cpp/MemoryOwnership/memory_analysis_redecl.h @@ -0,0 +1,10 @@ +#ifndef MEMORY_ANALYSIS_REDECL_H +#define MEMORY_ANALYSIS_REDECL_H +#include "../memory_analysis.h" + +namespace memory { +[[clang::annotate("cppAllocNew")]] +memOwn* allocDefaultMemOwn(); +} + +#endif diff --git a/test/cpp/MemoryOwnership/module.modulemap b/test/cpp/MemoryOwnership/module.modulemap new file mode 100644 index 00000000..123a620b --- /dev/null +++ b/test/cpp/MemoryOwnership/module.modulemap @@ -0,0 +1 @@ +module MemOwnrship { header "../memory_analysis.h" } diff --git a/test/cpp/memory_analysis.cxx b/test/cpp/memory_analysis.cxx new file mode 100644 index 00000000..d71cd817 --- /dev/null +++ b/test/cpp/memory_analysis.cxx @@ -0,0 +1,74 @@ +#include "memory_analysis.h" + +namespace memory { + +int memOwn::dtorCount = 0; + +memOwn::memOwn(int value) : val(value) {} + +memOwn::memOwn() { val = 0; } + +memOwn::~memOwn() { ++dtorCount; } + +__attribute__((malloc)) memAnalysisKlass* mallocAttr() { + return (memAnalysisKlass*)malloc(sizeof(memAnalysisKlass)); +} + +__attribute__((ownership_returns(malloc))) memAnalysisKlass* +ownershipReturnsAttr() { + return (memAnalysisKlass*)malloc(sizeof(memAnalysisKlass)); +} + +__attribute__((ownership_takes(malloc, 1))) void +ownershipTakesAttr(memAnalysisKlass* obj) { + free(obj); +} + +void takesPtrButNotOwnership(memAnalysisKlass* obj) { + // Does something with obj but does not take/holds its ownership + return; +} + +// Expected to not return ownership when analysis is off, and there is just +// attr-check +memAnalysisKlass* noAttr() { return new memAnalysisKlass; } + +memOwn* memOwnOperatorNew() { return (memOwn*)::operator new(sizeof(memOwn)); } + +memOwn* allocDefaultMemOwn() { return new memOwn; } + +memOwn* noAttrAlloc() { return new memOwn; } + +memOwn* allocOperatorNewArrAttr(size_t size) { + return (memOwn*)::operator new[](sizeof(memOwn) * size); +} + +memOwn* allocNewArrAttr(int count) { return new memOwn[count]; } + +memOwn* allocMallocAttr(size_t size) { + return (memOwn*)malloc(sizeof(memOwn) * size); +} + +memOwn* allocOperatorNewAttr() { + return (memOwn*)::operator new(sizeof(memOwn)); +} + +memCastDerived* allocMallocDerived() { + return (memCastDerived*)malloc(sizeof(memCastDerived)); +} + +void memOwner::release(memOwn* obj) { delete obj; } + +void memOwner::releaseSecond(memOwn* keep, memOwn* obj) { delete obj; } + +void memOwner::releaseAfterCount(int count, memOwn* obj) { delete obj; } + +void memOwner::keepOwnership(memOwn* obj) { return; } + +void memOwner::releaseStatic(memOwn* obj) { delete obj; } + +void memOwner::releaseOverloaded(memOwn* keep, memOwn* obj) { delete obj; } + +void releaseFree(memOwner* self, memOwn* obj) { delete obj; } + +} // namespace memory diff --git a/test/cpp/memory_analysis.h b/test/cpp/memory_analysis.h new file mode 100644 index 00000000..c288549b --- /dev/null +++ b/test/cpp/memory_analysis.h @@ -0,0 +1,95 @@ +#ifndef MEMORY_ANALYSIS_H +#define MEMORY_ANALYSIS_H + +#include +#include +namespace memory { + +class memAnalysisKlass { +public: + int val; +}; +__attribute__((malloc)) memAnalysisKlass* mallocAttr(); +__attribute__((ownership_returns(malloc))) memAnalysisKlass* +ownershipReturnsAttr(); +memAnalysisKlass* noAttr(); + +__attribute__((ownership_takes(malloc, 1))) void +ownershipTakesAttr(memAnalysisKlass* obj); + +void takesPtrButNotOwnership(memAnalysisKlass* obj); + +struct memOwn { + int val; + static int dtorCount; + memOwn(int value); + memOwn(); + // Attribute injected by APINotes + static memOwn* memOwnAllocator(int x) { return new memOwn(x); } + ~memOwn(); +}; + +// Attribute injected by APINotes +memOwn* memOwnOperatorNew(); + +// Attribute injected by redeclaration +memOwn* allocDefaultMemOwn(); + +// No ownership attribute anywhere +memOwn* noAttrAlloc(); + +[[clang::annotate("cppAllocOperatorNewArr")]] +memOwn* allocOperatorNewArrAttr(size_t size); + +[[clang::annotate("cppAllocNewArr")]] +memOwn* allocNewArrAttr(int count); + +[[clang::annotate("cppAllocMalloc")]] +memOwn* allocMallocAttr(size_t size); + +[[clang::annotate("cppAllocOperatorNew")]] +memOwn* allocOperatorNewAttr(); + +// A pair of classes that bind_object can cast between. Neither one is +// polymorphic, so the cast needs no runtime type information. +struct memCastBase { + int base = 0; +}; + +struct memCastDerived : memCastBase { + int derived = 0; +}; + +[[clang::annotate("cppAllocMalloc")]] +memCastDerived* allocMallocDerived(); + +struct memOwner { + + [[clang::annotate("cppDeallocDelete", 2)]] + void release(memOwn* obj); + + [[clang::annotate("cppDeallocDelete", 3)]] + void releaseSecond(memOwn* keep, memOwn* obj); + + [[clang::annotate("cppDeallocDelete", 3)]] + void releaseAfterCount(int count, memOwn* obj); + + [[clang::annotate("cppDeallocNone", 2)]] + void keepOwnership(memOwn* obj); + + [[clang::annotate("cppDeallocDelete", 1)]] + static void releaseStatic(memOwn* obj); + + // One test adds a Python overload to this method, and that overload stays in + // the shared overload group. The other tests use their own methods, so the + // added one reaches none of them. Two parameters make a call with a single + // argument miss this overload, which lets the Python one run. + [[clang::annotate("cppDeallocDelete", 3)]] + void releaseOverloaded(memOwn* keep, memOwn* obj); +}; + +[[clang::annotate("cppDeallocDelete", 2)]] +void releaseFree(memOwner* self, memOwn* obj); +} // namespace memory + +#endif // MEMORY_ANALYSIS_H diff --git a/test/test_memoryanalysis.py b/test/test_memoryanalysis.py new file mode 100644 index 00000000..72b3e53b --- /dev/null +++ b/test/test_memoryanalysis.py @@ -0,0 +1,503 @@ +import gc +import os +import subprocess +import sys + +import py +from pytest import mark +from support import IS_CLING, setup_make + +currpath = py.path.local(__file__).dirpath() +test_dct = str(currpath.join("cpp/memory_analysisDict")) + +FLAGS = "-fmodules -fimplicit-module-maps -fapinotes-modules" +IN_CHILD = "-fapinotes-modules" in os.getenv("CPPINTEROP_EXTRA_INTERPRETER_ARGS", "") + + +def setup_module(mod): + setup_make("memory_analysis") + + +@mark.skipif( + IN_CHILD or IS_CLING, + reason="Cling asserts in collectModuleMaps when built with " + FLAGS, +) +def test00_driver(): + env = os.environ.copy() + env["CPPINTEROP_EXTRA_INTERPRETER_ARGS"] = ( + env.get("CPPINTEROP_EXTRA_INTERPRETER_ARGS", "") + " " + FLAGS + ) + subprocess.check_call([sys.executable, "-m", "pytest", __file__], env=env) + + +class TestMEMORYANALYSIS: + def setup_class(cls): + cls.test_dct = test_dct + import cppjit + + cppjit.add_include_path(str(currpath.join("cpp", "MemoryOwnership"))) + cppjit.include("../memory_analysis.h") + cppjit.include("memory_analysis_redecl.h") + cls.memory_analysis = cppjit.load_library(cls.test_dct + ".so") + + def test01_malloc_attr(self): + import cppjit + + obj = cppjit.gbl.memory.mallocAttr() + assert type(obj) == cppjit.gbl.memory.memAnalysisKlass + assert obj.__python_owns__ + assert obj.__is_malloc__ + + def test02_ownership_returns_attr(self): + import cppjit + + obj = cppjit.gbl.memory.ownershipReturnsAttr() + assert type(obj) == cppjit.gbl.memory.memAnalysisKlass + assert obj.__python_owns__ + assert obj.__is_malloc__ + + def test03_no_attr(self): + import cppjit + + obj = cppjit.gbl.memory.noAttr() + assert type(obj) == cppjit.gbl.memory.memAnalysisKlass + assert not obj.__python_owns__ + obj.__python_owns__ = True + + def test04_redecl_attr(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + + obj = cppjit.gbl.memory.allocDefaultMemOwn() + assert type(obj) == cppjit.gbl.memory.memOwn + assert obj.__python_owns__ + assert not obj.__is_malloc__ + assert not obj.__is_no_construct__ + assert not obj.__is_array_alloc__ + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 1 + + def test05_redecl_no_attr(self): + import cppjit + + obj = cppjit.gbl.memory.noAttrAlloc() + assert type(obj) == cppjit.gbl.memory.memOwn + assert not obj.__python_owns__ + obj.__python_owns__ = True + + # Setting only python_owns, intends object is allocated with new + assert not obj.__is_malloc__ + assert not obj.__is_no_construct__ + assert not obj.__is_array_alloc__ + + def test08_allocwith_operator_newarr_attr(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + + obj = cppjit.gbl.memory.allocOperatorNewArrAttr(5) + assert obj.__python_owns__ + assert obj.__is_no_construct__ + assert obj.__is_array_alloc__ + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 0 + + def test09_allocwith_newarr_attr(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + + obj = cppjit.gbl.memory.allocNewArrAttr(5) + assert obj.__python_owns__ + assert not obj.__is_no_construct__ + assert obj.__is_array_alloc__ + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 5 + + def test10_allocwith_malloc_attr(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + + obj = cppjit.gbl.memory.allocMallocAttr(5) + assert obj.__python_owns__ + assert not obj.__is_no_construct__ + assert not obj.__is_array_alloc__ + assert obj.__is_malloc__ + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 0 + + def test11_allocwith_operator_new_attr(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + + obj = cppjit.gbl.memory.allocOperatorNewAttr() + assert obj.__python_owns__ + assert obj.__is_no_construct__ + assert not obj.__is_array_alloc__ + assert not obj.__is_malloc__ + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 0 + + def test12_double_free_with_gblfree(self): + import cppjit + + obj = cppjit.gbl.memory.mallocAttr() + assert obj.__python_owns__ + assert obj.__is_malloc__ + cppjit.gbl.free(obj) + assert not obj.__python_owns__ + + def test13_double_free_with_ownership_takes(self): + import cppjit + + obj = cppjit.gbl.memory.mallocAttr() + assert obj.__python_owns__ + assert obj.__is_malloc__ + cppjit.gbl.memory.ownershipTakesAttr(obj) + assert not obj.__python_owns__ + + def test14_double_free_with_not_ownership_taken(self): + import cppjit + + obj = cppjit.gbl.memory.mallocAttr() + assert obj.__python_owns__ + assert obj.__is_malloc__ + cppjit.gbl.memory.takesPtrButNotOwnership(obj) + assert obj.__python_owns__ + + # The tests below check the mapping from a call argument to the parameter + # that the deallocation attribute labels. Every call releases the object + # in C++, so Python has to give up the ownership of it. + + def owned_memown(self): + import cppjit + + obj = cppjit.gbl.memory.allocDefaultMemOwn() + assert obj.__python_owns__ + return obj + + def test15_release_bound_positional(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.release(obj) + assert not obj.__python_owns__ + assert cppjit.gbl.memory.memOwn.dtorCount == 1 + + del obj + gc.collect() + assert cppjit.gbl.memory.memOwn.dtorCount == 1 + + def test16_release_bound_keyword(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.release(obj=obj) + assert not obj.__python_owns__ + + def test17_release_unbound_positional(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + cppjit.gbl.memory.memOwner.release(owner, obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test18_release_unbound_keyword(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + cppjit.gbl.memory.memOwner.release(owner, obj=obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test19_release_second_positional(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + keep = self.owned_memown() + obj = self.owned_memown() + + owner.releaseSecond(keep, obj) + assert not obj.__python_owns__ + assert keep.__python_owns__ + + def test20_release_second_keyword_reversed(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + keep = self.owned_memown() + obj = self.owned_memown() + + # the values arrive in call order, so only the names say which + # parameter each one belongs to + owner.releaseSecond(obj=obj, keep=keep) + assert not obj.__python_owns__ + assert keep.__python_owns__ + + def test21_release_after_count(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.releaseAfterCount(3, obj) + assert not obj.__python_owns__ + + def test22_release_after_count_keyword(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.releaseAfterCount(count=3, obj=obj) + assert not obj.__python_owns__ + + def test23_keep_ownership(self): + import cppjit + + cppjit.gbl.memory.memOwn.dtorCount = 0 + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.keepOwnership(obj) + assert obj.__python_owns__ + assert cppjit.gbl.memory.memOwn.dtorCount == 0 + + owner.keepOwnership(obj=obj) + assert obj.__python_owns__ + + def test24_release_static_through_instance(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + # a static method has no implicit this slot, although the lookup on an + # instance binds self + owner.releaseStatic(obj) + assert not obj.__python_owns__ + + def test25_release_static_through_class(self): + import cppjit + + obj = self.owned_memown() + + cppjit.gbl.memory.memOwner.releaseStatic(obj) + assert not obj.__python_owns__ + + def test26_release_static_with_leading_instance(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + # cppjit drops the leading instance of such a call, so the first + # parameter is the second argument + cppjit.gbl.memory.memOwner.releaseStatic(owner, obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test27_release_free_function(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + cppjit.gbl.memory.releaseFree(owner, obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test28_release_free_function_keyword(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + cppjit.gbl.memory.releaseFree(owner, obj=obj) + assert not obj.__python_owns__ + + def test29_release_free_function_attached(self): + import cppjit + + # a free function whose first parameter is the object works as a + # method, and then cppjit passes self as that parameter + cppjit.gbl.memory.memOwner.releaseAttached = cppjit.gbl.memory.releaseFree + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.releaseAttached(obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test30_release_free_function_attached_keyword(self): + import cppjit + + cppjit.gbl.memory.memOwner.releaseAttached = cppjit.gbl.memory.releaseFree + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + owner.releaseAttached(obj=obj) + assert not obj.__python_owns__ + + # A direct call keeps self inside the argument array, as the vector call + # skips the descriptor. Storing the method first runs the descriptor, and + # then self arrives on its own. + + def test31_release_stored_bound_positional(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + release = owner.release + release(obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test32_release_stored_bound_keyword(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + release = owner.release + release(obj=obj) + assert not obj.__python_owns__ + + def test33_release_second_stored_bound(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + keep = self.owned_memown() + obj = self.owned_memown() + + release_second = owner.releaseSecond + release_second(keep, obj) + assert not obj.__python_owns__ + assert keep.__python_owns__ + + def test34_release_static_stored_bound(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + release_static = owner.releaseStatic + release_static(obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test35_release_free_function_attached_stored_bound(self): + import cppjit + + cppjit.gbl.memory.memOwner.releaseAttached = cppjit.gbl.memory.releaseFree + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + release_attached = owner.releaseAttached + release_attached(obj) + assert not obj.__python_owns__ + assert owner.__python_owns__ + + def test36_keep_ownership_stored_bound(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + obj = self.owned_memown() + + keep_ownership = owner.keepOwnership + keep_ownership(obj) + assert obj.__python_owns__ + + def test37_python_overload(self): + import cppjit + + owner = cppjit.gbl.memory.memOwner() + keep = self.owned_memown() + obj = self.owned_memown() + + def extra(self, obj): + return 0 + + cppjit.gbl.memory.memOwner.releaseOverloaded.__add_overload__(extra) + + # a Python overload carries no ownership information, so the arguments + # of such a call keep their ownership + owner.releaseOverloaded(obj) + assert obj.__python_owns__ + assert owner.__python_owns__ + + # the name of a Python argument is no C++ parameter name either + owner.releaseOverloaded(obj=obj) + assert obj.__python_owns__ + + # the C++ overload needs two arguments, and it does take the ownership + owner.releaseOverloaded(keep, obj) + assert not obj.__python_owns__ + assert keep.__python_owns__ + + def test38_bind_object_keeps_the_allocation_kind(self): + import cppjit + + obj = cppjit.gbl.memory.allocMallocDerived() + assert obj.__python_owns__ + assert obj.__is_malloc__ + + # a cast moves the ownership to the new proxy, so the deallocator of + # the memory has to move with it + cast = cppjit.bind_object(obj, cppjit.gbl.memory.memCastBase) + assert not obj.__python_owns__ + assert cast.__python_owns__ + assert cast.__is_malloc__ + + del cast + gc.collect() + + +@mark.skipif(not IN_CHILD, reason="needs " + FLAGS) +class TestMEMORYANALYSIS_APINOTES: + def setup_class(cls): + cls.test_dct = test_dct + import cppjit + + cppjit.add_include_path(str(currpath.join("cpp", "MemoryOwnership"))) + cppjit.include("../memory_analysis.h") + cls.memory_analysis = cppjit.load_library(cls.test_dct + ".so") + + def test01_apinotes_attr_method(self): + import cppjit + + obj = cppjit.gbl.memory.memOwn.memOwnAllocator(5) + assert type(obj) == cppjit.gbl.memory.memOwn + assert obj.__python_owns__ + + def test02_apinotes_attr_func(self): + import cppjit + + obj = cppjit.gbl.memory.memOwnOperatorNew() + assert type(obj) == cppjit.gbl.memory.memOwn + assert obj.__python_owns__ + assert obj.__is_no_construct__