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/7] 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/7] 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/7] 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/7] 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 133da28801fb843be99da42c8a27243f443175a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20=C5=9Eahin?= Date: Sun, 30 Aug 2026 22:20:26 +0300 Subject: [PATCH 5/7] [test] Fix failures with parallel pytest runs (#49) Changed definition of a function to the test it is actually used, and added a missing import --- test/test_doc_features.py | 12 ++++++++---- test/test_lowlevel.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/test_doc_features.py b/test/test_doc_features.py index 9822605..70fa02e 100644 --- a/test/test_doc_features.py +++ b/test/test_doc_features.py @@ -142,10 +142,6 @@ class Abstract2 { return f(i1, i2); } -template -C multiply(A a, B b) { - return static_cast(a * b); -} //----- namespace Namespace { @@ -714,6 +710,14 @@ def test09_templated_function(self): import cppjit + cppjit.cppdef(""" + +template +C multiply(A a, B b) { +return static_cast(a * b); +} + +""") mul = cppjit.gbl.multiply assert "multiply" in cppjit.gbl.__dict__ diff --git a/test/test_lowlevel.py b/test/test_lowlevel.py index 0208a5f..bcc7ac3 100644 --- a/test/test_lowlevel.py +++ b/test/test_lowlevel.py @@ -61,13 +61,14 @@ def test03_memory(self): """Memory allocation and free-ing""" import cppjit + from cppjit import ll # regular C malloc/free mem = cppjit.gbl.malloc(16) cppjit.gbl.free(mem) # typed styles - mem = cppjit.ll.malloc[int](self.N) + mem = ll.malloc[int](self.N) assert len(mem) == self.N assert not mem.__cpp_array__ for i in range(self.N): From 6d89803d34c768f622ca31d255c3b797baf330d3 Mon Sep 17 00:00:00 2001 From: Aaron Jomy <75925957+aaronj0@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:36:42 +0200 Subject: [PATCH 6/7] [build] Lean install: ship single stripped libclangCppInterOp (#50) Drops wheel sizes by about half. Previously the entire install tree of CppInterOp was staged that included duplicate shared libs due to versioning. This is fixed by adding a stripped shared-lib option in CppInterOp, leveraged in this patch. --- CMakeLists.txt | 26 ++++++++++++-------------- cmake/AddCppInterOp.cmake | 10 ++++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50403dc..465d158 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,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 "8d624c621a4b95e36ff73ac708c85a768287478f" CACHE STRING "") +set(CPPINTEROP_GIT_TAG "9802d61921ad5688ae42e4e628d754fc1192244d" CACHE STRING "") set(CPPINTEROP_SOURCE_DIR "" CACHE PATH "Override default CppInterOp built by ExternalProject_Add, with a path to local CppInterOp source") @@ -101,7 +101,10 @@ if(_python_platlib) else() set(CPPINTEROP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") endif() -set(CPPINTEROP_INSTALL_DIR "${CPPINTEROP_INSTALL_PREFIX}/cppjit/interop") + +# CppInterOp installs here; cppjit's own rules ship a subset, so the wheel +# owns every installed file. +set(CPPINTEROP_STAGE_DIR "${CMAKE_BINARY_DIR}/cppinterop-stage") # Include cmake for CppInterOp config and build using ExternalProject. include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddCppInterOp.cmake) @@ -134,7 +137,7 @@ target_include_directories(cppjit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/cpyrt ${CMAKE_CURRENT_SOURCE_DIR}/src/interop - ${CPPINTEROP_INSTALL_DIR}/include + ${CPPINTEROP_STAGE_DIR}/include ${Python_INCLUDE_DIRS} ) @@ -164,17 +167,12 @@ install(TARGETS cppjit 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/interop/lib) - endforeach() -") - -install(CODE " - file(INSTALL \"${CPPINTEROP_INSTALL_DIR}/include/\" DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit/interop/include) -") +install(DIRECTORY "${CPPINTEROP_STAGE_DIR}/lib/" + DESTINATION cppjit/interop/lib +) +install(DIRECTORY "${CPPINTEROP_STAGE_DIR}/include/" + DESTINATION cppjit/interop/include +) # ship the builtin headers of the build clang, laid out as a headers-only # resource dir: only include/ ships diff --git a/cmake/AddCppInterOp.cmake b/cmake/AddCppInterOp.cmake index 50c069e..be0fef2 100644 --- a/cmake/AddCppInterOp.cmake +++ b/cmake/AddCppInterOp.cmake @@ -22,7 +22,9 @@ function(cppjit_add_cppinterop) -DLLVM_DIR=${LLVM_DIR} -DCPPINTEROP_ENABLE_TESTING=${CPPJIT_ENABLE_CPPINTEROP_TESTS} -DBUILD_SHARED_LIBS=ON - -DCMAKE_INSTALL_PREFIX=${CPPINTEROP_INSTALL_DIR} + # The wheel ships a single unversioned library file. + -DCPPINTEROP_SHARED_LIBRARY_VERSIONING=OFF + -DCMAKE_INSTALL_PREFIX=${CPPINTEROP_STAGE_DIR} -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_CXX_STANDARD=17 @@ -85,12 +87,16 @@ function(cppjit_add_cppinterop) set(_log_args "") endif() + # Install only the library and headers, not CppInterOp's full install tree. ExternalProject_Add(CppInterOp ${_source_args} PREFIX "${CMAKE_BINARY_DIR}/CppInterOp" CMAKE_ARGS ${_args} + # -stripped keeps .dynsym, so the dlsym-based dispatch still resolves. + INSTALL_COMMAND ${CMAKE_COMMAND} --build + --target install-clangCppInterOp-stripped install-cppinterop-headers BUILD_BYPRODUCTS - "${CPPINTEROP_INSTALL_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}" + "${CPPINTEROP_STAGE_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}" ${_log_args} ) From e04312953b437b53ca90b7c1884a348fffff5e94 Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Tue, 25 Aug 2026 15:14:24 -0500 Subject: [PATCH 7/7] cpyrt: null-guard the cling.printValue lookup in op_str Commit 5a33027 dropped the faked cling::runtime::gCling and with it the last cling namespace in the interpreter. The pretty-print fallback in op_str then dereferenced the failed cppjit.gbl.cling lookup. str() of any instance without an ostream inserter crashed with SIGSEGV. Guard both lookups, clear the AttributeError, and fall back to the generic repr. clang-repl has no cling namespace to look up, so the whole pretty-print path now compiles out behind CPPJIT_USE_CLING. CMake gains the matching compile definition: the option existed but never reached the sources. The null-guards stay on the cling side. This drops the old escape hatch where a user-declared namespace cling { printValue } was honored on clang-repl, which matches the native value-printing direction in compiler-research/CppInterOp#1100. The regression test runs the repro in a subprocess, so a return of the crash cannot kill the test runner. The subprocess gets this process's sys.path through PYTHONPATH, because a build system can put cppjit on the path in-process instead. A clean exit passes, whichever repr form the backend prints. Output that names the upstream toString stub xfails. Anything else fails and reports the child's output. Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop) --- CMakeLists.txt | 2 ++ src/cpyrt/CPPInstance.cxx | 18 +++++++++---- test/test_regression.py | 57 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 465d158..8fe1c48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,8 @@ target_compile_definitions(cppjit PRIVATE CPPINTEROP_INCLUDE_DIR="interop/include" CPPJIT_CLANG_MAJOR="${LLVM_VERSION_MAJOR}" CPPJIT_CLANG_INCLUDE_DIR="interop/lib/clang/${LLVM_VERSION_MAJOR}" + # cling-only code paths need the flavor at compile time, not just in cmake + $<$:CPPJIT_USE_CLING> ) target_include_directories(cppjit PRIVATE diff --git a/src/cpyrt/CPPInstance.cxx b/src/cpyrt/CPPInstance.cxx index 59cde8a..edd2344 100644 --- a/src/cpyrt/CPPInstance.cxx +++ b/src/cpyrt/CPPInstance.cxx @@ -869,23 +869,30 @@ static PyObject* op_str(CPPInstance* self) { } // 2. Cling's pretty printing (not done through backend for performance - // reasons) + // reasons). Cling only: clang-repl has no cling namespace to look up, so the + // whole path compiles out and str() falls through to the generic repr. +#ifdef CPPJIT_USE_CLING if (!ScopeFlagCheck(self, CPPScope::kNoPrettyPrint)) { static PyObject* printValue = nullptr; if (!printValue) { PyObject* gbl = PyDict_GetItemString(PySys_GetObject((char*)"modules"), "cppjit.gbl"); - PyObject* cl = PyObject_GetAttrString(gbl, (char*)"cling"); - printValue = PyObject_GetAttrString(cl, (char*)"printValue"); - Py_DECREF(cl); + // no cling namespace exists unless user code declares one + PyObject* cl = + gbl ? PyObject_GetAttrString(gbl, (char*)"cling") : nullptr; + printValue = + cl ? PyObject_GetAttrString(cl, (char*)"printValue") : nullptr; + Py_XDECREF(cl); // gbl is borrowed if (printValue) { Py_DECREF(printValue); // make borrowed if (!PyCallable_Check(printValue)) printValue = nullptr; // unusable ... } - if (!printValue) // unlikely + if (!printValue) { + PyErr_Clear(); ScopeFlagSet(self, CPPScope::kNoPrettyPrint); + } } if (printValue) { @@ -929,6 +936,7 @@ static PyObject* op_str(CPPInstance* self) { // if not available/specialized, don't try again ScopeFlagSet(self, CPPScope::kNoPrettyPrint); } +#endif // CPPJIT_USE_CLING // 3. Generic printing as done in op_repr return op_repr(self); diff --git a/test/test_regression.py b/test/test_regression.py index 638fc50..df90a6d 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -1,7 +1,7 @@ import os import sys -from pytest import mark, raises, skip +from pytest import mark, raises, skip, xfail from support import ( IS_CLANG_REPL, IS_CLING, @@ -1637,3 +1637,58 @@ def test51_nontype_enum_template_arg(self): # ...nor leave the interpreter unable to compile a later call wrapper assert ns.probe(41) == 42 + + def test52_str_fallback_without_ostream_insertion(self): + """str() of an instance with no operator<< used to crash. + + With no ``cling`` namespace in the interpreter, the pretty-print + fallback dereferenced the failed ``cppjit.gbl.cling`` lookup and the + process died. A regression is therefore fatal, not an assertion + failure, so run the repro in a subprocess: the runner survives and the + output identifies which failure happened. + """ + + import os + import subprocess + import sys + + repro = """\ +import cppjit + +cppjit.cppdef("namespace StrFallback { struct Bare { int x; }; }") +print(repr(str(cppjit.gbl.StrFallback.Bare()))) +""" + + # A build system can put cppjit on sys.path without PYTHONPATH (bazel + # gives the runner a bootstrap instead), so hand the child this + # process's own path. + env = dict(os.environ) + env["PYTHONPATH"] = os.pathsep.join(p for p in sys.path if p) + + popen = subprocess.Popen( + [sys.executable, "-c", repro], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, _ = popen.communicate() + output = stdout.decode("utf-8", "replace") + + # the guard holds: cling prints the @0xADDR form through printValue and + # ClangRepl falls back to the generic repr, and neither crashes + if popen.returncode == 0: + return + + # Interpreter::toString is an assert(0) stub upstream. str() tries the + # ostream path first, which reaches it whenever assertions are on. + if "toString is not implemented" in output: + xfail( + "toString stub aborts, see compiler-research/CppInterOp#1100: " + "%s" % (output[:300],) + ) + + # a crash banner and its top frames come first, so keep the head + raise AssertionError( + "str() without an ostream inserter did not fall back cleanly: " + "returncode=%s output=%r" % (popen.returncode, output[:2000]) + )