2.2.3 Calling out-of-process components

The raison d’être of many component models, such as CORBA, is to enable distributed systems in the enterprise. These systems typically consist of many disparate services that run either on different computers1 or in different operating system processes on the same machine. Some of these services may be written explicitly for the component model, while others may consist of legacy software wrapped by components (acting as adapters, in the design pattern sense (Gamma et al. 1995)). Distributed architectures in this fashion have many names, including Service-Oriented Architecture (SOA) and Service Component Architecture (SCA).

Component models strive to make it easy to call remote objects, generally by providing a stand-in object that runs in the client’s context and forwards all calls to the remote object. The stand-in object masquerades as the remote object by implementing the interface that the client wishes to use to communicate with the remote object. A stand-in object is known as a client-side proxy. On the server end, a server-side proxy receives messages sent by the client-side proxy, and dispatches calls to the server-side object using standard calling conventions. This is illustrated in Figure 2.1. The classes that are instantiated to form proxies are often generated by an IDL compiler. It is said that component technology enables location-transparent invocations due to this ease-of-use property. Using component technology to call out-of-process components is one way of realizing inter-process communication (IPC) and inter-machine communication. This is similar to traditional remote procedure calls, but with object semantics.

Figure "proxies"
Figure 2.1: Client-side and server-side proxies

Component models that are binary standards, such as COM, have no need for proxies when two objects communicate that run in the same process; they communicate directly.2 Without such a binary standard, as with CORBA, objects rely on a runtime part of the component model implementation to facilitate in-process calls, hence requiring the use of proxies for all types of calls. A client-side proxy masquerades as the target object and forwards calls to the runtime system, which then forwards the call to the server-side proxy, which finally calls the target object (which is not necessarily remote).

When making an in-process call, execution leaves the client operation body for the duration of the call. This behavior does not occur naturally when calling on the services of a remote object—if no steps are taken to prevent this behavior, execution only leaves the client operation body while a proxy forwards the call to the remote object. To prevent the client process from continuing to execute while the remote object is processing a call, the client process or thread is blocked. When it resumes execution, the remote object has finished execution, and the return value (if any) of the invoked operation is delivered to the client, just as though the target object had been running in-process. This type of call is known as a synchronous call.

Synchronous calls are convenient, as they ostensibly work just like calls to objects running in-process.3 The component model implementation takes care to hide the technical machinery used to communicate with the remote object. There are times when it is desirable for the client code to continue execution while a remote object is executing. Calls enabling this behavior are known as asynchronous calls. The client and remote object thus execute concurrently, and the remote object notifies the client when it has finished executing, if a reply has been requested.4 This is beneficial for long-running operations, but is slightly less convenient for developers, as clients need to keep track of additional states (whether or not it is waiting for a remote operation to complete). With synchronous calls, the client is relieved from having to keep track of these states, as it is blocked from executing while the remote call is on-going. Some component models only support synchronous calls. Those that do support asynchronous calls often default to synchronous calls.

In order to call a remote object, a component model implementation must marshal calls across processes and, possibly, machines. Marshalling entails converting an invocation into an unambiguous binary format, the wire format, which holds information on the operation to be called, as well as its arguments. Component models thus standardize network protocols used to carry calls to out-of-process objects. Client-side proxies are responsible for creating a data stream that conforms to this protocol and sending it to a server-side proxy, which unmarshals the call and invokes the corresponding operation on the remote object. If the client asked to be notified upon completion of the remote operation, the server-side proxy packages the return value in a data stream that conforms to the protocol, and sends it to the client-side proxy which returns it to the original caller. The client and the remote object are both unaware of the work done by the server-side and client-side proxies to facilitate the call.

Figure "callback-proxies"
Figure 2.2: Interface reference passed to a server

Simple types such as strings and integers are always passed by value when marshalled, in effect placing the argument data in the data stream. References to an implementation of an interface, however, are often passed by reference, meaning that only a means of identifying the implementation is placed in the data stream. As noted, in order to communicate with a remote object, client-side and server-side proxies need to be set up. When a remote object receives an interface argument that refers to an object that runs in the client’s context, a reverse connection needs to be set up—in order to communicate with the object that runs in the client’s context, the remote object needs to have a client-side proxy in its context that communicates with a server-side proxy that runs in the client’s context (see Figure 2.2).

Some component models support passing interface references by value. The instance data of an object that supports being passed by value is written to the data stream (it is serialized) by the client-side proxy, and is reconstructed (deserialized) by the server-side proxy.

The network protocols used by component models should generally be well-specified in terms of how data is represented—for instance, different machine architectures often use different byte orders. A component model that does not allow calls across a network, and is thus limited to one machine, may relax these rules, as the client-side and server-side proxies are guaranteed to run on the same machine (and have likely been generated by the same IDL compiler).

Footnotes

  1. Increasingly, many of these “computers” are virtualized, and thus run on the same physical computer.
  2. COM sometimes does use proxies even for objects that run in the same process. See Chapter 7.
  3. There are subtle problems associated with synchronous calls. Notably, they may cause deadlocks (permanently blocked processes or threads), and they are inherently much slower than the in-process calls they try to mimic. These issues are briefly discussed in section 6.2.1.
  4. The reply will generally be delivered by sending a message to the client. Long-running programs that sit idle from time to time, such as servers and applications that interact with users, are generally message-oriented. The standard implementation of such programs is to have an outer message loop that waits for messages and dispatches them to the target objects. The message loop is often part of a system library, and is thus often hidden from developers.