From 571a436025fe69dd5845368441f20d657d9f8d50 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Sat, 22 Aug 2026 01:30:05 +0300 Subject: [PATCH 1/2] [memory-analysis] Added a field to CPPMethod to store memory-ownership information and this information effects kIsCreator flag of overload group , currently analyzer is not called, just attribute checker is called --- src/cpyrt/CPPMethod.cxx | 17 +++++++++++++++++ src/cpyrt/CPPMethod.h | 3 +++ src/cpyrt/CPPOverload.cxx | 6 ++++++ src/cpyrt/PyCallable.h | 3 +++ src/cpyrt/cppjit_interop.h | 15 +++++++++++++++ src/interop/cpp_cppjit.h | 3 +++ src/interop/interop_wrapper.cxx | 5 +++++ test/Makefile | 1 + test/cpp/memory_analysis.cxx | 9 +++++++++ test/cpp/memory_analysis.h | 13 +++++++++++++ test/test_memoryanalysis.py | 31 +++++++++++++++++++++++++++++++ 11 files changed, 106 insertions(+) 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/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 761d0ed..3edf4a3 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -749,6 +749,23 @@ PyObject* cpyrt::CPPMethod::GetArgDefault(int iarg, bool silent) { bool cpyrt::CPPMethod::IsConst() { return interop::IsConstMethod(GetMethod()); } +//---------------------------------------------------------------------------- +// FIXME: For now every allocation is assumed to be done with `new` +// will be fixed soon. Also the reason function returns an AllocType +// instead of bool, IsAllocator function from CppInterOp changed a little, +// but these changes did not merged to main yet, another reason is next PR +// will add user-optional analyzer so the function needs to return allocation +// way +interop::AllocType cpyrt::CPPMethod::GetAllocBehaviour() { + if (fAllocType.has_value()) + return *fAllocType; + if (interop::IsAllocator(GetMethod())) { + fAllocType = interop::AllocType::New; + return interop::AllocType::New; + } + fAllocType = interop::AllocType::None; + return interop::AllocType::None; +} //---------------------------------------------------------------------------- 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 f66016c..17e4546 100644 --- a/src/cpyrt/CPPMethod.h +++ b/src/cpyrt/CPPMethod.h @@ -5,6 +5,7 @@ #include "PyCallable.h" // Standard +#include #include #include #include @@ -63,6 +64,7 @@ class CPPMethod : public PyCallable { PyObject* GetCoVarNames() override; PyObject* GetArgDefault(int iarg, bool silent = true) override; bool IsConst() override; + cppjit::interop::AllocType GetAllocBehaviour() override; PyObject* GetScopeProxy() override; cppjit::interop::TCppFuncAddr_t GetFunctionAddress() override; @@ -117,6 +119,7 @@ class CPPMethod : public PyCallable { protected: // cached value that doubles as initialized flag (uninitialized if -1) int fArgsRequired; + std::optional fAllocType; }; } // namespace cppjit::cpyrt diff --git a/src/cpyrt/CPPOverload.cxx b/src/cpyrt/CPPOverload.cxx index 8bda467..a51b403 100644 --- a/src/cpyrt/CPPOverload.cxx +++ b/src/cpyrt/CPPOverload.cxx @@ -155,6 +155,12 @@ static inline PyObject* HandleReturn(CPPOverload* pymeth, CPPInstance* im_self, CPPInstance* cppres = (CPPInstance*)(CPPInstance_Check(result) ? result : nullptr); + interop::AllocType AT = + pymeth->fMethodInfo->fMethods[0]->GetAllocBehaviour(); + if (AT != interop::AllocType::None && AT != interop::AllocType::Null && + AT != interop::AllocType::Unknown) + pymeth->fMethodInfo->fFlags |= CallContext::kIsCreator; + // if this method creates new objects, always take ownership if (IsCreator(pymeth->fMethodInfo->fFlags)) { diff --git a/src/cpyrt/PyCallable.h b/src/cpyrt/PyCallable.h index cd38d98..8bc8835 100644 --- a/src/cpyrt/PyCallable.h +++ b/src/cpyrt/PyCallable.h @@ -37,6 +37,9 @@ class PyCallable { virtual PyObject* GetCoVarNames() = 0; virtual PyObject* GetArgDefault(int /* iarg */, bool silent = true) = 0; virtual bool IsConst() { return false; } + virtual cppjit::interop::AllocType GetAllocBehaviour() { + return cppjit::interop::AllocType::None; + } virtual PyObject* GetScopeProxy() = 0; virtual cppjit::interop::TCppFuncAddr_t GetFunctionAddress() = 0; diff --git a/src/cpyrt/cppjit_interop.h b/src/cpyrt/cppjit_interop.h index 9a6bc05..b6cfaf2 100644 --- a/src/cpyrt/cppjit_interop.h +++ b/src/cpyrt/cppjit_interop.h @@ -92,6 +92,18 @@ struct ObjectRef { friend bool operator==(ObjectRef a, ObjectRef b) { return a.data == b.data; } friend bool operator!=(ObjectRef a, ObjectRef b) { return !(a == b); } }; + +enum class AllocType : unsigned char { + None, + New, + NewArr, + Malloc, + Unknown, + CustomAlloc, + Null, + OperatorNew, + OperatorNewArr +}; } // namespace Cpp template <> struct std::hash { @@ -122,6 +134,7 @@ typedef Cpp::ObjectRef TCppObject_t; typedef Cpp::FuncRef TCppMethod_t; typedef size_t TCppIndex_t; typedef void* TCppFuncAddr_t; +typedef Cpp::AllocType AllocType; // direct interpreter access ------------------------------------------------- CPPJIT_IMPORT @@ -368,6 +381,8 @@ CPPJIT_IMPORT std::string GetDoxygenComment(TCppScope_t scope, bool strip_markers = true); CPPJIT_IMPORT bool IsConstMethod(TCppMethod_t); +CPPJIT_IMPORT +bool IsAllocator(TCppMethod_t); // Templated method/function reflection information // ------------------------------------ CPPJIT_IMPORT diff --git a/src/interop/cpp_cppjit.h b/src/interop/cpp_cppjit.h index c56bd19..21904f9 100644 --- a/src/interop/cpp_cppjit.h +++ b/src/interop/cpp_cppjit.h @@ -53,6 +53,7 @@ typedef Cpp::FuncRef TCppMethod_t; typedef Cpp::InterpRef TInterp_t; typedef size_t TCppIndex_t; typedef void* TCppFuncAddr_t; +typedef Cpp::AllocType AllocType; // direct interpreter access ------------------------------------------------- RPY_EXPORTED @@ -306,6 +307,8 @@ RPY_EXPORTED std::string GetDoxygenComment(TCppScope_t scope, bool strip_markers = true); RPY_EXPORTED bool IsConstMethod(TCppMethod_t); +RPY_EXPORTED +bool IsAllocator(TCppMethod_t); // Templated method/function reflection information // ------------------------------------ RPY_EXPORTED diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 2bb7698..adc2cc9 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1184,6 +1184,11 @@ interop::TCppType_t interop::GetMethodReturnType(TCppMethod_t method) { return Cpp::GetFunctionReturnType(method); } +bool 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 e07e775..4c9c019 100644 --- a/test/Makefile +++ b/test/Makefile @@ -10,6 +10,7 @@ dictnames = advancedcpp \ doc_helper \ example01 \ fragile \ + memory_analysis \ operators \ overloads \ pythonizables \ diff --git a/test/cpp/memory_analysis.cxx b/test/cpp/memory_analysis.cxx new file mode 100644 index 0000000..c9aff7e --- /dev/null +++ b/test/cpp/memory_analysis.cxx @@ -0,0 +1,9 @@ +#include "memory_analysis.h" +__attribute__((malloc)) memAnalysisKlass* allocTest() { + return new memAnalysisKlass; +} + +__attribute__((ownership_returns(malloc))) memAnalysisKlass* +allocTestReturns() { + return new memAnalysisKlass; +} \ No newline at end of file diff --git a/test/cpp/memory_analysis.h b/test/cpp/memory_analysis.h new file mode 100644 index 0000000..e4188d9 --- /dev/null +++ b/test/cpp/memory_analysis.h @@ -0,0 +1,13 @@ +#ifndef MEMORY_ANALYSIS_H +#define MEMORY_ANALYSIS_H + +#include +#include + +class memAnalysisKlass { + int val; +}; +__attribute__((malloc)) memAnalysisKlass* allocTest(); +__attribute__((ownership_returns(malloc))) memAnalysisKlass* allocTestReturns(); + +#endif // MEMORY_ANALYSIS_H \ No newline at end of file diff --git a/test/test_memoryanalysis.py b/test/test_memoryanalysis.py new file mode 100644 index 0000000..f518a70 --- /dev/null +++ b/test/test_memoryanalysis.py @@ -0,0 +1,31 @@ +import py +from support import setup_make + +currpath = py.path.local(__file__).dirpath() +test_dct = str(currpath.join("cpp/memory_analysisDict")) + + +def setup_module(mod): + setup_make("memory_analysis") + + +class TestMEMORYANALYSIS: + def setup_class(cls): + cls.test_dct = test_dct + import cppjit + + cls.memory_analysis = cppjit.load_reflection_info(cls.test_dct) + + def test01_malloc_attr(self): + import cppjit + + obj = cppjit.gbl.allocTest() + assert type(obj) == cppjit.gbl.memAnalysisKlass + assert obj.__python_owns__ + + def test02_ownership_returns_attr(self): + import cppjit + + obj = cppjit.gbl.allocTestReturns() + assert type(obj) == cppjit.gbl.memAnalysisKlass + assert obj.__python_owns__ From 29f069b933d0cd0c7e4950d3f3fbde43d3bcc0eb Mon Sep 17 00:00:00 2001 From: keremsahn Date: Sat, 22 Aug 2026 17:11:13 +0300 Subject: [PATCH 2/2] [memory-analysis] Support for GetAllocType added, with lower priority to attr-check --- src/cpyrt/CPPMethod.cxx | 6 ++++++ src/cpyrt/cppjit_interop.h | 2 ++ src/cpyrt/cpyrtModule.cxx | 17 +++++++++++++++++ src/interop/cpp_cppjit.h | 2 ++ src/interop/interop_wrapper.cxx | 5 +++++ test/cpp/memory_analysis.h | 2 ++ test/test_memoryanalysis.py | 17 +++++++++++++++++ 7 files changed, 51 insertions(+) diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 3edf4a3..157300e 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -34,6 +34,7 @@ extern PyObject* gBusException; extern PyObject* gSegvException; extern PyObject* gIllException; extern PyObject* gAbrtException; +extern bool gUseAllocAnalyzer; } // namespace cppjit::cpyrt //- public helper ------------------------------------------------------------ @@ -763,6 +764,11 @@ interop::AllocType cpyrt::CPPMethod::GetAllocBehaviour() { fAllocType = interop::AllocType::New; return interop::AllocType::New; } + if (gUseAllocAnalyzer) { + interop::AllocType AT = interop::GetAllocType(GetMethod()); + fAllocType = AT; + return AT; + } fAllocType = interop::AllocType::None; return interop::AllocType::None; } diff --git a/src/cpyrt/cppjit_interop.h b/src/cpyrt/cppjit_interop.h index b6cfaf2..2a52e0e 100644 --- a/src/cpyrt/cppjit_interop.h +++ b/src/cpyrt/cppjit_interop.h @@ -383,6 +383,8 @@ CPPJIT_IMPORT bool IsConstMethod(TCppMethod_t); CPPJIT_IMPORT bool IsAllocator(TCppMethod_t); +CPPJIT_IMPORT +AllocType GetAllocType(TCppMethod_t); // Templated method/function reflection information // ------------------------------------ CPPJIT_IMPORT diff --git a/src/cpyrt/cpyrtModule.cxx b/src/cpyrt/cpyrtModule.cxx index b2c0733..97e2baa 100644 --- a/src/cpyrt/cpyrtModule.cxx +++ b/src/cpyrt/cpyrtModule.cxx @@ -279,6 +279,7 @@ PyObject* gAbrtException = nullptr; std::unordered_set gPinnedTypes; std::ostringstream gCapturedError; std::streambuf* gOldErrorBuffer = nullptr; +bool gUseAllocAnalyzer = false; std::unordered_map>& pythonizations() { static std::unordered_map> pyzMap; @@ -1012,6 +1013,20 @@ static PyObject* EndCaptureStderr(PyObject*, PyObject*) { return Py_BuildValue("s", capturedError.c_str()); } + +//---------------------------------------------------------------------------- +static PyObject* SetUseAllocAnalyzer(PyObject*, PyObject* args) { + // Set allocation-analyzer policy, disabled by default + // Usage: enabling ->SetUseAllocAnalyzer(True) / SetUseAllocAnalyzer(1) + // disabling ->SetUseAllocAnalyzer(False) / SetUseAllocAnalyzer(0) + int enable = 0; + if (!PyArg_ParseTuple(args, const_cast("p"), &enable)) + return nullptr; + + gUseAllocAnalyzer = enable; + + Py_RETURN_NONE; +} } // unnamed namespace //- data ----------------------------------------------------------------------- @@ -1061,6 +1076,8 @@ static PyMethodDef gcpyrtMethods[] = { METH_NOARGS, (char*)"Begin capturing stderr to a in memory buffer."}, {(char*)"_end_capture_stderr", (PyCFunction)EndCaptureStderr, METH_NOARGS, (char*)"End capturing stderr and returns the captured buffer."}, + {(char*)"SetUseAllocAnalyzer", (PyCFunction)SetUseAllocAnalyzer, + METH_VARARGS, (char*)"Enable/disable memory-allocation analyzer."}, {nullptr, nullptr, 0, nullptr}}; struct module_state { diff --git a/src/interop/cpp_cppjit.h b/src/interop/cpp_cppjit.h index 21904f9..b9360b1 100644 --- a/src/interop/cpp_cppjit.h +++ b/src/interop/cpp_cppjit.h @@ -309,6 +309,8 @@ RPY_EXPORTED bool IsConstMethod(TCppMethod_t); RPY_EXPORTED bool IsAllocator(TCppMethod_t); +RPY_EXPORTED +AllocType GetAllocType(TCppMethod_t); // Templated method/function reflection information // ------------------------------------ RPY_EXPORTED diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index adc2cc9..358e926 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1189,6 +1189,11 @@ bool interop::IsAllocator(TCppMethod_t method) { return Cpp::IsAllocator(method); } +interop::AllocType interop::GetAllocType(TCppMethod_t method) { + std::lock_guard Lock(InterOpMutex); + return Cpp::GetAllocType(method); +} + std::string interop::GetMethodReturnTypeAsString(TCppMethod_t method) { std::lock_guard Lock(InterOpMutex); return Cpp::GetTypeAsString( diff --git a/test/cpp/memory_analysis.h b/test/cpp/memory_analysis.h index e4188d9..6e87085 100644 --- a/test/cpp/memory_analysis.h +++ b/test/cpp/memory_analysis.h @@ -10,4 +10,6 @@ class memAnalysisKlass { __attribute__((malloc)) memAnalysisKlass* allocTest(); __attribute__((ownership_returns(malloc))) memAnalysisKlass* allocTestReturns(); +inline memAnalysisKlass* allocNew() { return new memAnalysisKlass; } +inline memAnalysisKlass* allocNew2() { return new memAnalysisKlass; } #endif // MEMORY_ANALYSIS_H \ No newline at end of file diff --git a/test/test_memoryanalysis.py b/test/test_memoryanalysis.py index f518a70..b4f6c01 100644 --- a/test/test_memoryanalysis.py +++ b/test/test_memoryanalysis.py @@ -29,3 +29,20 @@ def test02_ownership_returns_attr(self): obj = cppjit.gbl.allocTestReturns() assert type(obj) == cppjit.gbl.memAnalysisKlass assert obj.__python_owns__ + + def test03_analyzer_new(self): + import cppjit + + cppjit._backend.SetUseAllocAnalyzer(True) + obj = cppjit.gbl.allocNew() + assert type(obj) == cppjit.gbl.memAnalysisKlass + assert obj.__python_owns__ + + def test04_analyzer_new_default(self): + import cppjit + + cppjit._backend.SetUseAllocAnalyzer(False) + obj = cppjit.gbl.allocNew2() + assert type(obj) == cppjit.gbl.memAnalysisKlass + assert not (obj.__python_owns__) + obj.__python_owns__ = True