blob: 88f5778fa5a12ccdeeb1a993987f91c63901f428 [file] [log] [blame]
\documentstyle[11pt]{article}
\newcommand{\Cpp}{C\protect\raisebox{.18ex}{++}}
\title{
Interactively Testing Remote Servers Using the Python Programming Language
}
\author{
Guido van Rossum \\
Dept. AA, CWI, P.O. Box 94079 \\
1090 GB Amsterdam, The Netherlands \\
E-mail: {\tt guido@cwi.nl}
\and
Jelke de Boer \\
HIO Enschede; P.O.Box 1326 \\
7500 BH Enschede, The Netherlands
}
\begin{document}
\maketitle
\begin{abstract}
This paper describes how two tools that were developed quite
independently gained in power by a well-designed connection between
them. The tools are Python, an interpreted prototyping language, and
AIL, a Remote Procedure Call stub generator. The context is Amoeba, a
well-known distributed operating system developed jointly by the Free
University and CWI in Amsterdam.
As a consequence of their integration, both tools have profited:
Python gained usability when used with Amoeba --- for which it was not
specifically developed --- and AIL users now have a powerful
interactive tool to test servers and to experiment with new
client/server interfaces.%
\footnote{
An earlier version of this paper was presented at the Spring 1991
EurOpen Conference in Troms{\o} under the title ``Linking a Stub
Generator (AIL) to a Prototyping Language (Python).''
}
\end{abstract}
\section{Introduction}
Remote Procedure Call (RPC) interfaces, used in distributed systems
like Amoeba
\cite{Amoeba:IEEE,Amoeba:CACM},
have a much more concrete character than local procedure call
interfaces in traditional systems. Because clients and servers may
run on different machines, with possibly different word size, byte
order, etc., much care is needed to describe interfaces exactly and to
implement them in such a way that they continue to work when a client
or server is moved to a different machine. Since machines may fail
independently, error handling must also be treated more carefully.
A common approach to such problems is to use a {\em stub generator}.
This is a program that takes an interface description and transforms
it into functions that must be compiled and linked with client and
server applications. These functions are called by the application
code to take care of details of interfacing to the system's RPC layer,
to implement transformations between data representations of different
machines, to check for errors, etc. They are called `stubs' because
they don't actually perform the action that they are called for but
only relay the parameters to the server
\cite{RPC}.
Amoeba's stub generator is called AIL, which stands for Amoeba
Interface Language
\cite{AIL}.
The first version of AIL generated only C functions, but an explicit
goal of AIL's design was {\em retargetability}: it should be possible
to add back-ends that generate stubs for different languages from the
same interface descriptions. Moreover, the stubs generated by
different back-ends must be {\em interoperable}: a client written in
Modula-3, say, should be able to use a server written in C, and vice
versa.
This interoperability is the key to the success of the marriage
between AIL and Python. Python is a versatile interpreted language
developed by the first author. Originally intended as an alternative
for the kind of odd jobs that are traditionally solved by a mixture of
shell scripts, manually given shell commands, and an occasional ad hoc
C program, Python has evolved into a general interactive prototyping
language. It has been applied to a wide range of problems, from
replacements for large shell scripts to fancy graphics demos and
multimedia applications.
One of Python's strengths is the ability for the user to type in some
code and immediately run it: no compilation or linking is necessary.
Interactive performance is further enhanced by Python's concise, clear
syntax, its very-high-level data types, and its lack of declarations
(which is compensated by run-time type checking). All this makes
programming in Python feel like a leisure trip compared to the hard
work involved in writing and debugging even a smallish C program.
It should be clear by now that Python will be the ideal tool to test
servers and their interfaces. Especially during the development of a
complex server, one often needs to generate test requests on an ad hoc
basis, to answer questions like ``what happens if request X arrives
when the server is in state Y,'' to test the behavior of the server
with requests that touch its limitations, to check server responses to
all sorts of wrong requests, etc. Python's ability to immediately
execute `improvised' code makes it a much better tool for this
situation than C.
The link to AIL extends Python with the necessary functionality to
connect to arbitrary servers, making the server testbed sketched above
a reality. Python's high-level data types, general programming
features, and system interface ensure that it has all the power and
flexibility needed for the job.
One could go even further than this. Current distributed operating
systems, based on client-server interaction, all lack a good command
language or `shell' to give adequate access to available services.
Python has considerable potential for becoming such a shell.
\subsection{Overview of this Paper}
The rest of this paper contains three major sections and a conclusion.
First an overview of the Python programming language is given. Next
comes a short description of AIL, together with some relevant details
about Amoeba. Finally, the design and construction of the link
between Python and AIL is described in much detail. The conclusion
looks back at the work and points out weaknesses and strengths of
Python and AIL that were discovered in the process.
\section{An Overview of Python}
Python%
\footnote{
Named after the funny TV show, not the nasty reptile.
}
owes much to ABC
\cite{ABC},
a language developed at CWI as a programming language for non-expert
computer users. Python borrows freely from ABC's syntax and data
types, but adds modules, exceptions and classes, extensibility, and
the ability to call system functions. The concepts of modules,
exceptions and (to some extent) classes are influenced strongly by
their occurrence in Modula-3
\cite{Modula-3}.
Although Python resembles ABC in many ways, there is a a clear
difference in application domain. ABC is intended to be the only
programming language for those who use a computer as a tool, but
occasionally need to write a program. For this reason, ABC is not
just a programming language but also a programming environment, which
comes with an integrated syntax-directed editor and some source
manipulation commands. Python, on the other hand, aims to be a tool
for professional (system) programmers, for whom having a choice of
languages with different feature sets makes it possible to choose `the
right tool for the job.' The features added to Python make it more
useful than ABC in an environment where access to system functions
(such as file and directory manipulations) are common. They also
support the building of larger systems and libraries. The Python
implementation offers little in the way of a programming environment,
but is designed to integrate seamlessly with existing programming
environments (e.g. UNIX and Emacs).
Perhaps the best introduction to Python is a short example. The
following is a complete Python program to list the contents of a UNIX
directory.
\begin{verbatim}
import sys, posix
def ls(dirname): # Print sorted directory contents
names = posix.listdir(dirname)
names.sort()
for name in names:
if name[0] != '.': print name
ls(sys.argv[1])
\end{verbatim}
The largest part of this program, in the middle starting with {\tt
def}, is a function definition. It defines a function named {\tt ls}
with a single parameter called {\tt dirname}. (Comments in Python
start with `\#' and extend to the end of the line.) The function body
is indented: Python uses indentation for statement grouping instead of
braces or begin/end keywords. This is shorter to type and avoids
frustrating mismatches between the perception of grouping by the user
and the parser. Python accepts one statement per line; long
statements may be broken in pieces using the standard backslash
convention. If the body of a compound statement is a single, simple
statement, it may be placed on the same line as the head.
The first statement of the function body calls the function {\tt
listdir} defined in the module {\tt posix}. This function returns a
list of strings representing the contents of the directory name passed
as a string argument, here the argument {\tt dirname}. If {\tt
dirname} were not a valid directory name, or perhaps not even a
string, {\tt listdir} would raise an exception and the next statement
would never be reached. (Exceptions can be caught in Python; see
later.) Assuming {\tt listdir} returns normally, its result is
assigned to the local variable {\tt names}.
The second statement calls the method {\tt sort} of the variable {\tt
names}. This method is defined for all lists in Python and does the
obvious thing: the elements of the list are reordered according to
their natural ordering relationship. Since in our example the list
contains strings, they are sorted in ascending \ASCII{} order.
The last two lines of the function contain a loop that prints all
elements of the list whose first character isn't a period. In each
iteration, the {\tt for} statement assigns an element of the list to
the local variable {\tt name}. The {\tt print} statement is intended
for simple-minded output; more elaborate formatting is possible with
Python's string handling functions.
The other two parts of the program are easily explained. The first
line is an {\tt import} statement that tells the interpreter to import
the modules {\tt sys} and {\tt posix}. As it happens these are both
built into the interpreter. Importing a module (built-in or
otherwise) only makes the module name available in the current scope;
functions and data defined in the module are accessed through the dot
notation as in {\tt posix.listdir}. The scope rules of Python are
such that the imported module name {\tt posix} is also available in
the function {\tt ls} (this will be discussed in more detail later).
Finally, the last line of the program calls the {\tt ls} function with
a definite argument. It must be last since Python objects must be
defined before they can be used; in particular, the function {\tt ls}
must be defined before it can be called. The argument to {\tt ls} is
{\tt sys.argv[1]}, which happens to be the Python equivalent of {\tt
\$1} in a shell script or {\tt argv[1]} in a C program's {\tt main}
function.
\subsection{Python Data Types}
(This and the following subsections describe Python in quite a lot of
detail. If you are more interested in AIL, Amoeba and how they are
linked with Python, you can skip to section 3 now.)
Python's syntax may not have big surprises (which is exactly as it
should be), but its data types are quite different from what is found
in languages like C, Ada or Modula-3. All data types in Python, even
integers, are `objects'. All objects participate in a common garbage
collection scheme (currently implemented using reference counting).
Assignment is cheap, independent of object size and type: only a
pointer to the assigned object is stored in the assigned-to variable.
No type checking is performed on assignment; only specific operations
like addition test for particular operand types.
The basic object types in Python are numbers, strings, tuples, lists
and dictionaries. Some other object types are open files, functions,
modules, classes, and class instances; even types themselves are
represented as objects. Extension modules written in C can define
additional object types; examples are objects representing windows and
Amoeba capabilities. Finally, the implementation itself makes heavy
use of objects, and defines some private object types that aren't
normally visible to the user. There is no explicit pointer type in
Python.
{\em Numbers}, both integers and floating point, are pretty
straightforward. The notation for numeric literals is the same as in
C, including octal and hexadecimal integers; precision is the same as
{\tt long} or {\tt double} in C\@. A third numeric type, `long
integer', written with an `L' suffix, can be used for arbitrary
precision calculations. All arithmetic, shifting and masking
operations from C are supported.
{\em Strings} are `primitive' objects just like numbers. String
literals are written between single quotes, using similar escape
sequences as in C\@. Operations are built into the language to
concatenate and to replicate strings, to extract substrings, etc.
There is no limit to the length of the strings created by a program.
There is no separate character data type; strings of length one do
nicely.
{\em Tuples} are a way to `pack' small amounts of heterogeneous data
together and carry them around as a unit. Unlike structure members in
C, tuple items are nameless. Packing and unpacking assignments allow
access to the items, for example:
\begin{verbatim}
x = 'Hi', (1, 2), 'World' # x is a 3-item tuple,
# its middle item is (1, 2)
p, q, r = x # unpack x into p, q and r
a, b = q # unpack q into a and b
\end{verbatim}
A combination of packing and unpacking assignment can be used as
parallel assignment, and is idiom for permutations, e.g.:
\begin{verbatim}
p, q = q, p # swap without temporary
a, b, c = b, c, a # cyclic permutation
\end{verbatim}
Tuples are also used for function argument lists if there is more than
one argument. A tuple object, once created, cannot be modified; but
it is easy enough to unpack it and create a new, modified tuple from
the unpacked items and assign this to the variable that held the
original tuple object (which will then be garbage-collected).
{\em Lists} are array-like objects. List items may be arbitrary
objects and can be accessed and changed using standard subscription
notation. Lists support item insertion and deletion, and can
therefore be used as queues, stacks etc.; there is no limit to their
size.
Strings, tuples and lists together are {\em sequence} types. These
share a common notation for generic operations on sequences such as
subscription, concatenation, slicing (taking subsequences) and
membership tests. As in C, subscripts start at 0.
{\em Dictionaries} are `mappings' from one domain to another. The
basic operations on dictionaries are item insertion, extraction and
deletion, using subscript notation with the key as subscript. (The
current implementation allows only strings in the key domain, but a
future version of the language may remove this restriction.)
\subsection{Statements}
Python has various kinds of simple statements, such as assignments
and {\tt print} statements, and several kinds of compound statements,
like {\tt if} and {\tt for} statements. Formally, function
definitions and {\tt import} statements are also statements, and there
are no restrictions on the ordering of statements or their nesting:
{\tt import} may be used inside a function, functions may be defined
conditionally using an {\tt if} statement, etc. The effect of a
declaration-like statement takes place only when it is executed.
All statements except assignments and expression statements begin with
a keyword: this makes the language easy to parse. An overview of the
most common statement forms in Python follows.
An {\em assignment} has the general form
\vspace{\itemsep}
\noindent
{\em variable $=$ variable $= ... =$ variable $=$ expression}
\vspace{\itemsep}
It assigns the value of the expression to all listed variables. (As
shown in the section on tuples, variables and expressions can in fact
be comma-separated lists.) The assignment operator is not an
expression operator; there are no horrible things in Python like
\begin{verbatim}
while (p = p->next) { ... }
\end{verbatim}
Expression syntax is mostly straightforward and will not be explained
in detail here.
An {\em expression statement} is just an expression on a line by
itself. This writes the value of the expression to standard output,
in a suitably unambiguous way, unless it is a `procedure call' (a
function call that returns no value). Writing the value is useful
when Python is used in `calculator mode', and reminds the programmer
not to ignore function results.
The {\tt if} statement allows conditional execution. It has optional
{\tt elif} and {\tt else} parts; a construct like {\tt
if...elif...elif...elif...else} can be used to compensate for the
absence of a {\em switch} or {\em case} statement.
Looping is done with {\tt while} and {\tt for} statements. The latter
(demonstrated in the `ls' example earlier) iterates over the elements
of a `sequence' (see the discussion of data types below). It is
possible to terminate a loop with a {\tt break} statement or to start
the next iteration with {\tt continue}. Both looping statements have
an optional {\tt else} clause which is executed after the loop is
terminated normally, but skipped when it is terminated by {\tt break}.
This can be handy for searches, to handle the case that the item is
not found.
Python's {\em exception} mechanism is modelled after that of Modula-3.
Exceptions are raised by the interpreter when an illegal operation is
tried. It is also possible to explicitly raise an exception with the
{\tt raise} statement:
\vspace{\itemsep}
\noindent
{\tt raise {\em expression}, {\em expression}}
\vspace{\itemsep}
The first expression identifies which exception should be raised;
there are several built-in exceptions and the user may define
additional ones. The second, optional expression is passed to the
handler, e.g. as a detailed error message.
Exceptions may be handled (caught) with the {\tt try} statement, which
has the following general form:
\vspace{\itemsep}
\noindent
{\tt
\begin{tabular}{l}
try: {\em block} \\
except {\em expression}, {\em variable}: {\em block} \\
except {\em expression}, {\em variable}: {\em block} \\
... \\
except: {\em block}
\end{tabular}
}
\vspace{\itemsep}
When an exception is raised during execution of the first block, a
search for an exception handler starts. The first {\tt except} clause
whose {\em expression} matches the exception is executed. The
expression may specify a list of exceptions to match against. A
handler without an expression serves as a `catch-all'. If there is no
match, the search for a handler continues with outer {\tt try}
statements; if no match is found on the entire invocation stack, an
error message and stack trace are printed, and the program is
terminated (interactively, the interpreter returns to its main loop).
Note that the form of the {\tt except} clauses encourages a style of
programming whereby only selected exceptions are caught, passing
unanticipated exceptions on to the caller and ultimately to the user.
This is preferable over a simpler `catch-all' error handling
mechanism, where a simplistic handler intended to catch a single type
of error like `file not found' can easily mask genuine programming
errors --- especially in a language like Python which relies strongly
on run-time checking and allows the catching of almost any type of
error.
Other common statement forms, which we have already encountered, are
function definitions, {\tt import} statements and {\tt print}
statements. There is also a {\tt del} statement to delete one or more
variables, a {\tt return} statement to return from a function, and a
{\tt global} statement to allow assignments to global variables.
Finally, the {\tt pass} statement is a no-op.
\subsection{Execution Model}
A Python program is executed by a stack-based interpreter.
When a function is called, a new `execution environment' for it is
pushed onto the stack. An execution environment contains (among other
data) pointers to two `symbol tables' that are used to hold variables:
the local and the global symbol table. The local symbol table
contains local variables of the current function invocation (including
the function arguments); the global symbol table contains variables
defined in the module containing the current function.
The `global' symbol table is thus only global with respect to the
current function. There are no system-wide global variables; using
the {\tt import} statement it is easy enough to reference variables
that are defined in other modules. A system-wide read-only symbol
table is used for built-in functions and constants though.
On assignment to a variable, by default an entry for it is made in the
local symbol table of the current execution environment. The {\tt
global} command can override this (it is not enough that a global
variable by the same name already exists). When a variable's value is
needed, it is searched first in the local symbol table, then in the
global one, and finally in the symbol table containing built-in
functions and constants.
The term `variable' in this context refers to any name: functions and
imported modules are searched in exactly the same way.
Names defined in a module's symbol table survive until the end of the
program. This approximates the semantics of file-static global
variables in C or module variables in Modula-3. A module is
initialized the first time it is imported, by executing the text of
the module as a parameterless function whose local and global symbol
tables are the same, so names are defined in module's symbol table.
(Modules implemented in C have another way to define symbols.)
A Python main program is read from standard input or from a script
file passed as an argument to the interpreter. It is executed as if
an anonymous module was imported. Since {\tt import} statements are
executed like all other statements, the initialization order of the
modules used in a program is defined by the flow of control through
the program.
The `attribute' notation {\em m.name}, where {\em m} is a module,
accesses the symbol {\em name} in that module's symbol table. It can
be assigned to as well. This is in fact a special case of the
construct {\em x.name} where {\em x} denotes an arbitrary object; the
type of {\em x} determines how this is to be interpreted, and what
assignment to it means.
For instance, when {\tt a} is a list object, {\tt a.append} yields a
built-in `method' object which, when called, appends an item to {\tt a}.
(If {\tt a} and {\tt b} are distinct list objects, {\tt a.append} and
{\tt b.append} are distinguishable method objects.) Normally, in
statements like {\tt a.append(x)}, the method object {\tt a.append} is
called and then discarded, but this is a matter of convention.
List attributes are read-only --- the user cannot define new list
methods. Some objects, like numbers and strings, have no attributes
at all. Like all type checking in Python, the meaning of an attribute
is determined at run-time --- when the parser sees {\em x.name}, it
has no idea of the type of {\em x}. Note that {\em x} here does not
have to be a variable --- it can be an arbitrary (perhaps
parenthesized) expression.
Given the flexibility of the attribute notation, one is tempted to use
methods to replace all standard operations. Yet, Python has kept a
small repertoire of built-in functions like {\tt len()} and {\tt
abs()}. The reason is that in some cases the function notation is
more familiar than the method notation; just like programs would
become less readable if all infix operators were replaced by function
calls, they would become less readable if all function calls had to be
replaced by method calls (and vice versa!).
The choice whether to make something a built-in function or a method
is a matter of taste. For arithmetic and string operations, function
notation is preferred, since frequently the argument to such an
operation is an expression using infix notation, as in {\tt abs(a+b)};
this definitely looks better than {\tt (a+b).abs()}. The choice
between make something a built-in function or a function defined in a
built-in method (requiring {\tt import}) is similarly guided by
intuition; all in all, only functions needed by `general' programming
techniques are built-in functions.
\subsection{Classes}
Python has a class mechanism distinct from the object-orientation
already explained. A class in Python is not much more than a
collection of methods and a way to create class instances. Class
methods are ordinary functions whose first parameter is the class
instance; they are called using the method notation.
For instance, a class can be defined as follows:
\begin{verbatim}
class Foo:
def meth1(self, arg): ...
def meth2(self): ...
\end{verbatim}
A class instance is created by
{\tt x = Foo()}
and its methods can be called thus:
\begin{verbatim}
x.meth1('Hi There!')
x.meth2()
\end{verbatim}
The functions used as methods are also available as attributes of the
class object, and the above method calls could also have been written
as follows:
\begin{verbatim}
Foo.meth1(x, 'Hi There!')
Foo.meth2(x)
\end{verbatim}
Class methods can store instance data by assigning to instance data
attributes, e.g.:
\begin{verbatim}
self.size = 100
self.title = 'Dear John'
\end{verbatim}
Data attributes do not have to be declared; as with local variables,
they spring into existence when assigned to. It is a matter of
discretion to avoid name conflicts with method names. This facility
is also available to class users; instances of a method-less class can
be used as records with named fields.
There is no built-in mechanism for instance initialization. Classes
by convention provide an {\tt init()} method which initializes the
instance and then returns it, so the user can write
\begin{verbatim}
x = Foo().init('Dr. Strangelove')
\end{verbatim}
Any user-defined class can be used as a base class to derive other
classes. However, built-in types like lists cannot be used as base
classes. (Incidentally, the same is true in \Cpp{} and Modula-3.) A
class may override any method of its base classes. Instance methods
are first searched in the method list of their class, and then,
recursively, in the method lists of their base class. Initialization
methods of derived classes should explicitly call the initialization
methods of their base class.
A simple form of multiple inheritance is also supported: a class can
have multiple base classes, but the language rules for resolving name
conflicts are somewhat simplistic, and consequently the feature has so
far found little usage.
\subsection{The Python Library}
Python comes with an extensive library, structured as a collection of
modules. A few modules are built into the interpreter: these
generally provide access to system libraries implemented in C such as
mathematical functions or operating system calls. Two built-in
modules provide access to internals of the interpreter and its
environment. Even abusing these internals will at most cause an
exception in the Python program; the interpreter will not dump core
because of errors in Python code.
Most modules however are written in Python and distributed with the
interpreter; they provide general programming tools like string
operations and random number generators, provide more convenient
interfaces to some built-in modules, or provide specialized services
like a {\em getopt}-style command line option processor for
stand-alone scripts.
There are also some modules written in Python that dig deep in the
internals of the interpreter; there is a module to browse the stack
backtrace when an unhandled exception has occurred, one to disassemble
the internal representation of Python code, and even an interactive
source code debugger which can trace Python code, set breakpoints,
etc.
\subsection{Extensibility}
It is easy to add new built-in modules written in C to the Python
interpreter. Extensions appear to the Python user as built-in
modules. Using a built-in module is no different from using a module
written in Python, but obviously the author of a built-in module can
do things that cannot be implemented purely in Python.
In particular, built-in modules can contain Python-callable functions
that call functions from particular system libraries (`wrapper
functions'), and they can define new object types. In general, if a
built-in module defines a new object type, it should also provide at
least one function that creates such objects. Attributes of such
object types are also implemented in C; they can return data
associated with the object or methods, implemented as C functions.
For instance, an extension was created for Amoeba: it provides wrapper
functions for the basic Amoeba name server functions, and defines a
`capability' object type, whose methods are file server operations.
Another extension is a built-in module called {\tt posix}; it provides
wrappers around post UNIX system calls. Extension modules also
provide access to two different windowing/graphics interfaces: STDWIN
\cite{STDWIN}
(which connects to X11 on UNIX and to the Mac Toolbox on the
Macintosh), and the Graphics Library (GL) for Silicon Graphics
machines.
Any function in an extension module is supposed to type-check its
arguments; the interpreter contains a convenience function to
facilitate extracting C values from arguments and type-checking them
at the same time. Returning values is also painless, using standard
functions to create Python objects from C values.
On some systems extension modules may be dynamically loaded, thus
avoiding the need to maintain a private copy of the Python interpreter
in order to use a private extension.
\section{A Short Description of AIL and Amoeba}
An RPC stub generator takes an interface description as input. The
designer of a stub generator has at least two choices for the input
language: use a suitably restricted version of the target language, or
design a new language. The first solution was chosen, for instance,
by the designers of Flume, the stub generator for the Topaz
distributed operating system built at DEC SRC
\cite{Flume,Evolving}.
Flume's one and only target language is Modula-2+ (the predecessor of
Modula-3). Modula-2+, like Modula-N for any N, has an interface
syntax that is well suited as a stub generator input language: an
interface module declares the functions that are `exported' by a
module implementation, with their parameter and return types, plus the
types and constants used for the parameters. Therefore, the input to
Flume is simply a Modula-2+ interface module. But even in this ideal
situation, an RPC stub generator needs to know things about functions
that are not stated explicitly in the interface module: for instance,
the transfer direction of VAR parameters (IN, OUT or both) is not
given. Flume solves this and other problems by a mixture of
directives hidden in comments and a convention for the names of
objects. Thus, one could say that the designers of Flume really
created a new language, even though it looks remarkably like their
target language.
\subsection{The AIL Input Language}
Amoeba uses C as its primary programming language. C function
declarations (at least in `Classic' C) don't specify the types of
the parameters, let alone their transfer direction. Using this as
input for a stub generator would require almost all information for
the stub generator to be hidden inside comments, which would require a
rather contorted scanner. Therefore we decided to design the input
syntax for Amoeba's stub generator `from scratch'. This gave us the
liberty to invent proper syntax not only for the transfer direction of
parameters, but also for variable-length arrays.
On the other hand we decided not to abuse our freedom, and borrowed as
much from C as we could. For instance, AIL runs its input through the
C preprocessor, so we get macros, include files and conditional
compilation for free. AIL's type declaration syntax is a superset of
C's, so the user can include C header files to use the types declared
there as function parameter types --- which are declared using
function prototypes as in \Cpp{} or Standard C\@. It should be clear by
now that AIL's lexical conventions are also identical to C's. The
same is true for its expression syntax.
Where does AIL differ from C, then? Function declarations in AIL are
grouped in {\em classes}. Classes in AIL are mostly intended as a
grouping mechanism: all functions implemented by a server are grouped
together in a class. Inheritance is used to form new groups by adding
elements to existing groups; multiple inheritance is supported to join
groups together. Classes can also contain constant and type
definitions, and one form of output that AIL can generate is a header
file for use by C programmers who wish to use functions from a
particular AIL class.
Let's have a look at some (unrealistically simple) class definitions:
\begin{verbatim}
#include <amoeba.h> /* Defines `capability', etc. */
class standard_ops [1000 .. 1999] {
/* Operations supported by most interfaces */
std_info(*, out char buf[size:100], out int size);
std_destroy(*);
};
\end{verbatim}
This defines a class called `standard\_ops' whose request codes are
chosen by AIL from the range 1000-1999. Request codes are small
integers used to identify remote operations. The author of the class
must specify a range from which AIL chooses, and class authors must
make sure they avoid conflicts, e.g. by using an `assigned number
administration office'. In the example, `std\_info' will be assigned
request code 1000 and `std\_destroy' will get code 1001. There is
also an option to explicitly assign request codes, for compatibility
with servers with manually written interfaces.
The class `standard\_ops' defines two operations, `std\_info' and
`std\_destroy'. The first parameter of each operation is a star
(`*'); this is a placeholder for a capability that must be passed when
the operation is called. The description of Amoeba below explains the
meaning and usage of capabilities; for now, it is sufficient to know
that a capability is a small structure that uniquely identifies an
object and a server or service.
The standard operation `std\_info' has two output parameters: a
variable-size character buffer (which will be filled with a short
descriptive string of the object to which the operation is applied)
and an integer giving the length of this string. The standard
operation `std\_destroy' has no further parameters --- it just
destroys the object, if the caller has the right to do so.
The next class is called `tty':
\begin{verbatim}
class tty [2000 .. 2099] {
inherit standard_ops;
const TTY_MAXBUF = 1000;
tty_write(*, char buf[size:TTY_MAXBUF], int size);
tty_read(*, out char buf[size:TTY_MAXBUF], out int size);
};
\end{verbatim}
The request codes for operations defined in this class lie in the
range 2000-2099; inherited operations use the request codes already
assigned to them. The operations defined by this class are
`tty\_read' and `tty\_write', which pass variable-sized data buffers
between client and server. Class `tty' inherits class
`standard\_ops', so tty objects also support the operations
`std\_info' and `std\_destroy'.
Only the {\em interface} for `std\_info' and `std\_destroy' is shared
between tty objects and other objects whose interface inherits
`standard\_ops'; the implementation may differ. Even multiple
implementations of the `tty' interface may exist, e.g. a driver for a
console terminal and a terminal emulator in a window. To expand on
the latter example, consider:
\begin{verbatim}
class window [2100 .. 2199] {
inherit standard_ops;
win_create(*, int x, int y, int width, int height,
out capability win_cap);
win_reconfigure(*, int x, int y, int width, int height);
};
class tty_emulator [2200 .. 2299] {
inherit tty, window;
};
\end{verbatim}
Here two new interface classes are defined.
Class `window' could be used for creating and manipulating windows.
Note that `win\_create' returns a capability for the new window.
This request should probably should be sent to a generic window
server capability, or it might create a subwindow when applied to a
window object.
Class `tty\_emulator' demonstrates the essence of multiple inheritance.
It is presumably the interface to a window-based terminal emulator.
Inheritance is transitive, so `tty\_emulator' also implicitly inherits
`standard\_ops'.
In fact, it inherits it twice: once via `tty' and once via `window'.
Since AIL class inheritance only means interface sharing, not
implementation sharing, inheriting the same class multiple times is
never a problem and has the same effect as inheriting it once.
Note that the power of AIL classes doesn't go as far as \Cpp{}.
AIL classes cannot have data members, and there is
no mechanism for a server that implements a derived class
to inherit the implementation of the base
class --- other than copying the source code.
The syntax for class definitions and inheritance is also different.
\subsection{Amoeba}
The smell of `object-orientedness' that the use of classes in AIL
creates matches nicely with Amoeba's object-oriented approach to
RPC\@. In Amoeba, almost all operating system entities (files,
directories, processes, devices etc.) are implemented as {\em
objects}. Objects are managed by {\em services} and represented by
{\em capabilities}. A capability gives its holder access to the
object it represents. Capabilities are protected cryptographically
against forgery and can thus be kept in user space. A capability is a
128-bit binary string, subdivided as follows:
% XXX Need a better version of this picture!
\begin{verbatim}
48 24 8 48 Bits
+----------------+------------+--------+---------------+
| Service | Object | Perm. | Check |
| port | number | bits | word |
+----------------+------------+--------+---------------+
\end{verbatim}
The service port is used by the RPC implementation in the Amoeba
kernel to locate a server implementing the service that manages the
object. In many cases there is a one-to-one correspondence between
servers and services (each service is implemented by exactly one
server process), but some services are replicated. For instance,
Amoeba's directory service, which is crucial for gaining access to most
other services, is implemented by two servers that listen on the same
port and know about exactly the same objects.
The object number in the capability is used by the server receiving
the request for identifying the object to which the operation applies.
The permission bits specify which operations the holder of the capability
may apply. The last part of a capability is a 48-bit long `check
word', which is used to prevent forgery. The check word is computed
by the server based upon the permission bits and a random key per object
that it keeps secret. If you change the permission bits you must compute
the proper check word or else the server will refuse the capability.
Due to the size of the check word and the nature of the cryptographic
`one-way function' used to compute it, inverting this function is
impractical, so forging capabilities is impossible.%
\footnote{
As computers become faster, inverting the one-way function becomes
less impractical.
Therefore, a next version of Amoeba will have 64-bit check words.
}
A working Amoeba system is a collection of diverse servers, managing
files, directories, processes, devices etc. While most servers have
their own interface, there are some requests that make sense for some
or all object types. For instance, the {\em std\_info()} request,
which returns a short descriptive string, applies to all object types.
Likewise, {\em std\_destroy()} applies to files, directories and
processes, but not to devices.
Similarly, different file server implementations may want to offer the
same interface for operations like {\em read()} and {\em write()} to
their clients. AIL's grouping of requests into classes is ideally
suited to describe this kind of interface sharing, and a class
hierarchy results which clearly shows the similarities between server
interfaces (not necessarily their implementations!).
The base class of all classes defines the {\em std\_info()} request.
Most server interfaces actually inherit a derived class that also
defines {\em std\_destroy().} File servers inherit a class that
defines the common operations on files, etc.
\subsection{How AIL Works}
The AIL stub generator functions in three phases:
\begin{itemize}
\item
parsing,
\item
strategy determination,
\item
code generation.
\end{itemize}
{\bf Phase one} parses the input and builds a symbol table containing
everything it knows about the classes and other definitions found in
the input.
{\bf Phase two} determines the strategy to use for each function
declaration in turn and decides upon the request and reply message
formats. This is not a simple matter, because of various optimization
attempts. Amoeba's kernel interface for RPC requests takes a
fixed-size header and one arbitrary-size buffer. A large part of the
header holds the capability of the object to which the request is
directed, but there is some space left for a few integer parameters
whose interpretation is left up to the server. AIL tries to use these
slots for simple integer parameters, for two reasons.
First, unlike the buffer, header fields are byte-swapped by the RPC
layer in the kernel if necessary, so it saves a few byte swapping
instructions in the user code. Second, and more important, a common
form of request transfers a few integers and one large buffer to or
from a server. The {\em read()} and {\em write()} requests of most
file servers have this form, for instance. If it is possible to place
all integer parameters in the header, the address of the buffer
parameter can be passed directly to the kernel RPC layer. While AIL
is perfectly capable of handling requests that do not fit this format,
the resulting code involves allocating a new buffer and copying all
parameters into it. It is a top priority to avoid this copying
(`marshalling') if at all possible, in order to maintain Amoeba's
famous RPC performance.
When AIL resorts to copying parameters into a buffer, it reorders them
so that integers indicating the lengths of variable-size arrays are
placed in the buffer before the arrays they describe, since otherwise
decoding the request would be impossible. It also adds occasional
padding bytes to ensure integers are aligned properly in the buffer ---
this can speed up (un)marshalling.
{\bf Phase three} is the code generator, or back-end. There are in
fact many different back-ends that may be called in a single run to
generate different types of output. The most important output types
are header files (for inclusion by the clients of an interface),
client stubs, and `server main loop' code. The latter decodes
incoming requests in the server. The generated code depends on the
programming language requested, and there are separate back-ends for
each supported language.
It is important that the strategy chosen by phase two is independent
of the language requested for phase three --- otherwise the
interoperability of servers and clients written in different languages
would be compromised.
\section{Linking AIL to Python}
From the previous section it can be concluded that linking AIL to
Python is a matter of writing a back-end for Python. This is indeed
what we did.
Considerable time went into the design of the back-end in order to
make the resulting RPC interface for Python fit as smoothly as
possible in Python's programming style. For instance, the issues of
parameter transfer, variable-size arrays, error handling, and call
syntax were all solved in a manner that favors ease of use in Python
rather than strict correspondence with the stubs generated for C,
without compromising network-level compatibility.
\subsection{Mapping AIL Entities to Python}
For each programming language that AIL is to support, a mapping must
be designed between the data types in AIL and those in that language.
Other aspects of the programming languages, such as differences in
function call semantics, must also be taken care of.
While the mapping for C is mostly straightforward, the mapping for
Python requires a little thinking to get the best results for Python
programmers.
\subsubsection{Parameter Transfer Direction}
Perhaps the simplest issue is that of parameter transfer direction.
Parameters of functions declared in AIL are categorized as being of
type {\tt in}, {\tt out} or {\tt in} {\tt out} (the same distinction
as made in Ada). Python only has call-by-value parameter semantics;
functions can return multiple values as a tuple. This means that,
unlike the C back-end, the Python back-end cannot always generate
Python functions with exactly the same parameter list as the AIL
functions.
Instead, the Python parameter list consists of all {\tt in} and {\tt
in} {\tt out} parameters, in the order in which they occur in the AIL
parameter list; similarly, the Python function returns a tuple
containing all {\tt in} {\tt out} and {\tt out} parameters. In fact
Python packs function parameters into a tuple as well, stressing the
symmetry between parameters and return value. For example, a stub
with this AIL parameter list:
\begin{verbatim}
(*, in int p1, in out int p2, in int p3, out int p4)
\end{verbatim}
will have the following parameter list and return values in Python:
\begin{verbatim}
(p1, p2, p3) -> (p2, p4)
\end{verbatim}
\subsubsection{Variable-size Entities}
The support for variable-size objects in AIL is strongly guided by the
limitations of C in this matter. Basically, AIL allows what is
feasible in C: functions may have variable-size arrays as parameters
(both input or output), provided their length is passed separately.
In practice this is narrowed to the following rule: for each
variable-size array parameter, there must be an integer parameter
giving its length. (An exception for null-terminated strings is
planned but not yet realized.)
Variable-size arrays in AIL or C correspond to {\em sequences} in
Python: lists, tuples or strings. These are much easier to use than
their C counterparts. Given a sequence object in Python, it is always
possible to determine its size: the built-in function {\tt len()}
returns it. It would be annoying to require the caller of an RPC stub
with a variable-size parameter to also pass a parameter that
explicitly gives its size. Therefore we eliminate all parameters from
the Python parameter list whose value is used as the size of a
variable-size array. Such parameters are easily found: the array
bound expression contains the name of the parameter giving its size.
This requires the stub code to work harder (it has to recover the
value for size parameters from the corresponding sequence parameter),
but at least part of this work would otherwise be needed as well, to
check that the given and actual sizes match.
Because of the symmetry in Python between the parameter list and the
return value of a function, the same elimination is performed on
return values containing variable-size arrays: integers returned
solely to tell the client the size of a returned array are not
returned explicitly to the caller in Python.
\subsubsection{Error Handling}
Another point where Python is really better than C is the issue of
error handling. It is a fact of life that everything involving RPC
may fail, for a variety of reasons outside the user's control: the
network may be disconnected, the server may be down, etc. Clients
must be prepared to handle such failures and recover from them, or at
least print an error message and die. In C this means that every
function returns an error status that must be checked by the caller,
causing programs to be cluttered with error checks --- or worse,
programs that ignore errors and carry on working with garbage data.
In Python, errors are generally indicated by exceptions, which can be
handled out of line from the main flow of control if necessary, and
cause immediate program termination (with a stack trace) if ignored.
To profit from this feature, all RPC errors that may be encountered by
AIL-generated stubs in Python are turned into exceptions. An extra
value passed together with the exception is used to relay the error
code returned by the server to the handler. Since in general RPC
failures are rare, Python test programs can usually ignore exceptions
--- making the program simpler --- without the risk of occasional
errors going undetected. (I still remember the embarrassment of a
hundredfold speed improvement reported, long, long, ago, about a new
version of a certain program, which later had to be attributed to a
benchmark that silently dumped core...)
\subsubsection{Function Call Syntax}
Amoeba RPC operations always need a capability parameter (this is what
the `*' in the AIL function templates stands for); the service is
identified by the port field of the capability. In C, the capability
must always be the first parameter of the stub function, but in Python
we can do better.
A Python capability is an opaque object type in its own right, which
is used, for instance, as parameter to and return value from Amoeba's
name server functions. Python objects can have methods, so it is
convenient to make all AIL-generated stubs methods of capabilities
instead of just functions. Therefore, instead of writing
\begin{verbatim}
some_stub(cap, other_parameters)
\end{verbatim}
as in C, Python programmers can write
\begin{verbatim}
cap.some_stub(other_parameters)
\end{verbatim}
This is better because it reduces name conflicts: in Python, no
confusion is possible between a stub and a local or global variable or
user-defined function with the same name.
\subsubsection{Example}
All the preceding principles can be seen at work in the following
example. Suppose a function is declared in AIL as follows:
\begin{verbatim}
some_stub(*, in char buf[size:1000], in int size,
out int n_done, out int status);
\end{verbatim}
In C it might be called by the following code (including declarations,
for clarity, but not initializations):
\begin{verbatim}
int err, n_done, status;
capability cap;
char buf[500];
...
err = some_stub(&cap, buf, sizeof buf, &n_done, &status);
if (err != 0) return err;
printf("%d done; status = %d\n", n_done, status);
\end{verbatim}
Equivalent code in Python might be the following:
\begin{verbatim}
cap = ...
buf = ...
n_done, status = cap.some_stub(buf)
print n_done, 'done;', 'status =', status
\end{verbatim}
No explicit error check is required in Python: if the RPC fails, an
exception is raised so the {\tt print} statement is never reached.
\subsection{The Implementation}
More or less orthogonal to the issue of how to map AIL operations to
the Python language is the question of how they should be implemented.
In principle it would be possible to use the same strategy that is
used for C: add an interface to Amoeba's low-level RPC primitives to
Python and generate Python code to marshal parameters into and out of
a buffer. However, Python's high-level data types are not well suited
for marshalling: byte-level operations are clumsy and expensive, with
the result that marshalling a single byte of data can take several
Python statements. This would mean that a large amount of code would
be needed to implement a stub, which would cost a lot of time to parse
and take up a lot of space in `compiled' form (as parse tree or pseudo
code). Execution of the marshalling code would be sluggish as well.
We therefore chose an alternate approach, writing the marshalling in
C, which is efficient at such byte-level operations. While it is easy
enough to generate C code that can be linked with the Python
interpreter, it would obviously not stimulate the use of Python for
server testing if each change to an interface required relinking the
interpreter (dynamic loading of C code is not yet available on
Amoeba). This is circumvented by the following solution: the
marshalling is handled by a simple {\em virtual machine}, and AIL
generates instructions for this machine. An interpreter for the
machine is linked into the Python interpreter and reads its
instructions from a file written by AIL.
The machine language for our virtual machine is dubbed {\em Stubcode}.
Stubcode is a super-specialized language. There are two sets of of
about a dozen instructions each: one set marshals Python objects
representing parameters into a buffer, the other set (similar but not
quite symmetric) unmarshals results from a buffer into Python objects.
The Stubcode interpreter uses a stack to hold Python intermediate
results. Other state elements are an Amoeba header and buffer, a
pointer indicating the current position in the buffer, and of course a
program counter. Besides (un)marshalling, the virtual machine must
also implement type checking, and raise a Python exception when a
parameter does not have the expected type.
The Stubcode interpreter marshals Python data types very efficiently,
since each instruction can marshal a large amount of data. For
instance, a whole Python string is marshalled by a single Stubcode
instruction, which (after some checking) executes the most efficient
byte-copying loop possible --- it calls {\tt memcpy()}.
Construction details of the Stubcode interpreter are straightforward.
Most complications are caused by the peculiarities of AIL's strategy
module and Python's type system. By far the most complex single
instruction is the `loop' instruction, which is used to marshal
arrays.
As an example, here is the complete Stubcode program (with spaces and
comments added for clarity) generated for the function {\tt
some\_stub()} of the example above. The stack contains pointers to
Python objects, and its initial contents is the parameter to the
function, the string {\tt buf}. The final stack contents will be the
function return value, the tuple {\tt (n\_done, status)}. The name
{\tt header} refers to the fixed size Amoeba RPC header structure.
\vspace{1em}
{\tt
\begin{tabular}{l l l}
BufSize & 1000 & {\em Allocate RPC buffer of 1000 bytes} \\
Dup & 1 & {\em Duplicate stack top} \\
StringS & & {\em Replace stack top by its string size} \\
PutI & h\_extra int32 & {\em Store top element in }header.h\_extra \\
TStringSlt & 1000 & {\em Assert string size less than 1000} \\
PutVS & & {\em Marshal variable-size string} \\
& & \\
Trans & 1234 & {\em Execute the RPC (request code 1234)} \\
& & \\
GetI & h\_extra int32 & {\em Push integer from} header.h\_extra \\
GetI & h\_size int32 & {\em Push integer from} header.h\_size \\
Pack & 2 & {\em Pack top 2 elements into a tuple} \\
\end{tabular}
}
\vspace{1em}
As much work as possible is done by the Python back-end in AIL, rather
than in the Stubcode interpreter, to make the latter both simple and
fast. For instance, the decision to eliminate an array size parameter
from the Python parameter list is taken by AIL, and Stubcode
instructions are generated to recover the size from the actual
parameter and to marshal it properly. Similarly, there is a special
alignment instruction (not used in the example) to meet alignment
requirements.
Communication between AIL and the Stubcode generator is via the file
system. For each stub function, AIL creates a file in its output
directory, named after the stub with a specific suffix. This file
contains a machine-readable version of the Stubcode program for the
stub. The Python user can specify a search path containing
directories which the interpreter searches for a Stubcode file the
first time the definition for a particular stub is needed.
The transformations on the parameter list and data types needed to map
AIL data types to Python data types make it necessary to help the
Python programmer a bit in figuring out the parameters to a call.
Although in most cases the rules are simple enough, it is sometimes
hard to figure out exactly what the parameter and return values of a
particular stub are. There are two sources of help in this case:
first, the exception contains enough information so that the user can
figure what type was expected; second, AIL's Python back-end
optionally generates a human-readable `interface specification' file.
\section{Conclusion}
We have succeeded in creating a useful extension to Python that
enables Amoeba server writers to test and experiment with their server
in a much more interactive manner. We hope that this facility will
add to the popularity of AIL amongst Amoeba programmers.
Python's extensibility was proven convincingly by the exercise
(performed by the second author) of adding the Stubcode interpreter to
Python. Standard data abstraction techniques are used to insulate
extension modules from details of the rest of the Python interpreter.
In the case of the Stubcode interpreter this worked well enough that
it survived a major overhaul of the main Python interpreter virtually
unchanged.
On the other hand, adding a new back-end to AIL turned out to be quite
a bit of work. One problem, specific to Python, was to be expected:
Python's variable-size data types differ considerably from the
C-derived data model that AIL favors. Two additional problems we
encountered were the complexity of the interface between AIL's second
and third phases, and a number of remaining bugs in the second phase
that surfaced when the implementation of the Python back-end was
tested. The bugs have been tracked down and fixed, but nothing
has been done about the complexity of the interface.
\subsection{Future Plans}
AIL's C back-end generates server main loop code as well as client
stubs. The Python back-end currently only generates client stubs, so
it is not yet possible to write servers in Python. While it is
clearly more important to be able to use Python as a client than as a
server, the ability to write server prototypes in Python would be a
valuable addition: it allows server designers to experiment with
interfaces in a much earlier stage of the design, with a much smaller
programming effort. This makes it possible to concentrate on concepts
first, before worrying about efficient implementation.
The unmarshalling done in the server is almost symmetric with the
marshalling in the client, and vice versa, so relative small
extensions to the Stubcode virtual machine will allow its use in a
server main loop. We hope to find the time to add this feature to a
future version of Python.
\section{Availability}
The Python source distribution is available to Internet users by
anonymous ftp to site {\tt ftp.cwi.nl} [IP address 192.16.184.180]
from directory {\tt /pub}, file name {\tt python*.tar.Z} (where the
{\tt *} stands for a version number). This is a compressed UNIX tar
file containing the C source and \LaTeX documentation for the Python
interpreter. It includes the Python library modules and the {\em
Stubcode} interpreter, as well as many example Python programs. Total
disk space occupied by the distribution is about 3 Mb; compilation
requires 1-3 Mb depending on the configuration built, the compile
options, etc.
\bibliographystyle{plain}
\bibliography{quabib}
\end{document}