C++

индекс
236,98

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

Введение


Как известно, многие С-библиотеки используют обратные вызовы для обеспечения какого-либо функционала. Так поступает, например, библиотека 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<List>::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_ = reinterpret_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;
}


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

+36
30 июля 2010, 20:52
35

комментарии (36)

+14
qehgt #
Я бы в своём проекте такое бы не использовал.

Обработка ошибок должна быть простой и понятной, а здесь не каждый средний программист вообще поймёт, как оно работает.
+1
wander #
Конечно, без крайней необходимости прибегать к такому я бы тоже не стал. Если проектировать систему с нуля, то такие вещи нужно прогнозировать. Однако у меня был уже готовый проект, который нужно было портировать на платформу, где QtXML (именно она и есть та С++ xml-библиотека) не существовало в природе, а был только expat.
+2
stas_agarkov #
боже, спаси меня от си/си++
+3
LIAL #
И что на тебя наслать в таком случае, сын мой?
+2
mikhanoid #
Си, кстати, более приятный язык. Да и Си++ не так страшен, только странный он немного. Но привыкнуть можно, при необходимости.
+1
stas_agarkov #
писал я на си/си++
после явы не хочется возвращаться в эти непроходимые доисторические джунгли
+2
mikhanoid #
А можно поподробнее описать то, какая всё же проблема решается. Что-то совсем не ясно, откуда в коде на Си берутся исключения… А если откуда-то берутся, то какие проблемы с их вылавливанием? Механизм же исключений специально разработан так, чтобы и с библиотеками проблем не было — всякие развёртки стека, всё такое. В чём конкретно проблема?
–1
wander #
Ну, давайте немного упростим ситуацию. Представьте, что вы пишете на языке Си и подключаете к проекту библиотеку написанную на С++. Библиотека эта, предположим, экспортирует функции, помеченные как extern «C», то есть такие функции свободно можно из Си вызывать. Так вот. Теперь представьте, что одна из таких функций бросила исключение. Куда оно попадет? И какие в Си есть возможности для его перехвата?
Пример на Си:
void foo(int (*callback)(char *))
{
char * buffer;
int ret;

buffer = malloc(100000);
ret = callback(buffer);
free(buffer);
}

Если сallback — это функция из С++, то она может бросить исключение. При этом будет утечка памяти, так как buffer не будет освобожден.
–1
AnatolyB #
> Теперь представьте, что одна из таких функций бросила исключение.

А зачем бросила?

Предлагается костыль для кривых библиотек?
0
wander #
> А зачем бросила?
0
wander #
> А зачем бросила?
Предлагается решение, при котором такого вопроса бы не возникало. Исключение не должно покидать пределы С++.

> Предлагается костыль для кривых библиотек?
В C++ исключения — это нормальное явление, new кидает исключение, dynamic_cast кидает искючения, STL кидает исключения. Кривизна библиотеки будет только тогда, когда она позволит исключению попасть в чужую среду.
0
EaS #
Как код на Си узнает, что есть проблемы? Если вызванная им функция вернула заведомо неправильный результат?
0
wander #
Так он и не знает. В этом и проблема. И при исключении функция не возращает никакого результата. Она прерывается альтернативно с раскруткой стека. Как это реализовано конкретно — зависит от компилятора.
0
AnatolyB #
> В C++ исключения — это нормальное явление

Это нормально внутри самой библиотеки, но если библиотека предоставляет интерфейс C/C++, то всплывших исключений просто не должно быть, они все должны быть отловлены в точке входа библиотеки. Если это не так — то это ошибка в библиотеке.
–2
wander #
Значит библиотеки boost или stl по-вашему ошибочны по своей природе, раз позволяют исключениям передаваться за свои границы?
0
mikhanoid #
так boost и stl — это не библиотеки для Си.
+1
wander #
Ну так а я не говорю про С. Я говорю для С++, почему бы не прочитать вопрос habrahabr.ru/blogs/cpp/100623/#comment_3113697, прежде чем дискуссию начинать? :) Я объяснял, что код который вызывается из Си не защищен от С++-исключений. В случае callback`ов в Си библиотеках, это тоже самое, что если бы код на Си вызывал С++ код.

Причем я как раз отметил чем это чревато и почему нельзя такого допускать.

В чем идея? Есть либина XML, к ней есть С++ враппер. С++ враппер С++ библиотека? Да. Почему бы не использовать вместе с ней исключения. Однако в недрах враппера юзаются коллбэки, который выполняются в Си коде. Потому что враппер — над С-XML-либиной. Так как разрешить проблему? СС одной стороны как бы юзер враппера не виноват, он же юзает С++, с другой стороны ему вдруг нельзя использовать исключения. Это решение этой проблемы.
0
AnatolyB #
> Потому что враппер — над С-XML-либиной. Так как разрешить проблему?

Это же очевидно! Не давать прорываться исключениям из коллбека.
Прорыв исключения — это нарушение контракта:

В объектно-ориентированном программировании контракт метода обычно включает следующую информацию:

— возможные типы входных данных и их значение;

— типы возвращаемых данных и их значение;

условия возникновения исключений, их типы и значения;

...
0
wander #
И где противоречие с тем, что я написал?
0
AnatolyB #
Если враппер не оговаривает ограничения на использование исключений, то он просто обязан их отлавливать в своих недрах при вызове коллбека.
0
wander #
Так он это и делает вообще-то. Вы статью читали?
+1
AnatolyB #
Да, вы правы, я невнимательно ознакомился со статьей.

Но я сомневаюсь, что есть смысл протаскивания исходного исключения наружу через С-код. По-моему, это ненужное усложнение.
+1
wander #
Как раз таки не через Си-код. А в обход его. Задача во-первых не допустить попадания в Си-код исключений и нарушения контракта. Во-вторых оставить для пользователя библиотеки возможность их генерировать (в моем случае неизбежно, потому что код уже написан).

Смысл «протаскивания» в том, что обработка исключения должна быть в том месте, где это нужно. Если переносить всю обработку в код враппера — это будет прямое нарушение инкапсуляции. Враппер просто не может знать об особенностях клиентского кода.
0
Speakus #
я Вам завидую если Вы работаете только с библиотеками в которых нет ошибок
0
AnatolyB #
Я не буду пользоваться библиотекой, в которой все так запущено…
0
mikhanoid #
Хорошо. Это понятно, но не понятно, как это всё связано с MyLib. И всё-равно общая идея не очень понятно. То есть, если мне надо написать какой-то callback на Си++ и передать его в код на Си… Ну и хорошо, я напишу нужную функцию, отловлю в ней стандартными методами все исключения интересные…

Или речь о том, чтобы запомнить все эти исключения? Ну. Хорошо. А что мне мешает просто занести их в некий список, зачем все эти шаблоны и хитрый try_?

Я, конечно, туплю в последнее время, может быть это у Вас где-то и написано, но понимание не приходит ко мне сего факта.
0
wander #
Да, основная идея в том, чтобы код, который использует MyLib не вынуждать отказываться от исключений. Чтобы этот код претерпел как можно меньше изменений. Для этого надо их где-то запомнить, а прежде чем запомнить, надо их отфильтровать.

Шаблоны — для удобстве фильтрации. Если в mylib не один callback, а 10, и в каждом callback`е написано нечто вроде:
try
{
this_->callback();
}
catch(exception1)
{
}
catch(exceptionN)
{
}

Особенно если нужно добавить новое исключение, все это превращается в жуткую кашу. А у меня достаточно в одном месте дописать нужный тип в список исключений.
0
Postsantum #
Извиняюсь, если задаю глупый вопрос, но почему вы одновременно заботитесь о переносимости программы («Нельзя переносимым способом получить в блоке catch(...) информацию о типе исключения») и в то же время используете reinterpret_cast, который является непортируемой штукой?
0
wander #
В данном контексте с ним все в порядке. Я сам передал указатель this, скастив его к void*, сам его же и забрал и скастил обратно.
pureCLibSetUserData(this);

//...

MyLib * this_ = reinterpret_cast<MyLib*>(userData);

0
synthesis #
Очевидное решение, именно то, которое я встречал чаще всего, когда только начал заниматься этим вопросом, состоит в полном отказе от исключений в таких callback`ах. Но это, вестимо, не наш случай.


А чем конкретно вас не устроил общепринятый подход, что потребовалось изобретать свой велосипед с не совсем круглыми колесами?
0
wander #
Цитата из статьи:

> Я не зря привел в пример expat. Проблема исключений встала именно при использовании этой
> библиотеки. Задача состояла в следующем:
> * Есть некий код, который раньше использовал библиотеку xml, написанную на С++ и этот код
> активно применял исключения;
> * Есть необходимость заменить библиотеку xml на другую без переписывания остального кода.
0
stas_agarkov #
а почему вы не можете просто перекомпилировать си-библиотеку в си++?
0
wander #
Даже если я это сделаю — она (си-библиотека) же не написана с учетом exception-safety.
0
f0b0s #
да нет вопросов, сейчас только код выкупим…
0
stas_agarkov #
если вы используете проприетарный код, то как вы решаете проблему с отладкой? ведь трудно поверить, что все используемые вами библиотеки не содержат ни единой ошибки. а раз так, то иногда возможность заглянуть в сторонний код оказывается очень кстати.
0
0x656b694d #
Ещё пример задачи, где приходилось использовал похожий приём с рекурсивным шаблоном: интеграция С-Javascript движка (SpiderMonkey) в С++ приложение. Исключения протаскивались через движок из обратных вызовов. Также, выбросы из Javascript-кода преобразовывались в С++ исключения. И обработчик в скрипте мог ловить С++-исключения по желанию. Красота.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.