This is a spell-checked, split-infinitive-repaired, typo-removed and slightly corrected version of my posting from yesterday. Also, note that the "Last Last Last" point has been deleted ;-) HOW TO CONVERT A METHOD INTERFACE INTO A (SHARED VARIANT) CALL CHANNEL ====================================================================== Base Plan ========= Consider an object, B, with a method interface, Foo, and an object, A, that wishes to `communicate' with B using Foo. For example: /////////////////////////////////////////////////////////////////////// interface Foo { public Thing calculate (...); public void processQuery (...); public Silly closeValve (...); } /////////////////////////////////////////////////////////////////////// All we do is give A a constructor (or mutator method) that takes a Foo argument, construct an instance of A and give it an instance of B (where B implements Foo). This is standard Java OO ... :-( Now, suppose we want A and B to be active processes with their own thread(s) of control. Sticking with the above is an open invitation to race hazards and countering them leads us into all that Java monitor goo. Instead, we need to convert that Foo interface into a (SHARED) variant CALL channel. [Aside: occam3 did not propose *variant* CALL channels - I'll mail about this separately.] A simple low-fuss way of doing this is to connect A and B by an ordinary channel: ______________________ ______________________ | | | | | | c | | | A |------->-------| B implements Foo | | | | | |______________________| |______________________| The channel is not used to communicate any data. It's used purely for synchronising the calls to the Foo interface of B by A - channels are really simple things to use for synchronisation! So, if A wishes to call any of the Foo methods, it first must obtain permission from B - who will then block until told the call is complete: c.write (null); // ready to make the CALL Thing result = B.calculate (...); // or some other Foo method c.write (null); // the CALL is complete At the other end, B must choose to service the CALL and then wait until it's all over: c.read (); // ready to ACCEPT the CALL c.read (); // wait until the CALL is complete where, of course, the first channel input can be part of an ALT :-). We have the full flexibility of method invocation - an arbitrary number of parameters and an optional result. It's variant since *any* of the Foo methods may be invoked by the CALL. We have full occam/CSP synchronisation - the CALL cannot proceed until both processes agree to participate in it. Whoever commits first has to wait for its partner. Crucially, either side may refuse to play ball. The actual Foo method call is made by the caller process. But the callee process is suspended. Semantically, it is the same as the caller and callee processes being `joined' for the duration of the call. This is the semantics we want. To make a SHARED CALL channel (i.e. many callers), we can't simply use a many-1 channel for c (because the channel needs to be locked to a single caller for two writes). We could achieve this with a channel technique like Gerald's claim/release methods. Another way is to use two channels: the first one is made many-1 and is used for the first communication (the `claim') and second need only be 1-1 and is used for the second (`release'). Or we can simply lock the CALL sequence using a special lock object (and leave the channel as 1-1): Object cLock = new Object (); ... synchronise (cLock) { c.write (null); // ready to make the CALL Thing result = B.calculate (...); // or some other Foo method c.write (null); // the CALL is complete } There are two obvious problems with this simple approach: (a) A still sees B directly (or, at least, has a reference to it as a Foo-implementing object - which amounts to the same thing); (b) both A and B have to abide by the above pattern for the CALL. It would go wrong if A were just to invoke B.calculate directly ... or *any* of the channel operations were omitted on either side. The (a) problem is serious and independent from the (b) one. One of the beauties of channel interfaces is that they completely decouple processes from each other. Processes see only their channels. They don't see each other. These things apart, the above is a simple enough design pattern that can be used straight away (with either JCSP or CJT) to give us variant CALL channels. What follows is an attempt to capture this securely in the JCSP context. Securing the Base Plan ====================== We need to wrap up the above design pattern into a class and methods that enforce it. This class will be the base One2OneCallChannel, from which users may specialise to produce interface-specific CALL channels ... like One2OneFooChannel. The full range of Many2One/One2Many/Many2Many CALL channels will also be made available. I tried very hard to make a One2OneCallChannel by *inheriting* from the basic One2OneChannel. That would have given us ALTing for free. As usual, inheritance doesn't quite work - defeated again by the lack of any safe multiple inheritance and the fact that Java interfaces may only contain public (and not package-visible) methods. Sigh. This makes no difference to the users, but makes it slightly more tedious to do the wrapping. The Callee-Interface to the CALL Channel ---------------------------------------- We define: /////////////////////////////////////////////////////////////////////// package jcsp.lang; public interface ChannelAccept { public void accept (Object o); } /////////////////////////////////////////////////////////////////////// The callee process will have a ChannelAccept field, c say, for the CALL channel associated with the Foo interface. If it wishes to accept such at call, all it does is: c.accept (this); which registers itself (and, by implication, its willingness to accept the call) inside the channel. When this method returns, the call (to one of its Foo methods) will have been made. If we wish to be able to ALT over CALL channel acceptances, we use: /////////////////////////////////////////////////////////////////////// package jcsp.lang; public abstract class AltingChannelAccept extends Guard implements ChannelAccept { // nothing to add ... } /////////////////////////////////////////////////////////////////////// The reason this isn't an interface is that Guard contains package-visible method headers (enable/disable) - these are of no concern to JCSP users. The callee process can add an AltingChannelAccept variable, c say, to a Guard array used in constructing an Alternative. If its index is selected, we are committed to respond with: c.accept (this); That's it for the callee interface. We can manage any number of interfaces with individual CALL channels and ALT between them (and ordinary channels, timeouts and SKIPs). The Caller-Interface to the CALL Channel ---------------------------------------- This has to be Foo-specific ... in fact, it is just Foo! The caller process will have a Foo field, c say, into which will be plugged the CALL channel - not the target process. To make a call, all we do is: Thing result = c.calculate (...); or whatever Foo method we wish to CALL. The Base CALL Channel Classes ----------------------------- These are concrete implementations of the AltingChannelAccept abstraction -- similar to the way One2OneChannel (etc.) are implementations of AltingChannel. The difference is that we don't implement the caller side of the CALL channel. The caller side is application specific and needs to be set up by the user. Here is the `One2One' base class. It *contains* a One2OneChannel rather than inherits from it (see above for reasons). This means propagating its package-visible enable/disable methods (that are used for ALTing) in the usual trivial way. JCSP users don't need to know or care about this though. /////////////////////////////////////////////////////////////////////// package jcsp.lang; public abstract class One2OneCallChannel extends AltingChannelAccept { final private One2OneChannel c = new One2OneChannel (); protected Object target; // made available to the caller public void accept (Object target) { // invoked by the callee this.target = target; c.read (); // ready to ACCEPT the CALL c.read (); // wait until the CALL is complete } protected void sync () { // indirectly invoked by the caller c.write (null); } boolean enable (Alternative alt) { // ignore this! return c.enable (alt); } boolean disable () { // ignore this! return c.disable (); } } /////////////////////////////////////////////////////////////////////// Ignoring the enable/disable methods, see how the accept works. It's invoked by the callee process with itself as the parameter. We save that parameter and commit to reading from the channel. The callee will be blocked here until the caller makes a CALL (see below). When the caller makes that CALL, it will write to the protected channel (by invoking `sync'). The caller will be blocked until the callee has invoked an accept (see above). When both thus rendezvous, the callee immediately blocks on reading from that channel again. Meanwhile, the caller makes its required Foo method call on the callee target - a reference to which is safely saved in this CALL channel. When that completes, the caller again writes to the real channel (c), unblocking the callee. Both, then, go their separate ways. Application-Specific CALL Channels ---------------------------------- We have set up an inheritance mechanism for letting the user define specific CALL channels that support the particular method interface need for their application. Usually, something goes very wrong whenever I try to design anything for use via inheritance ... so I'm keeping my fingers crossed about this one. [It could be changed to remove this inheritance mandate if that proves necessary (and it probably will).] Anyway, here goes for our Foo interface. Define: /////////////////////////////////////////////////////////////////////// class One2OneFooChannel extends One2OneCallChannel implements Foo { public Thing calculate (...) { sync (); // ready to make the CALL Thing t = ((Foo) target).calculate (...); sync (); // call finished return t; } ... similarly for processQuery and closeValve } /////////////////////////////////////////////////////////////////////// That's all the user has to generate. All that has to be done is surround the method call with a pair of `sync's and return the result (if any). All `sync' does is write to the private channel inside One2OneCallChannel. The explanation in the section above talks through how these work. Example Use of a CALL Channel ----------------------------- Let's go back to the earlier example: ______________________ ______________________ | | | | | | c | | | A |------->-------| B implements Foo | | | | | |______________________| |______________________| where, this time, c is going to be a CALL channel. First, here is an outline of an A process: /////////////////////////////////////////////////////////////////////// class A implements CSProcess { private final Foo out; public A (final Foo out) { this.out = out; } ... other fields, methods etc. public run () { ... Thing t = out.calculate (...); ... Silly s = out.closeValve (...); ... out.processQuery (...); ... } } /////////////////////////////////////////////////////////////////////// And here is an outline for a B process: /////////////////////////////////////////////////////////////////////// class B implements CSProcess, Foo { private final ChannelAccept in; public B (final ChannelAccept in) { this.in = in; } ... other fields, methods etc. ... implementation of Foo methods (calculate, processQuery and closeValve) public run () { ... in.accept (this); ... in.accept (this); ... in.accept (this); ... } } /////////////////////////////////////////////////////////////////////// When B commits to an accept, it doesn't know which of its Foo methods will be called. This is just like a variant (CASE) protocol in occam. If it isn't prepared to accept certain of those methods, it must only be at times in the exchange with the caller when it knows the caller won't be making them. Otherwise, the methods must not be grouped in the same CALL channel and we must ALT between the resulting different CALL channels, refusing acceptance on some of them in the usual ways. It might be an idea to get the accept method to return some indication of which CALL method was actually invoked by the caller. That's pretty easy to do - for example, by an extra protected INT field of the base CALL channel class (One2OneCallChannel) that gets set up by the caller in the user-defined subclass (e.g. One2OneFooChannel) and returned by the accept method. Of course, calls and accepts can occur anywhere in the process codes - not just straight-line stuff as in the above outlines. Finally, the network code is very neat: One2OneFooChannel c = new One2OneFooChannel (); new Parallel ( new CSProcess[] { new A (c), new B (c) } ).run (); and looks just how it should look. Many-1, 1-Many and Many-Many CALL Channels ------------------------------------------ As said in the Base Plan, we can't simply replace the One2OneChannel in the base class, One2OneCallChannel, with a One2ManyChannel (say) and obtain a safe One2ManyCallChannel (say). Here is one way to do it: /////////////////////////////////////////////////////////////////////// package jcsp.lang; public abstract class One2ManyCallChannel implements ChannelAccept { final private One2OneChannel c = new One2OneChannel (); final private Object acceptLock = new Object (); protected Object target; // made available to the caller public void accept (Object target) { // invoked by the callee synchronized (acceptLock) { this.target = target; c.read (); // ready to ACCEPT the CALL c.read (); // wait until the CALL is complete } } protected void sync () { // indirectly invoked by the caller c.write (null); } } /////////////////////////////////////////////////////////////////////// Notice that, as usual, we don't allow ALTing over '2Many channels. To make the respective One2ManyFooChannel, extend the above class as before: /////////////////////////////////////////////////////////////////////// class One2ManyFooChannel extends One2ManyCallChannel implements Foo { public Thing calculate (...) { sync (); // ready to make the CALL Thing t = ((Foo) target).calculate (...); sync (); // call finished return t; } ... similarly for processQuery and closeValve } /////////////////////////////////////////////////////////////////////// For the Many2' classes, we don't need to provide base classes. Users specialising the One2OneCallChannel or One2ManyCallChannel base classes just need to remember on more thing - synchronise the specialist methods. For example: /////////////////////////////////////////////////////////////////////// class Many2OneFooChannel extends One2OneCallChannel implements Foo { public synchronized Thing calculate (...) { sync (); // ready to make the CALL Thing t = ((Foo) target).calculate (...); sync (); // call finished return t; } ... similarly for processQuery and closeValve } /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// class Many2ManyFooChannel extends One2ManyCallChannel implements Foo { public synchronized Thing calculate (...) { sync (); // ready to make the CALL Thing t = ((Foo) target).calculate (...); sync (); // call finished return t; } ... similarly for processQuery and closeValve } /////////////////////////////////////////////////////////////////////// Note that callers and callees in the Many2Many case will be (Java) synchronising on different objects, so they will not be deadlocking each other! One Last Point -------------- I don't like the callee class (like B) advertising a standard method interface (like Foo). In the above example, there remains the danger that someone might try and invoke one of the Foo methods directly on an instance of B. Or plug an instance of B directly into an instance of A! Neither of these would be a good idea!!! It's also semantically misleading. B's interface is in its CALL channel -- not its Foo interface! So, at the risk of upsetting the OO community once again: > OO-people like method interfaces. If we don't provide a method interface > to your object, we get the chant: "that's not very OO therefore it's bad". let's hide that method interface and leave only the channel one visible. The way to do this is through *process* layering (which imposes absolutely no run-time penalty): /////////////////////////////////////////////////////////////////////// class B2 implements CSProcess { // no Foo interface final private B b; public B2 (final FooChannel in) { b = new B (in); } public run () { b.run (); } } /////////////////////////////////////////////////////////////////////// Now try and invoke a Foo method on an instance of B2 (or plug one into A)! FooChannel, by the way, is just the union of Foo and ChannelAccept: /////////////////////////////////////////////////////////////////////// interface FooChannel extends Foo, ChannelAccept {} /////////////////////////////////////////////////////////////////////// This also forces a network builder to use the correct kind of wire when connecting up to B2. For this trick to work, the various concrete xxx2xxxFooChannels had better implement FooChannel (rather than just Foo). The network diagram looks cleaner: ______________________ ______________________ | | | | | | c | | | A |------->--------| B2 | | | <FooChannel> | | |______________________| |______________________| and the network code can't be wrong - or it won't compile: One2OneFooChannel c = new One2OneFooChannel (); new Parallel ( new CSProcess[] { new A (c), new B2 (c) } ).run (); Note that any of the xxx2xxxFooChannels could be used to make connections to A or B2. For the above two process example, only the One2One version is needed. One Last Last Point ------------------- This mechanism isn't quite as good as the (proposed) occam3 one - except that it exists! In occam3, the accepts operate on local process state. They do here - but only for class variables ... not on locals declared within the method executing the accept :-( ... Peter.