Checking for Exceptions
Suppose, instead of the Add method, we'd called the Divide
method. In that case, we might have had to handle a DivideByZero
exception; that is, notice the exception and do something sensible. We do this
by establishing a handler for the exception:
A D V E R T I S E M E N T
...
try {
//now loop over the arguments, adding each in turn
int i = 1;
while (i<argv.length) {
Double v = Double.valueOf(argv[i]); //don't bother...
calc.Divide(v.doubleValue());
i = i+1;
}
} catch (Tutorial.DivideByZero e) {
System.err.println("raised DivideByZero exception: " + e);
}
And here's an example of what we get when it runs:
% java Tutorial.simple2 12345 6 7 8 9
the sum is 4.08234126984
% java simple2.py 12345 6 0 8 9
exception signalled: Tutorial: DivideByZero
Actually, every method may return an exception, as there are a number of
standard system exceptions which may be signalled even by methods which have no
declared exceptions. So we should check every method to see if it succeeded,
even simple ones like GetValue.
Providing the True Module as a Network Service
Now let's see what's involved in providing the calculator functionality as a
network service. Basically, there are three things to look at:
- providing a "factory" to build calculator objects;
- publishing the name of the factory; and
- writing a server program.
Using Factories to Build Objects
When one program uses code from another address space, it has to get its
hands on an instance of an ILU object, to be able to call methods. In our
library application, we simply made a call into the true module, to create an
instance of the calculator object. In the networked world, we need to do the
same kind of thing, but this time the call into the true module has to be a
method on an object type. In short, we need to have some object type which
exports a method something like
CreateCalculator () : Calculator
There are several ways to provide this. The standard way of doing it is to
add an object type to our Tutorial interface, which contains this
method. This kind of object type is sometimes called a factory, because
it exists only in order to build instances of other object types. We'll add the
following type definition to our `Tutorial.isl':
TYPE Factory = OBJECT
METHODS
CreateCalculator () : Calculator
END;
Then we need to provide an implementation of the Factory object
type, just as we did with the Calculator type:
//we will be lazy and not make an extra file
class FactoryImpl implements Tutorial.Factory {
xerox.ilu.IluServer server;
public FactoryImpl(xerox.ilu.IluServer server) {
this.server = server;
}
public Tutorial.Calculator CreateCalculator()
throws xerox.ilu.IluSystemException
{
Tutorial.Calculator calc = new Tutorial.CalculatorImpl();
Ilu.registerTrueObject(
Ilu.inventID(),
calc,
this.server,
Tutorial.CalculatorStub.iluClass(),
Ilu.unspec
);
return calc;
}
} //FactoryImpl
Now, to provide other programs a way of creating calculator objects, we'll
just create just one instance of Tutorial.Factory, and let programs
call the CreateCalculator method on that at will, to obtain new
calculator objects.
Publishing a Well-Known Instance
The question then arises, how does a program that wants to use the
Factory object get its hands on that one well-known instance? The answer
is to use the simple binding system built into ILU. Simple binding allows
a program acting as a "server" to publish the location of a well-known
object, and allows programs acting as "clients" of that server to look up the
location, given the object's name.
The name of an ILU object instance has two parts, which are the instance
handle of the object, and the name of its kernel server, called the
server ID. (The kernel server is a data structure maintained by the ILU
kernel which takes care of all communication between different address spaces.)
These two combined must form a universally unique ID for the object. Usually you
can simply let the ILU system choose names for your objects automatically, in
which case it takes care to choose names which will never conflict with names in
use by others. However, for objects which we wish to publish, we need to specify
what the name of an object will be, so that users of the well-known object can
find it.
When working with the Java programming language, this act of explicitly
specifying an object name is divided into two steps. First, we create a kernel
server with a specified server ID. Secondly, we create an instance of an object
on this new server, with a specified instance handle. Together, the server ID
and the instance handle form the name of the instance.
For instance, we might use a server ID of Tutorial.domain,
where domain is your Internet domain (typically something like
department.company.com, or department.univerity.edu).
This serves to distinguish your server from other servers on the net. Then we
can use a simple instance handle, like theFactory. The name, or
object ID, of this object would then be theFactory@Tutorial.domain,
where domain would vary from place to place. Note that this implies
that only one instance of this object is going to exist in the whole domain. If
you have many people using different versions of this object in your domain, you
should introduce more qualifiers in the server ID so that your kernel server can
be distinguished from that run by others.
The Server Program
Given this information, we can now write a complete program that will serve
as a provider of calculator objects to other programs. It will create a single
Factory instance with a well-known name, publish that instance,
then hang out servicing methods invoked on its objects. Here's what it looks
like:
package Tutorial;
import Tutorial.CalculatorImpl;
import xerox.ilu.Ilu;
import xerox.ilu.IluException;
import xerox.ilu.IluServer;
import Tutorial.DivideByZero;
//insert the factory code from above here...
public class TutorialServer {
static FactoryImpl factory;
static xerox.ilu.IluServer trueServer;
public static void main(String argv[]) {
try {
String serverId;
if (argv.length < 1) {
System.out.println("Must specify a server id");
return;
}
//Create a server with appropriate server id (which is
//taken from the first argument)
serverId = argv[0];
trueServer = xerox.ilu.IluServer.createServer(serverId);
//Now create an instance of a Factory object on the server
//with an instance handle "theFactory"
factory = new FactoryImpl(trueServer);
xerox.ilu.Ilu.registerTrueObject(
"theFactory",
factory,
trueServer,
Tutorial.FactoryStub.iluClass(),
xerox.ilu.IluLifetimeArgs.iluLifetimeRemember
);
//Make the factory well known by publishing it
xerox.ilu.IluSimpleBinding.publish(factory);
//Now we print the string binding handle (the object's name
//plus its location) of the new Factory instance
System.out.println("Factory instance published");
System.out.println("Its SBH is '" + Ilu.sbhOfObject(factory) + "'");
//the program doesn't terminate because the server is still alive...
} catch (xerox.ilu.IluException e) {
System.out.println("IluException: " + e);
}
}
} //TutorialServer
When we run this program, we'll see something like:
% java Tutorial.TutorialServer Tutorial.dept.company.com &
Factory instance published.
Its SBH is 'theFactory@Tutorial.dept.company.com@somegibberish'.
%
This indicates that the object known as theFactory@Tutorial.dept.company.com
is being exported in a particular way, which is encoded in the
somegibberish part of the string binding handle. Your specific numbers
will vary, but it should look similar.
Using the Network Service
Given that someone has exported a module as a network service, by publishing the
location of a well-known instance of an object type, a potential client of that
module can then use the module by binding to that well-known instance. It does
this by calling the standard ILU routine ilu.LookupObject(), which
takes the name and type of an instance, and attempts to find that instance on
the net. The name of the object is specified as a pair of strings, the server
ID of the object's kernel server, and the instance handle of
the object on that kernel server.
So, in our first example, we could replace the call to
Create_Tutorial_Calculator with a routine that calls
xerox.ilu.IluSimpleBinding.lookup() to find the factory, then creates an
instance of a Calculator. The full code of the revised example,
`simple3.java', is available as section
simple3.java, but here's what the new code for obtaining an instance of a
Calculator looks like:
/* We define a new routine, "Get_Tutorial_Calculator", which
* finds the tutorial factory, then creates a new Calculator
* object for us.
*/
public static Tutorial.Calculator
GetTutorialCalculator(String serverId, String factoryId) {
Tutorial.Factory factory = null;
Tutorial.Calculator calc = null;
System.out.println("Looking up factory");
try {
/* We have to call lookupObject with the object ID of
* the factory object, and the "type" of the object we're looking
* for.
*/
factory = (Tutorial.Factory)
xerox.ilu.IluSimpleBinding.lookup(
serverId,
factoryId,
Tutorial.FactoryStub.iluClass()
);
} catch (xerox.ilu.IluSystemException e) {
System.err.println("Failed to get factory: " + e);
System.exit(1);
}
if (factory==null) {
System.err.println("Got null factory");
System.exit(1);
}
System.out.println("Got factory " + factory);
System.out.println("Looking up Calculator");
try {
calc = factory.CreateCalculator();
} catch (xerox.ilu.IluException e) {
System.err.println("Failed to get Calculator: " + e);
System.exit(1);
}
if (calc==null) {
System.err.println("Got null Calculator");
System.exit(1);
}
System.out.println("Got Calculator " + calc);
return calc;
} //GetTutorialCalculator
We then can use the simple3 program:
% java Tutorial.simple3 Tutorial.dept.company.com theFactory 1 2 3 4 5 6
the sum is 2.10000
%
Subtyping and Other ISL Types
ILU ISL contains support for a number of types other than object types and
REAL. The primitive ISL types include 16, 32, and 64 bit signed and
unsigned integers, bytes, 8 and 16 bit characters, a boolean type, and 32, 64,
and 128 bit floating point types. A number of type constructors allow
specification of arrays, sequences, records, unions, and enumerations, as well
as object types. The ISL OPTIONAL type constructor provides an
implicit union of some type with NULL, which is useful for building
recursive data structures such as linked lists or binary trees.
To illustrate some of these types, we'll extend the Tutorial.Calculator
type. Many real-world desktop calculators include a register tape, a
printed listing of all the operations that have been performed, with a display
of what the value of the calculator was after each operation. We'll add a
register tape to Tutorial.Calculator.
We could do it by adding a new method to Tutorial.Calculator,
called GetTape. Unfortunately, this would break our existing code,
because it would change the Tutorial.Calculator object type, and
existing compiled clients wouldn't be able to recognize the new object type.
Instead, we'll extend the object type by subtyping; that is, by creating
a new object type which uses Tutorial.Calculator as a supertype,
but adds new methods of its own. This subtype will actually have two types; both
its own new type, and Tutorial.Calculator. We'll also define a
subtype of the Tutorial.Factory type, to allow us to create new
instances of the new Calculator subtype. Finally, we'll define a
new module interface for the new types, so that we don't have to modify the
Tutorial interface.
First, let's define the necessary type to represent the operations performed
on the calculator:
INTERFACE Tutorial2 IMPORTS Tutorial END;
TYPE OpType = ENUMERATION
SetValue, Add, Subtract, Multiply, Divide END;
TYPE Operation = RECORD
op : OpType,
value : REAL,
accumulator : REAL
END;
TYPE RegisterTape = SEQUENCE OF Operation;
The enumerated type OpType defines an abstract type with five
possible values. The type Operation defines a record type (in Java,
a dictionary) with 3 fields: the op field, which tells us which of
the five possible calculator operations was performed, the value
field, which tells us the value of the operand for the operation, and the
accumulator field, which tells us what the value of the calculator was
after the operation had been performed. Finally, the Operation type
is a simple sequence, or list, of Operation. Note that
Tutorial2 imports Tutorial; that is, it allows the
use of the Tutorial types, exceptions, and constants, in the
specifications in Tutorial2.
Now we define the new object types (in the same file):
TYPE TapeCalculator = OBJECT COLLECTIBLE
SUPERTYPES Tutorial.Calculator END
DOCUMENTATION "4 function calculator with register tape"
METHODS
GetTape () : RegisterTape
END;
TYPE Factory = OBJECT SUPERTYPES Tutorial.Factory END
METHODS
CreateTapeCalculator () : TapeCalculator
END;
The SUPERTYPES attribute of an object type may take multiple object
type names, so ISL supports multiple inheritance. The
Tutorial2.TapeCalculator type will now support the six methods of
Tutorial.Calculator, as well as its own method, GetTape.
We then need to provide an implementation for Tutorial2.TapeCalculator
(We will be lazy again and put the actual code in the file with the server). We
modify each method on the TapeCalculator object to record its
invocation, and add a slot to hold the contents of the `tape'. We also provide
an implementation for Tutorial2.Factory:
// lazy again... this is in other file
class Factory2Impl implements Tutorial2.Factory {
public Factory2Impl() {
}
public Tutorial.Calculator CreateCalculator()
throws xerox.ilu.IluSystemException {
System.out.println("Factory2Impl: request for a simple calculator");
return new Tutorial2.TapeCalculatorImpl();
} //CreateCalculator
public Tutorial2.TapeCalculator CreateTapeCalculator()
throws xerox.ilu.IluSystemException {
System.out.println("Factory2Impl: request for a tape calculator");
return new Tutorial2.TapeCalculatorImpl();
} //CreateTapeCalculator
} //Factory2Impl
Note that both the Tutorial2.FactoryImpl.CreateCalculator and
Tutorial2.FactoryImpl.CreateTapeCalculator methods create and return
instances of Tutorial2.TapeCalculator. This is valid, because
instances of Tutorial2.TapeCalculator are also instances of
Tutorial.Calculator.
Now we modify `TutorialServer.java' to create an instance of
Tutorial2.Factory, instead of Tutorial.Factory, and to
initialize the Tutorial2 true-side code
Note that one nice result of this approach to versioning is that old clients,
which know nothing about the new TapeCalculator class, or about the
whole Tutorial2 interface in general, will continue to function,
since every instance of Tutorial2.TapeCalculator is also an
instance of Tutorial.Calculator, and every instance of
Tutorial2.Factory is also an instance of Tutorial.Factory.
|