From bdab6cc575220b4604f532574c178afe0df8ec99 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 26 Jul 2026 14:37:12 +0000 Subject: [PATCH] [cppyy] Avoid JIT-compiling make_unique/make_shared wrappers for builtins std::make_unique/make_shared for a fundamental element type forces the interpreter to JIT a call wrapper whose variadic Args... template pack is mis-desugared on some platforms (macOS with C++ modules) into invalid code such as `make_unique`, breaking e.g. make_unique. Route arithmetic element types through dedicated, non-variadic helpers (__cppyy_internal::make_unique_scalar / make_shared_scalar) that build the smart pointer from a heap-allocated scalar (new T{} / new T(v)), side-stepping the make_unique/make_shared wrapper entirely. The helpers static_assert that the element type is arithmetic, mirroring the Python-side dispatch. Class types keep the existing pure-introspection adopt path, which never instantiated make_unique either. Arithmetic element types are detected by querying std::is_arithmetic through the interpreter, letting C++ be the source of truth: canonical spellings and typedefs (size_t, Int_t, ...) resolve correctly instead of silently yielding a null smart pointer. is_arithmetic (rather than is_fundamental) also excludes void and std::nullptr_t, which cannot back a heap scalar. This also fixes make_unique/make_shared for fundamental spellings other than int/float (e.g. 'double', 'long'), which previously errored. Re-enables the cppyy test15/test16 unit tests that were xfailed on macOS/Windows. Closes #19122 --- .../cppyy/cppyy/python/cppyy/__init__.py | 82 +++++++++++++++++-- .../cppyy/cppyy/test/test_cpp11features.py | 4 +- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/bindings/pyroot/cppyy/cppyy/python/cppyy/__init__.py b/bindings/pyroot/cppyy/cppyy/python/cppyy/__init__.py index c8dd458bfdd70..88154b57e5ab4 100644 --- a/bindings/pyroot/cppyy/cppyy/python/cppyy/__init__.py +++ b/bindings/pyroot/cppyy/cppyy/python/cppyy/__init__.py @@ -145,25 +145,93 @@ def __call__(self, *args): obj = self.cls(*args) return self.ptrcls[self.cls](obj) # C++ takes ownership +# The introspection trick above only works for class types the smart pointer can +# adopt from a Python-owned object. An arithmetic element type (int, double, ...) +# has no such object, so it needs a heap-allocated scalar. We test for *arithmetic* +# rather than *fundamental* types because std::is_fundamental also matches 'void' +# and 'std::nullptr_t', neither of which can back a 'new T{}' scalar. +# +# _py_arithmetic maps the Python numeric builtins onto a C++ spelling by convention +# (Python's int is arbitrary-precision, bool subclasses int); they carry no +# __cpp_name__ to query, so the mapping has to be explicit. +_py_arithmetic = {int: 'int', float: 'float', bool: 'bool'} + +def _as_arithmetic(cls): + """Return the C++ type name if 'cls' denotes an arithmetic type, else None. + + 'cls' may be a Python builtin type (int/float/bool), a string naming a C++ + type, or a cppyy type proxy (e.g. cppyy.gbl.int).""" + name = _py_arithmetic.get(cls) + if name is not None: + return name + name = cls if isinstance(cls, str) else getattr(cls, '__cpp_name__', None) + if name is None: + return None + # Let C++ be the source of truth: std::is_arithmetic resolves canonical + # spellings and typedefs (size_t, Int_t, ...) alike, with no hand-kept list to + # drift out of sync. cppyy caches the trait instantiation itself. + # + # A name that is no resolvable type (e.g. a typo) makes the trait lookup raise; + # treat that as "not arithmetic" so it falls through to the class path, which + # reports the bad name itself instead of leaking std::is_arithmetic<...>. + try: + is_arith = bool(gbl.std.is_arithmetic[name].value) + except Exception: + is_arith = False + return name if is_arith else None + +# Helpers that build a smart pointer to a heap-allocated scalar. They are +# deliberately *non-variadic*: instantiating std::make_unique/make_shared for an +# arithmetic type makes the interpreter JIT a call wrapper whose variadic Args... +# pack is mis-desugared on some platforms (macOS with C++ modules) into invalid +# code like 'make_unique'; these single-argument templates avoid it. +# See https://github.com/root-project/root/issues/19122. +# +# Each body static_asserts T is arithmetic, mirroring the Python-side +# _as_arithmetic() gate (class types take the adopt path instead). The assert is +# in the body, not the signature: an extra template parameter is exactly what +# triggers the mis-desugaring above. +_scalar_makers = {} +def _scalar_maker(name): + maker = _scalar_makers.get(name) + if maker is None: + if not _scalar_makers: + cppdef("""namespace __cppyy_internal { + template std::unique_ptr make_unique_scalar() { static_assert(std::is_arithmetic_v, "scalar maker requires an arithmetic type"); return std::unique_ptr(new T{}); } + template std::unique_ptr make_unique_scalar(T v) { static_assert(std::is_arithmetic_v, "scalar maker requires an arithmetic type"); return std::unique_ptr(new T(v)); } + template std::shared_ptr make_shared_scalar() { static_assert(std::is_arithmetic_v, "scalar maker requires an arithmetic type"); return std::shared_ptr(new T{}); } + template std::shared_ptr make_shared_scalar(T v) { static_assert(std::is_arithmetic_v, "scalar maker requires an arithmetic type"); return std::shared_ptr(new T(v)); } + }""") + maker = getattr(gbl.__cppyy_internal, name) + _scalar_makers[name] = maker + return maker + class make_smartptr(object): - __slots__ = ['ptrcls', 'maker'] - def __init__(self, ptrcls, maker): - self.ptrcls = ptrcls - self.maker = maker + __slots__ = ['ptrcls', 'maker', 'scalarname'] + def __init__(self, ptrcls, maker, scalarname): + self.ptrcls = ptrcls + self.maker = maker + self.scalarname = scalarname def __call__(self, ptr): return py_make_smartptr(type(ptr), self.ptrcls)(ptr) def __getitem__(self, cls): + # Arithmetic element types: build from a heap scalar (see _scalar_maker). + name = _as_arithmetic(cls) + if name is not None: + return _scalar_maker(self.scalarname)[name] + # Class types (cppyy proxies or Python classes, incl. Python-derived): + # construct in Python and let the smart pointer adopt the object. try: if not cls.__module__ == int.__module__: return py_make_smartptr(cls, self.ptrcls) except AttributeError: pass - if isinstance(cls, str) and not cls in ('int', 'float'): + if isinstance(cls, str): return py_make_smartptr(getattr(gbl, cls), self.ptrcls) return self.maker[cls] -gbl.std.make_shared = make_smartptr(gbl.std.shared_ptr, gbl.std.make_shared) -gbl.std.make_unique = make_smartptr(gbl.std.unique_ptr, gbl.std.make_unique) +gbl.std.make_shared = make_smartptr(gbl.std.shared_ptr, gbl.std.make_shared, 'make_shared_scalar') +gbl.std.make_unique = make_smartptr(gbl.std.unique_ptr, gbl.std.make_unique, 'make_unique_scalar') del make_smartptr diff --git a/bindings/pyroot/cppyy/cppyy/test/test_cpp11features.py b/bindings/pyroot/cppyy/cppyy/test/test_cpp11features.py index f659a59437356..f143501bb7f27 100644 --- a/bindings/pyroot/cppyy/cppyy/test/test_cpp11features.py +++ b/bindings/pyroot/cppyy/cppyy/test/test_cpp11features.py @@ -1,6 +1,6 @@ import sys, pytest, os from pytest import mark, raises -from support import setup_make, ispypy, IS_MAC_ARM, IS_WINDOWS +from support import setup_make, ispypy test_dct = "cpp11features_cxx" @@ -438,7 +438,6 @@ def test14_shared_ptr_passing(self): gc.collect() assert TestSmartPtr.s_counter == 0 - @mark.xfail(strict=True, condition=IS_WINDOWS | IS_MAC_ARM, reason='ValueError: Could not find "make_unique"') def test15_unique_ptr_template_deduction(self): """Argument type deduction with std::unique_ptr""" @@ -458,7 +457,6 @@ def test15_unique_ptr_template_deduction(self): with raises(ValueError): # not an RValue cppyy.gbl.UniqueTempl.returnptr[int](uptr_in) - @mark.xfail(strict=True, condition=IS_WINDOWS | IS_MAC_ARM, reason='TypeError: Could not find "make_unique"') def test16_unique_ptr_moves(self): """std::unique_ptr requires moves"""