Pull to refresh

Обратные вызовы и исключения С++

Reading time7 min
Views7.4K

Введение


Как известно, многие С-библиотеки используют обратные вызовы для обеспечения какого-либо функционала. Так поступает, например, библиотека expat для реализации SAX модели. Обратный вызов или callback используется для возможности выполнить пользовательский код на стороне библиотеки. Пока такой код не несет побочных эффектов — все нормально, но как только на арене появляется С++, все, как всегда, становится нетривиальным.


Итак, код С++ может генерировать исключения, к которым не подготовлен код на С. Ведь функция обратного вызова, выполняется в контексте библиотеки. И если выход из такой функции будет выполнен по генерации исключения, то оно пойдет дальше, разрушая стек.
Очевидное решение, именно то, которое я встречал чаще всего, когда только начал заниматься этим вопросом, состоит в полном отказе от исключений в таких callback`ах. Но это, вестимо, не наш случай.

Случай


Я не зря привел в пример expat. Проблема исключений встала именно при использовании этой библиотеки. Задача состояла в следующем:

  • Есть некий код, который раньше использовал библиотеку xml, написанную на С++ и этот код активно применял исключения;
  • Есть необходимость заменить библиотеку xml на другую без переписывания остального кода.

Я принялся за разработку обертки над expat, которая точь-в-точь повторяла интерфейс прежней библиотеки. Именно в процессе решения этой задачи родилась данная идея.

Идея


Нужно каким-то образом отложить выброс исключения до тех пор, пока выполнение не вернется в С++ код. Единственный способ — где-то сохранить исключение и выбросить его заново в безопасном месте. Но здесь есть ряд сложностей, связанных с реализацией.

Реализация


Класс exception_storage


Для начала нужно реализовать хранилище для брошенных исключений, выглядеть оно будет вот так просто:
class exception_storage
{
public:
  inline ~exception_storage() {}
  inline exception_storage() {}

  void rethrow() const;

  inline void store(clone_base const & exc);
  inline bool thrown() const;
private:
  std::auto_ptr<const clone_base> storedExc_;
};


Класс clone_base — это boost::exception_detail::clone_base очень хорошо подходит для нашей цели.
Реализация тоже не сложна:

inline
void exception_storage::store(clone_base const & exc)
{
  storedExc_.reset(exc.clone());
}
inline
bool exception_storage::thrown() const
{
  return storedExc_.get() != 0;
}
inline
void
exception_storage::rethrow()
{
  class exception_releaser
  {
  public:
    inline
    exception_releaser(std::auto_ptr<const clone_base> & excRef)
      : excRef_(excRef)
    {}
    inline
    ~exception_releaser()
    {
      excRef_.reset();
    }
  private:
    std::auto_ptr<const clone_base> & excRef_;
  };

  if(thrown())
  {
    volatile exception_releaser dummy(storedExc_);
    storedExc_->rethrow();
  }
}


Три метода, store — служит для сохранения текущего исключения с помощью clone_base::clone, rethrow генерирует заново последнее исключение, если оно случалось, thrown — сигнализирует было ли исключение вообще. Функция clone_base::clone — чисто виртуальная, а возможность использовать ее для любых исключений дает boost::enable_current_exception, которая убивает сразу двух зайцев: позволяет непосредственным исключениям вообще не знать о том, что его должны клонировать и сохранить информацию о типе за абстрактным интерфейсом clone_base.
Отдельно хочу поговорит о реализации метода rethrow. В нем вызывается виртуальная функция rethrow (и именно эта функция имеет понятие о том, что именно за исключение мы генерируем). Класс exception_releaser осуществляет очистку памяти сохраненного исключения, при его выбросе. Исключение при генерации копируется, следовательно объект на который указывает storedExc_ становится ненужным. Кроме того это позволило мне сэкономить на булевском флаге для обозначения необходимости генерации.

Фильтр исключений


Самая хитрая штука во всей этой системе, на мой взгляд, это фильтр исключений. В его реализации нам очень поможет замечательная библиотека boost::mpl. Рассмотрим его подробнее.

Очевидно, чтобы записать исключение в хранилище, нужно сначала его поймать. Фильтр призван решить именно эту задачу. Возьмем гипотетический callback:

void callback(void *userData, char const *libText);


Как сделать, чтобы исключения не вылетали за пределы этой функции?
Первое решение — перехватывать все. Но здесь мы натыкаемся на досадное ограничение языка. Нельзя переносимым способом получить в блоке catch(...) информацию о типе исключения. Тут-то мне и пришла мысль, что мы можем сделать это фичей. Дать возможность пользователю библиотеки задавать какие исключения нужно ловить в этих callback`ах.
Начал я с того, что реализовал расширяемый в compile-time фильтр исключений. Выглядит он так:

template <typename List, size_t i = boost::mpl::size&#60List&#62::value - 1>
class exception_filter
{
  typedef typename boost::mpl::at_c<List, i>::type exception_type;
public:
  static inline void try_(exception_storage & storage)
  {
    try
    {
      exception_filter<List, i - 1>::try_(storage);
    }
    catch(exception_type const & e)
    {
      storage.store(boost::enable_current_exception(e));
    }
  }
};
template <typename List>
class exception_filter<List, 0>
{
  typedef typename boost::mpl::at_c<List, 0>::type exception_type;
public:
  static inline void try_(exception_storage & storage)
  {
    try
    {
      throw;
    }
    catch(exception_type const & e)
    {
      storage.store(boost::enable_current_exception(e));
    }
  }
};


Это рекурсивно инстанцирующийся шаблон, который использует boost::mpl::vector в качестве параметра List. В векторе можно будет задать типы исключений, которые необходимо ловить. Шаблон при инстанцировании разворачивается в примерно такое (псевдокод):

try
{
  try
  {
    try
    {
      throw;
    }
    catch(exception_type_1)
    {
    }
  }
  catch(exception_type_2)
  {
  }
}
catch(exception_type_N)
{
}


Где N — это количество типов в mpl::vector.
Если исключение фильтру известно, то оно сохраняется в exception_storage с помощью boost::enable_current_exception. Управляет шаблоном функция smart_filter. Она, в дополнение, ловит все остальные исключения, которые не были заданы в списке типов.

template <typename List>
inline void smart_filter(exception_storage & storage)
{
  try
  {
    exception_filter<List>::try_(storage);
  }
  catch(...)
  {
    storage.store(boost::enable_current_exception(std::runtime_error("unexpected")));
  }
}


Итог


Теперь настало время объединить это в классе-обертке над С-библиотекой. Я покажу его схематично, для демонстрации идеи:

template <typename List>
class MyLib
{
public:
  MyLib()
  {
    pureCLibRegisterCallback(&MyLib::callback);
    pureCLibSetUserData(this);
  }
  virtual void doIt()
  {
    pureCLibDoIt();
    storage_.rethrow();
  }
protected:
  virtual void callback(char const *) = 0;
private:
  static void callback(void * userData, char const * libText)
  {
    if(MyLib<List> * this_ = static_cast<MyLib<List>*>(userData))
    {
      try
      {
        this_->callback(libText);
      }
      catch(...)
      {
        smart_filter<List>(this_->storage_);
      }
    }
  }
  exception_storage storage_;
};


Вот собственно и все. Теперь я приведу небольшой пример использования.

struct MyClass
  : public MyLib <
            boost::mpl::vector
            <
              std::runtime_error,
              std::logic_error,
              std::exception
            >
          >
{
private:
  void callback(char const * text)
  {
    throw std::runtime_error(text);
  }
};
int main()
{
  MyClass my;

  try
  {
    my.doIt();
  }
  catch(std::exception const & e)
  {
    std::cout << e.what() << std::endl;
  }
  return 0;
}


Данная реализация предназначена для однопоточного окружения.

Tags:
Hubs:
+36
Comments36

Articles