From 4e7f3a85c1290224cbd3fce518ec121e402e1c6f Mon Sep 17 00:00:00 2001 From: Danijel Zivoi Date: Tue, 18 Aug 2026 08:32:04 +0200 Subject: [PATCH] Make QuantLib::ext::shared_ptr resolvable by SWIG (#351) QuantLib-SWIG's common.i sets SWIG_SHARED_PTR_NAMESPACE to `ext`, and the C++ alias `namespace ext = QuantLib::ext` it depends on sits inside a %{ %} block that the SWIG parser never sees. Declarations spelled `QuantLib::ext::shared_ptr` were therefore registered as a type distinct from `ext::shared_ptr` and wrapped as an opaque pointer rather than a proper proxy. The two spellings are mixed throughout the interface files, so a value produced by one could not be passed to the other. In particular OREApp::getCube() (unqualified) could not be handed to InputParameters::setCube() (qualified), which made in-memory cube reuse unreachable from Python: TypeError: in method 'InputParameters_setCube', argument 2 of type 'QuantLib::ext::shared_ptr< ore::analytics::NPVCube > const &' setMarketCube/getMarketCube were affected identically, along with nine further declarations in orea_app.i alone -- setXvaSensiPricingEngine and its three siblings reject a freshly constructed EngineData on the released 1.8.16.0 wheel, while 1.8.15.0 (where both sides read `ext::shared_ptr`) accepts every one of them. Declare the namespace alias where SWIG can see it, collapsing both spellings onto the same wrapped type. Regenerating oreanalytics.i shows all 80 opaque SWIGTYPE_p_QuantLib__ext__shared_ptrT_* descriptors resolve to their registered ext::shared_ptr counterparts, with no Python symbols added or removed; the Python API surface diff is docstring-only. Verified by building the bindings and running them in a debian trixie container (clang 19, swig 4.3, apt boost): a freshly constructed in-memory cube passes InputParameters::setCube, all four EngineData setters accept a wrapped EngineData, and mismatched argument types are still rejected. Reviving the qualified overloads does surface new SWIG warnings (53 lines, all Warning 509 overload shadowing; 88 -> 140 in total with SWIG 4.4.1), confined to three constructors: DigitalCMSLegData and DigitalCMSSpreadLegData taking a shared_ptr to their underlying leg data, and BasketData taking a vector of shared_ptr constituents. Those constructors were dead under the qualified spelling and %extend value-copy workarounds had been added alongside them; this change brings the native constructors back to life, and they now shadow the workarounds. As a consequence, passing a CMSLegData to DigitalCMSLegData now shares the underlying object with the constructed leg data, as the C++ API does, instead of copying it through the workaround. The accompanying test drives the EngineData setters and the setCube round-trip; the round-trip constructs its cube directly and skips on builds where the in-memory cubes are still wrongly abstract (issue #354). It fails against the 1.8.16.0 wheel. --- ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i | 12 +++++ ORE-SWIG/test/test_cube_reuse.py | 59 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 ORE-SWIG/test/test_cube_reuse.py diff --git a/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i b/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i index 1ad9fe8f3f..68551d83a1 100644 --- a/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i +++ b/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i @@ -23,6 +23,18 @@ #include %} +// SWIG registers its smart-pointer machinery under the bare namespace `ext`: +// QuantLib-SWIG's common.i sets SWIG_SHARED_PTR_NAMESPACE to `ext`, and the C++ +// alias `namespace ext = QuantLib::ext` it relies on lives inside a %{ %} block +// that the SWIG parser never sees. A declaration spelled +// `QuantLib::ext::shared_ptr` is therefore registered as a type distinct from +// `ext::shared_ptr` and is wrapped as an opaque pointer instead of a proper +// proxy, so a value produced by one spelling cannot be passed to the other -- for +// example OREApp::getCube() to InputParameters::setCube(). Declaring the alias +// where SWIG can see it collapses both spellings onto the same wrapped type. +// See https://github.com/OpenSourceRisk/Engine/issues/351 +namespace QuantLib { namespace ext = ::ext; } + #if defined(SWIGPYTHON) %typemap(in) boost::optional %{ diff --git a/ORE-SWIG/test/test_cube_reuse.py b/ORE-SWIG/test/test_cube_reuse.py new file mode 100644 index 0000000000..f41dec35b9 --- /dev/null +++ b/ORE-SWIG/test/test_cube_reuse.py @@ -0,0 +1,59 @@ +"""Regression tests for https://github.com/OpenSourceRisk/Engine/issues/351. + +SWIG registers its smart-pointer machinery under the bare namespace ``ext``, so +a declaration spelled ``QuantLib::ext::shared_ptr`` used to be registered as +a type distinct from ``ext::shared_ptr`` and wrapped as an opaque pointer. +Setters written with the qualified spelling then rejected every value the +bindings could produce -- ``InputParameters.setCube`` could not be handed the +cube returned by ``OREApp.getCube``, which made in-memory cube reuse +unreachable from Python. +""" + +import unittest + +import ORE + + +class QualifiedSharedPtrSetterTest(unittest.TestCase): + """Setters declared with the qualified spelling must accept wrapped objects.""" + + #: Setters that take a qualified ``shared_ptr`` whose argument type can be + #: constructed directly from Python. + ENGINE_DATA_SETTERS = ( + "setXvaSensiPricingEngine", + "setParConversionPricingEngine", + "setParStressPricingEngine", + "setZeroToParShiftPricingEngine", + ) + + def test_engine_data_setters_accept_a_wrapped_engine_data(self) -> None: + """These rejected every EngineData the bindings could produce.""" + params = ORE.InputParameters() + for name in self.ENGINE_DATA_SETTERS: + if not hasattr(params, name): + continue # setter predates this release + with self.subTest(setter=name): + getattr(params, name)(ORE.EngineData()) + + def test_set_cube_accepts_a_wrapped_cube(self) -> None: + """The round-trip issue #351 is about: hand a live cube to setCube. + + Building the cube needs the in-memory cube classes to be concrete + (issue #354); where they are still abstract the round-trip cannot be + exercised and the test is skipped rather than failed. + + setMarketCube is fixed by the same change but stays untested here: + AggregationScenarioData has no constructible subclass in the bindings. + """ + asof = ORE.Date(2, ORE.March, 2026) + dates = [ORE.Date(2, ORE.March, 2027), ORE.Date(2, ORE.March, 2028)] + try: + cube = ORE.DoublePrecisionInMemoryCubeN(asof, {"trade1"}, dates, 4) + except AttributeError: + self.skipTest("in-memory cubes not constructible here (issue #354)") + params = ORE.InputParameters() + params.setCube(cube) + + +if __name__ == "__main__": + unittest.main()