Skip to content

Cppyy interop migration (draft PR for CI)#22810

Draft
guitargeek wants to merge 91 commits into
root-project:masterfrom
guitargeek:cppyy-interop-migration_jonas
Draft

Cppyy interop migration (draft PR for CI)#22810
guitargeek wants to merge 91 commits into
root-project:masterfrom
guitargeek:cppyy-interop-migration_jonas

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

Trying out some commits in the CI.

@guitargeek
guitargeek force-pushed the cppyy-interop-migration_jonas branch from 519ffb7 to 4ac6cd0 Compare July 14, 2026 15:03
@guitargeek guitargeek self-assigned this Jul 14, 2026
@guitargeek
guitargeek force-pushed the cppyy-interop-migration_jonas branch 2 times, most recently from 217c0d5 to 2dc76e4 Compare July 14, 2026 16:08
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results

    20 files      20 suites   2d 22h 27m 59s ⏱️
 3 849 tests  3 849 ✅ 0 💤 0 ❌
67 995 runs  67 995 ✅ 0 💤 0 ❌

Results for commit 3ed1c56.

♻️ This comment has been updated with latest results.

@guitargeek
guitargeek force-pushed the cppyy-interop-migration_jonas branch 15 times, most recently from 80861fa to b640011 Compare July 19, 2026 20:18
aaronj0 and others added 9 commits July 19, 2026 23:46
…stream]

Adapts upstream CppInterOp for use with ROOT & Cling:

- Add SynthesizingCodeRAII guard for scope/class reflection
- Call buildLookup in lookup paths, to populate Cling's lazy lookup tables
- Drop class_name:: scope qualification in make_narg_call so virtual
  dispatch works for pure-virtual methods (TInterpreter::Declare)
- Export CppGetProcAddress from libCling's linker script for dispatch mechanism
- GetTypeAsString: revert the PrintingPolicy ctor to
  PrintingPolicy((LangOptions())) instead of deriving it from the
  ASTContext. Using default LangOptions restores the string form
  (std::basic_string<char>) that the CPyCppyy factory expects
Source: compres forks cppyy-backend/master 597bcf4

Drops ROOT's old clingwrapper in favour of the CppInterOp-based implementation from the compres fork.
Replaces gInterpreter/ROOT-meta TCling API calls with CppInterOp via the dispatch mechanism (cppinterop_dispatch.cxx to load API)
Adds recursive_mutex thread safety, switches execution to JitCall. To be followed with ROOT-specific patches.
GetActualClass probes an object's RTTI to downcast it to the actual
class it points to. Three cases made that probe fail or crash:

- A null object must not be probed at all: return the declared class
  instead of dereferencing it.
- MSVC's type_info::name() returns a human-readable name prefixed with
  the tag kind ("class TWinNTSystem"), not a mangled name, so passing it
  through Cpp::Demangle and looking the result up verbatim always
  failed, silently disabling downcasting on Windows. Strip the tag
  prefix instead of demangling there. Template arguments keep their
  tags, which is harmless as templated names go through type resolution
  rather than plain lookup.
- MSVC's iostream classes use virtual inheritance, which places the
  vbptr - not a vfptr - at offset 0, so probing e.g. std::cout follows a
  garbage pointer: seen on the Windows CI as access violations in
  test_streams test02 (binding std::cout) and test_fragile test20
  (capturing output), with GetActualClass under BindCppObject in the
  stack. Port the filter from the old backend (added there originally
  for a crash on Mac ARM): do not autocast std:: stream classes, which
  is usually unnecessary anyway.
…patch]

CPPINTEROP_DIR is the include-root that contains the CppInterOp/ subdir
with Dispatch.h and the tablegen-generated CppInterOpAPI.inc. Pass it
straight to Cpp::AddIncludePath instead of concatenating "/include",
matching where the build system actually places those headers.
Based on ROOT:

- Load libCling via gSystem->DynamicPathName instead of the fork's
  CPPINTEROP_DIR/lib path
- include ROOT core headers and call TClass::GetClass in GetScope
  for dictionary/module autoloading.
- Guard GetActualClass and GetBaseOffset with Cpp::IsComplete to
  handle incomplete types (e.g. TCling with no public header)
- Initialise ROOT globals and required dylibs in ApplicationStarter
- Move precommondefs.h include into cpp_cppyy.h; use
  Cpp::TCppFuncAddr_t for proper alignment
- Update CMakeLists for ROOT build-tree integration
Dispatch.h transitively includes the tablegen-generated CppInterOpAPI.inc,
which lives in the build tree, not the source tree. ROOT stages CppInterOp's
public headers + .inc files into ${BINARY_DIR}/etc/cppinterop/CppInterOp/
via the CppInterOpEtc custom target. Point cppyy_backend's include dir
and the runtime CPPINTEROP_DIR macro at that staging dir, and depend on
CppInterOpEtc so staging completes before this library compiles.
Source: compres forks cppyy/master 273ed88

- Calls gCling.EnableAutoLoading()
- Adds cppyy.evaluate / cppyy.macro() / cppyy.ll.as_memoryview
- Numba pointer/reference support
- Metaclass naming fix, isinstance() idiom cleanup
- Bug fixes in basic_string / span / npos pythonizations
- Fallback decoding paths for non-UTF-8 compiler output

To be followed with ROOT-specific patches.
guitargeek and others added 29 commits July 19, 2026 23:46
Lookup::Named performs a redeclaration-style lookup
(RedeclarationKind::ForVisibleRedeclaration), for which
Sema::LookupQualifiedName deliberately stops at the queried context
itself: a redeclaration must live in the same scope. Lookup of an
existing member name per [class.qual] however also searches the base
classes. GetNamed therefore missed any member inherited from a base,
e.g. the `type` member typedef of std::tuple_element, which older
libstdc++ (gcc8/alma8) implements by recursive inheritance with `type`
declared in a base of the queried specialization; newer libstdc++
declares it directly in the specialization, hiding the problem. The
cling backend used LookupHelper with full C++ semantics and was not
affected.

Retry the lookup with RedeclarationKind::NotForRedeclaration when the
scope is a class and the direct lookup found nothing, accepting only an
unambiguous result.

Fixes test20_tuple_element of cppyy-test-cpp11features on alma8.
When auto-instantiating a templated method from Python argument types,
a Python complex fell through to the generic __name__-based fallback in
AddTypeName and was typed as "complex", which does not resolve to any
C++ type, so the instantiation failed. Map it to std::complex<double>,
which matches Python's complex (double precision), like Python floats
map to double.

This went unnoticed before C++23 because implicit conversion to e.g.
std::complex<float> went through the concrete converting constructor
complex<float>(const complex<double>&), always present in the overload
set, with the argument handled by the dedicated ComplexDConverter. In
C++23, libstdc++ (P1467) replaces these constructors with a constructor
template, which must first be instantiated from the Python argument
types for the conversion to be viable.

Fixes test01_instance_data_read_access of cppyy-test-datatypes on the
CMAKE_CXX_STANDARD=23 CI configurations (debian13 et al.), where
constructing CO<float> from a Python complex could not instantiate
CO(std::complex<float>)'s implicit argument conversion.
NamedDecl::printNestedNameSpecifier() suppresses inline namespace
qualifiers unconditionally in SuppressInlineNamespaceMode::All, but
TypePrinter::AppendScope() only ever implemented the Redundant
behavior: it always consults isRedundantInlineQualifierFor(), whose
lookup-based heuristic can fail to prove redundancy (e.g. in an
interpreter environment), leaving qualifiers like libc++'s std::__1
in printed type names even in All mode.

Short-circuit the redundancy check in All mode, matching the Decl
printer semantics. This is needed by CppInterOp to produce
platform-independent type names.

This is effectively a backport: LLVM main removed AppendScope() and
routes type scope printing through printNestedNameSpecifier() since
bb1b82af395f ("[clang][TypePrinter] Replace AppendScope with
printNestedNameSpecifier", llvm/llvm-project#168534), first contained
in LLVM 22. Drop this patch when the bundled LLVM is bumped to 22+.
…eam]

cppyy matches the names returned by GetTypeAsString and
Get(Qualified)CompleteName against string keys, e.g. to select the
executor that converts a std::string return value into a Python str
("std::basic_string<char>"). On macOS these matches failed because
clang's printer made the spelling platform-dependent in two ways:

- The default SuppressInlineNamespaceMode::Redundant relies on a
  name-lookup heuristic (isRedundantInlineQualifierFor) that often
  fails in the interpreter environment, so libc++ names kept their
  inline namespace ("std::__1::basic_string<char>"). Reproducible on
  Linux with any user-defined inline namespace.

- libc++ tags basic_string<char> with [[clang::preferred_name(string)]],
  so the canonical type printed as "std::string" instead of
  "std::basic_string<char>".

As a result, std::string(&) return values came back as generic proxies
instead of Python strings, breaking ~20 pyunittest suites on all four
mac CI runs (pyroot-roofit constructorCode/writedoc, rfile-py
GetClassName -> bind_object, pretty-printing "__str__ returned
non-string", ...). Argument passing kept working because the converter
map also registers the "std::string" spelling, which is why only
return values broke.

Make the printed names deterministic: always suppress inline
namespaces (SuppressInlineNamespaceMode::All, honored by the type
printer since the previous commit) and disable preferred-name
substitution, in GetTypeAsString and GetCompleteNameImpl. The
wrapper-codegen path (get_type_as_string) already disabled preferred
names; the API naming paths never got the same treatment. Also route
the qualified-name fallback in GetCompleteNameImpl through the same
policy instead of getQualifiedNameAsString(), which uses the default
policy.
Add an env-guarded (CPPYY_TRACE_CALLS) trace of every JIT function call
made through WrapperCall, printing a sequence number and the callee.
This costs one static bool check when disabled and makes it possible to
correlate wrapper numbers in JIT error messages (e.g. "__jc_7") with the
Python-level calls that triggered them.

Split out of the GetActualClass fixes it was originally committed with,
so that this debugging aid can be reviewed or dropped independently.
QualType::getFromOpaquePtr(nullptr) produces a null QualType, and the
subsequent getAs<TagType>() dereferences it. Return size 0 instead, so
callers can detect that the size is unavailable.
CallO allocates the buffer for a by-value return with
::operator new(SizeOfType(result_type)). If the result type is invalid or
incomplete, the size comes out as 0 and the JIT-compiled wrapper
constructs the returned object into a zero-sized allocation, corrupting
the heap (seen on Windows as an access violation on the first by-value
std::string return, in test_pythonify.py test19_keywords_and_defaults).
Fail the call with a clear message instead.
The Python version of this tutorial sporadically crashes on CI (e.g. on
opensuse16) with memory corruption detected in cling's JIT at interpreter
shutdown, as a segfault in llvm::Value::~Value() /
ReplaceableMetadataImpl::resolveAllUses() while ~IncrementalJIT destroys
the compiled modules. It is the same overlapping-heap-ownership problem
that was worked around for rf619_discrete_profiling.py in 49fb353:
with MALLOC_PERTURB_ poisoning enabled the crash becomes deterministic,
and the crashing reads then find the fresh-malloc fill pattern in live
LLVM metadata, i.e. the allocator handed the same memory to two owners.
An ASan-runtime preload run shows no double free, and disabling the glibc
tcache makes the crash disappear, so the corruption plausibly enters
through a stale write into the tcache header of a freed chunk.

The nll/pll pair used for the likelihood surface in pad 3 is not needed
after that plot. Deleting it right there tears down the NLL evaluation
machinery at a well-defined point instead of at interpreter shutdown,
where the order of cleanups is less controlled. With this change the
previously deterministic poisoned repro (4/4 crashing) passes 3/3, and
plain and ctest invocations pass as well.

This is a workaround at the tutorial level; the underlying overlapping
heap ownership still deserves an AddressSanitizer investigation (note the
CI ASan job currently excludes tutorials).
_find_used_classes() called str() on every variable of the target
namespace to detect class proxies by their string prefix. For a namespace
like cppyy.gbl that also holds bound C++ *instances* (e.g. gInterpreter),
str() of an instance triggers C++ overload resolution for operator<<,
which may attempt candidate implicit conversions via JIT-compiled
constructor calls.

On Windows this made every 'import ROOT' hang for many minutes: the MSVC
STL declares stream operators for its random-engine classes (present in
the AST via TRandomRanlux48), whose seed-sequence template constructor
accepts any class type, so str(gInterpreter) attempted to construct
std::_Swc_base<unsigned long long,5,12,...>(TInterpreter&). That wrapper
never compiles (unresolved _Swc_traits::_Reset<TInterpreter>), compile
failures are not cached, and the scan re-runs for every pythonization
module, adding up to hundreds of ~2 s JIT failures per process
(diagnosed via a temporary Python-stack tracer in CI; the flood made
python tutorials and the rootmv/rootcp/... CLI tools time out and the
whole Windows CI job exceed its time limit).

Skip bound instances entirely and only stringify actual types for the
class-proxy check. This is also a general speedup of pythonization
registration on all platforms.

The loop is restructured into mutually exclusive if/elif branches, one
per kind of value (class proxy, bound instance, template proxy).
Template proxies keep being recognized by their repr: they are not
Python types and cannot be reliably detected with isinstance, since
cppyy.types.Template is the backend proxy class, which the
cppyy.Template wrapper does not derive from.
GetParentScope() returned the raw decl context, so a declaration whose
canonical decl sits inside an `extern "C" / "C++"` linkage spec (or a
C++20 export block) got that block - which is not a NamedDecl - as its
parent scope, surfacing as an "<unnamed>" scope.

This broke all of std:: on Windows: the canonical declaration of
namespace std comes from an `extern "C++"` block in the MSVC CRT headers
(e.g. vcruntime_new.h), so every class proxy created from a type (rather
than through attribute access) was parented as
cppyy.gbl.<unnamed>.std.Klass. That created a second, distinct proxy
family for the same C++ classes, breaking proxy identity and instance
conversions, seen as the test_cpp11features failures on the Windows CI
(test04/test05/test21: shared_ptr<Derived> refusing to convert to
shared_ptr<Base>, smart-pointer downcast returning
"<unnamed>.std.shared_ptr<TestSmartPtr>").

Linkage specs and export blocks are transparent contexts, not scopes:
skip them when determining the parent. Reproduced platform-independently
with a namespace whose first declaration is inside `extern "C++"`.
…[upstream]

make_narg_call decided whether a by-value argument must be moved with a
triviality heuristic (trivial copy constructor present but not simple,
plus a move constructor). Those triviality bits are not reliable across
standard libraries: MSVC's std::unique_ptr has a non-trivial deleted
copy constructor, so the wrapper passed the argument as an lvalue and
failed to compile with "call to deleted constructor" on Windows. Seen
in the Windows CI as pyunittests-tree-ntuple-ntuple-py-basics failures:
every RNTupleWriter::Append/Recreate(std::unique_ptr<RNTupleModel>, ...)
call errored with "nullptr result where temporary expected".

Use the IsCopyConstructorDeleted helper (ported from TClingCallFunc,
already used by make_narg_ctor) which inspects the actual copy
constructor instead, matching the old backend's battle-tested behavior.
On Windows, the Dyld's library scan (used as last resort for symbols
that resolve neither in the JIT nor in the process) walks the search
path, which includes System32. Windows still ships the Visual C++ 6
runtime there (msvcp60.dll), which exports std::basic_string and other
std:: members whose mangled names match modern lookups - with a
pre-C++11, refcounted ABI. Binding JIT'd code to those exports silently
corrupts every object they touch.

Seen on the Windows CI of the CppInterOp migration branch: JIT'd
wrapper code calling outlined MSVC STL string methods (msvcp140.dll
exports almost none of basic_string, it is header-inline) landed in
msvcp60.dll, producing strings with NULL data pointers but positive
sizes (test_crossinheritance test37: "NULL string with positive size"
from call_fs building its result via operator+=), access violations in
string concatenation, streaming and default-argument materialization
(test_pythonify test10/test19, test_regression test18, test_stltypes
test09, test_streams, test_fragile test20, test_templates test16),
and a direct stack trace with msvcp60!basic_string::find under JIT
frames in tutorial-roofit-roostats-StandardHypoTestInvDemo-py.

Permanently ignore msvcp*/msvcr*/msvcirt* in the scan, in both cling's
Dyld and CppInterOp's port of it. The runtimes the process actually
links are already loaded and their exports are found in-process before
this scan is consulted, so this only removes the ABI-incompatible
legacy candidates; genuinely unresolved symbols now fail with a clear
"symbol unresolved" error instead of corrupting memory.

The CppInterOp part is [upstream] material.
GetSmartPtrInfo took the single operator-> returned by Cpp::GetOperator
and read the pointee type from its return type. MSVC's
std::shared_ptr::operator-> is a member template (SFINAE-constrained on
the element type not being an array), so the lookup yields the function
template pattern whose return type is dependent - the pointee scope came
back null and smart-pointer detection failed for every std::shared_ptr
on Windows. That disabled smart-pointer transparency, auto-downcasting
and derived-to-base conversions, seen in the Windows CI as the
test_cpp11features test04/test05/test21, test_pythonization
test04/05/06/10 and test_crossinheritance test11 failures.

Instantiate the operator with Cpp::BestOverloadFunctionMatch (it takes
no arguments; the defaulted template parameter supplies the element
type) before reading the return type, and hand the instantiation to the
caller so it is directly callable. Adds a CppInterOp unit test for the
zero-argument instantiation of such an operator [upstream].
…[upstream]

GetVariableOffset assumed every global's address can be found in a
loaded binary. Two cases break that:

- Inline constexpr variables such as std::nullopt have no symbol in any
  library and failed with "Symbols not found" when deferred-decl
  emission did not fire (cppyy-test-cpp11features test10_optional).
- On Windows, bindexplib deliberately does not export read-only data
  from DLLs, so const globals like test_advancedcpp's
  'extern const char my_global_string2[]' are unreachable from their
  ACLiC library (test21_access_to_global_variables access violations on
  the Windows CI).

Handle them in order of decreasing preference:

- Reproduce the value host-side when the initializer is in the AST:
  evaluate it to an APValue and copy it into a per-decl buffer, like
  the existing getRawData early-return does for integral constants.
  Covers constant-size arrays of integral/enum/floating elements, i.e.
  const char string arrays; arrays of pointers fall through.
- Otherwise force JIT emission when the definition with initializer is
  available. The emitted copy of a constant has the same value;
  writable globals are exported and resolved earlier, so they cannot
  acquire a diverging JIT copy.
- Otherwise ODR-use the variable in an interpreter-evaluated expression
  (&::std::nullopt;) to force CodeGen, mirroring
  TClingDataMemberInfo::Offset. The function-level PushTransactionRAII
  is narrowed to the AST-manipulating sections, since execution inside a
  nested transaction is deferred and the evaluation must run outside it.

Guard the path itself: keep the declaration when VarDecl::getDefinition()
returns null (an extern declaration whose definition lives in a
non-exporting binary has none), only attempt the evaluate fallback when
CodeGen could actually emit the variable, and only call
InstantiateVariableDefinition when getTemplateInstantiationPattern() is
non-null - it dereferences the result, and its assert is compiled out in
release builds. Variables that reach none of the paths now fail with a
clear diagnostic and come out as unavailable in the bindings, matching
the old backend's behavior.

Squashed from five commits whose hunks are strictly nested in one
region; each intermediate state crashed or failed on Windows.
…details

MSVC's std::_Swc_base (the base class of its subtract-with-carry random
engines, for which <random> declares stream operators) has an
unconstrained generator constructor that accepts any lvalue. Whenever a
CPyCppyy overload walk involves such a candidate - e.g. resolving
operator<< during __str__/repr, or iterator protocol comparisons - the
implicit-conversion round tries to construct the engine from the given
argument. Each attempt JIT-compiles a constructor wrapper that fails
(~2 s, and failures are deliberately not cached), so any code path doing
this per call slows down by orders of magnitude: seen on the Windows CI
as 1500 s timeouts of the datatypes, stltypes, doc-features,
rdataframe-asnumpy and pyroot-roofit suites, with thousands of
"Failed to materialize symbols: ... _Swc_base ..." errors in the log.
libstdc++ constrains these constructors, which is why Linux never
triggers the conversion.

Skip implicit conversion to a reserved-name class (leading underscore,
e.g. std::_Swc_base, std::_Array_iterator, __gnu_cxx::...), with one
exception: a conversion between instantiations of the same
implementation-detail template is legitimate and must keep working. On
libc++, vector<T>::begin() returns std::__wrap_iter<char*> while
insert()/erase() take std::__wrap_iter<const char*>, and the
iterator-to-const_iterator conversion goes through __wrap_iter's
converting constructor. Blocking that broke pyroot-pyz-stl-vector
test_stl_vector_iadd and doc-features test10_stl_algorithm on the macOS
CI. MSVC and libstdc++ are unaffected: MSVC's iterator derives from its
const_iterator, so no implicit conversion is needed there.
… [upstream]

GetEnumConstantValue returned size_t and round-tripped values above
INT64_MAX through a string and std::stoul. Both are broken on Windows:

- unsigned long is 32 bit on LLP64, so std::stoul threw
  std::out_of_range on any such value; the exception escaped through
  the bindings and took the process down (Windows CI,
  pyunittests-core-metacling-MetaClingTests test_GH_20925, enum value
  1<<63: uncaught std::_Xout_of_range in Cpp::GetEnumConstantValue via
  CPPEnum_New).
- size_t is 32 bit on ILP32/LLP64, so on the 32-bit Windows CI the same
  enumerator came back truncated to 0.

Return the zero-extended 64-bit bit pattern with APSInt::getZExtValue
as int64_t on all platforms; values above INT64_MAX are exactly the
unsigned 64-bit ones. Propagate the width through
Cppyy::GetEnumDataValue, whose CPyCppyy caller already stores into a
long long.

Squashed from three commits (the first added the std::stoul round-trip
that the second removed as an LLP64 bug) together with the
Cppyy::GetEnumDataValue definition change, which previously sat in an
unrelated commit without its matching declaration.
Fixes a sporadic crash of the
`pyunittests-roottest-python-distrdf-backends-all`
test (reported by CTest as "Subprocess aborted", i.e. the main pytest
process dies from a fatal signal, most often surfacing at
`TestInitialization::test_initialization_method[dask]`).

The problem was that the DistRDF client (the user's main process) drives
ROOT from more than one thread without ROOT thread safety being enabled.
cppyy.cppexec relied solely on the return code of Cpp::Process to
decide whether to raise SyntaxError. That return code does not cover
errors raised while executing a wrapped expression: with cling's
dynamic scopes enabled, a statement like "(doesnotexist)" compiles
successfully and fails at runtime in EvaluateDynamicExpression, which
reports the failure by throwing cling::CompilationException from the
JITed wrapper frame. On Linux the exception happens to propagate and
cppexec's exception handler turns it into SyntaxError, but on Windows
the exception cannot unwind through the JITed frame, nothing reaches
Python, and the return code stays 0 (cppyy test_fragile.py test22).

Check the captured diagnostics as well, the way the reference cppyy
implementation's _cling_report does.
bindexplib deliberately excluded all read-only data from the generated
.def files, so const globals in ACLiC-built libraries could never be
resolved at runtime. The interpreter needs exactly that: cppyy's
advancedcpp test accesses 'extern const char my_global_string2[]'
whose definition lives in the ACLiC library, and with the symbol
unexported the lookup fails and there is no way to reconstruct the
value (the initializer is not in the AST either).

Export external read-only data symbols as DATA, still excluding
compiler-generated ones (vtables ??_7, RTTI ??_R, string literals
??_C, deleting dtors ??_G/??_E, ...) via their ??_ prefix. This only
affects runtime lookup: compile-time clients never linked against
these symbols (without dllimport they could not), so no existing link
can change behavior.
The previous commit exported all external read-only data. That swept
in MSVC's compiler-generated constant pools (__real@<hex>, __xmm@<hex>)
and exception data (_CT*/_TI*), whose names the pre-existing cdecl
'@'-truncation garbles into non-existent symbols such as '_real', so
every ACLiC library containing a floating-point literal failed to link
(LNK2001/LNK1120, 242 CI test failures).

Only export '?'-prefixed (C++-mangled) data symbols: user const
globals are what the interpreter needs to resolve, and every garbled
category starts with '_' instead.
…tream]

GetClassDecls resolves a UsingShadowDecl to its target declaration, which
loses the access of the using-declaration itself. That widened access for
shadows in non-public sections (the target's own access would be reported)
and, conversely, made publicly re-exposed non-public targets look
inaccessible to callers checking IsPublicMethod on the resolved target.

Only forward shadows that are themselves public. A public target then keeps
looking public, and a non-public target re-exposed publicly (e.g. MSVC's
std::shared_ptr does `public: using _Ptr_base<T>::get;` with a protected
base method) is recognizable to callers by its foreign parent scope.

On Windows this restores shared_ptr<T>.get in cppyy, which the enumeration
reported as protected (windows_9 CI diagnostics: `get: pub=0 prot=1`),
breaking the smart-pointer tests and RNTuple's Python entry pythonization.
…tions

BuildScopeProxyDict skips non-public methods since their Cling wrappers
would not compile. However, a non-public method whose declaring class is
not the current scope can only have entered the enumeration through a
public using-declaration (the backend now drops non-public shadows), so it
is publicly callable through this class and its wrapper compiles fine.

This makes MSVC's std::shared_ptr<T>::get available: the target of its
`public: using _Mybase::get;` is the protected std::_Ptr_base<T>::get.
A member operator== defined on a base class is proxied into the base's
dict only and cached in the base's fOperators at its pythonization; the
derived class's dict gets the CPPInstance forwarding __eq__ via the mro,
whose lazy lookup only searches free operators and, if nothing is found,
silently degrades to address identity of the two proxies.

For value classes compared through copies this made == always false. In
particular MSVC's STL iterators define operator== on the const_iterator
base class only (libstdc++ uses free/friend operators, hence Linux never
hit this), so the STL iteration protocol's end-comparison never fired:
std::array iteration ran off the end (garbage reads and access violations
in the datatypes, doc-features and regression suites, 18013 extra
elements in rdataframe-asnumpy) and std::list/std::map iteration looped
forever on the circular node ring (stltypes and roofit timeouts).

Walk the mro for a C++-bound comparison operator cached on a base class
before falling back to the lazy free-operator search: the base class
method is what C++ overload resolution would select anyway. The found
operator is cached on the derived class, in the slot of the operation it
actually implements so the flip logic keeps working.
…tions [upstream]

Cpp::IsSubclass answers whether a Derived object may be passed where a
Base is expected. It historically ignores access specifiers, which
interactive and binding use cases rely on (e.g. cppyy's cross-inheritance
tests pass objects across a private base). Keep that behavior for
distinct classes, but require a public inheritance path when derived and
base are instantiations of the same class template: there, non-public
derivation is a recursive implementation detail, and accepting the
derived instantiation for its base instantiation silently reinterprets
the object.

Seen on the Windows CI (cppyy-test-stltypes TestSTLTUPLE test01): MSVC
implements std::tuple by deriving privately from the tuple of its tail
elements, so a cached std::get<0>(std::tuple<int,std::string>&) overload
accepted a std::tuple<double,int,std::string> argument as its own tail
and read the wrong element (1 instead of 7.). libstdc++ and libc++ do
not use user-visible recursive tuple inheritance, hence Linux and macOS
never hit this.

Also map distinct decl handles of the same class to true explicitly, as
Sema::IsDerivedFrom accepts identical types while the base-path walk
does not.
…or operators

FindBinaryOperator probed the namespace of the left operand's class
before the global scope and short-circuited on the first match. On
Windows, MSVC's rvalue-stream-inserter template

    template<class _Ostr, class _Ty> _Ostr&& operator<<(_Ostr&&, const _Ty&)

takes a forwarding reference and therefore matches any (ostream, T)
combination, so the std stage matched it for classes whose operator<< is
defined at global scope, shadowing the exact user overload (advancedcpp
test25: repr fell back to the address form). libstdc++'s equivalent
inserter takes a concrete basic_ostream&&, which is not viable for an
lvalue stream, hence the stage order never mattered on Linux.

Search the global scope first: a user operator for these exact types at
global scope is what C++ overload resolution would select over a generic
template found through ADL. For classes in std (the given/std scope
stage) the std namespace is still searched first, so STL operator
lookups are unchanged.
Running TPython from an interpreted macro requires code JIT-compiled in
the cling session to resolve the
CppInternal::DispatchRaw::MakeFunctionCallable_interp dispatch slot. On
Windows that fails: the slot is a data symbol (a function-pointer
variable), which the bindexplib-generated export tables do not cover, so
the IncrementalExecutor reports it unresolved and the test fails
deterministically.

Properly exporting writable data across a DLL boundary (or better,
registering the dispatch slots by value in the JIT instead of relying on
symbol lookup, an upstream CppInterOp change) is not worth blocking the
migration for this niche path. Disable the test on MSVC following the
existing precedent in this file, behind the usual win_broken_tests
escape hatch.
…T-patch]

Four rounds of expectation changes to the vendored cppyy test suite,
made while bringing up the CppInterOp-based bindings.

Markers removed because the tests now pass: a large set of xfails that
CppInterOp fixes outright (advancedcpp, eigen, lowlevel, numba,
operators, regression, stltypes, templates), including three
xfail(run=False) crash markers in test_concurrent; and the ROOT-side
Windows xfails for datatypes test39_aggregates and
test42_mixed_complex_arithmetic, doc-features
test03_use_of_ctypes_and_enum and stltypes test09_string_as_str_bytes,
which are stable on the Windows CI across all five ctest
--rerun-failed retries of the windows_11 run and whose strict markers
would otherwise turn into XPASS suite failures.

Markers added:

- test_numba test13_std_vector_dot_product is skipped outright, as it
  compares execution times and is therefore sporadic.
- Five tests are marked xfail(run=False) because they do not merely
  fail but take the process down, which would abort the whole suite:
  three in test_numba hit the cling assertion "Transaction.cpp:98 ...
  Must not nest within unloading transaction", and two in test_stltypes
  (TestSTLVECTOR, TestSTLPAIR) segfault.

Those five are open regressions of the new bindings rather than
long-standing breakage: they are disabled to keep the suite runnable
and still need to be fixed.
… bit

The fit JIT-compiles its formula through cling on top of an already
large Python + ROOT process, and on the win32 CI the JIT runs out of
32-bit address space during TH1::Fit:

    LLVM ERROR: out of memory
    Buffer allocation failed
    error code: Exit code 0xc0000409

The baseline does not fail, so this is a memory-footprint effect of the
larger per-method JIT wrapper volume of the CppInterOp-based bindings,
which the 32-bit address space no longer absorbs for this tutorial.
Veto it on MSVC x86 following the existing 32-bit veto precedents,
behind the usual win_broken_tests escape hatch.
…nversion

SmartPtrConverter::SetArg gated the implicit conversion of an ordinary
object to a smart pointer on ctxt->fFlags, but the Python-visible toggle
cppyy.SetImplicitSmartPointerConversion() writes kImplicitSmartPtrConversion
into CallContext::GlobalPolicyFlags(), a different word. Nothing ever ORs
the global policy flags into a call context - mp_vectorcall seeds fFlags
only from kReleaseGIL, kProtected, kIsConstructor, kCallDirect/kFromDescr,
kNoImplicit and kAllowImplicit - so the bit was never observed, the toggle
was inert and the conversion branch was unreachable.

Check both words through a new AllowImplicitSmartPtrConversion() helper,
as CPPMethod::Execute already does for kProtected, so the flag keeps
working both as a global policy and as a per-call request. The helper
tolerates a null context, which SmartPtrConverter::SetArg permits.

This predates the CppInterOp migration: master has the same mismatch
(Converters.cxx reads ctxt->fFlags while CPyCppyyModule.cxx toggles the
global word), so the fix applies there independently of this branch.

Verified by printing both conditions from the converter: with the toggle
enabled the old expression stays 0 while the new one becomes 1 and the
branch is entered; with it disabled both are 0. Passing an already
constructed smart pointer is unaffected.
@guitargeek
guitargeek force-pushed the cppyy-interop-migration_jonas branch from b640011 to 3ed1c56 Compare July 19, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants