This WWW version was prepared by the author from the text submitted for publication, and appears by permission of the copyright holder. Copyright © 1995 by the Association for Computing Machinery, Inc. Please click here for full ACM copyright notice.
This is the third article based on language-independent standardisation work being carried out by international standards working group ISO/IEC JTC1/SC22 WG11 (Language Bindings). The first, Programming languages - towards greater commonality [Meek 1994a], was an overview, which briefly described the various standards and the relationships between them. The second, A taxonomy of datatypes [Meek 1994a], went into one of these projects in greater detail. This article similarly surveys another major WG11 project. All three articles are based on presentations to the DECUS (UK and Ireland) symposia, this one given in May 1993, the earlier two in 1992 and 1994 respectively.
The first of the earlier articles discussed the need for a procedure calling standard in the context of other language-independent standards, and the things it is to be used for. These aspects will not be repeated here.
It is, however, worth recalling one part of that first article, because the initial reaction to the question "what is a procedure call?" might very often be "don't be silly, everyone knows what a procedure call is!" But then the same applies to what a datatype is, as was remarked in the second article. The first article pointed out
...whereas most if not all programming languages include the concepts of procedures and their invocation, they vary in the way that they view them, especially with respect to parameter passing - and this irrespective of any differences there may be about datatypes.
In Appendix B (RPC tutorial) of the ECMA-127 standard for remote procedure calling (RPC) using OSI (Open Systems Interconnection), it is argued that "the idea of remote procedure calls is simple" and that procedure calls are "a well-known and well-understood mechanism...within a program running on a single computer". Both statements are true at a given level. Procedure calling is a simple concept, at the level of provision of functionality. It becomes less so at the linguistic level (let alone the operational and representational or implementation levels) because of its interaction with both datatyping and program structure, as is testified by the numerous variations and (apparently) arbitrary restrictions on procedure definitions and calling in existing languages. It is also undoubtedly true that procedure calling is "well-understood" almost universally in the language community. The trouble is that this general understanding is not necessarily the same understanding.
This is to some extent acknowledged in section 3.4 of Appendix B of ECMA-127. However, once these points are put together, along with the absence hitherto of [a language-independent standard for datatypes], the "simple and well understood" view of procedure calling becomes increasingly elusive; more tenuous, harder to sustain.
The LIPC standard will fill the gap by providing "a common reference point to which all languages can relate", in the words again of the first article; actual languages will achieve this through mappings onto it of their native procedure calling facilities. To be of practical use, parameter passing has to be supported, so the aforementioned language-independent standard for datatypes, and mappings to and from that, will also be needed. That standard, Language Independent Datatypes or LID, is also progressing through ISO. The present article will not discuss again the details of LID or its mappings, though we shall refer to it later; for further information, see the second article in this series.
In order to arrive at a language-independent view of procedure calling, it is necessary first to separate the concepts of defining the procedure; invoking it; and the delivery of its results back to the point of call. Specific languages base all of these on shared concepts and language structures; we have to do it without.
Two important caveats must be entered before starting detailed discussion of LIPC. First, LIPC is still at ISO's "Committee Draft" (CD) stage, which means that though general consensus on it has been reached, it is still possible for some technical details to be changed. Second, what follows represents my own understanding of LIPC, and while my colleagues did see the DECUS 1993 presentation version and seemed to find it acceptable, it must not be regarded as definitive, and is anyway not complete. The definitive version is in the draft standard itself, and any errors, large or small, in this article, and anything misleading as a result of the incompleteness, is my responsibility alone.
The basic model
If a procedure call is to be language-independent then the definition and the call will in general be in different languages and will be executed by different language processors.
So the first separation is between client and server, the client being the program that calls the procedure (together with its language processor), and the server being the procedure that is called (together with its language processor). To emphasise this distinction we shall here refer to the client program and the server procedure. It is the purpose of LIPC to provide the means of bridging the syntactic and semantic gap between these two.
client program .....LIPC..... server procedureThis mental picture of a bridge is deceptively symmetrical, something which is reinforced by further features of the model, as we shall see. During the development of the standard, the various committees were at times beguiled by the symmetry and were tempted into trying to produce a "pretty" model by using symmetry as too much of a guide. It has its uses - for example, notionally at least every "command" by the client is matched by a "response" from the server - but one should never forget that the two sides of the bridge have very different characteristics, despite both being source text under the control of a language processor.
Different views of procedures
Languages take different view of procedures. Even the very word has different connotations when used in the context of different languages. For example, Pascal distinguishes between a "procedure" (which does not return a value) and a "function" (which does). Fortran calls the first a "subroutine" and now uses the word "procedure" to cover either, though in the past it did not use the term. This difference between "function" procedures and "subroutine" procedures is important because it usually affects the syntax of the call, and almost certainly affects the way the procedure is defined. Commonly, you write a function call in one way, and in a place where otherwise a value or expression of the appropriate datatype might be used, but write a subroutine call on its own, with altered syntax, e.g. the Fortran CALL or Cobol PERFORM keywords.
To complicate things even more (it would seem), APL uses the word "function" to mean what other languages call "operators", and as well as providing a vast armoury of built-in operators/functions going way beyond the familiar +, - etc, it allows the programmer to define more, and to use them just like operators. APL functions are, nevertheless, also procedures. Algol 68 also has user-defined operators and, with Ada, allows definitions that "overload" existing operators (i.e. defining how they can be applied to operands of further datatypes). These too are procedures, in the generic sense, that produce return values when given the values of the operands.
It is important to realise that in terms of "the view from the bridge" such syntactic differences are of no importance; the only thing that matters is whether a return value is expected by the client program, and if so what its datatype should be. The bridge needs to carry the requirement to the server procedure and carry back the appropriate response.
One can take this further. On the bridge, having seen the request go one way, there is no need to demand that the server procedure has to be of the same "variety" of procedure as was (syntactically) envisaged by the caller program. It matters not whether the returned value is going to be used directly in a calculation or stored somewhere. In the first case the client has expressed the call in terms of a function or operator, in the second as an "out" parameter of some envisaged server subroutine procedure, but provided the server procedure can accept the call and provided the needed responses, its actual form on the server side is immaterial to the client.
That being the case, the bridge doesn't need to know either; in other words, the LIPC model needs take no account of such distinctions. What will need to take it into account are of course the bindings, that map the native forms in the client and procedure languages to and from the LIPC form. To summarise, supposed addition were not predefined but had to be provided by a procedure called PLUS. However it looks on the server side, it makes no difference whether the client language calls it in any of the ways
C := A PLUS B C := PLUS(A,B) PLUS(A,B,C)as far as getting the required result is concerned. You could even write
ADD A TO B GIVING Cthough in this case the language binding would need to identify this syntactic form as a disguised procedure call of PLUS.
This separation between client and server, rather than a complication, is therefore in fact a simplification, because it limits what has to be communicated to the essentials of what is needed to get the job done. The model needs to specify this in a common, language-independent form, and the bindings need to express the relevant aspects (only) of their native models. The picture we now have is of the LIPC bridge enabling a virtual contract to be established between the client and the server:
client ---------------- virtual contract ----------------- server program procedure | | | | client's LIPC ----------- service contract ------------- server's LIPC service service (---------LIPC environment---------)Above the line the client and server (language) environments are distinct. The LIPC services perform the mappings which turn that into a service contract, and pass the necessary information, through the LIPC environment, from the one to the other and back again. LIPC assumes nothing about the environment other than that it understands LIPC; it can be a shared host environment, or a remote connection through a network. We shall return to that aspect later, albeit briefly.
In referring to the possible return of a value, in the call of a function procedure, we were rather running ahead of ourselves, because it involves the concept of a datatype. Let us go back to the start. It is sufficient to consider the simple case of a conventional, sequential, single-processor system. Introduction of parallelism and the like just complicates the picture without adding anything to understanding the concept.
Procedures with no parameters
Consider merely the call of a parameterless "subroutine" style procedure which returns no value. Inside a conventional processor this is handled by some scoping mechanism (in a nested, block-structured language, say), a link editor, or some such, which can identify the procedure being called, and pass to it the order to start processing - i.e. it transfers the thread of control to it - together with the link-back information of where to return control back to once it has finished processing. In LIPC we make a logical division into a client part which sets the link and passes control, and the server part which accepts control and then passes back control at the end. Note that the link itself need not be sent to the server; it can remain at the client end, where the LIPC service will use it to return control to the right place when the server signals that the execution of the called procedure has been completed. For reasons of efficiency, it could be part of the service contract that the server's LIPC service is given the link so return can take place automatically from the server side, but it is not logically necessary - and anyway, the client's LIPC service still needs to be told that the execution of the call is complete, since it must remain available in case an error condition is encountered during execution.
This client-server model is not the usual way of looking at procedure calling in the traditional monolithic view of language processors, but is common enough now in other areas, and should be clear enough. Having made the logical separation, LIPC then works from a situation where we have two different processors, each with its procedure-calling client and server, and seeks to establish a logical bridge between the client part of one and the server part of the other.
The word logical is stressed because it does not presume any particular instantiation of how either the concept of passing control, or the concept of link, is represented and transferred from client to server. Similarly, LIPC does not presume any particular method of the client "knowing" what server to call or where it is. The monolithic view tends to assume that it can be done without saying how, though language standards may specify an error if an attempt is made to call a "non-existent" procedure, and may indeed (e.g. in pure blocked-structured languages) specify what that means. (Language bindings of LIPC would need to take such matters into account.)
For LIPC, it could equally be that both share a common platform and hence the identical view of what a link is and what passing control is, or that they are on different and geographically separated platforms connected through a network. In the former case, the instantiation is as simple as it could be, short of collapsing the client and server back into the same language processor. In the second case it will involve a connection service, used to execute the actual contract, and interfaces will be needed in the client and server to convert the control transfer and link into transmission form at one end and accept the transmission at the other. This is the model designated "remote procedure call" or RPC, and the subject of another forthcoming standard, DIS 11579. It does no harm to postulate the existence of notional interfaces in the LIPC model; in the simplest common platform case the difference is that they are trivial and "conversion" notional. However, it is wrong to make the common mistake of assuming that conversion and transmission are always involved, because the very necessity of converting into transmissible form places constraints on the model which need not apply in the other case.
Parameters in the model
Thus far the model is not very interesting. When procedures have parameters, it becomes more fun. This is for two reasons: parameters have datatypes, and in general languages do not agree about datatypes; and procedure calling involves passing parameters (from the point of call to the procedure), and languages tend not to agree about what that is either.
We can at the outset dispose of the problem of whether or not the called procedure is a "function" procedure, returning a value: as indicated earlier, in LIPC this amounts to an extra ("out") parameter. Achieving agreement on the datatype and the passing mechanism can be done along with explicit parameters.
Of the two, the harder in general but the easier for LIPC is the datatype problem. It is in general harder because of the great variety of datatypes there are (especially of aggregate datatypes of various kinds made up from several component values), reflecting the great variety of data and of collections of data that exist in the world. It is easier for LIPC only because another forthcoming standard, Language Independent Datatypes or LID, does the job of defining a common set to which all languages can relate - again, by means of bindings or mappings, as with LIPC. LID is a separate standard because it is of use not just for procedure parameters in LIPC but in other cross-language areas as well, though if it did not exist LIPC would have had to invent it, just as database systems and transmission protocols have had to do in the past. LID is intended to render further such reinvention of wheels unnecessary.
LIPC uses LID by adding another interface to the client and server LIPC services, between the languages and the actual calling, an interface to convert the parameters from native form into LID form on the client side, and from LID form into the (in general different) native form on the server side. The conversions are to be defined not in LID itself, or LIPC, but in the mapping standards to and from LID for the various languages. It is these conversions that ensure that the server procedure will be able to recognise what the datatype is of a parameter it has been sent. The process of conversion into LID form on the client side is called marshalling and that of converting from LID into local form on the server side is called unmarshalling.
Parameter passing mechanisms
There is, however, one further thing that needs to be done, for each parameter, and that is to agree on the actual parameter passing mechanism. Various languages have concepts like call by value, call by reference, copy-in-copy-out, and so on. Sometime, language standards leave them implementation-dependent; for example, the popular image of parameter passing in Fortran (pre Fortran 90) is "call by reference", but the Fortran standards did not say that, and copy-in-copy-out was carefully not excluded as an implementation mechanism. (This led to curiosities like a function call F(X,X) being non-standard, since the result might depend on which passing mechanism was used!)
LIPC copes with this problem by defining just four passing mechanisms:
(a) call by value sent on initiation;
(b) call by value sent on request;
(c) call by value returned on termination;
(d) call by value returned when available.
These will not look very familiar, and getting to them was a long drawn out process. However, by a gradual process of refinement we arrived at these four, as being the simplest way that we could devise of covering, in combination with the datatyping facilities in LID, all of the logical possibilities that binding standards or those implementing standards-conforming language services are likely to need.
Call by value sent on initiation
Call by value sent on initiation is the simplest and most familiar, being that usually termed "call by value". In the virtual contract the server requires a value of a given type to be delivered at the time of the call, and the client undertakes to provide one. This of course does not mean that the actual parameter has to be a literal constant of the required type, any more than in actual languages; it can be an expression which is evaluated, or a variable holding such a value which is "dereferenced" (or whatever the native term is in that language for getting out the value), or even a value of a different datatype which undergoes a type conversion.
Formal and actual parameters
It is worth pausing here, before going on to the other three kinds of parameter passing. This view of the "virtual contract" in the call-by-value case simply expresses, in perhaps a new way, the familiar relationship between "formal parameters" and "actual parameters" in languages. Though expressed in somewhat different terms than is common with the monolithic view, it captures the same concept. In LIPC terms it is the server which "calls the tune" as far as parameter passing is concerned; explicitly or implicitly it says what must be provided and it is up to the call to provide them. The programmer writing the calling program may well not have written the procedure being called, but does need to know what the formal parameters are that have to be matched, i.e. what the constraints are on the actual parameters. Even in weakly typed languages - in some contexts, especially in weakly typed languages - that information is needed, witness the ENTRY attribute in PL/I. There is, in fact, nothing unfamiliar about this to anyone who has used a procedure library like NAG.
In the monolithic view, the procedures (even if called from a library) are "part of the program" and the fact that this information is known is implicitly assumed. It must be stressed that LIPC itself does not go beyond this. How such information is passed to the programmer, or even passed between the client program and the server procedure (i.e. datatype-checking that acceptable actual parameters are supplied), is not part of LIPC, which deals with the language aspects, not with the way the environment functions. If it is an open environment, i.e. it is a remote procedure call, then of course it is a matter that standards have to address - and the RPC standard does so. Where the additional requirements to achieve openness are not relevant, it is not only unnecessary but wrong for LIPC to specify a particular way.
For similar reasons LIPC stops short of specifying whether or where any datatype conversions that may be needed take place. Languages notoriously vary greatly in their strictness or laxity over such matters. Again, it is the server that calls the tune. If the server procedure is in a strongly-typed language and hence strict, the wrong datatype will be rejected. If it is in a weakly-typed language, it might be willing to try its hand at using anything sent to it. In fact, it will be the language bindings for the language, in particular the server mode bindings, that will specify what constraints there are in terms of what LID datatypes can be unmarshalled to provide an actual parameter corresponding a formal parameter of a given type. Again, it is not just unnecessary but wrong for LIPC to dictate to particular languages on such matters.
There is one other thing that must be stressed about call by value sent on initiation. Most languages place restrictions on what datatypes can be passed "call by value", most commonly that they must consist of primitive values only. In fact, constraints on what can be passed can exist for any parameter passing method - especially, for example, on the "parameter" (not in this case "call by value", of course) which returns the value associated with a function call. However, call by value sent on initiation has no restrictions whatever on the datatype value which can be passed, and indeed neither have any other LIPC passing methods. (This helps to explain why they are all called "call by value" with some qualification.) Parameter datatypes can be of any kind, primitive, aggregate, pointer, procedure, user-defined, whatever the passing method - and this includes special parameters returning a function value.
This is not to say that the bindings of LIPC to actual languages will not have restrictions. Of course they will: they will have the restrictions that that language imposes, but no more. Nor is it to say that the environment providing an LIPC service may not place restrictions; for example, in the open environment envisaged by RPC, there will be restrictions because of the constraints on transmissions using OSI protocols. However, again this is an instance of it being wrong for LIPC to impose restrictions at the outset, which may be unnecessary in particular cases - including, remember, future languages and future environments.
Before considering, much more briefly, other kinds of parameter passing, we can update our diagrammatic model:
client ---------------- virtual contract ----------------- server program procedure | | language-to-LIPC LIPC-to-language interface (marshalling) interface (unmarshalling) | | client's LIPC ----------- service contract ------------- server's LIPC service service (---------LIPC environment---------)It can be seen that on each side the language needs an interface with the LIPC service, one to do the marshalling on the client side, and another one on the server side to do the unmarshalling. These two interfaces display a kind of symmetry, but it is clear that there is also an asymmetry, arising ultimately from the difference in languages between formal and actual parameters. In the monolithic view of languages you don't see these interfaces, though even there they exist in embryo or "stub" form (without the need of course for datatype conversion into LID form); it is separating the client and server parts which brings it into the open.
If the LIPC environment is an open one using RPC, the service on each side will also need a further interface, to marshal for transmission and unmarshal at the far end; RPC defines what is needed here, not LIPC, because it is not a language issue. On a given host, LIPC servers could offer both local and RPC facilities, and all languages with client or server interfaces could use them. Note that the asymmetry means that a language processor can comply to LIPC in client mode, through a language-to-LIPC interface, to make use of LIPC services, or in server mode, to provide LIPC services. Compliance will be through the mapping standards for LIPC and LID for the language concerned.
If it wants to do both (so that, for example, the users of that language on that host can write both calling programs and server procedures) it will need both interfaces and comply in both modes. In practice, some aspects of marshalling must exist on the server side and some of unmarshalling on the client side, to send receive returned values. For that reason, it is possible that most language processors, if they comply at all, will in fact comply in both modes. However, the marshalling and unmarshalling for returned values is somewhat different in kind, and somewhat easier, because it is governed by the contracts, and hence the return conversions must automatically be the reverse of the originals: in effect, the datatypes involved are pre-matched. Since it is possible that simpler and more efficient processes could be provided for such reverse conversions, there is no sense in LIPC requiring full marshalling and unmarshalling in those circumstances, unless full capability in both modes is needed for other reasons.
Call by value sent on request
To return to the three remaining parameter passing methods, these can quickly be disposed of now we have fully explored the implications of the simplest case. Call by value sent on request means that the client undertakes to evaluate the actual parameter and provide the value, but only on receipt of a request from the server to do so. If no request is received, no value is sent. This "deferred passing" is not usually explicitly specified by language standards but is not always explicitly excluded either, and is used by implementors as an optimisation method. It is important since it can result in a different value being sent; its inclusion in LIPC is necessary so the mappings and servers can specify exactly what happens.
An important implementation note is that this form of passing results means that only one request is sent, at most. Once the value has been received, all other references to the associated formal parameter in the procedure server definition are to that value. If you want repeated transmissions, you use procedure datatypes.
Call by value returned on termination
Call by value returned on termination of course is the "out" equivalent of call by value sent on initiation, and is perhaps better known as "call by value return", including the return of result associated with a function procedure. As such it is familiar, and the only point to note is that in LIPC terms it is the client and not the server that ensures that the returned value reaches its destination. It may not be desirable, meaningful or even possible to send (say) an actual hardware address which the server can write to directly. That, however, is not excluded as an implementation mechanism at the service contract level, if for example a closely-coupled host LIPC environment makes it feasible and desirable for efficiency. It is a similar situation to that for the return-of-control link, which need not be sent to the server, but can be if agreed in the service contract. So here again, LIPC neither requires nor excludes; it is an enabling standard, not a disabling or constraining one.
Call by value returned when available
Call by value returned on termination specifies when the return must occur; call by value returned when available is provided in LIPC to allow more flexibility - if particular services or languages need it. Clearly a value cannot be returned before it is available, but this passing method allows the return to take place as soon as it is (i.e. while execution of the call is still in progress), or at any time thereafter - before, at, or even after termination of the call, since for LIPC the value is still "available". Indeed, it is not even precluded that such a value be returned more than once!
Even more than with call by value sent on request, this particular method is included for logical completeness, so if it is needed LIPC can accommodate it. Whether or not it is used is a matter for the LIPC services. No doubt some will regard this as a nonsense and not the sort of thing that should be in standards. Our view is that LIPC is supposed to be enabling and not constraining, in particular not constraining for future languages and environments. It costs very little to include these additional possible passing methods, and it may make it easier for particular needs to be met. That being so, we have seen no virtue in subjecting the LIPC model to the limitations of a subset of approaches found in traditional systems.
Accommodating conventional parameter passing methods
Nevertheless it is important for the well-known methods of parameter passing to be covered in the LIPC model. "Call by value" and "call by value return" ("in" and "out") have already been covered. In LIPC an "in-out" parameter (or "call by value send and return", or "copy in copy out") can be treated either as a parameter using both of the methods call by value sent on initiation and call by value returned on termination, or as implicitly being two parameters, but with the source of the one and the destination of the other being identical. It is probably easier to maintain the logical distinction between the in and the out by using this second model as a guide, but it doesn't matter provided the distinction is maintained.
As for "call by reference", the traditional monolithic view of "passing the address" has to be replaced in the virtual contract by the client providing the server with an access path to the actual parameter, whenever required. (Note the difference from call by value sent on request.) This is achieved by marshalling a parameter of datatype T, called by reference, into a parameter of datatype pointer to T (or, rather, the LID mapping thereof), called by value sent on initiation. What cannot be avoided in this case, as with any pointer or procedure datatypes, is continuing interaction between client and server while execution of the called procedure is in progress. This may not matter; on the other hand, it may matter a great deal, for example in an open, networked environment, where supporting this method could represent a significant overhead.
Where languages are not explicit on the required parameter passing method - indeed, sometimes deliberately vague, as with Fortran between call by reference and copy-in-copy-out, mentioned earlier - the LIPC binding for the language must address these issues, and state explicitly what is allowed or required and what is not. For example, a Fortran binding might well state that for LIPC calls, the passing mechanism must be copy-in-copy-out, in the LIPC terms described just now. This would not be in conflict with the Fortran standard, merely an extra constraint in the context of LIPC services that would not need to apply outside that context.
Global data and the execution context
This brings us to the question of global data, i.e. where a procedure definition is not self-contained but contains references to entities in an environment outside itself, something commonly allowed in languages. Conceptually, global data has to be marshalled and unmarshalled, just like parameters, though in some cases that process will be trivial. However, it is possible to visualise complicated situations where calls are cascaded and the worlds of the various clients and servers are not totally distinct but overlap in various ways. The LIPC model has to allow for it. The draft standard in fact recommends supporting global data in terms of implicit parameters passed at the point of call, but recognises that where marshalling and unmarshalling are trivial this need only be notional. There is in fact a whole section devoted to providing a model of the way the execution of a called procedure takes place, to aid those developing LIPC services and guide those producing language bindings where issues like this are important. Error conditions that may occur during acceptance of the call and execution of the called procedure are also covered in the standard.
Those of us engaged on this project have come a long way since the need for LIPC - to support portability between platforms and wider access of procedure libraries, and portable mixed-language programming, as explained in the first article - was first clearly identified a few years ago. It did not take us long to dispose of the assumption that, since procedure calling was "well-understood", developing a language-independent standard for it was trivial - indeed, most of us realised that at the outset. Distilling the essence of procedure calling from the various "understandings" has taken time, but perhaps most time has been taken in clearing out of the way the mass of inessentials which tend to confuse the issue.
LIPC is not perfect, it is doubtless not the only possible model and perhaps something else might be better (though no-one has put forward anything to us; the RPC model in the second DIS 11579 is too limiting). However, we believe that it has the vital feature that all standards need: that it works. It is hoped that this exposition has made you interested enough not just to learn more about LIPC, but to start thinking in terms of putting it to work, by developing the bindings and services which will bring its benefits to the myriad of users round the world who call procedures and who are waiting for some of the present barriers to be lifted.
Standards and drafts
ECMA-127, RPC (Remote procedure call using OSI), final draft 2nd edition, January 1990
ISO/IEC JTC1 CD 13886, Language Independent Procedure Calling, 1995 (due to be sent for ballot as DIS)
ISO/IEC JTC1 DIS 11404, Common Language Independent Datatypes, 1994 (due to be sent for publication as IS, 1995)
ISO/IEC JTC1 DIS 11579, OSI RPC (Remote Procedure Call) version, 1995 (due to be sent for ballot as second DIS)
B.L. Meek, What is a procedure call?, DECUS Symposium 1993
B.L. Meek, Programming languages - towards greater commonality, ACM Sigplan Notices Vol 29 No 4, April 1994, pp 49-57 (based on DECUS Symposium 1992 presentation)
B.L. Meek, A taxonomy of datatypes, ACM Sigplan Notices Vol 29 No 9, September 1994, pp 159-167 (based on DECUS Symposium 1994 presentation)
Copyright © 1995 by the Association for Computing Machinery, Inc. Please click here for full ACM copyright notice.