diff --git a/include/pybind11/detail/smart_holder_poc.h b/include/pybind11/detail/smart_holder_poc.h index e82b0b9c8..f594cb257 100644 --- a/include/pybind11/detail/smart_holder_poc.h +++ b/include/pybind11/detail/smart_holder_poc.h @@ -52,8 +52,8 @@ Details: #include //#include -//inline void to_cout(std::string msg) { std::cout << msg << std::endl; } -inline void to_cout(std::string) { } +// inline void to_cout(const std::string &msg) { std::cout << msg << std::endl; } +inline void to_cout(const std::string &) {} // pybindit = Python Bindings Innovation Track. // Currently not in pybind11 namespace to signal that this POC does not depend @@ -66,36 +66,28 @@ inline int shared_from_this_status(...) { return 0; } template #if defined(__cpp_lib_enable_shared_from_this) && (!defined(_MSC_VER) || _MSC_VER >= 1912) inline int shared_from_this_status(const std::enable_shared_from_this *ptr) { - if (ptr->weak_from_this().lock()) return 1; - return -1; + if (ptr->weak_from_this().lock()) + return 1; + return -1; } #else inline int shared_from_this_status(const std::enable_shared_from_this *) { - return 999; + return 999; } #endif -struct smart_holder; - struct guarded_delete { - smart_holder *hld = nullptr; - void (*callback_ptr)(void *) = nullptr; - void *callback_arg = nullptr; - void (*shd_ptr_reset_fptr)(std::shared_ptr&, void *, guarded_delete &&); - void (*del_fptr)(void *); + std::weak_ptr released_ptr; // Trick to keep the smart_holder memory footprint small. + void (*del_ptr)(void *); bool armed_flag; - guarded_delete(void (*shd_ptr_reset_fptr)(std::shared_ptr&, void *, guarded_delete &&), void (*del_fptr)(void *), bool armed_flag) - : shd_ptr_reset_fptr{shd_ptr_reset_fptr}, del_fptr{del_fptr}, armed_flag{armed_flag} { -to_cout("LOOOK guarded_delete ctor " + std::to_string(__LINE__) + " " + __FILE__); - } - void operator()(void *raw_ptr) const; + guarded_delete(void (*del_ptr)(void *), bool armed_flag) + : del_ptr{del_ptr}, armed_flag{armed_flag} {} + void operator()(void *raw_ptr) const { + if (armed_flag) + (*del_ptr)(raw_ptr); + } }; -template -inline void shd_ptr_reset(std::shared_ptr& shd_ptr, void *raw_ptr, guarded_delete &&gdel) { - shd_ptr.reset(static_cast(raw_ptr), gdel); -} - template ::value, int>::type = 0> inline void builtin_delete_if_destructible(void *raw_ptr) { delete static_cast(raw_ptr); @@ -111,7 +103,7 @@ inline void builtin_delete_if_destructible(void *) { template guarded_delete make_guarded_builtin_delete(bool armed_flag) { - return guarded_delete(shd_ptr_reset, builtin_delete_if_destructible, armed_flag); + return guarded_delete(builtin_delete_if_destructible, armed_flag); } template @@ -121,13 +113,7 @@ inline void custom_delete(void *raw_ptr) { template guarded_delete make_guarded_custom_deleter(bool armed_flag) { - return guarded_delete(shd_ptr_reset, custom_delete, armed_flag); -}; - -// Trick to keep the smart_holder memory footprint small. -struct noop_deleter_acting_as_weak_ptr_owner { - std::weak_ptr passenger; - void operator()(void *) {}; + return guarded_delete(custom_delete, armed_flag); }; template @@ -144,7 +130,6 @@ struct smart_holder { bool vptr_is_external_shared_ptr : 1; bool is_populated : 1; bool is_disowned : 1; - bool vptr_is_released : 1; bool pointee_depends_on_holder_owner : 1; // SMART_HOLDER_WIP: See PR #2839. // Design choice: smart_holder is movable but not copyable. @@ -156,7 +141,7 @@ struct smart_holder { smart_holder() : vptr_is_using_noop_deleter{false}, vptr_is_using_builtin_delete{false}, vptr_is_external_shared_ptr{false}, is_populated{false}, is_disowned{false}, - vptr_is_released{false}, pointee_depends_on_holder_owner{false} {} + pointee_depends_on_holder_owner{false} {} bool has_pointee() const { return vptr != nullptr; } @@ -231,17 +216,15 @@ struct smart_holder { } void reset_vptr_deleter_armed_flag(bool armed_flag) const { -to_cout("LOOOK smart_holder reset_vptr_deleter_armed_flag " + std::to_string(__LINE__) + " " + __FILE__); - auto vptr_del_fptr = std::get_deleter(vptr); - if (vptr_del_fptr == nullptr) { + auto vptr_del_ptr = std::get_deleter(vptr); + if (vptr_del_ptr == nullptr) { throw std::runtime_error( "smart_holder::reset_vptr_deleter_armed_flag() called in an invalid context."); } - vptr_del_fptr->armed_flag = armed_flag; + vptr_del_ptr->armed_flag = armed_flag; } static smart_holder from_raw_ptr_unowned(void *raw_ptr) { -to_cout("LOOOK smart_holder from_raw_ptr_unowned " + std::to_string(__LINE__) + " " + __FILE__); smart_holder hld; hld.vptr.reset(raw_ptr, [](void *) {}); hld.vptr_is_using_noop_deleter = true; @@ -251,7 +234,6 @@ to_cout("LOOOK smart_holder from_raw_ptr_unowned " + std::to_string(__LINE__) + template T *as_raw_ptr_unowned() const { -to_cout("LOOOK smart_holder as_raw_ptr_unowned " + std::to_string(__LINE__) + " " + __FILE__); return static_cast(vptr.get()); } @@ -273,8 +255,9 @@ to_cout("LOOOK smart_holder as_raw_ptr_unowned " + std::to_string(__LINE__) + " template static smart_holder from_raw_ptr_take_ownership(T *raw_ptr) { -to_cout(""); -to_cout("LOOOK smart_holder from_raw_ptr_take_ownership " + std::to_string(__LINE__) + " " + __FILE__); + to_cout(""); + to_cout("LOOOK smart_holder from_raw_ptr_take_ownership " + std::to_string(__LINE__) + " " + + __FILE__); ensure_pointee_is_destructible("from_raw_ptr_take_ownership"); smart_holder hld; hld.vptr.reset(static_cast(raw_ptr), make_guarded_builtin_delete(true)); @@ -313,7 +296,8 @@ to_cout("LOOOK smart_holder from_raw_ptr_take_ownership " + std::to_string(__LIN template T *as_raw_ptr_release_ownership(const char *context = "as_raw_ptr_release_ownership") { -to_cout("LOOOK smart_holder as_raw_ptr_release_ownership " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK smart_holder as_raw_ptr_release_ownership " + std::to_string(__LINE__) + " " + + __FILE__); ensure_can_release_ownership(context); T *raw_ptr = as_raw_ptr_unowned(); release_ownership(); @@ -322,7 +306,7 @@ to_cout("LOOOK smart_holder as_raw_ptr_release_ownership " + std::to_string(__LI template static smart_holder from_unique_ptr(std::unique_ptr &&unq_ptr) { -to_cout("LOOOK smart_holder from_unique_ptr " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK smart_holder from_unique_ptr " + std::to_string(__LINE__) + " " + __FILE__); smart_holder hld; hld.rtti_uqp_del = &typeid(D); hld.vptr_is_using_builtin_delete = is_std_default_delete(*hld.rtti_uqp_del); @@ -340,7 +324,7 @@ to_cout("LOOOK smart_holder from_unique_ptr " + std::to_string(__LINE__) + " " + template > std::unique_ptr as_unique_ptr() { -to_cout("LOOOK smart_holder as_unique_ptr " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK smart_holder as_unique_ptr " + std::to_string(__LINE__) + " " + __FILE__); static const char *context = "as_unique_ptr"; ensure_compatible_rtti_uqp_del(context); ensure_use_count_1(context); @@ -351,7 +335,7 @@ to_cout("LOOOK smart_holder as_unique_ptr " + std::to_string(__LINE__) + " " + _ template static smart_holder from_shared_ptr(std::shared_ptr shd_ptr) { -to_cout("LOOOK smart_holder from_shared_ptr " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK smart_holder from_shared_ptr " + std::to_string(__LINE__) + " " + __FILE__); smart_holder hld; hld.vptr = std::static_pointer_cast(shd_ptr); hld.vptr_is_external_shared_ptr = true; @@ -361,27 +345,10 @@ to_cout("LOOOK smart_holder from_shared_ptr " + std::to_string(__LINE__) + " " + template std::shared_ptr as_shared_ptr() const { -to_cout("LOOOK smart_holder as_shared_ptr " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK smart_holder as_shared_ptr " + std::to_string(__LINE__) + " " + __FILE__); return std::static_pointer_cast(vptr); } }; -inline void guarded_delete::operator()(void *raw_ptr) const { - if (hld) { -to_cout("RECLAIM guarded_delete call hld " + std::to_string(__LINE__) + " " + __FILE__); - assert(armed_flag); - assert(hld->vptr.get() == raw_ptr); - assert(hld->vptr_is_released); - (*shd_ptr_reset_fptr)(hld->vptr, hld->vptr.get(), guarded_delete{shd_ptr_reset_fptr, del_fptr, true}); - hld->vptr_is_released = false; - (*callback_ptr)(callback_arg); // Py_DECREF. - } else if (armed_flag) { -to_cout("LOOOK guarded_delete call del " + std::to_string(__LINE__) + " " + __FILE__); - (*del_fptr)(raw_ptr); - } else { -to_cout("LOOOK guarded_delete call noop " + std::to_string(__LINE__) + " " + __FILE__); - } -} - } // namespace memory } // namespace pybindit diff --git a/include/pybind11/detail/smart_holder_type_casters.h b/include/pybind11/detail/smart_holder_type_casters.h index 6ebc06c52..8cc161b34 100644 --- a/include/pybind11/detail/smart_holder_type_casters.h +++ b/include/pybind11/detail/smart_holder_type_casters.h @@ -370,16 +370,11 @@ struct smart_holder_type_caster_load { return *raw_ptr; } - static void dec_ref_void(void *ptr) { -to_cout("LOOOK dec_ref_void Py_REFCNT(" + std::to_string(Py_REFCNT(reinterpret_cast(ptr))) + ") " + std::to_string(__LINE__) + " " + __FILE__); - gil_scoped_acquire gil; - Py_DECREF(reinterpret_cast(ptr)); - } - struct shared_ptr_dec_ref_deleter { PyObject *self; void operator()(void *) { -to_cout("LOOOK shared_ptr_dec_ref_deleter call " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK shared_ptr_dec_ref_deleter call " + std::to_string(__LINE__) + " " + + __FILE__); gil_scoped_acquire gil; Py_DECREF(self); } @@ -399,41 +394,21 @@ to_cout("LOOOK shared_ptr_dec_ref_deleter call " + std::to_string(__LINE__) + " if (hld.pointee_depends_on_holder_owner) { auto vptr_gd_ptr = std::get_deleter(hld.vptr); if (vptr_gd_ptr != nullptr) { - assert(!hld.vptr_is_released); - std::shared_ptr to_be_returned(hld.vptr, type_raw_ptr); - std::shared_ptr non_owning( - hld.vptr.get(), - pybindit::memory::noop_deleter_acting_as_weak_ptr_owner{hld.vptr}); + std::shared_ptr released_ptr = vptr_gd_ptr->released_ptr.lock(); + if (released_ptr) + return std::shared_ptr(released_ptr, type_raw_ptr); auto self = reinterpret_cast(load_impl.loaded_v_h.inst); - // Critical transfer-of-ownership section. This must stay together. - vptr_gd_ptr->hld = &hld; - vptr_gd_ptr->callback_ptr = dec_ref_void; - vptr_gd_ptr->callback_arg = self; - hld.vptr = non_owning; - hld.vptr_is_released = true; Py_INCREF(self); - // Critical section end. -to_cout("FRESHLY released"); - return to_be_returned; + std::shared_ptr to_be_released(type_raw_ptr, shared_ptr_dec_ref_deleter{self}); + vptr_gd_ptr->released_ptr = to_be_released; + return to_be_released; } - auto vptr_ndaawp_ptr = std::get_deleter< - pybindit::memory::noop_deleter_acting_as_weak_ptr_owner>(hld.vptr); - if (vptr_ndaawp_ptr != nullptr) { - assert(hld.vptr_is_released); - auto released_vptr = vptr_ndaawp_ptr->passenger.lock(); - if (released_vptr != nullptr) { -to_cout("retrieved released"); - return std::shared_ptr(released_vptr, type_raw_ptr); - } - } - pybind11_fail( - "smart_holder_type_casters: loaded_as_shared_ptr failure:" - " fatal internal inconsistency."); + pybind11_fail("smart_holder_type_casters: loaded_as_shared_ptr failure: internal " + "inconsistency."); } if (hld.vptr_is_using_noop_deleter) { throw std::runtime_error("Non-owning holder (loaded_as_shared_ptr)."); } - assert(!hld.vptr_is_released); std::shared_ptr void_shd_ptr = hld.template as_shared_ptr(); return std::shared_ptr(void_shd_ptr, type_raw_ptr); } @@ -512,7 +487,8 @@ private: // have_holder() must be true or this function will fail. void throw_if_instance_is_currently_owned_by_shared_ptr() const { - if (holder().vptr_is_released) { + auto vptr_gd_ptr = std::get_deleter(holder().vptr); + if (vptr_gd_ptr != nullptr && !vptr_gd_ptr->released_ptr.expired()) { throw value_error("Python instance is currently owned by a std::shared_ptr."); } } @@ -729,7 +705,8 @@ struct smart_holder_type_caster> : smart_holder_type_caster_l if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { // SMART_HOLDER_WIP: MISSING: Enforcement of consistency with existing smart_holder. // SMART_HOLDER_WIP: MISSING: keep_alive. -to_cout("LOOOK shtc sh return existing_inst " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK shtc sh return existing_inst " + std::to_string(__LINE__) + " " + + __FILE__); return existing_inst; } @@ -745,7 +722,7 @@ to_cout("LOOOK shtc sh return existing_inst " + std::to_string(__LINE__) + " " + if (policy == return_value_policy::reference_internal) keep_alive_impl(inst, parent); -to_cout("LOOOK shtc sh return new inst " + std::to_string(__LINE__) + " " + __FILE__); + to_cout("LOOOK shtc sh return new inst " + std::to_string(__LINE__) + " " + __FILE__); return inst.release(); } diff --git a/tests/test_class_sh_trampoline_shared_from_this.cpp b/tests/test_class_sh_trampoline_shared_from_this.cpp index 3d607dbf5..8d2b800e8 100644 --- a/tests/test_class_sh_trampoline_shared_from_this.cpp +++ b/tests/test_class_sh_trampoline_shared_from_this.cpp @@ -13,7 +13,6 @@ namespace { struct Sft : std::enable_shared_from_this { std::string history; explicit Sft(const std::string &history) : history{history} {} - long use_count() const { return this->shared_from_this().use_count() - 1; } virtual ~Sft() = default; #if defined(__clang__) @@ -43,6 +42,7 @@ struct SftSharedPtrStash { int ser_no; std::vector> stash; explicit SftSharedPtrStash(int ser_no) : ser_no{ser_no} {} + void Clear() { stash.clear(); } void Add(const std::shared_ptr &obj) { if (!obj->history.empty()) { obj->history += "_Stash" + std::to_string(ser_no) + "Add"; @@ -72,11 +72,16 @@ struct SftTrampoline : Sft, py::trampoline_self_life_support { using Sft::Sft; }; +long use_count(const std::shared_ptr &obj) { return obj.use_count(); } + long pass_shared_ptr(const std::shared_ptr &obj) { + to_cout("pass_shared_ptr BEGIN"); auto sft = obj->shared_from_this(); + to_cout("pass_shared_ptr got sft"); if (!sft->history.empty()) { sft->history += "_PassSharedPtr"; } + to_cout("pass_shared_ptr END"); return sft.use_count(); } @@ -90,16 +95,17 @@ PYBIND11_SMART_HOLDER_TYPE_CASTERS(SftSharedPtrStash) TEST_SUBMODULE(class_sh_trampoline_shared_from_this, m) { py::classh(m, "Sft") .def(py::init()) - .def_readonly("history", &Sft::history) - .def("use_count", &Sft::use_count); + .def_readonly("history", &Sft::history); py::classh(m, "SftSharedPtrStash") .def(py::init()) + .def("Clear", &SftSharedPtrStash::Clear) .def("Add", &SftSharedPtrStash::Add) .def("AddSharedFromThis", &SftSharedPtrStash::AddSharedFromThis) .def("history", &SftSharedPtrStash::history) .def("use_count", &SftSharedPtrStash::use_count); + m.def("use_count", use_count); m.def("pass_shared_ptr", pass_shared_ptr); m.def("pass_unique_ptr", pass_unique_ptr); m.def("to_cout", to_cout); diff --git a/tests/test_class_sh_trampoline_shared_from_this.py b/tests/test_class_sh_trampoline_shared_from_this.py index 79b3ace9b..f240a1505 100644 --- a/tests/test_class_sh_trampoline_shared_from_this.py +++ b/tests/test_class_sh_trampoline_shared_from_this.py @@ -8,144 +8,112 @@ class PySft(m.Sft): pass -@pytest.mark.skip("WIP") -def test_release_and_immediate_reclaim(): +def test_release_and_shared_from_this(): obj = PySft("PySft") assert obj.history == "PySft" - assert obj.use_count() == 1 + assert m.use_count(obj) == 1 assert m.pass_shared_ptr(obj) == 2 assert obj.history == "PySft_PassSharedPtr" - assert obj.use_count() == 1 + assert m.use_count(obj) == 1 assert m.pass_shared_ptr(obj) == 2 assert obj.history == "PySft_PassSharedPtr_PassSharedPtr" - assert obj.use_count() == 1 + assert m.use_count(obj) == 1 + +def test_release_and_shared_from_this_leak(): obj = PySft("") while True: m.pass_shared_ptr(obj) assert obj.history == "" - assert obj.use_count() == 1 + assert m.use_count(obj) == 1 break # Comment out for manual leak checking (use `top` command). -@pytest.mark.skip("WIP") -def test_release_to_cpp_stash(): +def test_release_and_stash(): obj = PySft("PySft") stash1 = m.SftSharedPtrStash(1) stash1.Add(obj) - assert obj.history == "PySft_Stash1Add" - assert obj.use_count() == 1 - assert stash1.history(0) == "PySft_Stash1Add" - assert stash1.use_count(0) == 1 # obj does NOT own the shared_ptr anymore. + exp_hist = "PySft_Stash1Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 2 + assert stash1.history(0) == exp_hist + assert stash1.use_count(0) == 1 assert m.pass_shared_ptr(obj) == 3 - assert obj.history == "PySft_Stash1Add_PassSharedPtr" - assert obj.use_count() == 1 - assert stash1.history(0) == "PySft_Stash1Add_PassSharedPtr" + exp_hist += "_PassSharedPtr" + assert obj.history == exp_hist + assert m.use_count(obj) == 2 + assert stash1.history(0) == exp_hist assert stash1.use_count(0) == 1 stash2 = m.SftSharedPtrStash(2) stash2.Add(obj) - assert obj.history == "PySft_Stash1Add_PassSharedPtr_Stash2Add" - assert obj.use_count() == 2 - assert stash2.history(0) == "PySft_Stash1Add_PassSharedPtr_Stash2Add" + exp_hist += "_Stash2Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 3 + assert stash2.history(0) == exp_hist assert stash2.use_count(0) == 2 stash2.Add(obj) - exp_oh = "PySft_Stash1Add_PassSharedPtr_Stash2Add_Stash2Add" - assert obj.history == exp_oh - assert obj.use_count() == 3 - assert stash1.history(0) == exp_oh + exp_hist += "_Stash2Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 4 + assert stash1.history(0) == exp_hist assert stash1.use_count(0) == 3 - assert stash2.history(0) == exp_oh + assert stash2.history(0) == exp_hist assert stash2.use_count(0) == 3 - assert stash2.history(1) == exp_oh + assert stash2.history(1) == exp_hist assert stash2.use_count(1) == 3 del obj - assert stash2.history(0) == exp_oh + assert stash2.history(0) == exp_hist assert stash2.use_count(0) == 3 - assert stash2.history(1) == exp_oh + assert stash2.history(1) == exp_hist assert stash2.use_count(1) == 3 - del stash2 - assert stash1.history(0) == exp_oh + stash2.Clear() + assert stash1.history(0) == exp_hist assert stash1.use_count(0) == 1 -@pytest.mark.skip("WIP") -def test_release_to_cpp_stash_leak(): +def test_release_and_stash_leak(): obj = PySft("") while True: stash1 = m.SftSharedPtrStash(1) stash1.Add(obj) assert obj.history == "" - assert obj.use_count() == 1 + assert m.use_count(obj) == 2 assert stash1.use_count(0) == 1 stash1.Add(obj) assert obj.history == "" - assert obj.use_count() == 2 + assert m.use_count(obj) == 3 assert stash1.use_count(0) == 2 assert stash1.use_count(1) == 2 break # Comment out for manual leak checking (use `top` command). -@pytest.mark.skip("WIP") -def test_release_to_cpp_stash_via_shared_from_this(): +def test_release_and_stash_via_shared_from_this(): obj = PySft("PySft") stash1 = m.SftSharedPtrStash(1) + with pytest.raises(RuntimeError) as exc_info: + stash1.AddSharedFromThis(obj) + assert str(exc_info.value) == "bad_weak_ptr" + stash1.Add(obj) + assert obj.history == "PySft_Stash1Add" + assert stash1.use_count(0) == 1 stash1.AddSharedFromThis(obj) - assert obj.history == "PySft_Stash1AddSharedFromThis" + assert obj.history == "PySft_Stash1Add_Stash1AddSharedFromThis" assert stash1.use_count(0) == 2 - stash1.AddSharedFromThis(obj) - assert obj.history == "PySft_Stash1AddSharedFromThis_Stash1AddSharedFromThis" - assert stash1.use_count(0) == 3 - assert stash1.use_count(1) == 3 + assert stash1.use_count(1) == 2 -@pytest.mark.skip("WIP") -def test_release_to_cpp_stash_via_shared_from_this_leak_1(): # WIP - m.to_cout("") - m.to_cout("") - m.to_cout("Add first") +def test_release_and_stash_via_shared_from_this_leak(): obj = PySft("") - import weakref - - obj_wr = weakref.ref(obj) while True: stash1 = m.SftSharedPtrStash(1) + with pytest.raises(RuntimeError) as exc_info: + stash1.AddSharedFromThis(obj) + assert str(exc_info.value) == "bad_weak_ptr" stash1.Add(obj) assert obj.history == "" - assert obj.use_count() == 1 assert stash1.use_count(0) == 1 stash1.AddSharedFromThis(obj) assert obj.history == "" - assert obj.use_count() == 2 - assert stash1.use_count(0) == 2 - assert stash1.use_count(1) == 2 - del obj - assert obj_wr() is not None - assert stash1.use_count(0) == 2 - assert stash1.use_count(1) == 2 - break # Comment out for manual leak checking (use `top` command). - - -@pytest.mark.skip("WIP") -def test_release_to_cpp_stash_via_shared_from_this_leak_2(): # WIP - m.to_cout("") - m.to_cout("AddSharedFromThis only") - obj = PySft("") - import weakref - - obj_wr = weakref.ref(obj) - while True: - stash1 = m.SftSharedPtrStash(1) - stash1.AddSharedFromThis(obj) - assert obj.history == "" - assert obj.use_count() == 2 - assert stash1.use_count(0) == 2 - stash1.AddSharedFromThis(obj) - assert obj.history == "" - assert obj.use_count() == 3 - assert stash1.use_count(0) == 3 - assert stash1.use_count(1) == 3 - del obj - assert obj_wr() is None # BAD NEEDS FIXING assert stash1.use_count(0) == 2 assert stash1.use_count(1) == 2 break # Comment out for manual leak checking (use `top` command).