Boost logo

Boost :

From: Carlo Wood (carlo_at_[hidden])
Date: 2006-06-12 10:28:26


I've written something simular as is current talked about in
the thread 'Interest in a runtime dynamic dispatch library', but
since the callbacks are called when some event occurs, I call
them 'event servers'. I suppose that might include a 'dispatcher',
but I wouldn't have a clu which part ;). And to be honest, I can't
think of much that makes this useful without the whole thing.

The concept of my 'event servers' is the following:

The user declares(/derives) a class which represents an 'event server'.
Each event server services events of a certain 'type'.
In particular, the type of the data that is returned as parameter
of the callback function is fixed per event server (though totally
arbitrary per event server class). I call this 'the event type'.

Thus,

  FooEventServer foo_event_server;

It is possible to instantiate any number of event servers,
even of the same type, though mostly one would just instantiate
a single instance for each given 'event type', because each
instance of an event server would have it's own queue for
events that weren't handled yet.

At an arbitrary number of points in the code it is possible
to request events of such a type, by calling a method of the
server. Such calls pass a reference of an arbitrary class type
as well as a member function pointer to a member function of
that class type: the call back function.

  // Request events from foo_event_server.
  foo_event_server(my_event_client, &MyEventClient::call_back);

The only restrictions here are that the callback function must
return a bool and takes the corresponding 'event type' as parameter.

  class MyEventClient {
...
    bool call_back(FooEventServer::event_type const& event_type);
or just
    bool call_back(FooEventType const& event_type);
  };

Optionally, it is possible to pass a 'cookie' object (also of
arbitrary type), in which case the call back function also
needs to accept an argument of the type of this cookie object:
the cookie will be returned as-is to the caller, when the
event occurs.

  class MyEventClient {
...
    template<COOKIE>
      bool call_back(FooEventServer::event_type const& event_type, COOKIE cookie);
  };
...
  // Request events from foo_event_server.
  foo_event_server(my_event_client, &MyEventClient::call_back, cookie); // cookie can be anything.

Finally, it is possible to define the event
server such that a given type (arbitrary, but fixed per
event server) can be passed along with the request, I call
this the 'event data'. This data object can be used to decide
if an event of a given type is really matching the request
of that client or not.

  // Request events from foo_data_event_server.
  foo_data_event_server(request_data, my_event_client, &MyEventClient::call_back);

Once the callback function is called (being passed an object
of the 'event type', and optionally the 'cookie' object),
it can either return false-- meaning that the request should
stay and handled again, or true-- meaning that the request
should be removed (it was the last time the call back was
called).

>From the point of view of the 'client', there is just one
more thing: when requesting an event from the server, it can
also provide a 'busy interface object'; this object handles
cases where an event occurs, but the client (the owner object
of the call back function) is 'busy' and cannot handle the
event. Since this is part of a library for event driven
applications that I wrote, every call should return as soon
as possible. So, if the client is 'busy' then the fact that
the event occured should be queued somewhere until it becomes
not-busy again; this is done by the 'busy interface object'.

I've written a few event servers in the mean time; below are
some examples of working code.

Please note that this post shows what is possible from the
point of view of the 'client' (the one requesting an event,
and the object that gets the call back). It doesn't go into
the inner workings of the event servers that I wrote.

Example 1.

This requests a user answer to some text question.
Each 'client_session' (a socket connection of the current
client (not the event client, that would be current object *this)),
has it's own instance of a 'user_answer_event_server' that
dispatches the answers of the user behind that client.

    M_identity->client_session().user_answer_event_server
        (UserAnswerRequestData(M_identity,
         "What server should I connect to (type: <server> [<port>])?"),
         *this, &ServerConnection::received_server_port_answer);

The 'request data' of the user_answer_event_server is obviously
UserAnswerRequestData, and exists of a pointer to some application
specific 'identity', as well as the actual question. When the user
answers the question, the method 'received_server_port_answer'
is called (of the current object (*this)).

Example 2.

Each 'server_session' (a socket connection to a server on
the internet (not the event server)) has an 'event server' instance,
here returned by message_event_server().

  MatchRequest match_request(M_identity);

  match_request(396). // Numeric 396 reply
      localserver(prefix). // From local server
      mynick(0). // to me.
      wildcard("*.users.undernet.org", 1). // Parameter 1.
      exactmatch("is now your hidden host", 2); // Parameter 2.

  // Request an event for matching messages.
  M_server_session->message_event_server()(match_request, *this, &ServerConnection::hidden_host);

The last line requests 'hidden_host' to be called back (of the current object)
as soon as a message from the server is received that matches the given
match_request. Obviously, MatchRequest is an arbitrary custom object (that allows
me to very efficiently match incoming server messages) but it is completely
linked to this particular event server: it is the 'event data' type of this
event server.

Example 3.

    char service_mode = 'x';
    ASSERT(M_state == authentication_sent);
    M_state = umode_i_sent;
    *M_server_session << "MODE " << M_identity->server_out_nick() << " +i" << service_mode << "\r\n";

    UserModeMask mask('i');
    mask |= service_mode;

    // Request an event for an expression to become true.
    ON_TRUE((*M_identity->user_mode() & ConstExpression(mask)) == ConstExpression(mask),
        *this, &ServerConnection::invisible_event);

Ok, this one isn't entirely fair - as 'ON_TRUE' is a macro (see below).
The last line causes the callback 'invisible_event' to be called, again a
method of the current object (*this), as soon as the expression given in
the first argument of the macro becomes true.

The macro is defined as

#define ON_TRUE(expr, action...) do { (expr).create_event_server()(action); } while(0)

In this case, a temporary event server is created for every event request; one
for each expression that has to be evaluated. The to-be-evaluated expression is thus
not the event data - but incorporated into the event server a lot more than that.
[Here is how this works: user_mode() returns a reference to an object of type
Watched<UserModeMask>. By putting the '*' in front of it, it can be used in
an Expression as above, otherwise it just acts as a UserModeMask.
The fact that it is used in the expression above tells the Watched<UserModeMask>
object that it is used in that particular expression. When the Watched<UserModeMask>
changes value, it notifies the event server - which in turn re-evaluates the
whole expression to see if it became true, calling the call back if it did.]

Example 4.

This code requests a timer event. 'bi' is a busy interface object in this case:

  struct timeval timeout = { 2, 0 };
  timerRequest(timeout, *this, &alarm_tst_app_ct::its_time, bi);

This will call 'its_time' of the current object, after the event data 'timeout'
has expired and the busy-interface object (bi) is not set busy -- otherwise
the call back will be delayed until the moment that the busy-interface object
is set un-busy again. However, the 'event type' (the object passed to
the callback function) contains the actual time at which the timer
expired-- so, even though the callback might be a little later,
the client always is remembered at which the event really occured.

As you can see, the number of possibilities are endless. It is therefore not
possible to write one interface that fits all. Things like the expression
evaluation, the message matching routines and the code that handles the
user questions, all have to be customized code, of course. As a result,
writing an 'event server' is a lot of work - even when you already provide
the frame work, and all possible common code.

Nevertheless, the interface provided by the event library makes it possible
to get the cookie and busy interfaces for free. And often it is only needed
to declare the EventType class (the type of the parameter passed back the
call back function), define a base class for event requests, and write the
'trigger' function that actually figures out which request matches
the current event. The 'UserAnswer' event server exists of 179 lines of code
(including copy right notices and comments in the files). The expression
evaluation event server exists of 1247 lines, while the message matcher
needed 488 lines of extra code. The timer event server is part of my event
library.

What I like about this design is that the class types that are used for
the cookie as well as the event client (the owner of the call back function)
can be totally arbitrary for every event request (while remaining 100%
type-safe of course - it doesn't do any casting). Also, the interface
for the many ways an event can be requested (with or without cookie, and
with or without a busy interface) involving rather complex templated member
functions on top of that, could be totally re-used. It is not necessary
to put time into that for a new event server.

Finally (something I didn't even began to get into in the above), all of
this delete-safe. When any object involved is deleted at any moment,
then the events simply don't occur anymore and memory is freed as appropriate.
Ie, suppose you have a Watched<> object that is being used in multiple
expression, but the object owning the Watched<> object is destroyed...
no problem. The expression will simply not evaluate to true anymore:
the event requests are deleted, the temporary event server is deleted,
no pain. Same when an event client that requested the call back is destroyed:
that fact automatically causes the event server to not call it's call
back method anymore. Deleting an event server, obviously also cleans
up any pending events. In order to get this garbage collecting working
smoothly, I have added one more demand for event clients: they have to
be derived from 'EventClient'. This is not really necessary, but it
doesn't seem a problem to me (one of their methods already has to
know about the event server, as it takes the 'EventType' as parameter,
so the class clearly is event-server-aware anyway).

Regards,

-- 
Carlo Wood <carlo_at_[hidden]>

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk