Antony is a consulting engineer with Eclipse Technical Group where he is doing research on mobile computing and replicated storage systems. He can be contacted at [email protected].
Programmers faced with writing such applications must address fundamental issues of distribution, scale, concurrency, extensibility, and user interface. Many of these issues recur in every distributed, interactive application, making it desirable to provide a common, portable infrastructure for developing these applications. Without an adequate support infrastructure, you get bogged down in the details of complex state management, concurrency issues, and moving bits from place to place, instead of focusing on the target application domain. Phantom is an attempt to remedy this situation by providing an interpreted language and run-time environment that supports flexible, transparent, and secure distribution of both program code and data, while providing enough modern programming language features to support large-scale application development. The interpreter for Phantom is implemented entirely in ANSI C for portability, and may be extended with C procedures using the interpreter's foreign function interface.
For the language's distributed programming features, Phantom uses the distributed lexical scoping semantics of Obliq. Distributed lexical scoping provides transparent access to program code and data from remote sites, while still providing strong network security guarantees. For programmers already familiar with languages such as safe-Tcl or Java, in which the run-time environment supports mobile code (transmission of executable code across computer networks), distributed lexical scoping is of interest because it cleanly integrates run-time support for mobile code with the programming language concepts of higher-order functions and lexical scoping rules.
The language itself is based on a safe, extended subset of Modula-3. Modula-3 was selected as a starting point for Phantom because it provides a number of powerful language features (such as threads, garbage collection, and exception handling), and because its type system provides the level of detail required to automatically convert program values into a canonical form suitable for transmission across a network.
However, Phantom is not Modula-3. Phantom does not provide Modula-3's general-purpose reference types, partially opaque subtypes, or generic modules. Phantom replaces Modula-3's array types with dynamically sized lists, supports slice indexing notation, includes general-purpose higher-order functions and lambda expressions, and provides for type-safe implicit declarations. Phantom also integrates Modula-3's thread model more fully into the language core, including mutex, condition, and thread as predefined types, along with built-in wait, signal, and fork primitives. As a syntactic matter, Phantom uses lower-case keywords, in an attempt to make it clear to programmers that Phantom is not simply a Modula-3 subset.
I developed the Phantom interpreter on a Dell Dimension XPS 90 running Linux 1.2.7. It is also known to run on SunOS 4.1.3. The alpha release is a 2-MB compressed tar file that expands to 10 MB. You will need about 20 MB of disk space to build the interpreter. It's big because it includes versions of the pthreads, Tcl, and Tk libraries. For information on availability of the interpreter, see the Phantom home page at http:// www .apocalypse.org/pub/u/antony/phantom/phantom.html.
A Prime Numbers Sieve Example
To get a taste for the Phantom language, consider Listing One which implements a prime numbers sieve, and prints all prime numbers between 1 and the constant size. Listing One illustrates the syntax and most of the basic features of the language. The program consists of a single module that imports two interfaces (stdio and fmt), declares a constant (size) and a procedure (filter), and has a mainline consisting of a statement list. When the interpreter is invoked on the file containing this module, execution proceeds by executing the statements in the mainline.
The mainline constructs a list of integers in the range 0 to size, and assigns this list to the variable sieve. All primes are then "filtered" from the sieve by the subsequent for loop, which iterates over each prime in the sieve, calling the procedure filter for each successive prime. Finally, the results are displayed by iterating over the sieve once more, and printing each nonzero integer remaining in the list.
Note in Listing One that there are no explicit variable declarations in the mainline. In Phantom, it is not necessary to declare a local variable prior to its use in a statement block. The first assignment to an undeclared identifier (in an assignment statement or for loop) will declare that variable in the local scope, with a type derived from the expression on the right side of the assignment statement. All subsequent references to the identifier in the statement block will be type checked against this automatically derived type. Such type-safe implicit declarations allow Phantom programs to retain the safety guarantees provided by static typing, without the syntactic overhead of declaring each variable prior to use. (Explicit variable declarations may still be provided -- for example, there is a declaration for the local variable prime of procedure filter -- in which case the type given in the explicit declaration is used for type checking.)
Also note in Listing One that, in the formal parameter list of the filter procedure, there are no "mode" specifiers to indicate which parameters are passed by value and which are passed by reference. Instead, filter makes use of the call-by-object-reference semantics of Phantom. In call-by-object-reference, certain types (objects, lists, and thread primitives) are always passed by reference, and values of all other types are always passed by value. In this example, these semantics ensure that changes that filter makes to its formal parameter sieve are visible to the caller.
Higher-Order Functions
Phantom procedures are first-class values; procedures may be assigned to variables, passed as parameters, and returned from other procedures, just like other fundamental language types. (In Phantom, functions are just procedures that return a value. Hence, the terms "higher-order function" and "higher-order procedure" are used interchangeably.)
For example, consider Listing Two, which assigns a procedure incr() to a variable, and passes that variable as a parameter to the procedure apply(), which has a first formal parameter (func) that is a procedure. This program produces the output:
value of x is: 5
value of x after apply: 15
In Listing Two, the variable x is declared with module-level scope. Thus, the use of identifier x in procedure incr refers to the same variable x used in the mainline.
Lambda Expressions. Phantom provides for the dynamic creation of anonymous (unnamed) higher-order functions through the use of lambda expressions. Lambda expressions are modeled after the construct of the same name that appears in nearly all dialects of Lisp (Scheme, for instance).
A lambda expression consists of the keyword lambda followed by a procedure signature (in the same form as a procedure declaration), followed by the body of the lambda enclosed in braces. Braces are used to delimit the body of lambda expressions because they are slightly less verbose than the usual begin and end delimiters used for ordinary procedures.
To illustrate the use of lambda expressions in Listing Two, you could entirely eliminate the definition of incr, and replace the assignment myfunc:=incr; in the mainline with myfunc:=lambda (amount:int) {x:=x+amount; };.
Lambda expressions are particularly useful in situations where the body of the function is extremely small. In such cases, lambda expressions eliminate the need to give the function an artificial "name," and also improve readability, since the code for the function appears explicitly in the context where it is used.
Higher-Order Functions and Nested Scopes. At run time, Phantom procedures are represented as true closures. A closure is simply a pair consisting of:
- A representation of the code in the body of the procedure. This could be either a direct representation as source-code text, or some internal representation such as a parse-tree or bytecodes for a virtual machine.
- An environment that maps all free variable identifiers appearing in the procedure to their corresponding storage locations.
Using closures to represent higher-order functions ensures that higher-order functions have an intuitive and well-defined meaning, even in the presence of nested scopes. To illustrate this point, consider Listing Three, which produces the output:
g() called, x = 10
after incr(), x = 15
g() returning...
after incr(), x = 32
In Listing Three, the mainline assigns to variable f the result of calling function g(). The function g() declares a local variable x, and a local (internal) procedure incr(). incr() increments x by the amount given in its argument. The reference to x in incr() refers to the variable x in incr's surrounding scope (the scope of g).
When g() is called, it allocates storage for a fresh copy of local variable x, and initializes x to the value 10. Next, g() prints the value of x, invokes incr() with the argument 5, and returns the function incr as its result.
After g() returns, the closure for incr is assigned to variable f of the mainline. This closure contains the code for the body of incr, as well as an environment for execution of incr. This environment contains a reference to the variable x, taken from incr's surrounding scope. Note that this closure retains a reference to x, even though x was allocated as a local variable of procedure g, and g is no longer active.
Finally, the procedure variable f is passed to apply(), along with the argument 17. When apply() executes, it indirectly calls the closure for incr, passing 17 as an argument. Since the environment part of the closure retains a binding to the original location allocated for x (which currently holds the value 15), the call to apply() results in updating x to the value 32, and printing this result.
Most traditional imperative programming languages provide some support for higher-order functions, but do not provide higher-order functions with the level of generality found in Phantom or other symbolic programming languages (such as Scheme). C allows function pointers but does not provide for nested scopes or anonymous higher-order functions. Modula-3 provides nested scopes, but limits their use in higher-order functions by only permitting procedures at the outermost scope level to be assigned to variables or passed as parameters. These languages impose these limitations in order to enable the run-time optimization of allocating local variables in an "activation record" created on the run-time stack when the procedure is called, and destroyed when the procedure returns.
Phantom does not store local variables in an activation record because, as this example illustrates, the provision of general higher-order functions with nested scopes may result in a variable being accessed after the call (in which the variable is created) has returned. In this example, the local variable x of g() is accessed from within the body of incr(), and incr() is called indirectly after the call to procedure g() returns. Instead of using activation records, Phantom generates fresh heap locations for each local variable of a procedure, and this storage is only reclaimed (by the garbage collector) when the location becomes unreachable.
Object Model
Phantom supports object-oriented programming, in a manner similar to Modula-3. Phantom objects have attributes (containing state information), methods (for performing operations), and support single inheritance. Phantom uses a class-based object model (rather than prototypes, as in Obliq or Self).
Objects are the focus of communication in Phantom. A Phantom program will generally make its services available to other programs by registering object values with a name service -- a Phantom application server that maps string names to network addresses of Phantom objects.
Recall that Phantom uses call-by-object-reference semantics. This has an important property when passing object values to Phantom programs at remote sites: Objects are never implicitly migrated to remote sites as the result of an assignment, procedure call, or return statement. Instead, the object remains stationary and a network reference is passed to the remote site. If migration of objects across sites is required, it must be performed explicitly by the programmer. While this violates location transparency to some degree, we feel that only the programmer can make reasonable decisions about when and where to migrate objects.
Distribution Model
The Phantom distribution model (see Figure 1) borrows heavily from the distributed lexical scoping semantics of Obliq. A network connects a number of sites. A site is an invocation of the Phantom interpreter on some host machine. It has a site address that uniquely identifies the site throughout the network. In the current implementation, a site address is simply a pair consisting of the IP address and port number of a TCP socket owned by the interpreter process. A host running a multitasking operating system may contain several sites (corresponding to multiple invocations of the Phantom interpreter).
Within a site, the interpreter maintains a single memory space for the program it is executing. This memory space contains a number of locations. Each location has a location address that uniquely identifies the location within the interpreter's memory space, and holds a value.
Each Phantom program executes as a number of threads within the interpreter. Threads are provided through a library that implements the POSIX pthreads specification. Two data structures are associated with each thread:
A representation of the program code that the thread is executing. In the current implementation, the interpreter uses a sequence of bytecodes for a virtual stack machine for this purpose.
An environment that maps every variable or constant identifier appearing in the Phantom program to a global location address. A global location address is a pair consisting of a site address and a location address within the memory space of that site.
The Phantom interpreter uses environments to provide transparent distribution for Phantom programs. Each statement in a Phantom program may make reference to constant and variable identifiers. As the interpreter executes a statement, it uses the program's current environment to map these identifiers to their corresponding global location addresses. The interpreter then performs the appropriate operation on each location, according to the defined semantics of the language. If the global location address refers to a location within the local interpreter's memory space, the operation is performed directly by the interpreter. If, however, the global location address refers to a location in the memory space of another site, the interpreter sends a request to the remote site and asks it to perform the given operation.
A Sample Distributed Application
Listings Four, Five, and Six illustrate how the interpreter and run time provide Phantom's distributed semantics (and also illustrate the basic techniques for developing dynamically extensible, distributed, interactive applications in Phantom). There are two programs presented. Listing Four is an interface that is used by both the client and the server. Listing Five is a generic client program (such as might be launched as an "external viewer" from a web browser). Listing Six is an application-specific server with which the client can communicate.
The generic client uses the information in a URL to obtain a reference to a remote application server. Once the client has obtained a reference to the application server, it obtains an autonomous agent from the server: a higher-order procedure received from the remote site that the client executes locally.
The "Hello" server is as an example of an application-specific server. It accepts requests from clients and returns an agent to each client. This agent creates an instance of an interactive "hello world" object at the client's site.
The general-purpose client could be invoked using a command line such as:
$ phi AppClient phi://server.host.name/ HelloServer
which starts the Phantom interpreter (phi) executing the module AppClient (the name of the client program) with the URL for the Hello server as a command-line argument available to the client. The agent obtained from the Hello server creates a window on the client's display; see Figure 2.
All user-interface events for this window are handled by code for the agent executed at the client site. When users press the Hello button, the agent responds by printing the message "Hello, World" to its output stream. When users press the Quit button, the agent returns control to the client program, which then exits.
While simple, this example illustrates most of the important features of the application domain for which Phantom was designed. This example is both distributed and interactive, the client is dynamically extensible, and all interactive UI events are handled at the client site. These same principles apply to other, more sophisticated applications in the target domain.
Application-Server Interface
An application server makes its services available to clients by registering an instance of an AppServer.T object with a name server. The AppServer.T type is implemented by every application-specific server, and is also known to the generic client. This shared interface (see Listing Four) defines two types: AppServer.Agent (describing the type of the agent given to the client for local execution) and AppServer.T (the abstract type of an application server). The generic client obtains an agent from an application server by first obtaining a reference to an AppServer.T (using the name service), and then invoking its generate_agent() method. Application-specific servers are implemented by creating subtypes of AppServer.T.
Procedures are first-class types in Phantom, and may be assigned to variables, passed as parameters, and returned from procedures, just like values of other fundamental language types. In this example, the type Agent is declared as a procedure taking two parameters (that must be supplied by the client). Such parameters represent the services that an execution site (the client) makes available to agents received from across the network. In a more general interface, such services would encapsulate all of the local resources the execution site is willing to provide to the agent: a local audio service, a 3-D rendering service, and so on. For simplicity, the only services provided to agents in this example are I/O streams for reading/writing messages.
Ownership and Access Control
The generate_agent() method of type T has a name, a procedure signature, and a permissions specification (given by perm x following the signature). Permissions specifications are used to set the access-control properties of attributes and methods.
Each Phantom object is stored in the memory space of an interpreter that communicates with other interpreters across a network. Each object has an owner, represented at run time as a sys.user object corresponding to the user who started the interpreter containing the object.
The permissions specification specifies what operations on the object may be performed by users other than the owner. An operation on an object by a user other than the owner happens when the interpreter receives a request from a Phantom program running on a different site.
Each permissions specification consists of a bitmask of the three permission bits r, w, and x, corresponding to read, write, and execute permission, respectively. (The r and w bits apply only to attributes, and the x bit applies only to methods.) If no permissions specification is given for an attribute or method, the default is that all permission bits are turned off.
In the current example, the generate_ agent() method of AppServer.T has its x bit set in the permissions specification to allow clients at remote sites to invoke this method.
Client Program
The client program (Listing Five) is straightforward: It obtains the global location address of an AppServer.T object from the name service (defined in the interface ns) using the application's URL, invokes the generate_agent() method of the server to obtain an agent, and executes the agent locally, supplying the standard I/O streams of the client as the parameters to the agent.
Listing Five works as follows: First, the program calls urllib.parse() to parse the URL given by urlstring. In this example, urlstring is given as a constant; in practice it would use a command-line argument. The procedure urllib.parse() returns the URL as a record with separate protocol, host, and path fields. Next, the client attempts to contact a Phantom name server running on the host given in the URL, using the procedure ns.find(). The ns module and interface is part of the standard Phantom library. The name-server object is located on a local or remote site using a well-known TCP port. The variable identifier name_server is assigned the global location address of the name-server object, returned from the call to ns.find(). Any subsequent operation on the identifier name_server is forwarded transparently by the interpreter to the name-server object, which performs the operation and returns the result.
Next, the lookup() method is invoked on the name_server object to obtain a reference to the application server, using the pathname part of the URL ("HelloServer" in this example). The lookup() method returns its result as type any; the statement following the lookup performs a type-safe run-time type conversion using narrow() to convert this value to an object reference of the appropriate type. After the client performs the lookup() operation, the name server is no longer involved in the communication between the client and the server; it just provides a mechanism for bootstrapping the connection between them. Once the client has a reference to the remote AppServer.T object, operations can be performed on the object reference in the same manner as with the name_ server object; the language run time handles any network communication required.
Next, the client invokes the generate_ agent() method of the app_server to obtain an agent for local execution. Since the value returned from generate_agent() is a procedure, this will result in obtaining the closure for the procedure from the application server. This closure will be dynamically loaded into the memory space of the client, and the variable local_ agent will refer to the closure. The semantics of transmitting closures across sites ensures that this is a safe operation: The code in a closure received from a remote site and executed locally cannot gain unauthorized access to any local resources.
Finally, the client invokes local_agent, passing the standard I/O streams of the client program as parameters. Thus, the client has no information about specific applications hardcoded into it, but dynamically obtains application-specific behavior by obtaining a closure from the server. This generic client program could be used without modification as a client for any application-specific server.
The client wraps the entire body of its mainline in a try-except statement, to catch some of the exceptions that may be raised in the process of obtaining the application-specific agent, and reports these as errors to the user. More sophisticated error-recovery mechanisms could be implemented using this facility.
Server Program
The application-specific server (Listing Six) is a "Hello, World" server. It returns to clients an agent that, when executed at the client site, creates a graphical, interactive "Hello, World" window on the client's display. The agent uses the library interface between Phantom and the Tk toolkit to implement the GUI for the agent.
The server defines the type ServerImpl as a subtype of AppServer.T. This is a common technique in Phantom: An object type appearing in an interface will describe the external view presented to clients, and a subtype will be used to implement the application-specific server.
Listing Six is implemented as follows: First, the actual server object is implemented as a subtype of the object type AppServer.T. The subtype (ServerImpl) does not add any attributes or methods to AppServer.T, it simply overrides the generate_agent() method of AppServer.T. Hence, the body of the ServerImpl is empty, since it does not have any specific attributes or methods, and, in Phantom, method overrides are not stated explicitly in the object type.
Next, the application server defines the object type Hello as a subtype of Tk.Frame. No instance of this type is ever created at the server site; instead, an instance of it is created at the client site by the application-specific agent. When the agent is transmitted from the server to the client, all information about types used within the agent is transmitted across the network and reconstructed at the client site. For object types, this includes both the information necessary to construct instances of the type, and the code for any methods. Note that a type may refer to other types in its definition, and types may be recursive. The run time will transmit all necessary type information, including information about types referenced indirectly or recursively.
The agent returned to clients is the procedure client_agent() defined in the generate_agent() method of ServerImpl. The client_agent() procedure, executed at the client site, creates a new instance of type Hello at the client site, and invokes the main_loop() method of Hello to process GUI events that happen in this object. The main_loop() method of Hello is inherited from Tk.Frame, the parent type of Hello.
Finally, the mainline of HelloServer creates a new instance of ServerImpl, and registers this with the local name server. When the Phantom interpreter is invoked to run the server application, it would be invoked with the -noexit option, to ensure that the interpreter does not exit after initialization, but instead waits idly for requests from remote sites.
Semantics of Procedure Transmission
In Phantom, a higher-order procedure is stored as a closure containing the code for the procedure as well as an environment that maps free identifiers appearing in the procedure to their corresponding storage locations. These storage locations are, in fact, global location addresses. Using global location addresses in closures gives higher-order procedures an intuitive and secure meaning in a distributed context; see Figure 3.
Transmitting the environment along with the code for the procedure preserves the correct lexical scoping semantics when the procedure is executed at the remote site. When the procedure body makes reference to a free identifier, the binding to the global location address ensures that the operation is performed on the location where the identifier was bound originally.
The "Hello, World" example illustrates a limited case of transmitting procedures across sites. In that example, the procedure that is transmitted to the client site as the application-specific agent has no free variable identifiers. That is, the procedure client_agent() does not refer to any variables from its enclosing scope. This is an example of a "disconnected" agent: All information needed to execute the procedure at the client site can be encapsulated in the code of the closure, and the closure's environment will be empty. Although client_agent() does refer to types (such as the Hello object type) from enclosing scopes, this type information is transmitted to the client site as part of the code of client_agent()'s closure.
If the body of client_agent() made reference to a variable in its surrounding scope, the environment of the closure transmitted to the client would contain a binding to the location of the variable at the server site. This would have the effect of creating a "connected" agent: one that carries its network connections with it. This facility could be used to create a distributed multiplayer game, for example. The agent transmitted to the client could simply invoke operations on an object (representing the opposing player) declared in one of the agent's enclosing scopes. Any time the agent (executing at the client site) performed such an operation, the client run time would use the environment transmitted with the agent to forward the operation to the site where the object resides.
Security Considerations
Phantom's distribution model raises a number of interesting security issues. The most important issue has to do with the ability to send code across sites: The language and run time must provide strong guarantees about the safety of executing code received from a potentially untrustworthy server.
The language and run-time environment must guarantee that program code received from a remote site and executed locally will not have access to any local resources that could not have been accessed via RPC from the remote site.
Phantom makes this guarantee through adherence to lexical scoping in the context of distribution and higher-order functions. In practical terms, the implementation guarantees lexical scoping by passing a set of bindings for all free identifiers along with the code for a procedure. When an interpreter receives a procedure from a remote site, it can perform a single, static check to ensure that all free identifiers in the code for the procedure have a corresponding entry in the set of bindings received with the procedure. If any free identifier does not have a corresponding binding, the interpreter will abort the operation requested by the remote site and return a security-violation exception.
The language has no general-purpose pointer types. This is crucial to security. Eliminating general-purpose pointers ensures that the only way a procedure can refer to resources outside the body of the procedure is through free identifiers. Since the implementation ensures that free identifiers are handled through strict lexical scoping, executing procedures received from remote sites is guaranteed secure; there is simply no mechanism for the procedure to gain unauthorized access to any local resources.
Current Status
Currently, a prototype interpreter exists for the Phantom language core. The interpreter supports all of the basic features in the language, including static typing, type-safe implicit declarations, objects, interfaces, threads, exceptions, garbage collection, dynamically sized lists, and higher-order functions, and includes a library interface to the Tk toolkit. A number of demonstration programs have been written in Phantom. The initial results are limited but encouraging: The language's Modula-3 heritage affords it a number of powerful features, while still maintaining overall coherence and simplicity. The implementation of the networking subsystem in the interpreter required for distribution support is not complete.
Acknowledgment
Special thanks to David Abrahamson, Luca Cardelli, Dan Connolly, Bill Janssen, Danny Keogan, Ciaran McHale, Killian Murphy, and Brendan Tangney, who read drafts of earlier versions of the language report and this paper, and provided valuable feedback on the exposition and language design.
DDJ
Listing One
(* primes.pm -- a simple primes numbers sieve in Phantom *)module primes; </p> import stdio, fmt; const size = 5000; (* filter out all multiples of n from sieve *) proc filter(n: int; sieve: list<int>) var prime:=n; begin n:=n+prime; while n < len(sieve) do sieve[n]:=0; n:=n+prime; end; end; begin (* construct the initial (unfiltered) sieve *) sieve:=[0..size]; </p> (* filter out all primes from sieve *) for prime in sieve do if prime > 1 then filter(prime,sieve); end; end; (* and display the results *) for i:=2 to last(sieve) do if sieve[i] # 0 then stdio.puts(fmt.fint(sieve[i]) @ "\n"); end; end; end primes. </p>
Listing Two
module simplehigher;import stdio,fmt; var x: int:=0; proc incr(amount: int) begin x:=x+amount; end; proc apply(func: proc(v: int); arg: int) begin func(arg); end; begin x:=5; stdio.puts("value of x is: " @ fmt.fint(x) @ "\n"); myfunc:=incr; (* assign incr to procedure variable myfunc *) apply(myfunc,10); (* pass procedure as a parameter *) stdio.puts("value of x after apply: " @ fmt.fint(x) @ "\n"); end simplehigher. </p>
Listing Three
(* higher.pm -- an example that illustrates higher-order functions, * nested scopes, and scope escapement *) module higher; import stdio, fmt; (* g() returns its internally-nested procedure, incr() to the caller; as * a result, incr is said to "escape its scope". *) proc g(): proc (v: int) var x := 10; proc incr(y: int) begin x:=x+y; stdio.puts("after incr(), x = " @ fmt.fint(x) @ "\n"); end; begin stdio.puts("g() called, x = " @ fmt.fint(x) @ "\n"); incr(5); stdio.puts("g() returning...\n"); return incr; end; proc apply(p: proc(arg: int); v: int) begin p(v); end; begin f:=g(); apply(f,17); end higher. </p>
Listing Four
interface AppServer;import rd, wr; (* An Agent is simply a procedure executed at the client site *) type Agent = proc (istrm: rd.T; ostrm: wr.T); </p> (* AppServer.T -- an application server *) type T=object (serialized, protected) methods (* generate a new agent for execution at the client *) generate_agent(): Agent perm x; end; end AppServer. </p>
Listing Five
(* AppClient.pm -- implementation of a general-purpose network client *)module AppClient; import AppServer, Tk, stdio, ns, sys, urllib; const urlstring = "phi://server.host.name/HelloServer"; begin try url:=urllib.parse(urlstring); name_server:=ns.find(url.host); app_ref:=name_server.lookup(url.path); app_server:=narrow(app_ref, AppServer.T); (* obtain the agent from the server *) local_agent:=app_server.generate_agent(); (* and execute the agent locally *) local_agent(stdio.stdin,stdio.stdout); except urllib.malformed => stdio.stderr.puts("error: malformed URL: " @ urlstring @ "\n"); | sys.narrow_failure => stdio.stderr.puts("error: URL does not refer to an AppServer\n"); | ns.not_available => stdio.stderr.puts("error: could not contact name server at host " @ url.host @ "\n"); | ns.unknown_service => stdio.stderr.puts("error: application " @ url.path @ " not registered with nameserver.\n"); end; end AppClient. </p>
Listing Six
module HelloServer;import AppServer, Tk, rd, wr, ns, stdio; (* ServerImpl is the type of the "hello" application server; implemented as a * subtype of AppServer.T *) type ServerImpl=AppServer.T object end; (* Hello is the object type instantiated at the client site *) type Hello=Tk.Frame object quit: Tk.Button; msg: Tk.Button; wstrm: wr.T; (* stream on which to write messages *) methods CreateWidgets(); say_hi(); end; (* methods of Hello: *) proc Hello.CreateWidgets(self: Hello) begin self.quit:=new(Tk.Button, master:=self, text:="Quit", fg:="red", command:=lambda () { self.exit(); }); self.quit.pack(side:=Tk.left); self.msg:=new(Tk.Button, master:=self, text:="Hello", command:=lambda () { self.say_hi(); }); self.msg.pack(side:=Tk.left); end; (* init() method -- called automatically to initialize new instances *) proc Hello.init(self: Hello) begin Tk.Frame.init(self); (* call super-class init method *) self.pack(); self.CreateWidgets(); end; proc Hello.say_hi(self: Hello) begin self.wstrm.puts("Hello, world!\n"); end; (* methods of Hello Server: *) proc ServerImpl.generate_agent(self: ServerImpl): AppServer.Agent (* client_agent() is the procedure returned by generate_agent() and * executed at the client site *) proc client_agent(istrm: rd.T; ostrm: wr.T) begin hello_app:=new(Hello, wstrm:=ostrm); hello_app.main_loop(); end; begin return client_agent; end; begin (* create an instance of the server, and register it with the local name * service *) hello_server:=new(ServerImpl); name_server:=ns.find(); name_server.register("HelloServer",hello_server); end HelloServer.