From ca82231c579f1cf8d799a5de4d5324ea5d8472c8 Mon Sep 17 00:00:00 2001 From: Aaron Jomy <75925957+aaronj0@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:17:59 +0200 Subject: [PATCH 1/6] Improve error reporting for method calls without a C++ object (#41) * [cpyrt] Improve error reporting for method calls without C++ object Co-Authored-By: Claude Fable 5 * [test] Add test for method calls on an instance without a C++ object Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Grigori Rybkine Co-authored-by: Claude Fable 5 --- src/cpyrt/CPPMethod.cxx | 3 ++- src/cpyrt/CPPOverload.cxx | 3 ++- test/test_fragile.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 761d0ed..3f0d0e5 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -1058,7 +1058,8 @@ PyObject* cpyrt::CPPMethod::Call(CPPInstance*& self, cpyrt_PyArgs_t args, // validity check that should not fail if (!object) { - PyErr_SetString(PyExc_ReferenceError, "attempt to access a null-pointer"); + PyErr_SetString(PyExc_ReferenceError, "no C++ object available"); + ctxt->fFlags |= CallContext::kCppException; return nullptr; } diff --git a/src/cpyrt/CPPOverload.cxx b/src/cpyrt/CPPOverload.cxx index 8bda467..49778df 100644 --- a/src/cpyrt/CPPOverload.cxx +++ b/src/cpyrt/CPPOverload.cxx @@ -624,7 +624,8 @@ static PyObject* mp_vectorcall(CPPOverload* pymeth, PyObject* const* args, return HandleReturn(pymeth, im_self, result); // fall through: python is dynamic, and so, the hashing isn't infallible - ctxt.fFlags &= ~CallContext::kAllowImplicit; + ctxt.fFlags &= ~(CallContext::kAllowImplicit | CallContext::kPyException | + CallContext::kCppException); PyErr_Clear(); ResetCallState(pymeth->fSelf, im_self); } diff --git a/test/test_fragile.py b/test/test_fragile.py index 181a38b..e4cf3bc 100644 --- a/test/test_fragile.py +++ b/test/test_fragile.py @@ -761,6 +761,24 @@ def test31_template_with_class_enum(self): for ns, val in [(cppjit.gbl, 42), (cppjit.gbl.ClassEnumNS, 37)]: assert ns.EnumTemplate[ns.ClassEnumA.A]().foo() == val + def test32_overloaded_method_error_with_null_object(self): + """Check exception type and message when method invoked on instance without C++ object""" + + import cppjit + from cppjit import gbl + + cppjit.cppdef(r"""\ + using fragile::D; + D *something = new D; + D *nothing = nullptr; + """) + + assert gbl.something.check() == gbl.something.check(0, 1) + with raises(ReferenceError, match=r"^no C\+\+ object available$"): + gbl.nothing.check() # raises error + with raises(ReferenceError, match=r"^no C\+\+ object available$"): + gbl.nothing.check(0, 1) # raises error + class TestSIGNALS: def setup_class(cls): From 323503946e59904de07f4c4ede355a9bc514a8d2 Mon Sep 17 00:00:00 2001 From: Aaron Jomy <75925957+aaronj0@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:18:35 +0200 Subject: [PATCH 2/6] Penalize void* arguments in overload priority as intended (#40) * [cpyrt] Penalize void* arguments in overload priority as intended * [test] Add regression test for void* overload priority --------- Co-authored-by: Emery Conrad Co-authored-by: Claude Fable 5 --- src/cpyrt/CPPMethod.cxx | 13 ++++++++----- test/test_overloads.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 3f0d0e5..3fe3e37 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -529,7 +529,14 @@ int cpyrt::CPPMethod::GetPriority() { // type: // interop::TCppType_t type = interop::GetMethodArgType(fMethod, iarg); - if (interop::IsBuiltin(aname)) { + // Not builtin and spelled "const void *", so match the compacted name. + std::string compact = aname; + compact.erase(std::remove(compact.begin(), compact.end(), ' '), + compact.end()); + + if (compact.find("void*") != std::string::npos) { + priority -= 1000; // void*/void** shouldn't be too greedy + } else if (interop::IsBuiltin(aname)) { // complex type (note: double penalty: for complex and the template type) if (strstr(aname.c_str(), "std::complex")) priority -= 10; // prefer double, float, etc. over conversion @@ -557,10 +564,6 @@ int cpyrt::CPPMethod::GetPriority() { else if (strstr(aname.c_str(), "char") && aname[aname.size() - 1] != '*') priority += -60; // prefer (const) char* over char - // oddball - else if (strstr(aname.c_str(), "void*")) - priority -= 1000; // void*/void** shouldn't be too greedy - } else { // This is a user-defined type (class, struct, enum, etc.). diff --git a/test/test_overloads.py b/test/test_overloads.py index 24b8d0a..f736c8a 100644 --- a/test/test_overloads.py +++ b/test/test_overloads.py @@ -411,3 +411,32 @@ def test15_disallow_mutable_pointer_references(self): ptr = cppjit.gbl.MyClass() raises(TypeError, cppjit.gbl.changePtr, ptr) + + def test16_voidp_does_not_outrank_conversion(self): + """Verify that a const void* overload does not shadow a converting one.""" + + import cppjit + + cppjit.cppdef(""" + namespace VoidPPriority { + struct Handle { + void* data; + Handle() : data(nullptr) {} + Handle(void* p) : data(p) {} + }; + struct ConstHandle { + const void* data; + ConstHandle() : data(nullptr) {} + ConstHandle(const void* p) : data(p) {} // declared first on purpose + ConstHandle(Handle h) : data(h.data) {} + }; + Handle make_handle() { return Handle((void*)0xABCD1234); } + bool kept_value(ConstHandle c) { return c.data == (const void*)0xABCD1234; } + }""") + + ns = cppjit.gbl.VoidPPriority + + # taking ConstHandle(const void*) would pass the proxy's address instead + h = ns.make_handle() + assert ns.kept_value(h) + assert ns.kept_value(ns.make_handle()) From 46cde5532b02c824b02b1d624504f6b009573dfa Mon Sep 17 00:00:00 2001 From: Aaron Jomy <75925957+aaronj0@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:18:07 +0200 Subject: [PATCH 3/6] Unify installed layout under cppjit, drop backend (#43) --- CMakeLists.txt | 22 +++++++++++----------- pyproject.toml | 2 +- python/cppjit/__init__.py | 4 ++-- python/cppjit/_cpython_cppjit.py | 6 +++--- python/cppjit_backend/__init__.py | 1 - python/cppjit_backend/_version.py | 1 - 6 files changed, 17 insertions(+), 19 deletions(-) delete mode 100644 python/cppjit_backend/__init__.py delete mode 100644 python/cppjit_backend/_version.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 7158a15..50403dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,7 +101,7 @@ if(_python_platlib) else() set(CPPINTEROP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") endif() -set(CPPINTEROP_INSTALL_DIR "${CPPINTEROP_INSTALL_PREFIX}/cppjit_backend") +set(CPPINTEROP_INSTALL_DIR "${CPPINTEROP_INSTALL_PREFIX}/cppjit/interop") # Include cmake for CppInterOp config and build using ExternalProject. include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddCppInterOp.cmake) @@ -121,11 +121,11 @@ add_dependencies(cppjit CppInterOp) # falling back to the install prefix (see cppinterop_paths()); the clang # major names the versioned compiler probed for the runtime resource dir. target_compile_definitions(cppjit PRIVATE - CPPINTEROP_INSTALL_PREFIX="${CPPINTEROP_INSTALL_PREFIX}" - CPPINTEROP_LIBRARY="cppjit_backend/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}" - CPPINTEROP_INCLUDE_DIR="cppjit_backend/include" + CPPINTEROP_INSTALL_PREFIX="${CPPINTEROP_INSTALL_PREFIX}/cppjit" + CPPINTEROP_LIBRARY="interop/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}" + CPPINTEROP_INCLUDE_DIR="interop/include" CPPJIT_CLANG_MAJOR="${LLVM_VERSION_MAJOR}" - CPPJIT_CLANG_INCLUDE_DIR="cppjit_backend/lib/clang/${LLVM_VERSION_MAJOR}" + CPPJIT_CLANG_INCLUDE_DIR="interop/lib/clang/${LLVM_VERSION_MAJOR}" ) target_include_directories(cppjit PRIVATE @@ -159,21 +159,21 @@ set_target_properties(cppjit PROPERTIES PREFIX "lib" ) -# libcppjit.so is installed at the site-packages root (import libcppjit) +# the extension lives inside the package (import cppjit.libcppjit) install(TARGETS cppjit - LIBRARY DESTINATION . + LIBRARY DESTINATION cppjit ) # install CppInterOp libraries and headers install(CODE " file(GLOB _interop_libs \"${CPPINTEROP_INSTALL_DIR}/lib/libclangCppInterOp*\") foreach(_lib \${_interop_libs}) - file(INSTALL \${_lib} DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit_backend/lib) + file(INSTALL \${_lib} DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit/interop/lib) endforeach() ") install(CODE " - file(INSTALL \"${CPPINTEROP_INSTALL_DIR}/include/\" DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit_backend/include) + file(INSTALL \"${CPPINTEROP_INSTALL_DIR}/include/\" DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit/interop/include) ") # ship the builtin headers of the build clang, laid out as a headers-only @@ -185,7 +185,7 @@ if(NOT EXISTS "${_clang_resource_dir}/include") "${LLVM_DIR} carries no clang resource directory") endif() install(DIRECTORY "${_clang_resource_dir}/include/" - DESTINATION "cppjit_backend/lib/clang/${LLVM_VERSION_MAJOR}/include" + DESTINATION "cppjit/interop/lib/clang/${LLVM_VERSION_MAJOR}/include" ) # the public cpyrt API headers keep their installed cpyrt/ prefix @@ -195,5 +195,5 @@ install(FILES src/cpyrt/DispatchPtr.h src/cpyrt/PyException.h src/cpyrt/Reflex.h - DESTINATION cppjit_backend/include/cpyrt + DESTINATION cppjit/interop/include/cpyrt ) diff --git a/pyproject.toml b/pyproject.toml index 9878475..5308b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ maintainers = [ [tool.scikit-build] wheel.install-dir = "." -wheel.packages = ["python/cppjit", "python/cppjit_backend"] +wheel.packages = ["python/cppjit"] cmake.build-type = "Release" [[tool.dynamic-metadata]] diff --git a/python/cppjit/__init__.py b/python/cppjit/__init__.py index ae87217..6280d6e 100644 --- a/python/cppjit/__init__.py +++ b/python/cppjit/__init__.py @@ -348,10 +348,10 @@ def _setup_include_paths(): if os.path.basename(apipath_extra) == "cpyrt": apipath_extra = os.path.dirname(apipath_extra) else: - spec = importlib.util.find_spec("libcppjit") + spec = importlib.util.find_spec("cppjit.libcppjit") if spec is not None and spec.origin: apipath_extra = os.path.join( - os.path.dirname(spec.origin), "cppjit_backend", "include" + os.path.dirname(spec.origin), "interop", "include" ) if apipath_extra and apipath_extra.lower() != "none": diff --git a/python/cppjit/_cpython_cppjit.py b/python/cppjit/_cpython_cppjit.py index 0b11ec3..50a32f3 100644 --- a/python/cppjit/_cpython_cppjit.py +++ b/python/cppjit/_cpython_cppjit.py @@ -21,9 +21,9 @@ def _preload_backend_library(): # preload the merged extension with ctypes and run LoadCppInterOp() first, # so the interpreter is ready before the extension module initializes - spec = importlib.util.find_spec("libcppjit") + spec = importlib.util.find_spec("cppjit.libcppjit") if spec is None or not spec.origin: - raise ImportError("cannot locate the libcppjit extension module") + raise ImportError("cannot locate the cppjit.libcppjit extension module") lib = ctypes.CDLL(spec.origin, ctypes.RTLD_GLOBAL) if not lib.LoadCppInterOp(): raise RuntimeError("failed to load CppInterOp (LoadCppInterOp returned 0)") @@ -32,7 +32,7 @@ def _preload_backend_library(): _w = _preload_backend_library() -import libcppjit as _backend # noqa: E402 +from . import libcppjit as _backend # noqa: E402 ### template support --------------------------------------------------------- diff --git a/python/cppjit_backend/__init__.py b/python/cppjit_backend/__init__.py deleted file mode 100644 index aab79a8..0000000 --- a/python/cppjit_backend/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._version import __version__ as __version__ diff --git a/python/cppjit_backend/_version.py b/python/cppjit_backend/_version.py deleted file mode 100644 index 3dc1f76..0000000 --- a/python/cppjit_backend/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.0" From 821257bd68848e39ae44d7659bddbd710344bd83 Mon Sep 17 00:00:00 2001 From: Aaron Jomy <75925957+aaronj0@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:24:55 +0200 Subject: [PATCH 4/6] Add single header for interop API/types used by cpyrt (#42) Fixes the long-standing header duplication (previously `Cppyy.h` and `cpp_cppyy.h`) between cpyrt and interop, so that there is only a single source of definitions for the `cppjit::interop` API and types. --- src/cpyrt/CallContext.h | 36 +- src/cpyrt/cppjit_interop.h | 467 ------------------ src/interop/callcontext.h | 21 +- .../{cpp_cppjit.h => cppjit_interop.h} | 15 +- src/interop/interop_wrapper.cxx | 12 +- 5 files changed, 30 insertions(+), 521 deletions(-) delete mode 100644 src/cpyrt/cppjit_interop.h rename src/interop/{cpp_cppjit.h => cppjit_interop.h} (96%) diff --git a/src/cpyrt/CallContext.h b/src/cpyrt/CallContext.h index cf614f1..b52915b 100644 --- a/src/cpyrt/CallContext.h +++ b/src/cpyrt/CallContext.h @@ -12,40 +12,8 @@ namespace cppjit::cpyrt { -// small number that allows use of stack for argument passing -const int SMALL_ARGS_N = 8; - -// convention to pass flag for direct calls (similar to Python's vector calls) -#define DIRECT_CALL ((size_t)1 << (8 * sizeof(size_t) - 1)) - -#ifndef CPYRT_PARAMETER -#define CPYRT_PARAMETER -// general place holder for function parameters -struct Parameter { - union Value { - bool fBool; - int8_t fInt8; - uint8_t fUInt8; - short fShort; - unsigned short fUShort; - int fInt; - unsigned int fUInt; - long fLong; - intptr_t fIntPtr; - unsigned long fULong; - long long fLLong; - unsigned long long fULLong; - int64_t fInt64; - uint64_t fUInt64; - float fFloat; - double fDouble; - long double fLDouble; - void* fVoidp; - } fValue; - void* fRef; - char fTypeCode; -}; -#endif // CPYRT_PARAMETER +// Parameter and the call-ABI constants (SMALL_ARGS_N, DIRECT_CALL) come +// from the interop callcontext.h via cppjit_interop.h // extra call information struct CallContext { diff --git a/src/cpyrt/cppjit_interop.h b/src/cpyrt/cppjit_interop.h deleted file mode 100644 index f2cdb69..0000000 --- a/src/cpyrt/cppjit_interop.h +++ /dev/null @@ -1,467 +0,0 @@ -#ifndef CPYRT_CPPJIT_H -#define CPYRT_CPPJIT_H - -// Standard -#include -#include -#include -#include -#include - -// import/export (after precommondefs.h from PyPy) -#ifdef _MSC_VER -#define CPPJIT_IMPORT extern __declspec(dllimport) -#else -#define CPPJIT_IMPORT extern -#endif - -// some more types; assumes cppjit_interop.h follows Python.h -#ifndef PY_LONG_LONG -#ifdef _WIN32 -typedef __int64 PY_LONG_LONG; -#else -typedef long long PY_LONG_LONG; -#endif -#endif - -#ifndef PY_ULONG_LONG -#ifdef _WIN32 -typedef unsigned __int64 PY_ULONG_LONG; -#else -typedef unsigned long long PY_ULONG_LONG; -#endif -#endif - -#ifndef PY_LONG_DOUBLE -typedef long double PY_LONG_DOUBLE; -#endif - -// FIXME: We should not duplicate these definitions here and in CppInterOp.h -// The current setup relies on finding an identical symbol definition in -// libcppjitbackend.so which is fragile and requires updating both locations -// when changing. Ideally we should have the ability to set/get the template arg -// info provided through some factory methods in CppInterOp API, so the clients -// can rely completely on opaque pointers like we do for the rest of the -// argument types. -struct TemplateArgInfo { - void* m_Type; - const char* m_IntegralValue; - TemplateArgInfo(void* type, const char* integral_value = nullptr) - : m_Type(type), m_IntegralValue(integral_value) {} -}; - -namespace Cpp { -using TemplateArgInfo = ::TemplateArgInfo; - -struct DeclRef { - void* data; - DeclRef() : data(nullptr) {} - DeclRef(void* P) : data(P) {} - DeclRef(decltype(nullptr)) : data(nullptr) {} - explicit operator bool() const { return data != nullptr; } - friend bool operator==(DeclRef a, DeclRef b) { return a.data == b.data; } - friend bool operator!=(DeclRef a, DeclRef b) { return !(a == b); } -}; - -struct TypeRef { - void* data; - TypeRef() : data(nullptr) {} - TypeRef(void* P) : data(P) {} - TypeRef(decltype(nullptr)) : data(nullptr) {} - explicit operator bool() const { return data != nullptr; } - friend bool operator==(TypeRef a, TypeRef b) { return a.data == b.data; } - friend bool operator!=(TypeRef a, TypeRef b) { return !(a == b); } -}; - -struct FuncRef { - void* data; - FuncRef() : data(nullptr) {} - FuncRef(void* P) : data(P) {} - FuncRef(decltype(nullptr)) : data(nullptr) {} - explicit operator bool() const { return data != nullptr; } - friend bool operator==(FuncRef a, FuncRef b) { return a.data == b.data; } - friend bool operator!=(FuncRef a, FuncRef b) { return !(a == b); } -}; - -struct ObjectRef { - void* data; - ObjectRef() : data(nullptr) {} - ObjectRef(void* P) : data(P) {} - ObjectRef(decltype(nullptr)) : data(nullptr) {} - explicit operator bool() const { return data != nullptr; } - friend bool operator==(ObjectRef a, ObjectRef b) { return a.data == b.data; } - friend bool operator!=(ObjectRef a, ObjectRef b) { return !(a == b); } -}; -} // namespace Cpp - -template <> struct std::hash { - std::size_t operator()(const Cpp::DeclRef& obj) const { - return std::hash{}(obj.data); - } -}; -template <> struct std::hash { - std::size_t operator()(const Cpp::TypeRef& obj) const { - return std::hash{}(obj.data); - } -}; -template <> struct std::hash { - std::size_t operator()(const Cpp::FuncRef& obj) const { - return std::hash{}(obj.data); - } -}; -template <> struct std::hash { - std::size_t operator()(const Cpp::ObjectRef& obj) const { - return std::hash{}(obj.data); - } -}; - -namespace cppjit::interop { -typedef Cpp::DeclRef TCppScope_t; -typedef Cpp::TypeRef TCppType_t; -typedef Cpp::ObjectRef TCppObject_t; -typedef Cpp::FuncRef TCppMethod_t; -typedef size_t TCppIndex_t; -typedef void* TCppFuncAddr_t; - -// direct interpreter access ------------------------------------------------- -CPPJIT_IMPORT -bool Compile(const std::string& code, bool silent = false); -CPPJIT_IMPORT -std::string ToString(TCppScope_t klass, TCppObject_t obj); - -// name to opaque C++ scope representation ----------------------------------- -CPPJIT_IMPORT -std::string ResolveName(const std::string& cppitem_name); -CPPJIT_IMPORT -TCppType_t ResolveType(TCppType_t cppitem_name); -CPPJIT_IMPORT -TCppType_t ResolveEnumReferenceType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t ResolveEnumPointerType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t GetRealType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t GetPointerType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t GetReferencedType(TCppType_t type, bool rvalue = false); -CPPJIT_IMPORT -std::string ResolveEnum(TCppScope_t enum_scope); -CPPJIT_IMPORT -bool IsLValueReferenceType(TCppType_t type); -CPPJIT_IMPORT -bool IsRValueReferenceType(TCppType_t type); -CPPJIT_IMPORT -bool IsClassType(TCppType_t type); -CPPJIT_IMPORT -bool IsIntegerType(TCppType_t type, bool* is_signed = nullptr); -CPPJIT_IMPORT -bool IsPointerType(TCppType_t type); -CPPJIT_IMPORT -bool IsFunctionPointerType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t GetType(const std::string& name, bool enable_slow_lookup = false); -CPPJIT_IMPORT -bool AppendTypesSlow(const std::string& name, - std::vector& types, - interop::TCppScope_t parent = nullptr); -CPPJIT_IMPORT -TCppType_t GetComplexType(const std::string& element_type); -CPPJIT_IMPORT -TCppScope_t GetScope(const std::string& scope_name, - TCppScope_t parent_scope = TCppScope_t{}); -CPPJIT_IMPORT -TCppScope_t GetUnderlyingScope(TCppScope_t scope); -CPPJIT_IMPORT -TCppScope_t GetFullScope(const std::string& scope_name); -CPPJIT_IMPORT -TCppScope_t GetTypeScope(TCppScope_t klass); -CPPJIT_IMPORT -TCppScope_t GetNamed(const std::string& scope_name, - TCppScope_t parent_scope = TCppScope_t{}); -CPPJIT_IMPORT -TCppScope_t GetParentScope(TCppScope_t scope); -CPPJIT_IMPORT -TCppScope_t GetScopeFromType(TCppType_t type); -CPPJIT_IMPORT -TCppType_t GetTypeFromScope(TCppScope_t klass); -CPPJIT_IMPORT -TCppScope_t GetGlobalScope(); -CPPJIT_IMPORT -TCppScope_t GetActualClass(TCppScope_t klass, TCppObject_t obj); -CPPJIT_IMPORT -size_t SizeOf(TCppScope_t klass); -CPPJIT_IMPORT -size_t SizeOfType(TCppType_t type); - -CPPJIT_IMPORT -bool IsBuiltin(const std::string& type_name); - -CPPJIT_IMPORT -bool IsBuiltin(TCppType_t type); - -CPPJIT_IMPORT -bool IsComplete(TCppScope_t type); - -// memory management --------------------------------------------------------- -CPPJIT_IMPORT -TCppObject_t Allocate(TCppScope_t scope); -CPPJIT_IMPORT -void Deallocate(TCppScope_t scope, TCppObject_t instance); -CPPJIT_IMPORT -TCppObject_t Construct(TCppScope_t scope, void* arena = nullptr); -CPPJIT_IMPORT -void Destruct(TCppScope_t scope, TCppObject_t instance); - -// method/function dispatching ----------------------------------------------- -CPPJIT_IMPORT -void CallV(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -unsigned char CallB(TCppMethod_t method, TCppObject_t self, size_t nargs, - void* args); -CPPJIT_IMPORT -char CallC(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -short CallH(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -int CallI(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -long CallL(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -PY_LONG_LONG CallLL(TCppMethod_t method, TCppObject_t self, size_t nargs, - void* args); -CPPJIT_IMPORT -float CallF(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -double CallD(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -PY_LONG_DOUBLE CallLD(TCppMethod_t method, TCppObject_t self, size_t nargs, - void* args); - -CPPJIT_IMPORT -void* CallR(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args); -CPPJIT_IMPORT -char* CallS(TCppMethod_t method, TCppObject_t self, size_t nargs, void* args, - size_t* length); -CPPJIT_IMPORT -TCppObject_t CallConstructor(TCppMethod_t method, TCppScope_t klass, - size_t nargs, void* args); -CPPJIT_IMPORT -void CallDestructor(TCppScope_t type, TCppObject_t self); -CPPJIT_IMPORT -TCppObject_t CallO(TCppMethod_t method, TCppObject_t self, size_t nargs, - void* args, TCppType_t result_type); - -CPPJIT_IMPORT -TCppFuncAddr_t GetFunctionAddress(TCppMethod_t method, - bool check_enabled = true); - -// handling of function argument buffer -------------------------------------- -CPPJIT_IMPORT -void* AllocateFunctionArgs(size_t nargs); -CPPJIT_IMPORT -void DeallocateFunctionArgs(void* args); -CPPJIT_IMPORT -size_t GetFunctionArgSizeof(); -CPPJIT_IMPORT -size_t GetFunctionArgTypeoffset(); - -// scope reflection information ---------------------------------------------- -CPPJIT_IMPORT -bool IsNamespace(TCppScope_t scope); -CPPJIT_IMPORT -bool IsClass(TCppScope_t scope); -CPPJIT_IMPORT -bool IsTemplate(TCppScope_t scope); -CPPJIT_IMPORT -bool IsTemplateInstantiation(TCppScope_t scope); -CPPJIT_IMPORT -bool IsTypedefed(TCppScope_t scope); -CPPJIT_IMPORT -bool IsAbstract(TCppScope_t scope); -CPPJIT_IMPORT -bool IsEnumScope(TCppScope_t scope); -CPPJIT_IMPORT -bool IsEnumConstant(TCppScope_t scope); -CPPJIT_IMPORT -bool IsEnumType(TCppType_t type); -CPPJIT_IMPORT -bool IsAggregate(TCppScope_t type); -CPPJIT_IMPORT -bool IsDefaultConstructable(TCppScope_t scope); -CPPJIT_IMPORT -bool IsVariable(TCppScope_t scope); - -CPPJIT_IMPORT -void GetAllCppNames(TCppScope_t scope, std::set& cppnames); - -// namespace reflection information ------------------------------------------ -CPPJIT_IMPORT -std::vector GetUsingNamespaces(TCppScope_t); - -// class reflection information ---------------------------------------------- -CPPJIT_IMPORT -std::string GetFinalName(TCppScope_t type); -CPPJIT_IMPORT -std::string GetScopedFinalName(TCppScope_t type); -CPPJIT_IMPORT -bool HasVirtualDestructor(TCppScope_t type); -CPPJIT_IMPORT -TCppIndex_t GetNumBases(TCppScope_t klass); -CPPJIT_IMPORT -TCppIndex_t GetNumBasesLongestBranch(TCppScope_t klass); -CPPJIT_IMPORT -std::string GetBaseName(TCppScope_t klass, TCppIndex_t ibase); -CPPJIT_IMPORT -TCppScope_t GetBaseScope(TCppScope_t klass, TCppIndex_t ibase); -CPPJIT_IMPORT -bool IsSubclass(TCppScope_t derived, TCppScope_t base); -CPPJIT_IMPORT -bool IsSmartPtr(TCppScope_t klass); -CPPJIT_IMPORT -bool GetSmartPtrInfo(const std::string&, TCppScope_t* raw, TCppMethod_t* deref); -// calculate offsets between declared and actual type, up-cast: direction > 0; -// down-cast: direction < 0 -CPPJIT_IMPORT -ptrdiff_t GetBaseOffset(TCppScope_t derived, TCppScope_t base, - TCppObject_t address, int direction, - bool rerror = false); - -// method/function reflection information ------------------------------------ -CPPJIT_IMPORT -void GetClassMethods(TCppScope_t scope, std::vector& methods); -CPPJIT_IMPORT -std::vector GetMethodsFromName(TCppScope_t scope, - const std::string& name); -CPPJIT_IMPORT -std::string GetName(TCppScope_t); -CPPJIT_IMPORT -std::string GetFullName(TCppScope_t); -CPPJIT_IMPORT -TCppType_t GetMethodReturnType(TCppMethod_t); -CPPJIT_IMPORT -std::string GetMethodReturnTypeAsString(TCppMethod_t); -CPPJIT_IMPORT -TCppIndex_t GetMethodNumArgs(TCppMethod_t); -CPPJIT_IMPORT -TCppIndex_t GetMethodReqArgs(TCppMethod_t); -CPPJIT_IMPORT -std::string GetMethodArgName(TCppMethod_t, TCppIndex_t iarg); -CPPJIT_IMPORT -TCppType_t GetMethodArgType(TCppMethod_t, TCppIndex_t iarg); -CPPJIT_IMPORT -TCppIndex_t CompareMethodArgType(TCppMethod_t, TCppIndex_t iarg, - const std::string& req_type); -CPPJIT_IMPORT -std::string GetMethodArgTypeAsString(TCppMethod_t method, TCppIndex_t iarg); -CPPJIT_IMPORT -std::string GetMethodArgCanonTypeAsString(TCppMethod_t method, - TCppIndex_t iarg); -CPPJIT_IMPORT -std::string GetMethodArgDefault(TCppMethod_t, TCppIndex_t iarg); -CPPJIT_IMPORT -std::string GetMethodSignature(TCppMethod_t, bool show_formal_args, - TCppIndex_t max_args = (TCppIndex_t)-1); -// GetMethodPrototype is unused. -CPPJIT_IMPORT -std::string GetMethodPrototype(TCppMethod_t, bool show_formal_args); -CPPJIT_IMPORT -std::string GetDoxygenComment(TCppScope_t scope, bool strip_markers = true); -CPPJIT_IMPORT -bool IsConstMethod(TCppMethod_t); -// Templated method/function reflection information -// ------------------------------------ -CPPJIT_IMPORT -void GetTemplatedMethods(TCppScope_t scope, std::vector& methods); -CPPJIT_IMPORT -TCppIndex_t GetNumTemplatedMethods(TCppScope_t scope, - bool accept_namespace = false); -CPPJIT_IMPORT -std::string GetTemplatedMethodName(TCppScope_t scope, TCppIndex_t imeth); -CPPJIT_IMPORT -bool ExistsMethodTemplate(TCppScope_t scope, const std::string& name); -CPPJIT_IMPORT -bool IsTemplatedMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsStaticTemplate(TCppScope_t scope, const std::string& name); -CPPJIT_IMPORT -TCppMethod_t GetMethodTemplate(TCppScope_t scope, const std::string& name, - const std::string& proto); -CPPJIT_IMPORT -void GetClassOperators(interop::TCppScope_t klass, const std::string& opname, - std::vector& operators); -CPPJIT_IMPORT -TCppMethod_t GetGlobalOperator(TCppScope_t scope, const std::string& lc, - const std::string& rc, const std::string& op); - -// method properties --------------------------------------------------------- -CPPJIT_IMPORT -bool IsDeletedMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsPublicMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsProtectedMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsPrivateMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsConstructor(TCppMethod_t method); -CPPJIT_IMPORT -bool IsDestructor(TCppMethod_t method); -CPPJIT_IMPORT -bool IsStaticMethod(TCppMethod_t method); -CPPJIT_IMPORT -bool IsExplicit(TCppMethod_t method); - -// data member reflection information ---------------------------------------- -CPPJIT_IMPORT -void GetDatamembers(TCppScope_t scope, std::vector& datamembers); -CPPJIT_IMPORT -bool IsLambdaClass(TCppType_t type); -CPPJIT_IMPORT -TCppScope_t WrapLambdaFromVariable(TCppScope_t var); -CPPJIT_IMPORT -TCppMethod_t AdaptFunctionForLambdaReturn(TCppMethod_t fn); -CPPJIT_IMPORT -TCppType_t GetDatamemberType(TCppScope_t data); -CPPJIT_IMPORT -std::string GetDatamemberTypeAsString(TCppScope_t var); -CPPJIT_IMPORT -std::string GetTypeAsString(TCppType_t type); -CPPJIT_IMPORT -intptr_t GetDatamemberOffset(TCppScope_t var, TCppScope_t klass = nullptr); -CPPJIT_IMPORT -bool CheckDatamember(TCppScope_t scope, const std::string& name); - -// // data member properties -// ---------------------------------------------------- -CPPJIT_IMPORT -bool IsPublicData(TCppScope_t var); -CPPJIT_IMPORT -bool IsProtectedData(TCppScope_t var); -CPPJIT_IMPORT -bool IsPrivateData(TCppScope_t var); -CPPJIT_IMPORT -bool IsStaticDatamember(TCppScope_t var); -CPPJIT_IMPORT -bool IsConstVar(TCppScope_t var); -CPPJIT_IMPORT -TCppMethod_t ReduceReturnType(TCppMethod_t fn, TCppType_t reduce); -CPPJIT_IMPORT -std::vector GetDimensions(TCppType_t type); - -// enum properties ----------------------------------------------------------- -CPPJIT_IMPORT -std::vector GetEnumConstants(TCppScope_t scope); -CPPJIT_IMPORT -TCppType_t GetEnumConstantType(TCppScope_t scope); -CPPJIT_IMPORT -TCppIndex_t GetEnumDataValue(TCppScope_t scope); - -CPPJIT_IMPORT -TCppScope_t InstantiateTemplate(TCppScope_t tmpl, Cpp::TemplateArgInfo* args, - size_t args_size); - -CPPJIT_IMPORT -void DumpScope(TCppScope_t scope); -} // namespace cppjit::interop - -#endif // !CPYRT_CPPJIT_H diff --git a/src/interop/callcontext.h b/src/interop/callcontext.h index d0f04f9..5573dba 100644 --- a/src/interop/callcontext.h +++ b/src/interop/callcontext.h @@ -1,11 +1,23 @@ -#ifndef CPYRT_CALLCONTEXT_H -#define CPYRT_CALLCONTEXT_H +#ifndef CPPJIT_INTEROP_CALLCONTEXT_H +#define CPPJIT_INTEROP_CALLCONTEXT_H // Standard -#include +#include +#include + +// convention to pass flag for direct calls (similar to Python's vector calls) +#define DIRECT_CALL ((size_t)1 << (8 * sizeof(size_t) - 1)) namespace cppjit::cpyrt { +// small number that allows use of stack for argument passing +const int SMALL_ARGS_N = 8; + +// The shipped cpyrt/API.h carries an identical Parameter for JIT-side +// code, which cannot see this in-tree header; the shared CPYRT_PARAMETER +// guard keeps one definition per TU. Keep both copies identical. +#ifndef CPYRT_PARAMETER +#define CPYRT_PARAMETER // general place holder for function parameters struct Parameter { union Value { @@ -31,7 +43,8 @@ struct Parameter { void* fRef; char fTypeCode; }; +#endif // CPYRT_PARAMETER } // namespace cppjit::cpyrt -#endif // !CPYRT_CALLCONTEXT_H +#endif // !CPPJIT_INTEROP_CALLCONTEXT_H diff --git a/src/interop/cpp_cppjit.h b/src/interop/cppjit_interop.h similarity index 96% rename from src/interop/cpp_cppjit.h rename to src/interop/cppjit_interop.h index 5fb4be4..ca7c07e 100644 --- a/src/interop/cpp_cppjit.h +++ b/src/interop/cppjit_interop.h @@ -1,5 +1,5 @@ -#ifndef CPYRT_CPPJIT_H -#define CPYRT_CPPJIT_H +#ifndef CPPJIT_INTEROP_H +#define CPPJIT_INTEROP_H #include #include @@ -36,15 +36,6 @@ typedef unsigned long long PY_ULONG_LONG; typedef long double PY_LONG_DOUBLE; #endif -typedef cppjit::cpyrt::Parameter Parameter; - -// small number that allows use of stack for argument passing -const int SMALL_ARGS_N = 8; - -// convention to pass flag for direct calls (similar to Python's vector calls) -#define DIRECT_CALL ((size_t)1 << (8 * sizeof(size_t) - 1)) -static inline size_t CALL_NARGS(size_t nargs) { return nargs & ~DIRECT_CALL; } - namespace cppjit::interop { typedef Cpp::DeclRef TCppScope_t; typedef Cpp::TypeRef TCppType_t; @@ -402,4 +393,4 @@ RPY_EXPORTED void DumpScope(TCppScope_t scope); } // namespace cppjit::interop -#endif // !CPYRT_CPPJIT_H +#endif // !CPPJIT_INTEROP_H diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 0b71747..cce30fa 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -8,11 +8,15 @@ #include "precommondefs.h" // This defines several system feature macros and should be included before any system header. // Bindings -#include "cpp_cppjit.h" +#include "cppjit_interop.h" using namespace cppjit; #include "callcontext.h" +typedef cppjit::cpyrt::Parameter Parameter; + +static inline size_t CALL_NARGS(size_t nargs) { return nargs & ~DIRECT_CALL; } + #ifndef _WIN32 #include #endif @@ -111,7 +115,7 @@ static InterOpPaths cppinterop_paths() { // The one place libclangCppInterOp is dlopen'd. static bool loadDispatchAPI(const InterOpPaths& Paths) { if (!Cpp::LoadDispatchAPI(Paths.Library.c_str())) { - std::cerr << "[cppjit-backend] Failed to load CppInterOp" << std::endl; + std::cerr << "[cppjit] Failed to load CppInterOp" << std::endl; return false; } return true; @@ -859,8 +863,8 @@ static inline bool WrapperCall(interop::TCppMethod_t method, size_t nargs, InterOpMutex.unlock(); bool runRelease = false; // const auto& fgen = /* is_direct ? faceptr.fDirect : */ faceptr; - if (nargs <= SMALL_ARGS_N) { - void* smallbuf[SMALL_ARGS_N]; + if (nargs <= cpyrt::SMALL_ARGS_N) { + void* smallbuf[cpyrt::SMALL_ARGS_N]; if (nargs) runRelease = copy_args(args, nargs, smallbuf); // CLING_CATCH_UNCAUGHT_ From 103bd5e2173053262d5deb186031934d7635f8d4 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Sat, 22 Aug 2026 01:30:05 +0300 Subject: [PATCH 5/6] [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 --- CMakeLists.txt | 4 +- src/cpyrt/CPPMethod.cxx | 8 ++ src/cpyrt/CPPMethod.h | 3 + src/cpyrt/CPPOverload.cxx | 6 ++ src/cpyrt/PyCallable.h | 3 + src/interop/cppjit_interop.h | 3 + src/interop/interop_wrapper.cxx | 5 + 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 | 24 +++++ test/cpp/memory_analysis.h | 36 +++++++ test/test_memoryanalysis.py | 102 ++++++++++++++++++ 14 files changed, 213 insertions(+), 2 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 50403dc..c4d7902 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,8 @@ include(GNUInstallDirs) # This option won't make a lot of sense since we only ship the shared library in site-packages # 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 "8d624c621a4b95e36ff73ac708c85a768287478f" CACHE STRING "") +set(CPPINTEROP_GIT_REPOSITORY "https://github.com/keremsahn/CppInterOp.git" CACHE STRING "") +set(CPPINTEROP_GIT_TAG "attr-design" 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/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 3fe3e37..61b3c9a 100644 --- a/src/cpyrt/CPPMethod.cxx +++ b/src/cpyrt/CPPMethod.cxx @@ -752,6 +752,14 @@ 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::AllocType attrResult = interop::IsAllocator(GetMethod()); + fAllocType = attrResult; + return attrResult; +} //---------------------------------------------------------------------------- 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 54429a2..09ed617 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,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; interop::TCppFuncAddr_t GetFunctionAddress() override; @@ -116,6 +118,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 49778df..8a5d729 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 4b79ab0..e1238ab 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 interop::TCppFuncAddr_t GetFunctionAddress() = 0; diff --git a/src/interop/cppjit_interop.h b/src/interop/cppjit_interop.h index ca7c07e..d38af5e 100644 --- a/src/interop/cppjit_interop.h +++ b/src/interop/cppjit_interop.h @@ -44,6 +44,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 @@ -297,6 +298,8 @@ RPY_EXPORTED std::string GetDoxygenComment(TCppScope_t scope, bool strip_markers = true); RPY_EXPORTED bool IsConstMethod(TCppMethod_t); +RPY_EXPORTED +AllocType 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 cce30fa..b38ed5a 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1200,6 +1200,11 @@ interop::TCppType_t interop::GetMethodReturnType(TCppMethod_t method) { return Cpp::GetFunctionReturnType(method); } +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 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/MemoryOwnership/MemOwnrship.apinotes b/test/cpp/MemoryOwnership/MemOwnrship.apinotes new file mode 100644 index 0000000..2ebd2d0 --- /dev/null +++ b/test/cpp/MemoryOwnership/MemOwnrship.apinotes @@ -0,0 +1,9 @@ +Name: MemOwnrship +Functions: + - Name: memOwnAllocGlobal + SwiftReturnOwnership: cppAllocNew +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 0000000..d8e9e63 --- /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 0000000..123a620 --- /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 0000000..2dc7f05 --- /dev/null +++ b/test/cpp/memory_analysis.cxx @@ -0,0 +1,24 @@ +#include "memory_analysis.h" + +namespace memory { + +__attribute__((malloc)) memAnalysisKlass* mallocAttr() { + return new memAnalysisKlass; +} + +__attribute__((ownership_returns(malloc))) memAnalysisKlass* +ownershipReturnsAttr() { + return new memAnalysisKlass; +} + +// Expected to not return ownership when analysis is off, and there is just +// attr-check +memAnalysisKlass* noAttr() { return new memAnalysisKlass; } + +memOwn* memOwnAllocGlobal() { return (memOwn*)malloc(sizeof(memOwn)); } + +memOwn* allocDefaultMemOwn() { return new memOwn; } + +memOwn* noAttrAlloc() { return new memOwn; } + +} // namespace memory diff --git a/test/cpp/memory_analysis.h b/test/cpp/memory_analysis.h new file mode 100644 index 0000000..fad0258 --- /dev/null +++ b/test/cpp/memory_analysis.h @@ -0,0 +1,36 @@ +#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(); + +struct memOwn { + int val; + memOwn(int value) : val(value) {} + memOwn() { val = 0; } + // Attribute injected by APINotes + static memOwn* memOwnAllocator(int x) { return new memOwn(x); } +}; + +// Attribute injected by APINotes +memOwn* memOwnAllocGlobal(); + +// Attribute injected by redeclaration +memOwn* allocDefaultMemOwn(); + +// No ownership attribute anywhere +memOwn* noAttrAlloc(); + +} // namespace memory + +#endif // MEMORY_ANALYSIS_H diff --git a/test/test_memoryanalysis.py b/test/test_memoryanalysis.py new file mode 100644 index 0000000..646e708 --- /dev/null +++ b/test/test_memoryanalysis.py @@ -0,0 +1,102 @@ +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__ + + def test02_ownership_returns_attr(self): + import cppjit + + obj = cppjit.gbl.memory.ownershipReturnsAttr() + assert type(obj) == cppjit.gbl.memory.memAnalysisKlass + assert obj.__python_owns__ + + 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 + + obj = cppjit.gbl.memory.allocDefaultMemOwn() + assert type(obj) == cppjit.gbl.memory.memOwn + assert obj.__python_owns__ + + 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 + + +@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.memOwnAllocGlobal() + assert type(obj) == cppjit.gbl.memory.memOwn + assert obj.__python_owns__ From 1c5bac8f4b1227612aa3fb365fa17eabfa0d74d5 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Sun, 30 Aug 2026 21:10:42 +0300 Subject: [PATCH 6/6] Added usage for memory allocation analyzer in the case there is no memory-related attributes in the FunctionDecl, analyzer is disabled by default and set by cppjit.use_alloc_analyzer(True/False) --- python/cppjit/__init__.py | 6 ++++++ src/cpyrt/CPPMethod.cxx | 6 ++++++ src/cpyrt/cpyrtModule.cxx | 17 +++++++++++++++++ src/interop/cppjit_interop.h | 2 ++ src/interop/interop_wrapper.cxx | 5 +++++ test/cpp/memory_analysis.h | 2 ++ test/test_memoryanalysis.py | 25 +++++++++++++++++++++++++ 7 files changed, 63 insertions(+) diff --git a/python/cppjit/__init__.py b/python/cppjit/__init__.py index 6280d6e..b024415 100644 --- a/python/cppjit/__init__.py +++ b/python/cppjit/__init__.py @@ -48,6 +48,7 @@ "add_library_path", # add a path to search for libraries "add_autoload_map", # explicitly include an autoload map "set_debug", # enable/disable debug output + "use_alloc_analyzer", # enable/disable memory ownership analyzer ] import ctypes @@ -397,6 +398,11 @@ def set_debug(enable=True): gbl.Cpp.EnableDebugOutput(enable) +def use_alloc_analyzer(enable=True): + """Enable/disable memory ownership analyzer""" + _backend.UseAllocAnalyzer(enable) + + def _get_name(tt): if isinstance(tt, str): return tt diff --git a/src/cpyrt/CPPMethod.cxx b/src/cpyrt/CPPMethod.cxx index 61b3c9a..1d52adf 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 ------------------------------------------------------------ @@ -757,6 +758,11 @@ interop::AllocType cpyrt::CPPMethod::GetAllocBehaviour() { if (fAllocType.has_value()) return *fAllocType; interop::AllocType attrResult = interop::IsAllocator(GetMethod()); + if (attrResult == interop::AllocType::Unknown && gUseAllocAnalyzer) { + interop::AllocType analyzeResult = interop::GetAllocType(GetMethod()); + fAllocType = analyzeResult; + return analyzeResult; + } fAllocType = attrResult; return attrResult; } diff --git a/src/cpyrt/cpyrtModule.cxx b/src/cpyrt/cpyrtModule.cxx index b2c0733..4b16ffe 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* UseAllocAnalyzer(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*)"UseAllocAnalyzer", (PyCFunction)UseAllocAnalyzer, METH_VARARGS, + (char*)"Enable/disable memory-allocation analyzer."}, {nullptr, nullptr, 0, nullptr}}; struct module_state { diff --git a/src/interop/cppjit_interop.h b/src/interop/cppjit_interop.h index d38af5e..15da55f 100644 --- a/src/interop/cppjit_interop.h +++ b/src/interop/cppjit_interop.h @@ -300,6 +300,8 @@ RPY_EXPORTED bool IsConstMethod(TCppMethod_t); RPY_EXPORTED AllocType 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 b38ed5a..d069e57 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1205,6 +1205,11 @@ interop::AllocType 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 fad0258..77aaf87 100644 --- a/test/cpp/memory_analysis.h +++ b/test/cpp/memory_analysis.h @@ -31,6 +31,8 @@ memOwn* allocDefaultMemOwn(); // No ownership attribute anywhere memOwn* noAttrAlloc(); +inline memAnalysisKlass* allocAnalyzerOn() { return new memAnalysisKlass; } +inline memAnalysisKlass* allocAnalyzerOff() { return new memAnalysisKlass; } } // namespace memory #endif // MEMORY_ANALYSIS_H diff --git a/test/test_memoryanalysis.py b/test/test_memoryanalysis.py index 646e708..4805eab 100644 --- a/test/test_memoryanalysis.py +++ b/test/test_memoryanalysis.py @@ -76,6 +76,31 @@ def test05_redecl_no_attr(self): assert not obj.__python_owns__ obj.__python_owns__ = True + def test06_analyzer_on(self): + import cppjit + + cppjit.use_alloc_analyzer(True) + obj = cppjit.gbl.memory.allocAnalyzerOn() + assert obj.__python_owns__ + + def test07_analyzer_off(self): + import cppjit + + cppjit.use_alloc_analyzer(False) + obj = cppjit.gbl.memory.allocAnalyzerOff() + assert not (obj.__python_owns__) + + def test08_analyzer_off_but_cache(self): + import cppjit + + cppjit.use_alloc_analyzer(True) + obj = cppjit.gbl.memory.allocAnalyzerOn() + assert obj.__python_owns__ + + cppjit.use_alloc_analyzer(False) + obj2 = cppjit.gbl.memory.allocAnalyzerOn() + assert obj2.__python_owns__ + @mark.skipif(not IN_CHILD, reason="needs " + FLAGS) class TestMEMORYANALYSIS_APINOTES: