2015-07-01 2 views
2

Я интегрирую устаревшую библиотеку C++ с Python с использованием boost-python. Унаследованная библиотека имеет некоторую глобальную инициализацию, а затем классы в ней используют данные приложения. Мне нужно убедиться, что функция выключения устаревшей библиотеки вызывается после того, как все обернутые объекты уничтожены, и подумал, что это может быть достигнуто путем регистрации функции выключения с использованием atexit. Тем не менее, я обнаружил, что обернутые объекты очищаются после того, как atexit вызывает функцию выключения, вызывая множественные segfaults в старой библиотеке!Последовательность очистки объекта и функций, вызываемых atexit в модуле Python

Я могу добиться желаемого поведения, вызвав del на обернутых объектах перед выходом, но надеялся оставить удаление на Python. Я проверил красный предупреждающий ящик в documentation of object.__del__, и мне интересно, недоступен ли мой идеальный мир.

Любые предложения по обеспечению метода выключения вызываются после того, как все объекты очищаются при упаковке устаревшего кода в модуле python?

Некоторые детали платформы в случае, если они имеют важное значение:

  • Python 2.7.2
  • Visual Studio 2013
  • 64-разрядное построить

Минимальный код:

#include <iostream> 
#include <boost/python.hpp> 

using namespace std; 

namespace legacy 
{ 
    void initialize() { cout << "legacy::initialize" << endl; } 
    void shutdown() { cout << "legacy::shutdown" << endl; } 

    class Test 
    { 
    public: 
     Test(); 
     virtual ~Test(); 
    }; 

    Test::Test() { } 
    Test::~Test() { cout << "legacy::Test::~Test" << endl; } 
} 

BOOST_PYTHON_MODULE(legacy) 
{ 
    using namespace boost::python; 
    legacy::initialize(); 
    class_<legacy::Test>("Test"); 
    def("_finalize", &legacy::shutdown); 
    object atexit = object(handle<>(PyImport_ImportModule("atexit"))); 
    object finalize = scope().attr("_finalize"); 
    atexit.attr("register")(finalize); 
} 

После компиляции это может быть запустить с помощью Python со следующим входом и выходами отображается:

>>> импорт устаревших
устаревших :: инициализации
>>> тест = legacy.Test()
>>>^Z
наследие :: выключение
наследия :: Тест :: ~ Test

ответ

2

Короче говоря, создать тип охраны, которая будет инициализировать и выключение прежней библиотеки в конструкторе и деструкторе, а затем управлять охранником с помощью смарт-указателя в каждый выставленный объект.


Есть некоторые тонкие детали, которые могут сделать получение процесс разрушения правильно сложен:

  • Порядка уничтожения для объектов и объектов в модулях в Py_Finalize() случаен.
  • Нет окончательной версии модуля. В частности, динамически загруженные модули расширения не разгружаются.
  • Устаревший API должен быть выключен только после уничтожения всех объектов, использующих его. Однако сами объекты могут не знать друг друга.

Для этого объекты Boost.Python должны согласовывать, когда инициализировать и выключать устаревший API. Эти объекты также должны иметь право собственности на устаревший объект, который использует устаревший API. Используя single responsibility principle, можно разделить обязанности на несколько классов.

Можно использовать идиому resource acquisition is initialization (RAII) для инициализации и выключения устаревшей точки доступа. Например, со следующим legacy_api_guard, когда объект legacy_api_guard сконструирован, он инициализирует устаревший API. Когда объект legacy_api_guard разрушен, он отключит устаревший API.

/// @brief Guard that will initialize or shutdown the legacy API. 
struct legacy_api_guard 
{ 
    legacy_api_guard() { legacy::initialize(); } 
    ~legacy_api_guard() { legacy::shutdown(); } 
}; 

В нескольких объектов необходимо будут разделить управление более, когда для инициализации и завершения работы унаследованного API, можно использовать смарт-указатель, например std::shared_ptr, чтобы нести ответственность за управление охраной. В следующем примере лениво инициализирует и завершение работы унаследованного API:

/// @brief Global shared guard for the legacy API. 
std::weak_ptr<legacy_api_guard> legacy_api_guard_; 

/// @brief Get (or create) guard for legacy API. 
std::shared_ptr<legacy_api_guard> get_api_guard() 
{ 
    auto shared = legacy_api_guard_.lock(); 
    if (!shared) 
    { 
    shared = std::make_shared<legacy_api_guard>(); 
    legacy_api_guard_ = shared; 
    } 
    return shared; 
} 

Наконец, фактический тип, который будет встроен в объект Boost.Python нужно, чтобы получить дескриптор охраннику наследство API перед созданием экземпляра наследия объект. Кроме того, после уничтожения устаревший защитник API должен быть выпущен после уничтожения старого объекта. Один неинтрузивный способ сделать это - использовать настраиваемый HeldType при экспонировании устаревших типов Boost.Python. При экспонировании типа, по умолчанию Boost.Python сгенерированного Инициализаторы необходимо подавить, как функция пользовательского завода будет использоваться вместо того, чтобы обеспечить контроль над созданием объекта:

/// @brief legacy_object_holder is a smart pointer that will hold 
///  legacy types and help guarantee the legacy API is initialized 
///  while these objects are alive. This smart pointer will remain 
///  transparent to the legacy library and the user-facing Python. 
template <typename T> 
class legacy_object_holder 
{ 
public: 

    typedef T element_type; 

    template <typename... Args> 
    legacy_object_holder(Args&&... args) 
    : legacy_guard_(::get_api_guard()), 
     ptr_(std::make_shared<T>(std::forward<Args>(args)...)) 
    {} 

    legacy_object_holder(legacy_object_holder& rhs) = default; 

    element_type* get() const { return ptr_.get(); } 

private: 

    // Order of declaration is critical here. The guard should be 
    // allocated first, then the element. This allows for the 
    // element to be destroyed first, followed by the guard. 
    std::shared_ptr<legacy_api_guard> legacy_guard_; 
    std::shared_ptr<element_type> ptr_; 
}; 

/// @brief Helper function used to extract the pointed to object from 
///  an object_holder. Boost.Python will use this through ADL. 
template <typename T> 
T* get_pointer(const legacy_object_holder<T>& holder) 
{ 
    return holder.get(); 
} 

/// Auxiliary function to make exposing legacy objects easier. 
template <typename T, typename ...Args> 
legacy_object_holder<T>* make_legacy_object(Args&&... args) 
{ 
    return new legacy_object_holder<T>(std::forward<Args>(args)...); 
} 

BOOST_PYTHON_MODULE(example) 
{ 
    namespace python = boost::python; 
    python::class_< 
     legacy::Test, legacy_object_holder<legacy::Test>, 
     boost::noncopyable>("Test", python::no_init) 
    .def("__init__", python::make_constructor(
     &make_legacy_object<legacy::Test>)) 
    ; 
} 

Вот полный пример demonstrating с использованием обычай HeldType к ненавязчиво лениво охранять ресурс с общим управлением:

#include <iostream> // std::cout, std::endl 
#include <memory> // std::shared_ptr, std::weak_ptr 
#include <boost/python.hpp> 

/// @brief legacy namespace that cannot be changed. 
namespace legacy { 

void initialize() { std::cout << "legacy::initialize()" << std::endl; } 
void shutdown() { std::cout << "legacy::shutdown()" << std::endl; } 

class Test 
{ 
public: 
    Test()   { std::cout << "legacy::Test::Test()" << std::endl; } 
    virtual ~Test() { std::cout << "legacy::Test::~Test()" << std::endl; } 
}; 

void use_test(Test&) {} 

} // namespace legacy 

namespace { 

/// @brief Guard that will initialize or shutdown the legacy API. 
struct legacy_api_guard 
{ 
    legacy_api_guard() { legacy::initialize(); } 
    ~legacy_api_guard() { legacy::shutdown(); } 
}; 

/// @brief Global shared guard for the legacy API. 
std::weak_ptr<legacy_api_guard> legacy_api_guard_; 

/// @brief Get (or create) guard for legacy API. 
std::shared_ptr<legacy_api_guard> get_api_guard() 
{ 
    auto shared = legacy_api_guard_.lock(); 
    if (!shared) 
    { 
    shared = std::make_shared<legacy_api_guard>(); 
    legacy_api_guard_ = shared; 
    } 
    return shared; 
} 

} // namespace 

/// @brief legacy_object_holder is a smart pointer that will hold 
///  legacy types and help guarantee the legacy API is initialized 
///  while these objects are alive. This smart pointer will remain 
///  transparent to the legacy library and the user-facing Python. 
template <typename T> 
class legacy_object_holder 
{ 
public: 

    typedef T element_type; 

    template <typename... Args> 
    legacy_object_holder(Args&&... args) 
    : legacy_guard_(::get_api_guard()), 
     ptr_(std::make_shared<T>(std::forward<Args>(args)...)) 
    {} 

    legacy_object_holder(legacy_object_holder& rhs) = default; 

    element_type* get() const { return ptr_.get(); } 

private: 

    // Order of declaration is critical here. The guard should be 
    // allocated first, then the element. This allows for the 
    // element to be destroyed first, followed by the guard. 
    std::shared_ptr<legacy_api_guard> legacy_guard_; 
    std::shared_ptr<element_type> ptr_; 
}; 

/// @brief Helper function used to extract the pointed to object from 
///  an object_holder. Boost.Python will use this through ADL. 
template <typename T> 
T* get_pointer(const legacy_object_holder<T>& holder) 
{ 
    return holder.get(); 
} 

/// Auxiliary function to make exposing legacy objects easier. 
template <typename T, typename ...Args> 
legacy_object_holder<T>* make_legacy_object(Args&&... args) 
{ 
    return new legacy_object_holder<T>(std::forward<Args>(args)...); 
} 

// Wrap the legacy::use_test function, passing the managed object. 
void legacy_use_test_wrap(legacy_object_holder<legacy::Test>& holder) 
{ 
    return legacy::use_test(*holder.get()); 
} 

BOOST_PYTHON_MODULE(example) 
{ 
    namespace python = boost::python; 
    python::class_< 
     legacy::Test, legacy_object_holder<legacy::Test>, 
     boost::noncopyable>("Test", python::no_init) 
    .def("__init__", python::make_constructor(
     &make_legacy_object<legacy::Test>)) 
    ; 

    python::def("use_test", &legacy_use_test_wrap); 
} 

Интерактивное использование:

>>> import example 
>>> test1 = example.Test() 
legacy::initialize() 
legacy::Test::Test() 
>>> test2 = example.Test() 
legacy::Test::Test() 
>>> test1 = None 
legacy::Test::~Test() 
>>> example.use_test(test2) 
>>> exit() 
legacy::Test::~Test() 
legacy::shutdown() 

Обратите внимание, что базовый общий подход также применим к нелакому решению, где устаревший API инициализируется при импорте модуля. Один должен был бы использовать shared_ptr вместо weak_ptr и зарегистрировать функцию очистки с atexit.register():

/// @brief Global shared guard for the legacy API. 
std::shared_ptr<legacy_api_guard> legacy_api_guard_; 

/// @brief Get (or create) guard for legacy API. 
std::shared_ptr<legacy_api_guard> get_api_guard() 
{ 
    if (!legacy_api_guard_) 
    { 
    legacy_api_guard_ = std::make_shared<legacy_api_guard>(); 
    } 
    return legacy_api_guard_; 
} 

void release_guard() 
{ 
    legacy_api_guard_.reset(); 
} 

... 

BOOST_PYTHON_MODULE(example) 
{ 
    // Boost.Python may throw an exception, so try/catch around 
    // it to initialize and shutdown legacy API on failure. 
    namespace python = boost::python; 
    try 
    { 
    ::get_api_guard(); // Initialize. 

    ... 

    // Register a cleanup function to run at exit. 
    python::import("atexit").attr("register")(
     python::make_function(&::release_guard) 
    ); 
    } 
    // If an exception is thrown, perform cleanup and re-throw. 
    catch (const python::error_already_set&) 
    { 
    ::release_guard(); 
    throw; 
    } 
} 

См here для демонстрации.

Смежные вопросы