Introduction
As is known, many C-libraries use callbacks to provide any functionality. This is, for example, the expat library for implementing the SAX model. Callback or callback is used to be able to execute custom code on the side of the library. While this code does not have side effects - everything is fine, but as soon as C ++ appears in the arena, everything, as always, becomes nontrivial.
So, C ++ code can generate exceptions for which C code is not prepared. After all, the callback function is executed in the context of the library. And if the exit from such a function is performed by generating an exception, it will go further, destroying the stack.
The obvious solution, the one that I met most often when I first began to deal with this issue, is to completely reject the exceptions in such callbacks. But this, of course, is not our case.
Happening
I knowingly gave expat an example. The problem of exceptions arose when using this library. The task was as follows:
')
- There is some code that used to use the xml library written in C ++ and this code actively applied exceptions;
- There is a need to replace the xml library with another one without rewriting the rest of the code.
I began to develop a wrapper over expat, which was exactly the same as the interface of the previous library. It was in the process of solving this problem that this idea was born.
Idea
It is necessary to somehow postpone the throwing of the exception until the execution returns to the C ++ code. The only way is to save the exception somewhere and throw it again in a safe place. But there are a number of difficulties associated with the implementation.
Implementation
Class exception_storage
First you need to implement a storage for the thrown exceptions, it will look so simple:
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_;
};
The clone_base class is boost :: exception_detail :: clone_base very well suited for our purpose.
The implementation is also not difficult:
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();
}
}
Three methods, store — serves to save the current exception using clone_base :: clone, rethrow re-generates the last exception, if it happened, thrown indicates whether there was an exception at all. The clone_base :: clone function is purely virtual, and being able to use it for any exceptions gives boost :: enable_current_exception, which kills two birds with one stone: allows direct exceptions not to know that it should be cloned and save information about the type behind the abstract interface clone_base .
Separately, I want to talk about the implementation of the rethrow method. It calls the virtual function rethrow (and it is this function that has the notion of what we are generating for the exception). The class exception_releaser clears the memory of the stored exception when it is thrown. The exception is generated during generation, therefore the object pointed to by the storedExc_ becomes unnecessary. In addition, it allowed me to save on the boolean flag to indicate the need for generation.
Exception filter
The most tricky thing in this whole system, in my opinion, is the exception filter. A great library boost :: mpl will help us a lot in its implementation. Consider it in more detail.
Obviously, to write an exception to the repository, you must first catch it. The filter is designed to solve this particular problem. Take a hypothetical callback:
void callback( void *userData, char const *libText);
How to make exceptions not fly out of this function?
The first solution is to intercept everything. But here we stumble upon an annoying limitation of language. There is no portable way to get information about the type of exception in the catch (...) block. It was then that the idea came to me that we could make it a feature. To enable the library user to set which exceptions to catch in these callbacks.
I started by implementing an exception filter that is extensible to compile-time. It looks like this:
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));
}
}
};
This is a recursively instantiable template that uses boost :: mpl :: vector as the List parameter. In the vector, you can specify the types of exceptions that you want to catch. When instantiated, the template expands to something like this (pseudocode):
try
{
try
{
try
{
throw ;
}
catch (exception_type_1)
{
}
}
catch (exception_type_2)
{
}
}
catch (exception_type_N)
{
}
Where N is the number of types in mpl :: vector.
If the exception is known by the filter, it is stored in exception_storage using boost :: enable_current_exception. The template is controlled by the smart_filter function. In addition, it catches all other exceptions that were not specified in the type list.
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")));
}
}
Total
Now it is time to combine this in a wrapper class above the C library. I will show it schematically, to demonstrate the idea:
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_;
};
That's all. Now I will give a small example of use.
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;
}
This implementation is intended for single-threaded environments.