Topic: Callbacks -- suggested approaches??
Author: davidm@consilium.com (David S. Masterson)
Date: 26 Jan 93 01:13:43 GMT Raw View
I'm looking for suggestions on how to handle callback functions in C++ that
will be more standard than the one we're currently using.
Our application is a distributed system composed of many processes performing
specialized functions. Each process is represented as a Process object with
specializations derived from this base. Processes communicate by passing
Message objects that contains any necessary data. The receiving Process
object looks into the incoming Message, determines its type (a number), and
dispatches it (via table lookup) to a callback function on the Process object
that has been previously registered for that type of Message. The table where
messages are looked up is (basically) of the following definition:
typedef void (Process::*Method)(Message*);
struct Callback {
Method method
Process* methodthis; // could be multi-threaded
} table[MAXCALLBACKS];
Methods would be dispatched like:
void Process::Dispatch(Message* m) {
return (table[Lookup(m)].methodthis->*
table[Lookup(m)].method)(m);
}
The registration method for callbacks looks like:
void Process::Register(Method m, Process* p) {
table[i].method = m;
table[i].methodthis = p;
}
and is called like:
mainproc->Register((Method) MYProcess::callback1, myproc);
My problem that precipitated this posting was the Method cast in that last
statement. Our applications could have potentially hundreds of callbacks in
any one process (especially the database interface), so keeping the callbacks
that a process could handle with the process seemed to be a win for
maintenance. However, that last statement doesn't seem to be a legal one as
callback1 is not necessarily declared as a Method and the explicit cast is
overriding the warning that you would get. Much of this design was done
originally with a C++ 2.0 compiler, but we are now moving to a C++ 2.1
compiler and trying to clean up the design in the process. Is there a more
appropriate design for the above (that, hopefully, doesn't mean scrapping
everything)? I've been looking at the idea of Functors, but that seems like a
maintenance nightmare in that there would be potentially hundreds of
single-function functors lying around in our system (a table lookup seems a
much more straightforward approach).
Any ideas?
--
====================================================================
David Masterson Consilium, Inc.
(415) 691-6311 640 Clyde Ct.
davidm@consilium.com Mtn. View, CA 94043
====================================================================
I try to stay away from liberalism. I know how dangerous it is for
our... Don't ask me to take a liberal position where I am right now.
It's contrary to the administration's viewpoints. It would not be
terribly helpful to anyone, especially me.
-- Vice President Dan Quayle.
Author: hlf@nic.cerf.net (Howard Ferguson)
Date: 26 Jan 1993 21:10:08 GMT Raw View
In article <1k41efINN2oj@darkstar.UCSC.EDU> spencer@cats.ucsc.edu (Michael Spencer) writes:
>
>davidm@consilium.com (David S. Masterson) writes:
>
>>I'm looking for suggestions on how to handle callback functions in C++ that
>>will be more standard than the one we're currently using.
>
>Would the following approach be helpful. If so, drop me a message.
>
>
> WIDGET CALLBACKS
>
>Widgets which use callbacks are similarly easy. XSimple widgets attach
>their own callback and pass to it a reference to a coder written
>Procedure. The callback calls the Procedure and passes to it a
>Choice object which contains the data needed to respond to the event.
>For example, to activate a popup dialog widget:
>
> PopupYesno (parent, "Do it ?", YesnoProc); // evoke popup
>
> void YesnoProc (Choice chose) // coder written to handle event
> {
> if (chose.flag)
> // do something
> }
> else
> // do something else
> }
>
>The coder written YesnoProc() defines what occurs based on the
>user's election. Choice is an object (declared in XUTILS.H) which
>will hold any information generated by any callback or event handler.
>
>BUT, the coder need not become involved in the sordid details of
>adding the callback to the widget or extracting the relevant
>information from the returned callback struct. That is done
>automatically and the relevant information is passed via the
>Choice data members.
Yes but the coder does have to get involved in the sordid details
of the prototype of the YesnoProc. Ther type of the function being
passed in has to be considered. In good old C the function would
be automatically cast to whatever form it was stored in.
hlf
Author: rmartin@stripe.Rational.COM (Bob Martin)
Date: Tue, 26 Jan 1993 23:21:17 GMT Raw View
davidm@consilium.com (David S. Masterson) writes:
|Processes communicate by passing
|Message objects that contains any necessary data. The receiving Process
|object looks into the incoming Message, determines its type (a number), and
|dispatches it (via table lookup) to a callback function on the Process object
|that has been previously registered for that type of Message.
Passing messages between processes is always a challenge, especially
when what you really want to do is pass messages between two objects
which happen to live in different processes. It would be very nice if
the sending and receiving objects did not know that the message was
crossing a process boundary. Below is described a technique for
achieving this.
Given that class A wants to send the message X to class B. (This is the
OO concept of a message. I will refer to the data exchanged between
processes as a packet.)
A::f(B* theB)
{
theB->X();
}
Unfortunately, theB lives in another process. No problem. Create an
abstract class called AbstractB. It contains a set of pure virtual
functions defining all the messages that B can receive. B will
inherit from AbstractB. So will another class called BSurrogate.
BSurrogate knows which process the real B object lives in and knows
how to convert the message into a packet and ship it to that process.
The packet will contain all the arguments to X, an identifier denoting
that it represents an X message and the identifier of the B object to
which the message should be sent.
When the process receives the it looks up the name of the object in a
dictionary. The dictionary associates the name with a derivative of
the class Reciever. BReceiver is derived from Receiver, and knows how
to unpack the X packet. It also knows where the B object is (Probably
it contains a pointer to it), so it unpacks all the arguments and then
sends the message to its B object.
Return values can be passed back by the same route by simply having
the Surrogate add a return address onto the packet.
--
Robert Martin | Design Consulting | Training courses offered:
R. C. M. Consulting | rmartin@rational.com | Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004 | Object Oriented Design
Green Oaks, Il 60048| Fax: (708) 918-1023 | C++
Author: davidm@consilium.com (David S. Masterson)
Date: 28 Jan 93 18:58:05 GMT Raw View
>>>>> On 26 Jan 93 23:21:17 GMT, rmartin@stripe.Rational.COM (Bob Martin) said:
> Given that class A wants to send the message X to class B.
> Unfortunately, theB lives in another process. No problem. Create an
> abstract class called AbstractB. It contains a set of pure virtual
> functions defining all the messages that B can receive. B will
> inherit from AbstractB. So will another class called BSurrogate.
> BSurrogate knows which process the real B object lives in and knows
> how to convert the message into a packet and ship it to that process.
> The packet will contain all the arguments to X, an identifier denoting
> that it represents an X message and the identifier of the B object to
> which the message should be sent.
> When the process receives the it looks up the name of the object in a
> dictionary. The dictionary associates the name with a derivative of
> the class Reciever. BReceiver is derived from Receiver, and knows how
> to unpack the X packet. It also knows where the B object is (Probably
> it contains a pointer to it), so it unpacks all the arguments and then
> sends the message to its B object.
> Return values can be passed back by the same route by simply having
> the Surrogate add a return address onto the packet.
In a sense, this is what we've done -- our modelling is somewhat different
than your's, though. The best way I could think of describing the difference
between your model and our model is to look at the IOStream library. In the
model you describe above, you are basically defining the user interface for
the B::operator<< while we are describing the definition of an Ostream that
will carry the data that operator<< builds to its intended destination. The
next level (or layer) in our system is really the same as your description.
The problem in what you describe is still one of dispatching the message
received by a process to the appropriate object that will handle the message.
The issue is what does the dictionary look like and how does the process read
it and know what to call. Here you must assume that the number of messages
that the dictionary can interpret is sufficiently large and diverse (ie. not
all the same type) in order to make the solution interesting. For the most
part, the question is one of syntax allowed by C++ (ie. what's the most
type-safe way of doing it?) rather than one of modelling (there are many
models that may be chosen).
--
====================================================================
David Masterson Consilium, Inc.
(415) 691-6311 640 Clyde Ct.
davidm@consilium.com Mtn. View, CA 94043
====================================================================
"I'll put an end to the idea that a woman's body belongs to her . . . the
practice of abortion shall be exterminated with a strong hand."
-- Adolf Hitler, _Mein Kampf_
Author: davidm@consilium.com (David S. Masterson)
Date: 28 Jan 93 19:10:29 GMT Raw View
>>>>> On 26 Jan 93 18:55:11 GMT, spencer@cats.ucsc.edu (Michael Spencer) said:
> WIDGET CALLBACKS
> Widgets which use callbacks are similarly easy. XSimple widgets attach their
> own callback and pass to it a reference to a coder written Procedure. The
> callback calls the Procedure and passes to it a Choice object which contains
> the data needed to respond to the event. For example, to activate a popup
> dialog widget:
> PopupYesno (parent, "Do it ?", YesnoProc); // evoke popup
> void YesnoProc (Choice chose) // coder written to handle event
> {
> if (chose.flag)
> // do something
> else
> // do something else
> }
> The coder written YesnoProc() defines what occurs based on the user's
> election. Choice is an object (declared in XUTILS.H) which will hold any
> information generated by any callback or event handler.
> BUT, the coder need not become involved in the sordid details of adding the
> callback to the widget or extracting the relevant information from the
> returned callback struct. That is done automatically and the relevant
> information is passed via the Choice data members.
An interesting variation on the X-windows approach. However, I see a few
(what I would call) holes in what you've shown above:
1. I assume PopupYesno is an object. If so, why isn't YesnoProc() a member
function of the object? Wouldn't that help for maintenance?
2. Your Choice object appears to carry a type flag. Couldn't things be done
virtually and eliminate this flag?
3. What if the information generated for a callback is not of type Choice?
--
====================================================================
David Masterson Consilium, Inc.
(415) 691-6311 640 Clyde Ct.
davidm@consilium.com Mtn. View, CA 94043
====================================================================
"I'll put an end to the idea that a woman's body belongs to her . . . the
practice of abortion shall be exterminated with a strong hand."
-- Adolf Hitler, _Mein Kampf_
Author: rmartin@thor.Rational.COM (Bob Martin)
Date: Thu, 28 Jan 1993 23:57:12 GMT Raw View
davidm@consilium.com (David S. Masterson) writes:
|The problem in what you describe is still one of dispatching the message
|received by a process to the appropriate object that will handle the message.
|The issue is what does the dictionary look like and how does the process read
|it and know what to call. Here you must assume that the number of messages
|that the dictionary can interpret is sufficiently large and diverse (ie. not
|all the same type) in order to make the solution interesting. For the most
|part, the question is one of syntax allowed by C++ (ie. what's the most
|type-safe way of doing it?) rather than one of modelling (there are many
|models that may be chosen).
All messages derive from an abstract class Packet. Although the
structure of each packet is different, they all have one thing in
common, they know the ID of the object that they are being sent to.
The process receiving the packet asks the packet who it is for and
then looks that ID up in a Dictionary. The Dictionary associates the
ID with a Receiver object. Receiver is an abstract base class which
knows how to receive a Packet. Subclasses of Receiver know how to
decipher a packet and build a message for the target object. They
also contain a pointer to the target object so that they can send the
message.
The only type-system problem in this scheme is in the Receiver which
must accept a generic Packet and then decide what "Type" of packet it
is. It may than be forced to downcast the packet to the correct type.
Even this downcast may be eliminated if a reasonable presentation
layer scheme is used to codify the packet. Something like X.209 (the
companion to ASN.1)
--
Robert Martin | Design Consulting | Training courses offered:
R. C. M. Consulting | rmartin@rational.com | Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004 | Object Oriented Design
Green Oaks, Il 60048| Fax: (708) 918-1023 | C++
Author: nikki@trmphrst.demon.co.uk (Nikki Locke)
Date: Thu, 28 Jan 1993 13:27:12 +0000 Raw View
In article <DAVIDM.93Jan25171343@consilium.com> davidm@consilium.com (David S. Masterson) writes:
> I'm looking for suggestions on how to handle callback functions in C++ that
> will be more standard than the one we're currently using.
..
> typedef void (Process::*Method)(Message*);
> struct Callback {
> Method method
> Process* methodthis; // could be multi-threaded
> } table[MAXCALLBACKS];
>
> Methods would be dispatched like:
>
> void Process::Dispatch(Message* m) {
> return (table[Lookup(m)].methodthis->*
> table[Lookup(m)].method)(m);
> }
>
> The registration method for callbacks looks like:
>
> void Process::Register(Method m, Process* p) {
> table[i].method = m;
> table[i].methodthis = p;
> }
>
> and is called like:
>
> mainproc->Register((Method) MYProcess::callback1, myproc);
>
Personally, I use templates (or generics) for this sort of thing.
Something like ...
class CallbackBase
{
public:
virtual void despatch(Message *m) = 0;
virtual ~CallbackBase() {} // Just in case someone creates
// a new kind of Callback which needs a destructor
};
template <class X> class Callback : public CallbackBase
{
void (X::*method)(Message *);
X& methodthis;
public:
Callback(void (X::*m)(Message *), X& t) : method(m), methodthis(t) {}
void despatch(Message *m) { (methodthis->*method)(m); }
};
Your table is of CallbackBase pointers.
Methods would be dispatched like:
void Process::Dispatch(Message* m)
{
table[Lookup(m)]->despatch(m);
}
The registration method for callbacks looks like:
void Process::Register(CallbackBase *p)
{
table[i].callback = p;
}
and may be called like:
mainproc->Register(new Callback<MYProcess>(MYProcess::callback1, myproc));
You do have to take care of deleting the CallbackBase items, but that can
probably be done in the Process destructor.
[Note - code not compiled or tested, but I do use something very similar.]
--
Nikki Locke,Trumphurst Ltd.(PC and Unix consultancy) nikki@trmphrst.demon.co.uk
trmphrst.demon.co.uk is NOT affiliated with ANY other sites at demon.co.uk.
Author: bill@amber.csd.harris.com (Bill Leonard)
Date: 28 Jan 1993 22:41:49 GMT Raw View
In article <rmartin.728090477@stripe>, rmartin@stripe.Rational.COM (Bob Martin) writes:
> Passing messages between processes is always a challenge, especially
> when what you really want to do is pass messages between two objects
> which happen to live in different processes. It would be very nice if
> the sending and receiving objects did not know that the message was
> crossing a process boundary. Below is described a technique for
> achieving this.
>
> Unfortunately, theB lives in another process. No problem. Create an
> abstract class called AbstractB. It contains a set of pure virtual
> functions defining all the messages that B can receive. B will
> inherit from AbstractB. So will another class called BSurrogate.
> BSurrogate knows which process the real B object lives in and knows
> how to convert the message into a packet and ship it to that process.
> The packet will contain all the arguments to X, an identifier denoting
> that it represents an X message and the identifier of the B object to
> which the message should be sent.
>
> When the process receives the it looks up the name of the object in a
> dictionary. The dictionary associates the name with a derivative of
> the class Reciever. BReceiver is derived from Receiver, and knows how
> to unpack the X packet. It also knows where the B object is (Probably
> it contains a pointer to it), so it unpacks all the arguments and then
> sends the message to its B object.
We have implemented a scheme very much like this in a project here, with
some minor differences. In so doing we have learned some lessons that
I will pass on for what they're worth. First, a brief description of
our scheme:
For each class of object that needs to implement inter-process
communication, we define 3 classes: There's the AbstractB as Bob says
above, there's the BSurrogate derived from AbstractB, and there's also the
BReal derived from AbstractB. AbstractB just defines the interface to the
B class of objects. BSurrogate implements the interface by passing
messages to another process. BReal is the actual implementation of the
functionality of B.
We also define a class called Router (similar to Bob's Receiver class).
For each class B, there is a BRouter class, derived from Router, that
receives the messages that BSurrogate sent and calls the appropriate
function using a pointer to a BReal object.
By the way, we don't "look up the object in a dictionary". We just pass
the address of the BRouter object as part of the message; the generic
message handler casts that value to a Router * and calls a virtual function
using the resulting pointer. (More on this below...)
Now, the lessons learned:
1. Adding a function to the interface to B is a real pain! You must add
the function signature in 3 places: AbstractB, BSurrogate, and BReal.
You generally also have to invent a new type of message, which entails
adding code to BRouter to know how to interpret that new message.
2. Getting these objects created is often non-trivial. Someone has to
create the BReal object in the process where he is to reside. That
someone must typically also create the BRouter object, who needs to
keep a pointer to the BReal object around so he can call member
functions of it. Then the BSurrogate object must be created in the
other process, and he needs to know some ID for the BRouter object so
that messages he sends get to the right place.
We've had a few cases where the BSurrogate and BReal objects each
needed to talk to the other. That further complicates the creation,
usually entailing having one of the objects send a message to the
other one telling him the address he should use if he wants to
initiate messages in the other direction.
3. Destroying these objects is likewise tricky.
4. If, for some reason, you need to make copies of a B object, then that
entails having BSurrogate implement a copy constructor that can send a
message over to BReal telling him to make a copy of himself and his
BRouter.
5. If I had to do this over, I would make the BReal object use multiple
inheritance, deriving it from both the AbstractB class and the Router
class. That way, the BReal object can be its *own* Router, thus
eliminating the need to create an extra object and coordinating its
creation and destruction.
6. If you ever screw up a message and pass the wrong Router pointer, you
will get very bizarre and hard-to-track-down bugs. This method has
the advantage of being fast, but we still talk about adding some kind
of dictionary so that we can at least verify that the Router pointer
we have is valid.
We made a small attempt at automating the creation of the BSurrogate and
BRouter classes, with no success, but that doesn't mean it couldn't be
done. If I had to do this for a very large number of classes and
functions, I'd probably develop a very-high-level language of some kind to
describe the interface, so that I could automate at least parts of this.
Well, hope this helps somebody...
--
Bill Leonard
Harris Computer Systems Division
2101 W. Cypress Creek Road
Fort Lauderdale, FL 33309
bill@ssd.csd.harris.com
These opinions and statements are my own and do not reflect the opinions or
positions of Harris Corporation.
---------------------------------------------------------------------------
Prism: A place for light waves that commit minor refractions.
---------------------------------------------------------------------------
Author: bcohen@scherzo.mentorg.com (Speaker-to-Managers)
Date: Wed, 03 Feb 1993 21:18:44 GMT Raw View
In article <1k9nfdINN56n@travis.csd.harris.com>, bill@amber.csd.harris.com (Bill Leonard) writes:
|> In article <rmartin.728090477@stripe>, rmartin@stripe.Rational.COM (Bob Martin) writes:
|>
|> We have implemented a scheme very much like this in a project here, with
|> some minor differences. In so doing we have learned some lessons that
|> I will pass on for what they're worth. First, a brief description of
|> our scheme:
|>
|> For each class of object that needs to implement inter-process
|> communication, we define 3 classes: There's the AbstractB as Bob says
|> above, there's the BSurrogate derived from AbstractB, and there's also the
|> BReal derived from AbstractB. AbstractB just defines the interface to the
|> B class of objects. BSurrogate implements the interface by passing
|> messages to another process. BReal is the actual implementation of the
|> functionality of B.
|>
|> We also define a class called Router (similar to Bob's Receiver class).
|> For each class B, there is a BRouter class, derived from Router, that
|> receives the messages that BSurrogate sent and calls the appropriate
|> function using a pointer to a BReal object.
|>
|> By the way, we don't "look up the object in a dictionary". We just pass
|> the address of the BRouter object as part of the message; the generic
|> message handler casts that value to a Router * and calls a virtual function
|> using the resulting pointer. (More on this below...)
Yet another implementation with some lessons learned. In 1991, as part
of a demonstration project, I prototyped a remote object system similar
to the ones Bill Leonard and Bob Martin describe. It was more like
Bob's system in that I had Surrogate m(I called this a Deputy) and Real
classes for each type of object which could engage in interprocess
method invocation. The differences were primarily in the communication
architecture, resulting from a particular set of requirements.
What I actually needed to do was have a framework of objects in one
process act as the model for any number of other processes. The Model
(also called the Server, for obvious reasons) maintained a shared state
for a (possibly time-varying) set of other processes (called Views or
Clients), and would synchronize change requests from the Clients. In
addition, any change to the Model's state resulted in asynchronous
notification of all Clients registered with the Server as interested
inthat part of the state.
The obvious partitioning of responsibility in the system was to make the
granularity of interest for notification be one of the objects in the
Model, and to allow each View to construct a Deputy for that object.
The communication both synch and asynch between Deputy and Real objects
was the responsibility of a Client object in the View and a Server
object in the model. Each maintained dictionaries to map local objects
to remote ones, and each View could have multiple Clients, so that there
could be multiple Models for each View as well as multiple Views for
each model.
I bring all this up because it demonstrates what I think is an important
lesson: the design of a remote invocation system needs to depend to a
great extent on the communication requirements among the various
objects, and in particular on what the application running on top of the
objects will expect to do with them. I chose to implement a general
remote method send (read "virtual member function call") capability with
a transparent notification mechanism; this meant that there had to be
central Server and Client objects to guarantee the needed
synchronization. If I hadn't been implementing a set of cooperating
objects in a framework, I might have built a less complex, but more
generally useful mechanism.
|>
|> Now, the lessons learned:
|>
|> 1. Adding a function to the interface to B is a real pain! You must add
|> the function signature in 3 places: AbstractB, BSurrogate, and BReal.
|> You generally also have to invent a new type of message, which entails
|> adding code to BRouter to know how to interpret that new message.
Yes, it surely is. I went through about three different designs before
settling on a set of dictionaries in the server to map Deputies to Real
Objects, function call signatures to packet formats, and asynch
notifications to interest lists. I thought hard about automating the
procedure somehow, but never had the time to implement a scheme in
detail that would be flexible enough to suit me. Maybe if there had
been another rev ...? :-)
|>
|> 2. Getting these objects created is often non-trivial. Someone has to
|> create the BReal object in the process where he is to reside. That
|> someone must typically also create the BRouter object, who needs to
|> keep a pointer to the BReal object around so he can call member
|> functions of it. Then the BSurrogate object must be created in the
|> other process, and he needs to know some ID for the BRouter object so
|> that messages he sends get to the right place.
Again, some of this may fall out of the application architecture you
need. In my case, the View would create DeputyModel object (thinking it
was a real model of course, in fact with the throwing of a compile-time
switch, it was, and the whole system ran in a single process). The
DeputyModel would return pointers to Deputies for objects in the Model
interface when requested by the Client. All objects created in this way
are automagically registered in the dictionaries of the Client and
Server objects.
|> 3. Destroying these objects is likewise tricky.
Destruction in my system was handled by the Client/Server pair: the
Deputy goes away normally, informing the Client, which cleans up by
te3lling the Server. The Server decrements a reference count in the
real object, or otherwise marks it, depending on the lifetime model for
the object, then destroys it when appropriate.
|> 5. If I had to do this over, I would make the BReal object use multiple
|> inheritance, deriving it from both the AbstractB class and the Router
|> class. That way, the BReal object can be its *own* Router, thus
|> eliminating the need to create an extra object and coordinating its
|> creation and destruction.
I tried both this and delegation, and am convinced that neither by
itself is satisfactory. If I were still working on the project I would
try combining MI and templates.
|> 6. If you ever screw up a message and pass the wrong Router pointer, you
|> will get very bizarre and hard-to-track-down bugs. This method has
|> the advantage of being fast, but we still talk about adding some kind
|> of dictionary so that we can at least verify that the Router pointer
|> we have is valid.
Definitely, the dictionaries are worth the trouble.
|> We made a small attempt at automating the creation of the BSurrogate and
|> BRouter classes, with no success, but that doesn't mean it couldn't be
|> done. If I had to do this for a very large number of classes and
|> functions, I'd probably develop a very-high-level language of some kind to
|> describe the interface, so that I could automate at least parts of this.
My thought was to simply add some keywords like Deputy to my code and
build some sort of preprocessor to generate the C++ from that. Of
course, the preprocessor should also design the packet format by
analyzing the signature of the virtual functions, much as rpcgen does.
------------------------------------------------------------------------
Bruce Cohen, Mentor Graphics Corpooration | email: Bruce_Cohen@mentorg.com
8005 SW Boeckman Road | phone: (503)685-1808
Wilsonville, OR 97070-7777 |