- Abstract
- Distribution
- Feedback / support
- Portability
- Tutorial
- Synopsis
- Reference
emit
connect
release
translate
connection
pconnection
set_user_data
get_user_data
emitter
receiver
block
blocker
emitter
thread_local_queue
- Programming techniques
- Case study: synapsifying GLFW
- Alternatives to Synapse
- Macros and configuration
- Building and installation
- Building the unit tests and the examples
- Q&A
- Acknowledgements
Synapse is a non-intrusive C++ signal programming library.
It is able to register connections and emit signals from any C or C++ object of any type whatsoever. If two contexts share access to a C or C++ object, they can use that object as a randezvouz point of sorts, and communicate with each other through Synapse signals.
A system of meta signals provides interoperability with other signal programming libraries and callback APIs. In particular Synapse can be used to safely register C++ function objects, including lambda functions, with any C-style callback API that is designed to work only with C function pointers.
In a multi-thread environment signals can be emitted asynchronously and scheduled for synchronous execution when requested in other threads.
Synapse is distributed under the Boost Software License, Version 1.0.
The source code is available in this GitHub repository.
Note
|
Synapse is not part of Boost. |
Please use the Boost Developers mailing list.
Synapse requires compiler support for the following C++11 features:
-
thread-safe dynamic initialization of local static objects;
-
thread_local
objects; -
lambda functions
-
<thread>
-
<atomic>
See the Synapse Travis CI Builds.
Signal programming libraries allow signals to be associated with emitter objects. Like function types, each signal has a signature. Emitting a signal is similar to function invocation, except it may call multiple functions that currently connect that particular signal from that particular emitter object. Naturally, the signature of any connected function must match the signature of the signal.
In Synapse, signals are defined as function pointer typedefs. When a signal is emitted, the value returned from any connected function is discarded, but the return type of the signal definition is still important: it is used as an identifier of the signal, a way to tell apart different signals that have otherwise identical signatures. For example, the following typedefs define two different signals, even though they both take one int
argument:
struct this_signal_;
typedef this_signal_(*this_signal)(int);
struct that_signal_;
typedef that_signal_(*that_signal)(int);
The two defined signals are different because they use different return types (this_signal_
vs. that_signal_
). By convention, the return types are defined implicitly within each typedef
. This makes the signal definitions more compact:
typedef struct this_signal_(*this_signal)(int);
typedef struct that_signal_(*that_signal)(int);
To emit a Synapse signal, we instantiate the emit
function template with a pre-defined signal typedef
(e.g. this_signal
), passing the emitter object as the first argument. The rest of the arguments follow, as defined by the signal signature (in this case a single int
argument). Note that the emitter object passed as the first argument to emit<S>
is not forwarded implicitly to the connected functions; its only purpose is to specify the emitter, that is, which object is emitting the signal S
.
As a less abstract example, let’s define a type button
that emits a Synapse signal clicked
(which takes no arguments) when the member function click
is called:
typedef struct clicked_(*clicked)();
class button {
public:
void click() {
synapse::emit<clicked>(this);
}
};
Tip
|
It is possible to define the clicked typedef as a member of class button , but this coupling is usually not appropriate when using Synapse. It is better to treat signals as types with independent semantics that can be used with any appropriate object. In this case, anything clickable could emit the clicked signal.
|
Next, let’s connect the signal clicked
to the accept
member function of a dialog box object:
class dialog {
public:
void accept();
};
....
shared_ptr<button> emitter=make_shared<button>();
shared_ptr<dialog> receiver=make_shared<dialog>();
synapse::connect<clicked>(emitter, receiver, &dialog::accept);
Or we could use a lambda instead:
synapse::connect<clicked>(emitter, receiver,
[ ](dialog * d) {
d->accept();
} );
With this setup, click()
-ing the button
will accept()
the dialog
.
Note
|
The receiver argument to connect is optional. If it is specified, a pointer to the receiver object is passed implicitly as the first argument to the connected function, followed by all other arguments as specified by the signal signature.
|
The button
/dialog
example from the previous section could have been similarly implemented using any other signal programming library, because the button
type is specifically designed to be able to emit the clicked
signal.
But in Synapse, any object whatsoever can be used as an emitter. This makes it possible to emit
non-intrusively even if the type of the emitter object was not designed to support signals. For example, a function that processes a file can use a standard FILE
pointer as a Synapse emitter to report on its progress:
typedef struct report_progress_(*report_progress)(int progress);
void process_file( FILE * f ) {
for( int progress=0; !feof(f); ) {
....
progress += fread(buf,1,nread,f);
....
synapse::emit<report_progress>(f,progress);
}
}
Outside of process_file
the report_progress
signal can be connected to some user interface function that updates a progress bar. For example, using Synapse, we could easily connect it to a Qt QProgressBar
object:
if( FILE * f=fopen("file.dat","rb") ) {
QProgressBar pb(....);
auto c=synapse::connect<report_progress>(f, &pb,
&QProgressBar::setValue);
process_file(f);
fclose(f);
}
Notice that process_file
is not coupled with QProgressBar
: the report_progress
signal could be connected to a different function or not connected at all, in which case the call to emit
in process_file
would be a no-op.
The observant reader has surely noticed that in the above example we had to capture the return value of synapse::connect<report_progress>
in the local variable c
, while we didn’t have to do this in the previous button
/dialog
example. This is explained below.
In Synapse there are two types of connection objects: connection
and pconnection
:
-
shared_ptr<connection>
objects are returned by overloads ofconnect
which take the emitter (and, if specified, the receiver) as a raw pointer. The user is required to keep theconnection
object alive; the function passed toconnect
will be disconnected when that object expires. -
weak_ptr<pconnection>
objects are returned by overloads ofconnect
which take at least one of the emitter or the receiver as aweak_ptr
orshared_ptr
. The user is not required to keeppconnection
objects alive; the connected function is disconnected when Synapse detects that the emitter or the receiver have expired.
Tip
|
The release function can be used to convert a weak_ptr<pconnection> object to a shared_ptr<connection> object, in case we need to disconnect the function before the receiver or the emitter have expired.
|
It is possible to temporarily block a specific signal for a specific emitter. This allows users to dynamically disable functionality implemented by emitting signals — without having to disconnect them.
Note
|
Blocking affects pre-existing as well as future connections. |
#include <boost/synapse/connect.hpp>
#include <boost/synapse/block.hpp>
#include <string>
#include <iostream>
namespace synapse = boost::synapse;
typedef struct print_(*print)(std::string const & s);
int main() {
int emitter;
auto c = synapse::connect<print>(&emitter,
[ ](std::string const & s) {
std::cout << s;
} );
synapse::emit<print>(&emitter,"Hello World"); # (1)
shared_ptr<synapse::blocker> b = synapse::block<print>(&emitter); # (2)
synapse::emit<print>(&emitter,"no-op");
b.reset();
synapse::emit<print>(&emitter,"Hello World"); # (3)
}
-
emit
calls the connected lambda, printingHello World
. -
The
print
signal will be blocked untilb
expires, therefore the call toemit
on the next line is a no-op, even though the signal is still connected. -
At this point
b
has expired, so the call toemit
will call the connected lambda, printingHello World
, again.
Synapse features a special global emitter that emits meta signals to notify connected functions about user interactions with other signals. It can be accessed by the meta::emitter
function.
When a signal S
is blocked or unblocked, the meta emitter emits the meta::blocked<S>
signal. Connecting this meta::blocked<S>
signal allows the blocked state of the signal S
to be automatically reflected in other systems, for example in user interface.
Similarly, when a signal S
is connected or disconnected, the meta emitter emits the meta::connected<S>
signal, which is useful when integrating Synapse with 3rd-party callback systems; see Synapsifying C callbacks.
Note
|
To further facilitate interoperability between Synapse and other callback APIs, connection /pconnection objects (returned by overloads of connect ) can store arbitrary user data.
|
Synapse can be used to implement interthread communication using signals. The data structures created by connect
(or translate
) use thread-local storage, so by default calling emit
will call only functions connected by the calling thread (and will not return until all such functions have been called in order, or one of them throws.)
The following diagram shows the connections created (by calls to connect<S>
) in a single thread for a given signal type S
, each connecting an emitter to a function. When emit<S>(e1,arg,…)
is called, all functions connecing the signal S
from the given emitter e1
are called in the order in which the connections were created:
It is also possible for any thread to request to receive all signals emitted by other threads, by creating its own thread_local_queue
object.
In this case, in addition to the behavior described above, emit<S>(e1,arg,…)
will capture its arguments (depending on the signature of S
) and queue them into the thread_local_queue
object created by any thread other than the calling thread. Each such thread must poll its own thread_local_queue
regularly; this "emits" the queued objects locally and removes them from the queue (note that poll
is not given an emitter or a signal type, it emits locally all queued objects, regardless of signal type or emitter).
This is illustrated by the following diagram:
A typical use case for this system is to update user interface objects with data generated by one or multiple worker threads: the user interface objects themselves need not be thread-safe, because they will be updated only synchronously, at the time poll
is called.
Special care must be taken to ensure that any objects referred to by arguments passed to emit
will remain valid at least until all other threads have polled their thread_local_queue
objects. For example, the following code is incorrect in the presence of thread_local_queue
objects:
typedef struct my_signal_(*my_signal)( int * );
void emit_my_signal( void * emitter, int x ) {
emit<my_signal>(emitter,&x); //Undefined behavior in the presence of thread_local_queues!
}
The problem is that the address of x
may be queued into other threads' queues, and since x
is local to emit_my_signal
, it may be destroyed by the time these threads call poll
.
The post
function can be used to queue into a thread_local_queue
arbitrary functions for execution at the time poll
is called. This feature allows critical worker threads to minimize the amount of time they consume by offloading expensive non-critical computations to another, non-critical thread. This also removes the need for synchronization, since the queued functions are executed synchronously in the thread that owns the thread_local_queue
object.
- Effects:
-
Calls all function objects that are connected to the specified
Signal
from the emittere
, in the order in which they were connected byconnect
ortranslate
, passing the specified arguments depending on theSignal
signature, subject to the connection lifetime/blocking restrictions. - Returns:
-
The count of the connected function objects that were called. Signals that are currently blocked are not included in the count returned by
emit
.
Important
|
It is the responsibility of the caller to ensure that the emitter object e does not expire before emit returns, otherwise the behavior is undefined.
|
- Throws:
-
Any exception thrown by one of the connected function objects, in which case the remaining function objects are not called.
- Notes:
-
-
Values returned by the connected function objects are discarded.
-
If before
emit
returnsconnect
is called on the same signal and the same emitter, any newly connected functions are not called during the sameemit
. -
If before
emit
returns aconnection
object expires, it may or may not get called during the sameemit
. -
If
e
is0
,emit
simply returns0
without calling any functions. Because of this feature, if the emitter is held by ashared_ptr
objectsp
, there is no harm in callingemit<Signal>(sp.get(),…)
even ifsp
is empty. Similarly, if the caller holds aweak_ptr
referencewp
to an emitter object which has expired, callingemit<Signal>(wp.lock().get(),…)
will simply return0
. -
emit
takes its arguments by value. Usestd::ref
to pass by reference (but beware ofthread_local_queue
objects).
-
- Thread safety:
-
By default
emit
will only call functions connected from the calling thread. In addition, the signal is pushed onto anythread_local_queue
objects created by other threads, but only if those threads currently have at least one active connection for the specifiedSignal
. In this caseemit
captures its arguments similarly tostd::bind
, and it is the responsibility of the caller to ensure that they remain valid until the posted signal is processed in all other threads, by a call tothread_local_queue::poll
orthread_local_queue::wait
.
namespace boost { namespace synapse {
class connection;
template <class Signal,class Emitter,class F>
shared_ptr<connection> connect( Emitter * e, F f );
template <class Signal,class Emitter,class Receiver,class F>
shared_ptr<connection> connect( Emitter * e, Receiver * r, F f );
class pconnection;
template <class Signal,class Emitter,class F>
weak_ptr<pconnection> connect( <<Emitter>> e, F f ); (1)
template <class Signal,class Emitter,class Receiver,class F>
weak_ptr<pconnection> connect( <<Emitter>> e, <<Receiver>> r, F f ); (2)
} }
-
Two overloads are provided:
<<Emitter>>
isweak_ptr<Emitter> const &
or, equivalently,shared_ptr<Emitter> const &
; -
Multiple overloads are provided:
<<Emitter>>
isEmitter *
,weak_ptr<Emitter> const &
or, equivalently,shared_ptr<Emitter> const &
; at least one ofe
andr
is not a raw pointer.
Overloads of connect
that return shared_ptr<connection>
create connections whose lifetime is explicitly managed by the user. Such connections require that the caller keeps the returned connection
object alive for as long as the connection should persist.
Overloads of connect
that return weak_ptr<pconnection>
take at least one of e
and r
as a weak_ptr
or shared_ptr
. They create persistent connections which expire automatically when either e
or r
expire.
- Effects:
-
-
Connects the specified
Signal
from the emittere
to the function objectf
. The arguments ofF
must match the arguments ofSignal
, except that ifr
is specified, a pointer to the receiver object is passed as the first argument toF
, followed by the rest of the arguments as specified by theSignal
signature. The signal is considered disconnected when either of the following occurs:-
The returned
shared_ptr<connection>
object expires (this applies only to overloads that returnshared_ptr<connection>
); -
e
(passed as eitherweak_ptr<Emitter>
orshared_ptr<Emitter>
) expires; -
r
(passed as eitherweak_ptr<Emitter>
orshared_ptr<Emitter>
) expires.NoteThe returned object does not assume ownership of e
orr
: passingshared_ptr
toconnect
is equivalent to passingweak_ptr
.ImportantIf either the emitter or the receiver, if passed as raw pointers, expire before the returned connection
object has expired, the behavior is undefined.
-
-
The
meta::emitter
emits themeta::connected<Signal>
signal:namespace boost { namespace synapse { namespace meta { weak_ptr<void const> emitter(); template <class Signal> struct connected { //unspecified }; namespace connect_flags { unsigned const connecting=1; unsigned const first_for_this_emitter=2; unsigned const last_for_this_emitter=4; } } } }
The
meta::connected<Signal>
signal is also emitted when the returned object expires. Handlers of the meta signal take a reference to theconnection
object being created or destroyed, and a secondunsigned
argument,flags
, which indicates the circumstances under which the meta signal is emitted:-
If the
connection
object is being created, theconnecting
bit is set, otherwise it is clear; -
If this is the first
Signal
connection being created for the emittere
, thefirst_for_this_emitter
bit is set, otherwise it is clear; -
If this is the last
Signal
connection being destroyed for the emittere
, thelast_for_this_emitter
bit is set, otherwise it is clear.Note-
Because class
connection
is the protected base of classpconnection
, handlers ofmeta::connected<Signal>
takeconnection &
regardless of whichconnect
overload was used. -
The
meta::connected<Signal>
signal is thread-local; it will never be queued into other threads'thread_local_queue
objects.
TipThe passed connection
object can be used to access the emitter and receiver objects passed toconnect
. -
-
-
- Thread safety:
-
Please see
emit
andthread_local_queue
.
namespace boost { namespace synapse {
class connection;
class pconnection;
shared_ptr<connection const> release( weak_ptr<pconnection const> const & c );
shared_ptr<connection> release( weak_ptr<pconnection> const & c );
} }
- Effects:
-
Converts a weak
pconnection
reference to shared ownershipconnection
reference. The lifetime of the connection is now explicitly managed by the returnedshared_ptr
object; seeconnect
.
- Effects:
-
The
translate
function template creates a connection which causes the emittere2
to emitTranslatedSignal
each time the emittere1
emitsOriginalSignal
(the two signals must have compatible signatures). This behavior persists until:-
the returned
connection
object expires (this applies only to thetranslate
overload that takese1
ande2
as raw pointers); -
e1
(passed as eitherweak_ptr
orshared_ptr
expires; -
e2
(passed as eitherweak_ptr
orshared_ptr
expires.
-
Note
|
The returned connection object does not assume ownership of e1 or e2 : passing shared_ptr is equivalent to passing weak_ptr .
|
Important
|
If either e1 or e2 , passed as raw pointers, expire before the returned connection object has expired, the behavior is undefined.
|
Overloads of connect
and translate
return either shared_ptr<connection>
or weak_ptr<pconnection>
, depending on whether or not the emitter and the receiver are passed as raw pointers. The former is used to control the lifetime of the connection explicitly, while the latter represents persisent connections, which expire implicitly with the expiration of the emitter or the receiver object.
Before being returned to the caller, connection
objects are passed to handlers of the meta::connected
signal, which can use the emitter
/receiver
member function templates to access the emitter/receiver object passed to connect
. The set_user_data
/get_user_data
member function templates can be used to store and access auxiliary information.
Tip
|
Use release to convert a non-owning weak_ptr<pconnection> reference to an owning shared_ptr<connection> reference.
|
template <class T>
void set_user_data( T const & data );
- Description:
-
Stores a copy of
data
intothis
. Useget_user_data
to access it.
template <class T>
T * get_user_data() const;
- Returns:
-
-
If
this
contains object of typeT
previously copied by a call toset_user_data
, returns a pointer to that data. -
If
set_user_data
has not been called forthis
, or if the type used to instantiate theset_user_data
function template doesn’t match the type used withget_user_data
, returns0
.
-
template <class T>
shared_ptr<T> emitter() const;
- Returns:
-
A
shared_ptr
that points the emitter that was passed to an overload of theconnect
(ortranslate
) function template that returned theconnection
object. - Notes:
-
template <class T>
shared_ptr<T> receiver() const;
- Returns:
-
A
shared_ptr
that points the receiver that was passed to an overload of theconnect
(ortranslate
) function template that returned theconnection
object. - Notes:
-
namespace boost { namespace synapse {
class blocker;
template <class Signal,class Emitter>
shared_ptr<blocker> block( <<Emitter>> e ); (1)
} }
-
Multiple overloads are provided:
<<Emitter>>
is eitherEmitter *
,weak_ptr<Emitter>
or, equivalently,shared_ptr<Emitter>
.
- Effects:
-
-
Blocks the specified
Signal
from the emittere
until the returnedblocker
object expires. While theSignal
is blocked, calls toemit<Signal>
fore
are ignored and return0
. The returnedblocker
object does not owne
even ifblock
was passed ashared_ptr
. -
The
meta::emitter
emits themeta::blocked<Signal>
signal:namespace boost { namespace synapse { namespace meta { template <class Signal> struct blocked { //unspecified }; } } }
The
meta::blocked<Signal>
signal is also emitted when the returnedblocker
object expires. Handlers of the meta signal take a reference to theblocker
object being created or destroyed, and a secondbool
argument,is_blocked
, which is true if the signal is becoming blocked, false if it is becoming unblocked.
-
Note
|
|
Important
|
If block is passed a raw pointer, deleting the emitter before the returned blocker object has expired results in undefined behavior.
|
The block
function returns shared_ptr<blocker>
that is used to control the time the signal remains blocked. As well, blocker
objects are passed to handlers of the meta::blocked
signal, which can use the emitter
member function template to access the emitter object passed to block
.
template <class T>
shared_ptr<T> emitter() const;
namespace boost { namespace synapse {
struct thread_local_queue;
shared_ptr<thread_local_queue> create_thread_local_queue();
} }
- Returns:
-
A thread-local object that can be used to queue signals emitted asynchronously from other threads. Use
poll
to emit the queued signals synchronously into the calling thread. See Interthread communication.
Important
|
While any number of threads can use this function to create their own thread_local_queue , it is invalid to create more than one thread_local_queue object per thread.
|
namespace boost { namespace synapse {
int poll( thread_local_queue & q );
} }
- Effects:
-
Synchronously emits all signals queued asynchronously into
q
by calls toemit
from other threads. See Interthread communication. - Returns:
-
The total number of signals emitted.
namespace boost { namespace synapse {
int wait( thread_local_queue & q );
} }
- Effects:
-
The same as
poll(q)
, except that it blocks and does not return until at least one signal was delivered. - Returns:
-
The total number of signals emitted (always greater than 0).
namespace boost { namespace synapse {
void post( thread_local_queue & q, function<void()> const & f );
} }
- Effects:
-
Queues
f
to be called next timeq
is polled; that is,f
will be executed synchronously in the thread that has createdq
.
Note
|
While poll (or wait ) must be called from the thread that created the thread_local_queue object, post may be called from any thread.
|
It is often needed to monitor the operations of a complex dynamic system beyond the facilities available in its public API. One possible option to accomplish this is to use a logging library. Synapse provides another.
Consider a dynamic object environment in a video game, where various art assets may be loaded on the fly, cached, and eventually unloaded when they are no longer needed. Such events are typically not accessible through a formal interface because they are implementation details; yet there is still a need to analyze the efficiency of the caching algorithm.
Using Synapse, we can easily define a set of signal typedefs to represent such events:
typedef struct object_loaded_(*object_loaded)( char const * type, char const * name );
typedef struct object_unloaded_(*object_unloaded)( char const * type, char const * name );
typedef struct cache_miss_(*cache_miss)( char const * type, char const * name );
typedef struct cache_hit_(*cache_hit)( char const * type, char const * name );
As part of the implementation of the object_cache
class, we call emit
to signal the corresponding events as they occur:
void object_cache::load_object( char const * type, char const * name ) {
....
//load the object
....
synapse::emit<object_loaded>(this,type,name);
}
During development, users of the object_cache
class may connect the Synapse signals in order to analyze its efficiency, yet there is no need to compile calls to emit
out of release builds; typically it is sufficient to not connect the signals. Synapse is carefully designed to support this use case: programs that do not call connect
do not need to link with Synapse.
On the other hand, because Synapse connections are dynamic, it is possible to connect the signals only when/if we need to monitor the object_cache
operations. For example, they can be connected only while a diagnostic information window is active.
It is common for C APIs to use function pointers to implement callback systems. A typical example is the SSL_set_info_callback
function from OpenSSL:
void SSL_set_info_callback(SSL *ssl,
void (*cb) (const SSL *ssl, int type, int val));
Once the user calls SSL_set_info_callback
, the C function pointed to by cb
will be called with state information for ssl
during connection setup and use.
One difficulty with such low level C APIs is that often the user needs to pass to the callback function program-specific data. Sometimes such callback setters can be given an additional void * user_data
argument which they retain and pass verbatim when they invoke the callback function, together with its other arguments. While this solution is rather cumbersome, it’s not even supported by SSL_set_info_callback
.
Synapse can be used with this, as well as any other C-style callback API, to install C++ function objects — including lambda functions — as callbacks. This enables additional objects needed by the callback to be captured as usual.
To do this for the SSL_set_info_callback
function, we first define a Synapse signal with a matching signature:
typedef struct SSL_info_callback_(*SSL_info_callback)( const SSL *ssl, int type, int val );
Next, at global scope or during initialization, we install a handler for the meta::connected<SSL_info_callback>
signal, which the meta::emitter
emits every time the user connects or disconnects the SSL_info_callback
signal we defined:
void emit_fwd( SSL const * ssl, int type, int val );
int main( int argc, char const * argv[ ] ) {
connect<meta::connected<SSL_info_callback> >( meta::emitter(),
[ ]( connection & c, unsigned flags ) { (1)
if( flags & meta::connect_flags::connecting ) { (2)
if( flags & meta::connect_flags::first_for_this_emitter ) (3)
SSL_set_info_callback(c.emitter<SSL>().get(),&emit_fwd); (4)
} else { (5)
if( flags & meta::connect_flags::last_for_this_emitter ) (6)
if( auto ssl = c.emitter<SSL>() ) (7)
SSL_set_info_callback(ssl.get(),0); (8)
}
} );
}
void emit_fwd( SSL const * ssl, int type, int val ) {
emit<SSL_info_callback>(ssl,ssl,type,val);
}
-
This lambda function is called every time the user connects or disconnects the
SSL_info_callback
Synapse signal. -
The
SSL_info_callback
signal is being connected. -
This is the first time the
SSL_info_callback
signal is being connected for a particularSSL
object (emitter). -
Call
connection::emitter
to get the emitter as ashared_ptr<SSL>
(we know the emitter is of typeSSL *
), and then use the OpenSSL API to install a C callbackemit_fwd
, which usesemit<SSL_info_callback>
to call all connected Synapse functions. -
The
SSL_info_callback
signal is being disconnected. -
This is the last
SSL_info_callback
connection being destroyed for a particularSSL
object (emitter). -
Check if the SSL object is still accessible (it may have been destroyed already).
-
Uninstall the
emit_fwd
callback.
Once the above handler for the meta::connected<SSL_info_callback>
signal is installed, we can simply use connect
to install a C++ lamda handler for the SSL_info_callback
signal we defined:
shared_ptr<SSL> ssl(SSL_new(ctx),&SSL_free);
connect<SSL_info_callback>( ssl,
[ ]( const SSL *ssl, int type, int val ) noexcept {
} );
Sometimes connected functions are not permitted to throw exceptions — this is usually the case when the callback originates in C code. With Synapse, such exceptions can be reported safely to a C++ context that can store them for later processing.
First, we define a Synapse signal we will use to report exceptions:
typedef struct exception_caught_(*exception_caught)();
If we take a handler of the SSL_info_callback
(see above) as an example, we could modify it like this:
shared_ptr<SSL> ssl(SSL_new(ctx),&SSL_free);
connect<SSL_info_callback>( ssl,
[ ]( const SSL *ssl, int type, int val ) noexcept {
try {
//code which may throw
} catch(...) {
int n=synapse::emit<exception_caught>(ssl); (1)
assert(n>0); (2)
}
} );
-
emit
theexception_caught
Synapse signal from thessl
object. Handlers of this signal must be able to deal with any exception, for example they can usestd::current_exception
to capture the exception and rethrow it once control has exited the criticalnoexcept
path. -
emit
returns the number of connected functions it called, so thisassert
ensures that the exception won’t get ignored.
The signal programming API that is used in Qt is intrusive: signals must be specified in the definition of each type. For this reason, it is not possible to add signals to existing Qt types. When this is needed, users are directed to define the new signals in their own class which derives from the Qt type they wanted to add signal(s) to.
There is a special example that illustrates this approach. Unfortunately, this requires the use of the proprietary Qt Meta Object Compiler which the author finds cumbersome. Below is the same example modified to use Synapse signals, which requires no MOCing (the changes made to the original program are marked with numbers):
#include <boost/synapse/connect.hpp>
#define QT_NO_EMIT //Suppress the #define emit from Qt since it clashes with synapse::emit.
#include <QtWidgets/QApplication>
#include <QtWidgets/QPushButton>
namespace synapse=boost::synapse;
class Window : public QWidget
{
public:
explicit Window(QWidget *parent = 0);
signals: //Not needed with Synapse but okay
typedef struct counterReached_(*counterReached)(); (1)
private slots: //<-- Not needed with Synapse but okay
void slotButtonClicked(bool checked);
private:
int m_counter;
QPushButton *m_button;
shared_ptr<synapse::connection> c_; (2)
};
Window::Window(QWidget *parent) :
QWidget(parent)
{
// Set size of the window
setFixedSize(100, 50);
// Create and position the button
m_button = new QPushButton("Hello World", this);
m_button->setGeometry(10, 10, 80, 30);
m_button->setCheckable(true);
// Set the counter to 0
m_counter = 0;
connect(m_button,&QPushButton::clicked,
[this]( bool checked )
{
slotButtonClicked(checked);
} ); (3)
c_=synapse::connect<counterReached>(this,&QApplication::quit); (4)
}
void Window::slotButtonClicked(bool checked)
{
if (checked)
m_button->setText("Checked");
else
m_button->setText("Hello World");
m_counter ++;
if (m_counter == 10)
synapse::emit<counterReached>(this); (5)
}
int main(int argc, char **argv)
{
QApplication app (argc, argv);
Window window;
window.show();
return app.exec();
}
-
Was:
void counterReached();
-
Needed to keep the Synapse connection alive.
-
Was: connect(m_button, SIGNAL (clicked(bool)), this, SLOT (slotButtonClicked(bool)));
-
Was:
connect(this, SIGNAL (counterReached()), QApplication::instance(), SLOT (quit()));
-
Was:
emit counterReached();
Synapse integrates well with some C event handling APIs. As an example, let’s consider GLFW.
Note
|
GLFW is an Open Source, multi-platform library for OpenGL, OpenGL ES and Vulkan development on the desktop. It provides a simple API for creating windows, contexts and surfaces, receiving input and events. |
Here is the function provided by GLFW for installing a key event handler for a window:
GLFWkeyfun glfwSetKeyCallback( GLFWwindow *window, GLFWkeyfun cbfun );
where GLFWkeyfun
is declared as:
typedef void (*GLFWkeyfun)( GLFWwindow * window, int key, int scancode, int action, int mods );
With Synapse, we can define signals to represent this as well as all other GLFW input and window state events:
extern "C" { typedef struct GLFWwindow GLFWwindow; }
namespace glfw_signals
{
//User input callbacks
typedef struct Key_(*Key)( GLFWwindow *, int key, int scancode, int action, int mods );
typedef struct Char_(*Char)( GLFWwindow *, unsigned int codepoint );
typedef struct CharMods_(*CharMods)( GLFWwindow *, unsigned int codepoint, int mods );
typedef struct CursorPos_ (*CursorPos)( GLFWwindow *, double xpos, double ypos );
typedef struct CursorEnter_(*CursorEnter)( GLFWwindow *, int entered );
typedef struct MouseButton_(*MouseButton)( GLFWwindow *, int button, int action, int mods );
typedef struct Scroll_(*Scroll)( GLFWwindow *, double xoffset, double yoffset );
typedef struct Drop_(*Drop)( GLFWwindow *, int count, char const * * paths );
//Window state callbacks
typedef struct WindowClose_(*WindowClose)( GLFWwindow * );
typedef struct WindowSize_(*WindowSize)( GLFWwindow *, int width, int height );
typedef struct FramebufferSize_(*FramebufferSize)( GLFWwindow *, int width, int height );
typedef struct WindowPos_(*WindowPos)( GLFWwindow *, int xpos, int ypos );
typedef struct WindowIconify_(*WindowIconify)( GLFWwindow *, int iconified );
typedef struct WindowFocus_(*WindowFocus)( GLFWwindow *, int focused );
typedef struct WindowRefresh_(*WindowRefresh)( GLFWwindow * );
//This is emitted from the GLFWwindow object to report exceptions from connected signal handlers
typedef struct exception_caught_(*exception_caught)( GLFWwindow * );
}
Next, in a different header we install meta::connected
signal handlers for the signals above:
#include "glfw_signals.hpp"
#include <boost/synapse/connect.hpp>
#include <boost/synapse/connection.hpp>
#include "GLFW/glfw3.h"
template <class Signal>
class synapsifier;
template <class R,class... A>
class synapsifier<R(*)(GLFWwindow *,A...)>
{
typedef R(*Signal)(GLFWwindow *,A...);
typedef void (*GLFWfun)( GLFWwindow *,A... );
static GLFWfun prev_;
//This is the handler that GLFW calls. It emits the corresponding Synapse
//signal and calls the previous GLFW handler for the same event, if any.
static void handler( GLFWwindow * w, A... a )
{
using namespace boost::synapse;
try
{
(void) emit<Signal>(w,w,a...);
}
catch(...)
{
//We can't let exceptions propagate up into C code, so the window
//emits the exception_caught signal, which (if exceptions are
//expected) should be connected to capture and handle the current
//exception.
bool handled = emit<glfw_signals::exception_caught>(w,w)>0;
assert(handled);
}
if( prev_ )
prev_(w,a...);
}
public:
explicit synapsifier( GLFWfun (*setter)(GLFWwindow *,GLFWfun) )
{
using namespace boost::synapse;
//Here we connect the Synapse meta::connected<Signal> signal. This
//signal is emitted by the meta::emitter() when the Signal is being
//connected (the user calls synapse::connect<Signal>) or disconnected
//(when the connection expires). The emitter pointer passed to connect
//(which in this case is of type GLFWwindow) is stored in the
//synapse::connection object passed to the lambda below, and can be
//accessed by the connection::emitter member function template.
connect<meta::connected<Signal> >( meta::emitter(),
[setter]( connection & c, unsigned flags )
{
if( flags&meta::connect_flags::connecting )
{
//When the Signal is being connected for the first time,
//use the GLFW API to install our handler.
if( flags&meta::connect_flags::first_for_this_emitter )
prev_=setter(c.emitter<GLFWwindow>().get(),&handler);
}
else
{
//When the last Signal connection expires, use the GLFW API
//to uninstall our handler and restore the previous handler.
if( flags&meta::connect_flags::last_for_this_emitter )
{
GLFWfun p=setter(c.emitter<GLFWwindow>().get(),prev_);
assert(p==&handler);
}
}
} );
}
};
template <class R,class... A>
typename synapsifier<R(*)(GLFWwindow *,A...)>::GLFWfun synapsifier<R(*)(GLFWwindow *,A...)>::prev_;
//Install all the synapse::meta::connected<....> handlers
synapsifier<glfw_signals::WindowClose> s1(&glfwSetWindowCloseCallback);
synapsifier<glfw_signals::WindowSize> s2(&glfwSetWindowSizeCallback);
synapsifier<glfw_signals::FramebufferSize> s3(&glfwSetFramebufferSizeCallback);
synapsifier<glfw_signals::WindowPos> s4(&glfwSetWindowPosCallback);
synapsifier<glfw_signals::WindowIconify> s5(&glfwSetWindowIconifyCallback);
synapsifier<glfw_signals::WindowFocus> s6(&glfwSetWindowFocusCallback);
synapsifier<glfw_signals::WindowRefresh> s7(&glfwSetWindowRefreshCallback);
synapsifier<glfw_signals::Key> s8(&glfwSetKeyCallback);
synapsifier<glfw_signals::Char> s9(&glfwSetCharCallback);
synapsifier<glfw_signals::CharMods> s10(&glfwSetCharModsCallback);
synapsifier<glfw_signals::CursorPos> s11(&glfwSetCursorPosCallback);
synapsifier<glfw_signals::CursorEnter> s12(&glfwSetCursorEnterCallback);
synapsifier<glfw_signals::MouseButton> s13(&glfwSetMouseButtonCallback);
synapsifier<glfw_signals::Scroll> s14(&glfwSetScrollCallback);
synapsifier<glfw_signals::Drop> s15(&glfwSetDropCallback);
Important
|
The above glfw_synapsify.hpp should be included in exactly one compilation unit of a GLFW program, for example the main compilation unit. This will automatically install all meta::connected signal handlers.
|
With this, we simply use connect
to hook up any GLFWwindow
event. For example, if we have a GLFWwindow
pointer w
, we can install a key event handler like so:
auto c = synapse::connect<glfw_signals::key>(w,
[ ]( GLFWwindow * w, int key, int scancode, int action, int mods )
{
....
}
Finally, this is the example from the GLFW Getting started page, modified to use the "synapsify" framework above (changes to the original example are marked with numbers):
//========================================================================
// Simple GLFW example
// Copyright (c) Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#include "glfw_synapsify.hpp" (1)
namespace synapse = boost::synapse;
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "linmath.h"
#include <stdlib.h>
#include <stdio.h>
static const struct
{
float x, y;
float r, g, b;
} vertices[3] =
{
{ -0.6f, -0.4f, 1.f, 0.f, 0.f },
{ 0.6f, -0.4f, 0.f, 1.f, 0.f },
{ 0.f, 0.6f, 0.f, 0.f, 1.f }
};
static const char* vertex_shader_text =
"#version 110\n"
"uniform mat4 MVP;\n"
"attribute vec3 vCol;\n"
"attribute vec2 vPos;\n"
"varying vec3 color;\n"
"void main()\n"
"{\n"
" gl_Position = MVP * vec4(vPos, 0.0, 1.0);\n"
" color = vCol;\n"
"}\n";
static const char* fragment_shader_text =
"#version 110\n"
"varying vec3 color;\n"
"void main()\n"
"{\n"
" gl_FragColor = vec4(color, 1.0);\n"
"}\n";
static void error_callback(int error, const char* description)
{
fprintf(stderr, "Error: %s\n", description);
}
/* (2)
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
*/
int main(void)
{
GLFWwindow* window;
GLuint vertex_buffer, vertex_shader, fragment_shader, program;
GLint mvp_location, vpos_location, vcol_location;
glfwSetErrorCallback(error_callback);
if (!glfwInit())
exit(EXIT_FAILURE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);
if (!window)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
//glfwSetKeyCallback(window, key_callback); (2)
auto connected = synapse::connect<glfw_signals::Key>(window, (3)
[ ]( GLFWwindow * window, int key, int /*scancode*/, int action, int /*mods*/ )
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
} );
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc) glfwGetProcAddress);
glfwSwapInterval(1);
// NOTE: OpenGL error checks have been omitted for brevity
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
glCompileShader(vertex_shader);
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
glCompileShader(fragment_shader);
program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
mvp_location = glGetUniformLocation(program, "MVP");
vpos_location = glGetAttribLocation(program, "vPos");
vcol_location = glGetAttribLocation(program, "vCol");
glEnableVertexAttribArray(vpos_location);
glVertexAttribPointer(vpos_location, 2, GL_FLOAT, GL_FALSE,
sizeof(vertices[0]), (void*) 0);
glEnableVertexAttribArray(vcol_location);
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,
sizeof(vertices[0]), (void*) (sizeof(float) * 2));
while (!glfwWindowShouldClose(window))
{
float ratio;
int width, height;
mat4x4 m, p, mvp;
glfwGetFramebufferSize(window, &width, &height);
ratio = width / (float) height;
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
mat4x4_identity(m);
mat4x4_rotate_Z(m, m, (float) glfwGetTime());
mat4x4_ortho(p, -ratio, ratio, -1.f, 1.f, 1.f, -1.f);
mat4x4_mul(mvp, p, m);
glUseProgram(program);
glUniformMatrix4fv(mvp_location, 1, GL_FALSE, (const GLfloat*) mvp);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
-
Automatically install
meta::connected
signal handlers to synapsify GLFW. -
Commented-out parts of the original example, and…
-
…the C++ lambda function connected to the
Key
Synapse signal fromglfw_signals.hpp
to handle theEsc
key.
The unique design feature of Synapse is that it is non-intrusive with respect to the emitter object type. While other libraries provide users with types that can emit signals, Synapse is able to emit any signal from any object of any type whatsoever.
For a list of other signal programming libraries, see this Wikipedia page.
Because Synapse is formatted for Boost review, people often ask what are the differences between Synapse and Boost Signals2.
Signals2 | Synapse | |
---|---|---|
What is a signal? |
An object of type |
A type, a C function pointer |
How does emitting a signal work? |
Invoking |
Invoking |
What objects can emit signals? |
Only instances of the |
Any object of any type whatsoever: the |
How does connecting a signal work? |
Calling |
Calling |
Support for meta signals? |
No (not possible, a signal is an object). |
Yes. Connecting a signal of type |
Integration with C-style callback APIs? |
No (not possible). |
Yes, through meta signals, see Synapsifying C callbacks. |
Multi-threading support? |
Yes, the connection list maintained in each signal object is thread-safe. |
Yes, the connection lists are thread-local, and signals are transported to other threads using |
Can connected functions return values? |
Yes, there is an elaborate system for dealing with multiple returns when a signal is invoked. |
No, but of course it’s possible to pass an argument by reference or a custom object to collect and/or accumulate the results if needed. |
BOOST_SYNAPSE_ASSERT
All assertions in Synapse use this macro; if not #defined
, Synapse header files #define
it as BOOST_ASSERT
.
BOOST_SYNAPSE_USE_STD_FUNCTION
By default, Synapse uses boost::function
. If this macro is #defined
, it will use std::function
instead.
BOOST_SYNAPSE_USE_STD_SMART_PTR
By default, Synapse uses the following Boost smart pointer components:
-
boost::shared_ptr
-
boost::weak_ptr
-
boost::make_shared
-
boost::get_deleter
If BOOST_SYNAPSE_USE_STD_SMART_PTR
is #defined
, the corresponding standard components will be used.
BOOST_SYNAPSE_NO_THREADS
If #defined
, Synapse assumes that static storage is equivalent to thread-local storage, and interthread communication support is disabled.
If BOOST_SYNAPSE_NO_THREADS
is not explicitly #defined
, thread-safety depends on BOOST_NO_THREADS
.
BOOST_SYNAPSE_THREAD_LOCAL(type,object)
This macro is used to define objects with static thread-local storage; if not #defined
, Synapse header files #define
it as:
#define BOOST_SYNAPSE_THREAD_LOCAL(type,object) static thread_local type object
or, under BOOST_SYNAPSE_NO_THREADS
, as:
#define BOOST_SYNAPSE_THREAD_LOCAL(type,object) static type object
BOOST_SYNAPSE_THREAD_LOCAL_INIT(type,object,init)
This macro is used to define objects with static thread-local storage and provide their initialization; if not #defined
, Synapse header files #define
it as:
#define BOOST_SYNAPSE_THREAD_LOCAL_INIT(type,object,init) static thread_local type object init
or, under BOOST_SYNAPSE_NO_THREADS
, as:
#define BOOST_SYNAPSE_THREAD_LOCAL(type,object,init) static type object init
Note
|
The init argument passed will contain parentheses, so when the macro is expanded it results in calling a constructor.
|
BOOST_SYNAPSE_STATIC(type,object)
This macro is used to define objects with static thread-local storage; if not #defined
, Synapse header files #define
it as:
#define BOOST_SYNAPSE_STATIC(type,object) static type object
BOOST_SYNAPSE_STATIC_INIT(type,object,init)
This macro is used to define objects with static thread-local storage and provide their initialization; if not #defined
, Synapse header files #define
it as:
#define BOOST_SYNAPSE_STATIC_INIT(type,object,init) static type object init
Important
|
Except under BOOST_SYNAPSE_NO_THREADS , initialization of objects with static storage is expected to be serialized.
|
Note
|
The init argument passed will contain parentheses, so when the macro is expanded it results in calling a constructor.
|
Synapse has been formatted to be submitted for a Boost review, so its directory structure follows the common directory structure for Boost libraries—and it’s built with Boost Build.
Alternatively, just put connect.cpp
and either thread_local_queue.cpp
or interthread_stub.cpp
into your favorite build system, depending on whether or not your program calls create_thread_local_queue
. If your program calls the block
function, you also need to compile and link block.cpp
.
However, note that if your program only emits signals (that is, it calls emit
but it never creates any connections), there is no need to link any .cpp
files; that is, boost/synapse/emit.hpp
by itself is an independent header-only library. This way low level libraries that emit signals do not require the user to link Synapse, unless he cares to connect them.
The unit tests and the examples can be built within the Boost framework: clone Synapse under the libs
subdirectory in your boost installation, then cd
into synapse/build/test
and execute b2
as usual.
-
Is there a way to stop the emit loop before all connected functions have been called?
No, except by throwing an exception.
-
I am concerned about code size, does Synapse use a lot of templates?
Yes, there are templates instantiated for each signal type. This is done so that the dispatch by signal type occurs at compile-time, leaving only emitter dispatch at run-time. However, static types are erased as soon as possible, so template bloat is kept to a minimum.
-
I do not need thread-safety, is there a way to configure Synapse to eliminate thread safety overhead?
Most data structures in Synapse are not thread-safe, instead they are thread-local. The overhead of using the library across multiple threads is contained only in
thread_local_queue.cpp
, which is an optional component.