Mixing and matching disparate object models
Jean-Marie is a cofounder and VP of technology of Neuron Data. He can be reached at [email protected]. Marc is a senior R&D engineer at Neuron Data and can be reached at [email protected].
As Internet protocols and languages are increasingly being recognized as vehicles for the new generation of business applications, distributed objects are quickly becoming the critical foundation for core business applications. Currently, there are two major approaches to distributed-object technology -- namely Microsoft's OLE/ActiveX technologies and the Object Management Group's (OMG) Common Object Request Broker Architecture (CORBA) specifications. In this article, we will show how to glue together these disparate object models using Java. To illustrate, we'll present an accounting application as a single Java applet that exchanges data with a local ActiveX control, local Java object, a remote CORBA object, remote COM object, and remote Java object. Each object implements the same interface: a simplified bank-account management API that will store a balance, and allow users to query the balance or make a deposit or withdrawal.
Our development environment consists of Windows NT, Microsoft Visual Studio, and Internet Explorer, which we use to display our final applet and HTML page. Both the Java COM object and the functionally similar ActiveX C++ component are generated from Microsoft's Object Description Language (ODL).
ODL is similar in purpose to CORBA's Interface Description Language (IDL), in that it describes the interfaces of the COM or DCOM objects used in distributed-object applications. ODL syntax differs from IDL chiefly in the registration annotations (in ODL) that are required by the COM/DCOM object registration mechanism, and the data types and their mappings to the underlying programming language used to implement the actual objects. Both approaches claim total independence from the programming language used for implementation. OMG provides standard mappings to C, C++, Ada, Smalltalk, and Java. Microsoft provides mappings to C++, Java, and C.
Listing One is ODL code that defines an Account object with one attribute (balance) and three methods (makeDeposit, makeWithdrawal, and queryBalance). We generate the ODL file itself with the Microsoft development environment's ClassWizard, which supports the definition and editing of OLE objects and controls through a GUI that hides the details of the registration information (visible here in all the uuid annotations).
The interface is an IDispatch interface, which is the OLE API required for dynamic method invocation. In distributed-object systems, method invocations can be static or dynamic. In the case of static invocations, the client object knows at compile time which interfaces of the remote objects it wishes to invoke. The local interface to a remote object (usually called a stub) is linked in with the client program. In dynamic invocations, the client program gets a reference to an object at run time and, from this reference, is able to extract information about the methods supported by the remote object. CORBA, OLE, and Java RMI all support static invocation. CORBA, OLE, and JavaBeans also allow dynamic invocation, albeit through different mechanisms. The IDispatch interface is the OLE-specified interface for run-time method invocation, and it is a required interface for OLE controls (OCX or ActiveX controls).
C++ ActiveX Control
Building an ActiveX control from the ODL file is straightforward, since the ClassWizard again generates most of the code. Listing Two shows the C++ implementation of the methods.
SetModifiedFlag() in AccountCtrl::SetBalance notifies the applet that the value has been modified and that the user interface needs to be refreshed. The other methods operate on a private member, m_balance, of the control class.
Visual Studio generates the OCX file, which contains the implementation of the control and the TLB type library file that contains the methods and attribute information required at run time for dynamic invocation. It also registers the newly created control in the central COM registry file. The control can be tested right away by calling the OLE Container applications in the Tools menu of the development environment and inserting an Account control object. The OLE Container also allows live testing of the methods through the Invoke Methods menu item.
Java COM Object
Although the programming languages are different, the steps involved for generating a Java COM object are similar to those required for C++: You provide an implementation of the COM object and register it to the central OLE registry. In both cases, Microsoft Studio provides tools to assist and automate most of these operations.
We use the same ODL file and object description to create a Java implementation of the bank account object. When we created our C++ ActiveX control, we also generated a type library for the Account OCX. The Visual J++ javatlb tool converts this type library into Java interfaces and code. This conversion utility generates the required Java interface for the COM client and server programs. The command line in Example 1(a) creates a new Account directory in the default Java directory for Windows NT (usually winnt\Java\TrustLib) containing the compiled Java classes Account, _DAccount, _DAccountEvents, and a text file (summary.txt) containing the Java interface description.
Listing Three includes the Java interface generated from the ODL and type library file. Account is the interface for the whole control. _DAccount is the Java version of the IDispatch interface of the control: It is the primary interface that needs to be implemented on the server side. This implementation (Listing Four) is as straightforward in Java as it is in C++. Note that we import the Microsoft implementation of the COM model in Java and the account package from the javatlb-generated code.
You can compile the source code with either Visual J++ or the JDK's javac compiler. The final step is to use the Visual J++ utility javareg to let the OLE registry know that this implementation is available for the Account object; see Example 1(b). Basically, javareg links a compiled Java class to a given CLSID identifier in the registry, where we used a different identifier from the one used in the OCX we've just created.
Java Meets CORBA
There are a number of different implementations of the CORBA specifications. Three major implementations that currently support Java are Iona's OrbixWeb, Visigenic's VisiBroker for Java, and SunSoft's Joe. (Other vendors, such as Hewlett-Packard, have also announced support for Java in the future releases of their ORB implementations.)
We use code from Iona's OrbixWeb implementation. Because CORBA and IIOP are open standards, the mix and match of implementations should be possible to a reasonable extent. OrbixWeb 2.0 supports both client-side and server-side programming in Java. We build a Java client to a CORBA C++ server.
The interface definition language (IDL) standardized by the OMG exists for defining CORBA objects. Listing Five provides the IDL file for the bank account. The most striking differences with ODL are in the annotations. There is no registry information in IDL, but there are qualifiers for signatures where each argument can be, for instance, in, out, or inout. Although we use similar data types -- double, in this example -- that are supported by both COM and CORBA, CORBA defines many other types -- such as sequence -- for which there is no direct COM equivalent.
Writing a CORBA Server
Running the Orbix IDL compiler on the IDL file produces a stub and a skeleton. The skeleton is the server-side mapping of the IDL to the target programming language -- in this case, C++. You need to provide an implementation of the IDL attributes and operations and make it available to the broker. Again the implementation is straightforward: We actually reuse the C++ code from the implementation of the ActiveX control in the previous section. The signatures, however, are slightly different. Methods that implement IDL operations all have an additional argument, a reference to a CORBA environment. The initialization code in the ORB (see Listing Six) provides this argument at run time.
The server set-up code (Listing Seven) uses the CORBA-compliant interfaces for server-ORB communications. The initial declaration instantiates an account object from the C++ class and ties it to the account interface described in the IDL file, which is the only piece known to the ORB and to clients wishing to invoke operations on this object. The call to the impl_is_ready method is specified by CORBA: It is used to notify the ORB that the implementation of the object is ready to be invoked.
The server is compiled using a standard C++ compiler to produce the server.exe executable. All that's left is to start the ORB -- a daemon process running in the background on the network -- and register this executable to the running ORB so that it knows which program to execute when brokering requests from CORBA client programs. The Iona ORB is started with the command line in Example 2(a), which initiates a GUI to the orbixd daemon itself. The executable code is registered with the ORB using the command line in Example 2(b). Orbix comes with several utilities to display, edit, and modify registrations (lsit, catit, and so on), simplifying the administration of CORBA objects on the network.
Writing a CORBA Java Client
To create a Java client for this CORBA object, we need a Java interface that maps the IDL operations and attributes. The OrbixWeb IDL compiler automates this task. Running the OrbixWeb IDL compiler produces Java code for each IDL interface, plus a directory containing two additional source code files for each IDL interface: the Java Ref interface and Java Holder class. The account.Ref interface (Listing Eight) is the translation of the IDL definitions into a ready-to-use Java interface.
Each method throws a CORBA generic exception in addition to the optional IDL-defined exceptions. Besides these latter exceptions, the Java interface is almost identical to that generated by the Microsoft javatlb utility. Ref extends the default CORBA.Object.Ref interface, whose methods need to be implemented. These methods handle communications between the Java client, which uses the Ref interface, and the ORB itself, which routes the requests and method invocations back to the server.
The account.java file (generated automatically by the OrbixWeb IDL compiler) contains a class that implements the Ref interface and handles all interactions with the ORB and the remote CORBA object. For instance, the implementation of the makeWithdrawal methods (Listing Nine), uses CORBA's Dynamic Invocation Interface (DII) to build a request for the invocation of the makeWithdrawal method with the proper argument on the remote object. Most of this "behind-the-scenes" work is gracefully handled by the account class implementation created by the IDL compiler, and is usually kept hidden from you. The account class also knows how to bind to an Orbix ORB daemon process on the network through the _bind method, inherited from CORBA.Object.Ref and overridden in the account class implementation.
The standard sequence of operations for the Java client is to bind to the Orbix broker daemon, obtain a reference to the account object, and call methods on the acquired reference; see Example 3(a). The arguments to the bind call are the server name (as registered with the PUTIT command) and the host name of the Orbix server. Once bound, the remote CORBA object is used like any other local Java object (see Example 3(b)) even though the remote implementation is based on a different programming language, in this case C++.
Distributed Java Objects
For the sake of completeness, we implemented the account client and server as Java programs that interoperate with both CORBA and ActiveX implementations using lightweight remote method invocation (RMI) between Java programs. This is the approach suggested by JavaSoft with Java RMI in JDK 1.1. Since the Java RMI is well documented, we chose to use another remote pure Java method invocation mechanism, called HORB (available at http://ring.etl.go.jp/openlab/horb/). HORB enables remote object creation and connections, method invocations, object passing, and the usual remote object manipulations. It works with both the JDK 1.0.2 and the JDK 1.1, and therefore it works in the current generation of web browsers.
HORB consists of a lightweight Java object broker called horb and a preprocessor for Java code called horbc. The HORB daemon is either started on a server where the remote object implementation resides, or, in the case of a client applet, on the web server machine to accommodate the security restrictions of the applet model. The horbc preprocessor/compiler is used the same way as JavaSoft's rmic or Iona's IDL compiler is used: It creates the interfaces and classes that separate code usage from code implementation over the network. Instead of accepting an IDL or an ODL file as an input (as in the CORBA and COM cases) horbc works directly from a Java interface definition. So, the first step is to feed horbc with the Java interface file generated by either Microsoft's javatlb or Iona's IDL compiler. We use the javatlb output, since we are not interested in the CORBA system exceptions.
The horbc Account.java command line generates the classes Account_Proxy, which is used by the client, and Account_Skeleton, which implements the remote object. By convention, the skeleton looks for an Account_Impl class for the actual implementation of the methods defined in the Account interface. Here, we reuse the implementation we built earlier for our Java COM object without importing the Microsoft COM class libraries. In effect, the COM remote object and the HORB remote object are actually the same Java class, although they are used differently by two distinct remote invocation mechanisms.
The Account_Proxy class defines an object that is a shadow of the remote object in the client Java VM. Its role is to delegate all invocations and exception handling back to the remote object through the HORB lightweight Java broker. Example 4(a) illustrates a typical use in a client Java program, which is similar to the initialization sequence we saw in the CORBA case. HORB defines a specific URL binding where the server location is encapsulated in the HorbURL object. Once created, the proxy object is used as a local object that passes invocations and results back and forth to the remote object; see Example 4(b). HORB also handles exceptions and provides additional services, such as interface repositories and persistence, that can be used for highly distributed Java applications.
Bringing it all Together
We have a combination of five server-side object implementations: one in Java following the Java object model, one in C++ following the COM object model, one in Java following the COM object model, one in C++ following the CORBA object model, and one in Java following the distributed Java object model (as exemplified by RMI or HORB). On the client side, we really have one client, written in Java, that transparently uses four different invocation mechanisms to connect back to the remote objects: standard intra-Java-VM calls, Java VM to COM calls using Microsoft's implementation of the Java VM, CORBA compliant request brokerage by an ORB, and lightweight Java VM to Java VM method invocation.
This unique interoperable client is an applet, embedded in a web page, that exercises all of the previous distribution mechanisms. Since it depends on the Java VM/COM mechanism this applet will only run properly, at this time, within Internet Explorer 3.0. It also assumes that we have a running Orbix broker daemon and HORB daemon on the web server machine.
Listing Ten is the HTML page for our applet. The bare bones structure only contains our interoperable applet, named JavaAtm, and an instance of our C++ ActiveX control. The Visual Basic script at the top of the HTML file is used to call a public method in the applet and pass it the ActiveX control as a COM object. The applet method receives this argument as a Java object implementing the _DAccount interface.
The Java code for the JavaAtm applet uses standard AWT APIs to create a simple user interface to the account operations: deposit and withdraw. The applet uses five member variables to encapsulate the various remote objects, all using the same interface (see Listing Eleven.)
The method called from Visual Basic Script to pass the initialized COM object -- which is our ActiveX control -- is straightforward (Listing Twelve). It basically stores the passed ActiveX control as an Account object as defined by the type library class. Finally, in response to button-press events, the applet invokes the specified operations on all objects (Listing Thirteen).
The critical aspect here is the handleDeposit function, which operates on an account regardless of the actual implementation of the account itself. In this example, we call it five times to exercise the five different servers in a mix of local/remote VMs, C++ and Java implementations and across CORBA and COM/DCOM object models.
Listing One
[ uuid(3b377230-C3FF-11D0-A14F-0060B000632C), version(1.0), helpstring("Account OLE Control module"), control ] library ACCOUNTLib { importlib(STDOLE_TLB); importlib(STDTYPE_TLB); </p> </p> // Primary dispatch interface for CAccountCtrl </p> [ uuid(3b377231-C3FF-11D0-A14F-0060B000632C), helpstring("Dispatch interface for Account Control"), hidden ] dispinterface _DAccount { properties: // NOTE - ClassWizard will maintain property information here. // Use extreme caution when editing this section. //{{AFX_ODL_PROP(CAccountCtrl) [id(1)] double balance; //}}AFX_ODL_PROP </p> </p> methods: // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section. //{{AFX_ODL_METHOD(CAccountCtrl) [id(2)] void makeDeposit(double amount); [id(3)] void makeWithdrawal(double amount); [id(4)] VARIANT queryBalance(); //}}AFX_ODL_METHOD }; // Class information for CAccountCtrl </p> </p> [ uuid(3b377233-C3FF-11D0-A14F-0060B000632C), helpstring("Account Control"), control ] coclass Account { [default] dispinterface _DAccount; }; //{{AFX_APPEND_ODL}} };
Listing Two
void AccountCtrl::deposit(double amount) { m_balance += amount; } void AccountCtrl::withdraw(double amount) { m_balance -= amount; } double AccountCtrl::queryBalance() { return m_balance; } double AccountCtrl::GetBalance() { return m_balance; } void AccountCtrl::SetBalance(double newValue) { m_balance = newValue; SetModifiedFlag(); }
Listing Three
public class account/Account extends java.lang.Object{ } public interface account/_DAccountEvents extends com.ms.com.IUnknown { } public interface account/_DAccount extends com.ms.com.IUnknown { public abstract void makeWithdrawal(double); public abstract double getbalance(); public abstract void putbalance(double); public abstract void makeDeposit(double); public abstract double queryBalance(); }
Listing Four
import com.*;import account.*; </p> public class Account_Impl implements _DAccount{ private static double balance = 0.0; public void makeWithdrawal( double amount ){ balance -= amount; } public double getbalance(){ return balance; } public void putbalance( double amount){ balance = amount; } public void makeDeposit( double amount ){ balance += amount; } public double queryBalance(){ return balance; } }
Listing Five
interface account{ readonly attribute double balance; void makeDeposit( in double amount ); void makeWithdrawal( in double amount ); };
Listing Six
virtual double balance (CORBA_Environment&) { return m_balance; } virtual void makeDeposit (double amount, CORBA_Environment&) { m_balance += amount; } virtual void makeWithdrawal (double amount, CORBA_Environment&) { m_balance -= amount; }
Listing Seven
int main() {// // Create server application object account* server = new TIE_account(account_impl) (new account_impl()); // // Export the server to the network IT_TRY { CORBA_Orbix.impl_is_ready("account",IT_X); } IT_CATCHANY { cout << IT_X << endl; } IT_ENDTRY server->_release (); return 0; }
Listing Eight
package account;public interface Ref extends IE.Iona.Orbix2.CORBA.Object.Ref { public double balance() throws IE.Iona.Orbix2.CORBA.SystemException; public void makeDeposit(double amount) throws IE.Iona.Orbix2.CORBA.SystemException; public void makeWithdrawal(double amount) throws IE.Iona.Orbix2.CORBA.SystemException; }
Listing Nine
public void makeWithdrawal(double amount) throws IE.Iona.Orbix2.CORBA.SystemException { IE.Iona.Orbix2.CORBA.Request _mb = new IE.Iona.Orbix2.CORBA.Request(this, "makeWithdrawal",false); _mb.insertDouble(amount); try { _mb.invoke(); _mb.extractOutParams(); } catch (IE.Iona.Orbix2.CORBA.SystemException _ex) { throw _ex; } catch (IE.Iona.Orbix2.CORBA.CORBAException _ex) { throw new IE.Iona.Orbix2.CORBA.UNKNOWN(12001, IE.Iona.Orbix2.CORBA.CompletionStatus.MAYBE); } }
Listing Ten
<html><head> <title>JavaAtm</title> </head> <script language=VBScript> <!-- sub Window_OnLoad document.JavaAtm.setOLEAccount OLEAccount end sub --> </script> <body><body> <hr> This is an applet written in Java using MS/Visual J++ 1.1<P> <applet code=JavaAtm.class name=JavaAtm width=320 height=240 > </applet> <hr> This is an Account control written in C++ with MS/Visual C++ v4.2<P> <object classid="clsid:58738CB3-C4AB-11D0-A14F-0060B000632C" id=OLEAccount width=160 height=180 > </object> <P> <a href="JavaAtm.java">The source.</a> </body> </html> </p>
Listing Eleven
private HorbURL m_url = new HorbURL("horb://" + "-" ); private _DAccount m_HORBacc = new Account_Proxy( m_url ); private _DAccount m_javaOLEacc = (_DAccount)new Account(); private _DAccount m_localacc = new Account_Impl(); private _DAccount m_oleacc; private _DAccount m_orbixacc = (_DAccount)new account(); </p>
Listing Twelve
public void setOLEAccount( Object oleacc ){ m_oleacc = (_DAccount)new Account( oleacc ); } </p>
Listing Thirteen
private void handleDeposit( _DAccount acc, double amount, TextField outBalance, String s ){ acc.makeDeposit( amount ); outBalance.setText( s + acc.queryBalance() ); } public boolean action(Event evt, Object what){ if(evt.target == m_deposit){ Double D = new Double(m_amount.getText()); handleDeposit(m_oleacc,D.doubleValue(),m_balance,"OCX Balance is: $" ); handleDeposit(m_javaOLEacc,D.doubleValue(),m_combalance,"Java/COM Balance is: $"); handleDeposit(m_HORBacc,D.doubleValue(),m_corbabalance,"Java/HORB Balance is: $"); handleDeposit(m_localacc,D.doubleValue(),m_javabalance,"Java Balance is: $"); handleDeposit(m_orbixacc,D.doubleValue(),m_orbixbalance,"Orbix Balance is: $"); // Event handled return true; } // Event not handled return false; }
DDJ
Copyright © 1997, Dr. Dobb's Journal