Using ILU with Python
Introduction
This tutorial will show how to use the ILU system with the programming
language Python, both as a way of developing software libraries, and as a way of
building distributed systems.
A D V E R T I S E M E N T
In an extended example, we'll build an ILU module
that implements a simple four-function calculator, capable of addition,
subtraction, multiplication, and division. It will signal an error if the user
attempts to divide by zero. The example demonstrates how to specify the
interface for the module; how to implement the module in Python; how to use that
implementation as a simple library; how to provide the module as a remote
service; how to write a client of that remote service; and how to use subtyping
to extend an object type and provide different versions of a module. We'll also
demonstrate how to use OMG IDL with ILU, and discuss the notion of network
garbage collection.
Each of the programs and files referenced in this tutorial is available as a
complete program in a separate appendix to this document; parts of programs are
quoted in the text of the tutorial.
Specifying the Interface
Our first task is to specify more exactly what it is we're trying to provide.
A typical four-function calculator lets a user enter a value, then press an
operation key, either +, -, /, or *, then enter another number, then press = to
actually have the operation happen. There's usually a CLEAR button to press to
reset the state of the calculator. We want to provide something like that.
We'll recast this a bit more formally as the interface of our module;
that is, the way the module will appear to clients of its functionality. The
interface typically describes a number of function calls which can be made into
the module, listing their arguments and return types, and describing their
effects. ILU uses object-oriented interfaces, in which the functions in
the interface are grouped into sets, each of which applies to an object type.
These functions are called methods.
For example, we can think of the calculator as an object type, with several
methods: Add, Subtract, Multiply, Divide, Clear, etc. ILU provides a standard
notation to write this down with, called ISL (which stands for "Interface
Specification Language"). ISL is a declarative language which can be processed
by computer programs. It allows you to define object types (with methods), other
non-object types, exceptions, and constants.
The interface for our calculator would be written in ISL as:
INTERFACE Tutorial;
EXCEPTION DivideByZero;
TYPE Calculator = OBJECT
METHODS
SetValue (v : REAL),
GetValue () : REAL,
Add (v : REAL),
Subtract (v : REAL),
Multiply (v : REAL),
Divide (v : REAL) RAISES DivideByZero END
END;
This defines an interface Tutorial, an exception DivideByZero,
and an object type Calculator. Let's consider these one by one.
The interface, Tutorial, is a way of grouping a number of type
and exception definitions. This is important to prevent collisions between names
defined by one group and names defined by another group. For example, suppose
two different people had defined two different object types, with different
methods, but both called Calculator! It would be impossible to tell
which calculator was meant. By defining the Calculator object type
within the scope of the Tutorial interface, this confusion can be
avoided.
The exception, DivideByZero, is a formal name for a particular
kind of error, division by zero. Exceptions in ILU can specify an
exception-value type, as well, which means that real errors of that kind
have a value of the exception-value type associated with them. This allows the
error to contain useful information about why it might have come about. However,
DivideByZero is a simple exception, and has no exception-value type
defined. We should note that the full name of this exception is
Tutorial.DivideByZero, but for this tutorial we'll simply call our
exceptions and types by their short name.
The object type, Calculator (again, really
Tutorial.Calculator), is a set of six methods. Two of those methods,
SetValue and GetValue, allow us to enter a number into
the calculator object, and "read" the number. Note that SetValue
takes a single argument, v, of type REAL. REAL
is a built-in ISL type, denoting a 64-bit floating point number. Built-in ISL
types are things like INTEGER (32-bit signed integer), BYTE
(8-bit unsigned byte), and CHARACTER (16-bit Unicode character).
Other more complicated types are built up from these simple types using ISL
type constructors, such as SEQUENCE OF, RECORD, or
ARRAY OF.
Note also that SetValue does not return a value, and neither do
Add, Subtract, Multiply, or Divide.
Rather, when you want to see what the current value of the calculator is, you
must call GetValue, a method which has no arguments, but which
returns a REAL value, which is the value of the calculator object.
This is an arbitrary decision on our part; we could have written the interface
differently, say as
TYPE NotOurCalculator = OBJECT
METHODS
SetValue () : REAL,
Add (v : REAL) : REAL,
Subtract (v : REAL) : REAL,
Multiply (v : REAL) : REAL,
Divide (v : REAL) : REAL RAISES DivideByZero END
END;
-- but we didn't.
Our list of methods on Calculator is bracketed by the two
keywords METHODS and END, and the elements are
separated from each other by commas. This is pretty standard in ISL: elements of
a list are separated by commas; the keyword END is used when an
explicit list-end marker is needed (but not when it's not necessary, as in the
list of arguments to a method); the list often begins with some keyword, like
METHODS. The raises clause (the list of exceptions which a
method might raise) of the method Divide provides another example
of a list, this time with only one member, introduced by the keyword
RAISES.
Another standard feature of ISL is separating a name, like v,
from a type, like REAL, with a colon character. For example,
constants are defined with syntax like
CONSTANT Zero : INTEGER = 0;
Definitions, of interface, types, constants, and exceptions, are terminated with
a semicolon.
We should expand our interface a bit by adding more documentation on what our
methods actually do. We can do this with the docstring feature of ISL,
which allows the user to add arbitrary text to object type definitions and
method definitions. Using this, we can write
INTERFACE Tutorial;
EXCEPTION DivideByZero
"this error is signalled if the client of the Calculator calls
the Divide method with a value of 0";
TYPE Calculator = OBJECT
COLLECTIBLE
DOCUMENTATION "4-function calculator"
METHODS
SetValue (v : REAL) "Set the value of the calculator to `v'",
GetValue () : REAL "Return the value of the calculator",
Add (v : REAL) "Adds `v' to the calculator's value",
Subtract (v : REAL) "Subtracts `v' from the calculator's value",
Multiply (v : REAL) "Multiplies the calculator's value by `v'",
Divide (v : REAL) RAISES DivideByZero END
"Divides the calculator's value by `v'"
END;
Note that we can use the DOCUMENTATION keyword on object types to
add documentation about the object type, and can simply add documentation
strings to the end of exception and method definitions. These docstrings are
passed on to the Python docstring system, so that they are available at runtime
from Python. Documentation strings cannot currently be used for non-object
types.
ILU provides a program, islscan, which can be used to check the
syntax of an ISL specification. islscan parses the specification
and summarizes it to standard output:
% islscan Tutorial.isl
Interface "Tutorial", imports "ilu"
{defined on line 1
of file /tmp/tutorial/Tutorial.isl (Fri Jan 27 09:41:12 1995)}
Types:
real {<built-in>, referenced on 10 11 12 13 14 15}
Classes:
Calculator {defined on line 17}
methods:
SetValue (v : real); {defined 10, id 1}
"Set the value of the calculator to `v'"
GetValue () : real; {defined 11, id 2}
"Return the value of the calculator"
Add (v : real); {defined 12, id 3}
"Adds `v' to the calculator's value"
Subtract (v : real); {defined 13, id 4}
"Subtracts `v' from the calculator's value"
Multiply (v : real); {defined 14, id 5}
"Multiplies the calculator's value by `v'"
Divide (v : real) {DivideByZero}; {defined 16, id 6}
"Divides the calculator's value by `v'"
documentation:
"4-function calculator"
unique id: ilu:cigqcW09P1FF98gYVOhf5XxGf15
Exceptions:
DivideByZero {defined on line 5, refs 15}
%
islscan simply lists the types defined in the interface,
separating out object types (which it calls "classes"), the exceptions, and the
constants. Note that for the Calculator object type, it also lists
something called its unique id. This is a 160-bit number (expressed in
base 64) that ILU assigns automatically to every type, as a way of
distinguishing them. While it might interesting to know that it exists (:-), the
ILU user never has know what it is; islscan supplies it for the
convenience of the ILU implementors, who sometimes do have to know it.
Implementing the True Module
After we've defined an interface, we then need to supply an implementation of
our module. Implementations can be done in any language supported by ILU. Which
language you choose often depends on what sort of operations have to be
performed in implementing the specific functions of the module. Different
languages have specific advantages and disadvantages in different areas. Another
consideration is whether you wish to use the implementation mainly as a library,
in which case it should probably be done in the same language as the rest of
your applications, or mainly as a remote service, in which case the specific
implementation language is less important.
We'll demonstrate an implementation of the Calculator object
type in Python, which is one of the most capable of all the ILU-supported
languages. This is just a matter of defining a Python class, corresponding to
the Tutorial.Calculator type. Before we do that, though, we'll
explain how the names and signatures of the Python functions are arrived at.
What the Interface Looks Like in Python
For every programming language supported by ILU, there is a standard
mapping defined from ISL to that programming language. This mapping defines
what ISL type names, exception names, method names, and so on look like in that
programming language.
The mapping for Python is straightforward. For type names, such as
Tutorial.Calculator, the Python name of the ISL type Interface.Name
is Interface.Name, with any hyphens replaced by underscores. That
is, the name of the interface in ISL becomes the name of the module in Python.
So the name of our Calculator type in Python would be
Tutorial.Calculator, which is really the name of a Python class.
The Python mapping for a method name such as SetValue is the
method name, with any hyphens replaced by underscores. The return type of this
Python method is whatever is specified in the ISL specification for the method,
or None if no type is specified. The arguments for the Python
method are the same as specified in the ISL; their types are the Python types
corresponding to the ISL types, except that one extra argument is added
to the beginning of each Python version of an ISL method; it is an instance
of the object type on which the method is defined. An instance is simply a value
of that type. Thus the Python method corresponding to our ISL SetValue
would have the prototype signature
Similarly, the signatures for the other methods, in Python, are
def GetValue (self):
def Add (self, v):
def Subtract (self, v):
def Multiply (self, v):
def Divide (self, v):
Note that even though the Divide method can raise an exception, the
signature looks like those of the other methods. This is because the normal
Python exception signalling mechanism is used to signal exceptions back to the
caller. The mapping of exception names is similar to the mapping used for types.
So the exception Tutorial.DivideByZero would also have the name
Tutorial.DivideByZero, in Python.
One way to see what all the Python names for an interface look like is to run
the program python-stubber. This program reads an ISL file, and
generates the necessary Python code to support that interface in Python. One of
the files generated is `Interface.py', which contains the
definitions of all the Python types for that interface.
% python-stubber Tutorial.isl
client stubs for interface "Tutorial" to Tutorial.py ...
server stubs for interface "Tutorial" to Tutorial__skel.py ...
%
Building the Implementation
To provide an implementation of our interface, we subclass the
generated Python class for our Calculator class:
# CalculatorImpl.py
import Tutorial, Tutorial__skel
class Calculator (Tutorial__skel.Calculator):
def __init__ (self):
self.the_value = 0.0
def SetValue (self, v):
self.the_value = v
def GetValue (self):
return self.the_value
def Add (self, v):
self.the_value = self.the_value + v
def Subtract (self, v):
self.the_value = self.the_value - v
def Multiply (self, v):
self.the_value = self.the_value * v
def Divide (self, v):
try:
self.the_value = self.the_value / v
except ZeroDivisionError:
raise Tutorial.DivideByZero
Each instance of a CalculatorImpl.Calculator object inherits
from Tutorial__skel.Calculator, which in turn inherits from
Tutorial.Calculator. Each has an instance variable called the_value,
which maintains a running total of the `accumulator' for that instance. We can
create an instance of a Tutorial.Calculator object by simply
calling CalculatorImpl.Calculator().
So, a very simple program to use the Tutorial module might be
the following:
# simple1.py, a simple program that demonstrates the use of the
# Tutorial true module as a library.
#
# run this with the command "python simple1.py NUMBER [NUMBER...]"
#
import Tutorial, CalculatorImpl, string, sys
# A simple program:
# 1) make an instance of Tutorial.Calculator
# 2) add all the arguments by invoking the Add method
# 3) print the resultant value.
def main (argv):
c = CalculatorImpl.Calculator()
if not c:
error("Couldn't create calculator")
# clear the calculator before using it
c.SetValue (0.0)
# now loop over the arguments, adding each in turn */
for arg in argv[1:]:
v = string.atof(arg)
c.Add (v)
# and print the result
print "the sum is", c.GetValue()
sys.exit(0)
main(sys.argv)
This program would be compiled and run as follows:
% python simple1.py 34.9 45.23111 12
the sum is 92.13111
%
This is a completely self-contained use of the Tutorial
implementation; when a method is called, it is the true method that is invoked.
The use of ILU in this program adds some overhead in terms of included code, but
has almost the same performance as a version of this program that does not use
ILU.
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:
...
# now loop over the arguments, Dividing by each in turn */
try:
for arg in argv[2:]:
v = string.atof(arg)
c.Divide (v)
except:
print 'exception signalled: ' + str(sys.exc_type)
sys.exit(1)
...
And here's an example of what we get when it runs:
% python simple2.py 12345 6 7 8 9
the sum is 4.08234126984
% python 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:
import Tutorial, Tutorial__skel, CalculatorImpl
class Factory (Tutorial__skel.Factory):
# have the __init__ method take handle and server args
# so that we can control which ILU kernel server is used,
# and what the instance handle of the Factory object on
# that server is. This allows us to control the object ID
# of the new Factory instance.
def __init__(self, handle=None, server=None):
self.IluInstHandle = handle
self.IluServer = server
def CreateCalculator (self):
return (CalculatorImpl.Calculator())
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 Python 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:
# server.py -- a program that runs a Tutorial.Calculator server
#
import ilu, FactoryImpl, sys
def main(argv):
if (len(argv) < 2):
print "Usage: python server.py SERVER-ID"
sys.exit(1)
# Create a kernel server with appropriate server ID, which
# is passed in as the first argument
theServer = ilu.CreateServer (argv[1])
# Now create an instance of a Factory object on that server,
# with the instance handle "theFactory"
theFactory = FactoryImpl.Factory ("theFactory", theServer)
# Now make the Factory object "well-known" by publishing it.
theFactory.IluPublish()
# Now we print the string binding handle (the object's name plus
# its location) of the new instance.
print "Factory instance published."
print "Its SBH is '" + theFactory.IluSBH() + "'."
handle = ilu.CreateLoopHandle()
ilu.RunMainLoop (handle)
main(sys.argv)
When we run this program, we'll see something like:
% python server.py 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.
|