Getting started with ORBLink: A tutorial


Prerequisites for working through the tutorial

These are the prerequisites for running the tutorial:
  1. You should be able to load Allegro Common Lisp and ORBLink.
  2. You should have some familiarity with CORBA
  3. You should have read the IDL/Lisp mapping document.
  4. You should know where the ORBLink home directory is located.

However, even if you do not have all the prerequisites, you might find the tutorial useful to get an idea of what an ORBLink application entails.

Because of the length of this tutorial, you may find it more convenient to print out rather than to read on-line.

Overview of the tutorial

In this tutorial, we will work through a very simple example of implementing IDL in Lisp and then invoking the Lisp client. After doing this, we will see how to modify the server itself on the fly.

We will assume that there are two distinct Lisp worlds that have been started: one is called the server and the other the client.

Commands that should be typed into the server or client listener are prefaced with [server-listener]USER> or [client-listener]USER> in the text below.

In broad terms, to start our application we will:

Our tasks are thus logically partitioned into two categories: server-side and client-side.

Server side steps

  1. Load ORBLink
  2. Compile the IDL on the server
  3. Define the implementation classes and methods
  4. Instantiate a server object
  5. Publish the IOR of the server object

Client side steps

  1. Load ORBLink on the client
  2. Compile the IDL on the client
  3. Retrieve the IOR of the server object and generate a client proxy from this IOR
  4. Invoke methods on the client proxy.

    These method invocations will result in the forwarding of remote requests to the server object.

It is important to note that either the client or the server side can be implemented in any CORBA-compliant language; that the client or server process can reside on the same or on a different computer in numerous platforms; and that the overall structure of these steps is the same for any language/platform configuration.

After these basic steps we will show how a Lisp server or client can be modified dynamically.

Let us, thus, begin by starting the server side:

Loading ORBLink on the server

The Lisp source code for this example is included in the directory examples/grid/cl/. You can load the example code from any directory. However, for specificity we will assume here that the current working directory is the root of the ORBLink home installation. You can use the :cd from within a Lisp listener to set the current working directory of the listener, e.g.

[server-listener]USER> :cd /usr/acl-5/code/orblink
[server-listener]USER> (require :orblink)

We recommend as well setting the current package to user:

:package :user

Compile the IDL on the server

The IDL source code for this example encapsulates the interface to a simple two-dimensional array of strings:
module example {
  interface grid;
  interface grid {
    readonly attribute short height;
    readonly attribute short width;
    void set(
      in short n,
      in short m,
      in string value
    );
    string get(
      in short n,
      in short m
    );
  };
};

To compile the IDL, give the pathname of the IDL as the argument to the IDL compiler:

[server-listener]USER>  (corba:idl "examples/grid/idl/grid.idl")
This will define the classes example:grid, example:grid-proxy, and example:grid-servant. The corba:idl function will return an Interface Repository object that encapsulates in CORBA compliant format the definitions in the IDL file.

The class example:grid-servant already has defined slots named op:width and op:height, corresponding to the attributes in the IDL definition of the grid interface. These slots have pre-defined readers named op:width and op:height with corresponding initialization arguments :width and :height.

Define the implementation classes and methods

In order to write a server for the grid interface, it is first necessary to define a class that extends example:grid-servant (technically this step is unnecessary, insofar as the new methods could be defined on the example:grid-servant class directly, but this usage would be poor style.

We will name our user-defined class, which extends example:grid-servant, user::grid-implementation.

Our class user::grid-implementation will extend example:grid-servant and will include a single extra slot named array that holds the actual values in the grid.

The example:grid-servant class defines slots named op:width and op:height and, since these are readonly attributes, it defines readers of the same name. Our grid-implementation will add default initforms to these slots of values 4 and 5 respectively. Users who wish to initialize the grid with a different size can do so using the automatically defined :width and :height initargs.

Our class definition thus looks like this (or see the file examples/grid/cl/grid-implementation.cl):

(defclass grid-implementation (example:grid-servant)
  (
   (op:width  :initform 4)
   (op:height :initform 5)
   (array)))
We define an initializer for this class that simply initializes the array to the size specified by the values of the slots named op:width and op:height, with initial element the string "initial":
(defmethod initialize-instance :after ((this grid-implementation) &rest args)
  (setf (slot-value this 'array)
    (make-array `(,(op:width this) ,(op:height this)) :initial-element "Initial")))

Finally, we implement the IDL operations named get and set. Because these are IDL operations, their implementation must be via the corba:define-method macro. The syntax of corba:define-method is specified as part of the CORBA IDL/Lisp mapping and closely follows the syntax of the usual defmethod macro.

For example the get method is implemented as:

(corba:define-method get ((this grid-implementation) row column)
  (aref (slot-value this 'array) row column))

You should now load the file that contains the definitions of the grid implementation class and its associatiated methods:

[server-listener]USER> :ld examples/grid/cl/grid-implementation.cl

Instantiate a server object

We can now verify that the appropriate classes have been loaded by instantiating an instance of user::grid-implementation:

[server-listener]USER> (setq test-grid (make-instance 'grid-implementation))
[server-listener]USER> (op:set test-grid 1 2 "This is a test.")
[server-listener]USER> (op:get test-grid 1 2)
---> "This is a test"

(Note that not all responses from the Lisp listeners are printed here).

The ORB itself is only involved implicitly in this computation; there is no marshalling or unmarshalling, and no socket connections, involved. The op:get and op:set methods are normal CLOS methods.

Publishing the IOR

In order to invoke methods on test-grid remotely, it is necessary to publish the IOR (interoperable object reference) of test-grid.

The IOR of the test-grid object can be obtained by invoking the op:object_to_string method on the ORB itself. The ORB is always bound to corba:orb:

USER>	(op:object_to_string corba:orb test-grid)
---> [a long string of characters beginning with "IOR"]
As a side effect of computing this string, called a stringified IOR, a TCP socket listener has been started that waits from invocations on the test-grid object. Since no client yet knows the IOR of test-grid, however, no invocations can be forthcoming until we make this IOR available to a client.

The simplest way to make the IOR available is for the server to write the IOR to a file that is then read by the client. This method is not particularly general, of course, but it will suffice to run simple examples. Choose a file for storing the IOR that is both writeable by the server and that can be read by any client. For example, you can try using the filename [directory]/grid.ior where the string "[directory]" in the following should be replaced by some directory to which you have write access:

USER> (orblink:write-ior-to-file test-grid "[directory]/grid.ior")
You should verify now that the IOR string you computed above has indeed been written to the file [directory]/grid.ior. Note that you can examine the source to the write-ior-to-file function in the file examples/ior-io/cl/sample-ior-io.cl:
(defun orblink:write-ior-to-file (object pathname)
  "Writes the IOR of object, the first argument, to the file denoted by pathname, the
  second argument. Because this routine is primary explanatory, little error checking is
  performed. If *default-ior-directory* is non-nil, pathname is first merged with
  *default-ior-directory*"
  (when *default-ior-directory*
    (setq pathname (merge-pathnames pathname *default-ior-directory*)))
  (ensure-directories-exist pathname)	; Create intermediate directories if necessary
  (with-open-file
      (stream pathname :direction :output :if-exists :supersede)
    (format stream ("~A" (op:object_to_string corba:orb object)))
    (format t "Wrote ior to file: ~a~%" pathname)
    (force-output)
    t))

Starting the client: Load ORBLink and compile the IDL on the client

Now that the IOR of the test-grid object has been published in a "well-known" place, a client can bind to it. You should start a new Lisp world for this portion of the tutorial, perhaps on a different machine, and then restart ORBLink and recompile the file examples/grid/idl/grid.idl. Thus there are now two Lisp listeners: the client and the server. (In fact this example will work just as well if the client and server are implemented in the same image and the same listener, but it is clearer for the exposition to separate them).
[client-listener]USER> (require :orblink)
[client-listener]USER> (corba:idl "examples/grid/idl/grid.idl")

Generate a client proxy for the server object

The process of generating a proxy is conceptually divided into two phases: reading the IOR and converting the IOR into a proxy.

Since the IOR now resides in the file [directory]/grid.ior, it may be read simply via:

[client-listener]USER> (setq ior
                         (with-open-file (stream "[directory]/grid.ior" :direction :input)
                           (read-line stream)))

This form should return, in the client listener, the same long string that was returned above in the server listener as the result of calling op:object_to_string on the test-grid object.

The next step is to create a proxy from this IOR. This can be done using the CORBA compliant string_to_object operation on the ORB:

[client-listener] USER> (setq test-grid-proxy (op:string_to_object corba:orb ior))

This should return an instance of type example:grid-proxy. This proxy may then be used to invoke operation on the server-side object in the server image from the client image.

Note: the preceding two steps, reading an IOR from a file and forming a proxy from that IOR could have been coalesced into the single invocation:

	            (setq test-grid-proxy (orblink:read-ior-from-file "[directory]/grid.ior))
The source code for the orblink:read-ior-from-file function is also located in the file examples/ior-io/cl/sample-ior-io.cl.

Invoke methods on the client proxy

You can now invoke methods on the test-grid-proxy object using exactly the same calling sequence as you did to invoke methods directly on the test-grid object:

[client-listener]USER> (op:set test-grid-proxy 1 3 "proxy-test")
[client-listener]USER> (op:get test-grid-proxy 1 3)
---> "proxy-test"

You can verify from the server world that these values really have changed:

[server-listener]USER> (op:get test-grid 1 3)
----> "proxy-test"

This concludes the first part of the tutorial.

Next, we discuss the issue of modifying the server on the fly.

Modifying the server

One convenient feature of Lisp is its ability to add functionality to the server without stopping and restarting the application. To demonstrate this functionality, we presume that it is desired to augment the grid object with a new attribute, say name. Make a copy of the file examples/grid/idl/grid.idl and modify the copy by adding the line
           attribute string name;
in the interface definition. Now recompile the modified file using the corba:idl function.

For your convenience the modified version is in the file examples/grid/idl/grid-modified.idl and thus the recompilation step is done via:

[server-listener]USER> (corba:idl "examples/grid/idl/grid-modified.idl")

If you now evaluate the form (describe test-grid) on the server side, you will see that a new slot named name has indeed been added to the test-grid object. Now let's set the value of this new slot:

[server-listener]USER> (setf (op:name test-grid) "Modified grid")
[server-listener]USER> (op:name test-grid)
----> "Modified grid"

Modifying the client

In order for the client to invoke the newly defined methods on the test-grid proxy, it also needs to recompile the IDL source:
[client-listener]USER> (corba:idl "examples/grid/idl/grid-modified.idl")

Now the client can invoke the new methods:

[client-listener]USER> (op:name test-grid-proxy)
----> "Modified grid"
[client-listener]USER> (setf (op:name test-grid-proxy) "client-modified name")
[client-listener]USER> (op:name test-grid-proxy)
----> "client-modified name"

Moving on

ORBLink offers many features not discussed in this introductory tutorial, among which are customizable exception handling, handling many other data types, support for persistent IORs, any handling, and so forth.

Parting words on the tutorial

CORBA is not always as simple as in this case. In general, there will be configuration problems in starting up different ORBs: all sorts of environment variables have to be set up correctly and various daemons need to be started. Once these configuration issues are resolved, the actual invocation of methods on remote objects is normally straightforward.