Using ILU with ANSI C: A Tutorial
Introduction
This document is a tutorial on how to use the ILU system with the programming
language ANSI C, 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 ANSI C; 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.
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. Documentation strings
cannot currently be used for non-object types. We also use the COLLECTIBLE
keyword to mark the object type as participating in a distributed
`garbage-collection' protocol; this is not discussed in this tutorial, but is
covered in the Python tutorial.
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 ANSI C, which is perhaps the most primitive of all the ILU-supported
languages. This is just a matter of writing 6 C functions, corresponding to the
6 methods defined on the Tutorial.Calculator type. Before we do
that, though, we'll explain how the names and signatures of the C functions are
arrived at.
What the Interface Looks Like in ANSI C
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 ANSI C is straightforward. For type names, such as
Tutorial.Calculator, the C name of the ISL type Interface.Name
is Interface_Name. That is, the period is replaced with an
underscore. So the name of our Calculator type in C would be
Tutorial_Calculator, which is really just a typedef for the
standard ILU-C type ILU_C_Object, the type which all ILU object
types have in C.
The C mapping for a method name such as SetValue, is just the
method name appended to the C name of the of the object type:
Tutorial_Calculator_SetValue. The return type of this C function is
whatever is specified in the ISL specification for the method, or void
if no type is specified. The arguments for the C are the same as specified in
the ISL; their types are the C types corresponding to the ISL types, except
that two extra arguments are added to each C version of an ISL method. The first
extra argument is added at the beginning of the parameter list; it supports the
object-oriented paradigm used in ILU, and is an instance of the object
type on which the method is defined. An instance is simply a value of that type.
The second extra argument is added at the end of the parameter list; it is a
value of type CORBA_Environment *, which is used to pass
meta-information such as the identity of the caller into the function, and to
pass information about exception conditions back to the caller. Thus the C
method corresponding to our ISL SetValue would have the prototype
signature
void
Tutorial_Calculator_SetValue (
Tutorial_Calculator,
CORBA_double v,
CORBA_Environment *
);
Note that the ISL type REAL is mapped to the C type
CORBA_double, which is just another name for double.
Similarly, the signatures for the other methods, in C, are
CORBA_double
Tutorial_Calculator_GetValue (
Tutorial_Calculator,
CORBA_Environment *
);
void
Tutorial_Calculator_Add (
Tutorial_Calculator,
CORBA_double v,
CORBA_Environment *
);
void
Tutorial_Calculator_Subtract (
Tutorial_Calculator,
CORBA_double v,
CORBA_Environment *
);
void
Tutorial_Calculator_Multiply (
Tutorial_Calculator,
CORBA_double v,
CORBA_Environment *
);
void
Tutorial_Calculator_Divide (
Tutorial_Calculator,
CORBA_double v,
CORBA_Environment *
);
Note that even though the Divide method can raise an exception, the
signature looks like those of the other methods. This is because the
CORBA_Environment * parameter is used, in C, to signal exceptions back to
the caller. Exceptions are represented in C by a value of the standard ILU-C
type ILU_C_ExceptionCode. The mapping of exception names is similar
to the mapping used for types, except that exception names are prefixed with the
characters "ex_". So the exception Tutorial.DivideByZero
would have the name ex_Tutorial_DivideByZero, in C.
There is one further refinement of the C mapping we have to know before we
can proceed with the implementation. Object systems in programming languages
typically differentiate between two different kinds of procedure calls,
sometimes called generic functions and methods. The generic
function represents the abstract form of the procedure call, the one that a user
of a routine would call. However, in an object system, invocation of a generic
function might cause one of several different actual subroutines to be called,
because each object type might implement the generic function separately. These
implementations are called methods. This means that in a language like C, there
will at least two names used for each ILU procedure, the name by which a user of
the procedure calls it (the name of the generic function), and the name by which
the implementor of the procedure defines it (the name of the method). The C
procedure names we've been using so far has been the generic function names. The
method name is the same as the generic function name, but with the prefix
server_, as in server_Interface_Type_Method.
ILU generates code that matches an invocation of a function named with its
generic function name to execution of the function named with the true name, or
method name. Since we are making an implementation of the Tutorial
module, we'll use the method names for each of the functions we write. Just to
confuse things a bit more, I should mention that in the ILU world, we call the
generic function a surrogate method, and the method a true method!
One way to see what all the C names for an interface look like is to run the
program c-stubber. This program reads an ISL file, and generates
the necessary C code to support that interface in C. One of the files generated
is `Interface.h', which contains the definitions of all the
C types for that interface, along with prototypes for both the generic functions
and methods.
% c-stubber Tutorial.isl
header file interface Tutorial to ./Tutorial.h...
common code for interface Tutorial to ./Tutorial-common.c...
code for surrogate stubs of interface Tutorial to ./Tutorial-surrogate.c...
code for true stubs of interface Tutorial to ./Tutorial-true.c...
%
A Faulty Implementation
Let's consider a simple implementation of our six true methods:
/* faulty-impl.c */
[ The first thing we need to do is to include the generated header
file, which describes the types and methods used by the Tutorial
interface. ]
#include <Tutorial.h>
[ We'll then define a static variable of type "CORBA_double" to hold the
value of the calculator object, and call it "the_Value". ]
static CORBA_double the_Value = 0.0;
[ Now to implement the method, we simply take the true prototype
and add whatever code is necessary to actually perform the operation. ]
void
server_Tutorial_Calculator_SetValue (
Tutorial_Calculator self,
CORBA_double v,
CORBA_Environment *env)
{
the_Value = v;
}
CORBA_double
server_Tutorial_Calculator_GetValue (
Tutorial_Calculator self,
CORBA_Environment *env)
{
return (the_Value);
}
void
server_Tutorial_Calculator_Add (
Tutorial_Calculator self,
CORBA_double v,
CORBA_Environment *env)
{
the_Value += v;
}
void
server_Tutorial_Calculator_Subtract (
Tutorial_Calculator self,
CORBA_double v,
CORBA_Environment *env)
{
the_Value -= v;
}
void
server_Tutorial_Calculator_Multiply (
Tutorial_Calculator self,
CORBA_double v,
CORBA_Environment *env)
{
the_Value *= v;
}
[ The Divide method gets a little trickier. We have to compare the
value "v" to zero, which for floating point values actually means
comparing it to some epsilon to see whether it is less than that
epsilon, and then if it is "zero" we need to signal an error, by
"raising" the DivideByZero exception. The way of raising exceptions
in ILU C is rather clumsy, so we'll define a macro to make it look
prettier. We also define some macros to make testing the value
of "v" a bit prettier. ]
#define ABS(x) (((x)<0)?(-(x)):(x))
#define SOME_EPSILON 0.000000001 /* zero, practically speaking */
#define RAISE(env,exception) { (env)->returnCode=(exception);\
(env)->_major=CORBA_USER_EXCEPTION; }
void
server_Tutorial_Calculator_Divide (
Tutorial_Calculator self,
CORBA_double v,
CORBA_Environment *env)
{
if (ABS(v) < SOME_EPSILON)
RAISE(env, ex_Tutorial_DivideByZero)
else
the_Value /= v;
}
The problem with this implementation is that all instances of the
Calculator type share the same value. This doesn't seem to mirror the way
real 4-function calculators work. They have individual values that can differ
for different calculators, instead of one shared value. We need to provide a way
for each calculator object to have its own state, its own value.
|