In that last section we saw how to pass basic types between C++ and Java. Now let's look at exchanging objects. The key component for exchanging objects between a C++ program and a Java program is a Java streamer class. A streamer is any class that implements the ObjectStreamer interface from package com.roguewave.vsj. Each streamer represents a mapping between a C++ class and a Java class. When reading objects, the streamer is responsible for decoding the information representing the C++ object written from the C++ program and creating an instance of the Java class. When writing objects, the streamer is responsible for taking a Java object and writing its representation on the stream in the format expected by the C++ program.
Prebuilt streamers are provided in package com.roguewave.vsj.streamer as public members of the following classes:
Mappings between C++ built-in types and corresponding Java wrappers. For example, BasicMappings.int maps a C++ int to a java.lang.Integer.
Mappings between simple Tools.h++ classes, such as RWCString and RWTime, and corresponding Java classes, such as java.lang.String and java.util.Date.
Mappings between Tools.h++ classes that inherit from RWCollectable, such as RWSet and RWCollectableString, and corresponding Java classes, such as com.roguewave.tools.v2-0.Set and java.lang.String.
Mappings between Tools.h++ template collections and counterpart collections in Java. For example, TemplateMappings.Dlist maps a Tools.h++ RWTPtrDlist<T> to a com.roguewave.tools.v2-0.Dlist.
An application that exchanges objects between a C++ program and a Java program must have the following components:
A Java streamer.
A Java program that uses the streamer to read or write an object using classes PortableInputStream or PortableOutputStream.
A C++ class of which instances can be saved to an RWvostream and/or restored from an RWvistream.
A C++ program that saves such an object to an RWpostream and/or restores one from an RWpistream.
Let's look at an example that writes out a Tools.h++ RWTime from a C++ program, and reads it into a Java program as a java.util.Date. Like most Tools.h++ classes, RWTime objects may be saved and restored via virtual streams. The Java streamer we use, SimpleMappings.Time, is one of the prebuilt streamers provided by package com.roguewave.vsj.streamer.
The C++ program
#include <fstream.h> #include <rw/rwtime.h> #include <rw/pstream.h> int main() { // default ctor creates current date and time RWTime t; ofstream fstr("test.dat"); // create an RWpostream RWpostream pstr(fstr); // save the time object to the RWpostream pstr << t; return 0; }
The Java program
import java.io.FileInputStream; import java.util.Date; import com.roguewave.vsj.PortableInputStream; import com.roguewave.vsj.streamer.SimpleMappings; public class ReadDate { public static void main(String[] args) { try { // Create a PortableInputStream PortableInputStream pstr = new PortableInputStream(new FileInputStream("test.dat")); // Restore the date/time and print it Date d = (Date) pstr.restoreObject(SimpleMappings.Time); System.out.println(d); } catch (IOException e) { System.out.println("Error: " + e); } } }
Here's a transcript of running the above example at approximately 11:17 PM on 25 Feb 1997:
> writeTime # run the C++ program > java ReadDate # run the Java program Tue Feb 25 23:17:46 PST 1997 # (output)
The Java virtual streams have two methods for reading and writing objects: VirtualInputStream.restoreObject(ObjectStreamer) and VirtualOutputStream.saveObject(Object, ObjectStreamer) respectively. It is the job of the ObjectStreamer to effect the mapping between the C++ and Java object. As noted earlier, we aren't really putting objects into the stream; we are putting information from which a corresponding object can be constructed.
There are two sorts of information one might find in the stream. First, there is information representing the internal state of an object; for example, the salary in an Employee object. Second, there is information about the object, or meta-information. In Tools.h++ we have two examples of meta-information:
Back references
These signal that the object is identical to one that occurs prior in the stream. This allows you to maintain the shape of a graph of objects; that is, if an object A containing two pointers to the same object B is saved to a stream, the object created upon restoration also contains two pointers pointing to the same object.
Class IDs
These give the run time type of the object that was saved to the stream. This allows you to restore objects without knowing their exact type. For example, you might save a random assortment of Circle, Square, and Triangle objects to a stream. The restoring program knows only that the objects in the stream are instances of classes that derive from a class Shape. This is all the knowledge that is necessary as long as: (1) the stream includes the class IDs; and (2) there is a mechanism for automatically creating objects based on those IDs.
The ObjectStreamer must understand both sorts of information well enough to serialize and de-serialize Java objects that are compatible with the information expected on the C++ side, or provided from it. But before we get too deep, let's look at a simple streamer that deals only with the first type of information, the state of an object.
Simple streamers are so called because they map classes that do not include meta-information as part of their serialized representations. We've already seen how to use a simple streamer in the example above with SimpleMappings.Time. Creating one is conceptually very simple: implement the ObjectStreamer interface and provide the two member functions restore() and save().
Let's create a streamer that maps a C++ Point struct to a Java Point class. If we know that the format of a C++ Point in a virtual stream is an int followed by an int, we can easily write the following Java Point class and streamer:
C++ Program
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } }
Java Program
import java.io.IOException; import com.roguewave.vsj.ObjectStreamer; import com.roguewave.vsj.VirtualInputStream; import com.roguewave.vsj.VirtualOutputStream; public class PointStreamer implements ObjectStreamer { public Object restore(VirtualInputStream vstr) throws IOException { int x, y; x = vstr.extractInt(); y = vstr.extractInt(); return new Point(x,y); } public void save(Object obj, VirtualOutputStream vstr) throws IOException { Point p = (Point) obj; vstr.insertInt(p.getX()); vstr.insertInt(p.getY()); } }
Using this streamer with the Java virtual stream methods saveObject() and restoreObject() allows you to exchange Point objects between C++ and Java programs, as in the RWTime example in Section 5.3.3.
While simple streamers are useful, the more powerful forms of serialization make use of information beyond an object's state. These objects require more complex streamers and, although the jtools components protect you from much of this added complexity, it helps to look more closely at the different levels of persistence found in Tools.h++. In the Tools.h++ Class Reference, every class whose instances can be saved to or restored from a virtual stream contains a persistence section that gives one of three levels of persistence relating to the types of meta-information above.
Simple
Only state information is written to the stream.
Isomorphic
State information and back references are written to the stream.
Polymorphic
State information, back references, and class Ids are written to the stream.
Of course you aren't limited to saving instances of Tools.h++ classes to virtual streams; you can save instances of your own classes as well. Each level of serialization demands different requirements of you, as shown in Table 9.
Level | Use Tools.h++ Macro... | Implement in C++... |
Simple |
none |
Member or global functions to read and write the state of your object |
Isomorphic |
RWDEFINE_PERSISTABLE |
Global functions to read and write the state of your object |
Polymorphic |
RWDEFINE_COLLECTABLE |
Derive from class RWCollectable. Provide a unique Class Id (or String Id). Member functions to read and write the state of your object |
In Tools.h++, meta-information such as back references and class Ids is handled behind the scenes by the library via inheritance and/or macros. You are responsible only for reading and writing the state of your objects. The same is true in this Java framework. The packages include the following prebuilt streamers that can decode and encode back references and class Ids:
Class PersistableStreamer (back references only)
Class CollectableStreamer (both back references and class Ids)
These streamers know only about the meta-information, and nothing about the actual classes they might encounter when streaming. Each has a corresponding interface that you use to provide the specific information about your class. For class PersistableStreamer you must supply a class that implements the DefinePersistable interface, while for class CollectableStreamer you must provide one or more classes that implement the DefineCollectable interface.
When implementing either the DefinePersistable or DefineCollectable interface, please note an important restriction regarding your implementation of the create(VirtualInputStream vstr) member function. Although the PersistableStreamer provides an input stream when calling your create() function, you must not do anything that would result in a call to vstr.restoreObject(). The PersistableStreamer keeps track of the ordinal position of each object in the stream and a recursive call to vstr.restoreObject() during create() causes these positions to be recorded incorrectly. The results could be catastrophic.
You may, however, use the provided input stream to read built-in types such as int or double if they are necessary for the creation of your object. For example, since the value of a java.lang.Integer can be set only at construction time, the following create() function must read an int from the input stream:
/** * DCCollectableInt maps the Tools.h++ class RWCollectableInt to * the Java class java.lang.Integer. */ class DCCollectableInt implements DefineCollectable { /** Return a new Integer (to be called from CollectableStreamer) */ public Object create(VirtualInputStream vstr) throws IOException { int value = vstr.extractInt(); return new Integer(value); } . . .
You may freely make recursive calls to vstr.restoreObject() from your restoreGuts() function.
Class PersistableStreamer must be used for any Tools.h++ class marked Isomorphic in the persistence section of its class reference entry, or for any of your own classes made persistent through the macro RWDEFINE_PERSISTABLE.
PersistableStreamer needs to know how to read and write the state of the object being saved or restored. This information is provided in the PersistableStreamer constructor. Specifically, you must provide an object whose class implements the DefinePersistable interface. This interface includes a method create(), for creating an instance of the object being restored, and methods saveGuts() and restoreGuts(), for writing and reading the state of the object.
The next example, called the Employee example, describes a C++ program that saves a templatized doubly-linked list, an RWTPtrDlist<Employee> of Employee objects, with a Java program that reads in the list as a com.roguewave.tools.v2-0.Dlist. Since this code example is quite long, only relevant portions are quoted below.
NOTE: Complete code for this example is located in the examples directories created for your installation of Tools.h++ Professional. The "Examples" chapter in Part V, "Resources," describes the locations of those directories, or you can check the online build guide for your installation media.
In both the C++ and Java programs, the Employee classes contain an employee's name, manager, and department.
class Employee { RWCString name_; Employee* mgr_; int dept_; public: Employee(RWCString name, Employee* mgr, int dept) : name_(name), mgr_(mgr), dept_(dept) { } // Default constructor required for RWDEFINE_PERSISTABLE macro Employee() : name_(""), mgr_(NULL), dept_(-1) { } . . . };
public class Employee { private String name_; private Employee mgr_; private int dept_; public Employee(String name, Employee mgr, int dept) { name_ = name; mgr_ = mgr; dept_ = dept; } // Default constructor required for Java virtual streams public Employee() { this("", null, -1); } . . . };
Below we show how the Employee class in the C++ program is made persistable via the macro RWDEFINE_PERSISTABLE. The rwSaveGuts() and rwRestoreGuts() methods are implemented to save and restore the contents of an Employee object.
RWDEFINE_PERSISTABLE(Employee) void rwSaveGuts(RWvostream& vstr, const Employee& e) { vstr << e.name() << *e.mgr() << e.dept(); }
void rwRestoreGuts(RWvistream& vstr, Employee& e) { RWCString name; Employee* mgr; int dept; vstr >> name; e.setName(name); vstr >> mgr; e.setMgr(mgr); vstr >> dept; e.setDept(dept); }
Since the C++ Employee class in the code above was made persistable via the macro RWDEFINE_PERSISTABLE, the Java program uses a PersistableStreamer. The PersistableStreamer is constructed with an instance of DefinePersistableEmployee, a class that implements the DefinePersistable interface to save and restore the state of the Java Employee objects.
public class DefinePersistableEmployee implements DefinePersistable { /** * Called by PersistableStreamer to create new Employee object */ public Object create(VirtualInputStream notUsed) { return new Employee(); } /** * Called by PersistableStreamer to save internal state of * Employee object */ public void saveGuts(Object obj, VirtualOutputStream vstr) throws IOException { Employee e = (Employee) obj; vstr.saveObject(e.getName(), SimpleMappings.CString); vstr.saveObject(e.getMgr(), empStreamer); vstr.insertInt(e.getDept()); } /** * Called by PersistableStreamer to restore internal state of * Employee object */ public void restoreGuts(Object obj, VirtualInputStream vstr) throws IOException { Employee e = (Employee) obj; String name = (String) vstr.restoreObject(SimpleMappings.CString); Employee mgr = (Employee) vstr.restoreObject(empStreamer); int dept = vstr.extractInt(); e.setName(name); e.setMgr(mgr); e.setDept(dept); } // Used internally by saveGuts() and restoreGuts() methods above private static final ObjectStreamer empStreamer = new PersistableStreamer(new DefinePersistableEmployee()); }
For the mapping of an RWTPtrDlist<T> to a com.roguewave.tools.v2-0.Dlist, we use one of the streamers provided by class com.roguewave.vsj.streamer.TemplateMappings. Note that we don't do anything that would result in a call to restoreObject() from the create() method. Note also that the Employee objects point to one another in their manager fields, and that the graph of these objects in maintained, as shown in the sample output.
int main() { RWTPtrDlist emplist; Employee* millie = new Employee("Millie", NULL, 1); millie->setMgr(millie); // millie is president---her own boss Employee* willy = new Employee("Willy", millie, 2); Employee* billy = new Employee("Billy", millie, 3); Employee* tillie = new Employee("Tillie",billy, 3); emplist.insert(millie); emplist.insert(willy); emplist.insert(billy); emplist.insert(tillie); ofstream fstr("emp.dat"); RWpostream pstr(fstr); pstr << emplist; // Save the graph of objects emplist.clearAndDestroy(); return 0; }
static public void main(String[] args) { try { PortableInputStream pstr = new PortableInputStream(new FileInputStream("emp.dat")); // Create a PersistableStreamer for Employee objects: ObjectStreamer empStreamer = new PersistableStreamer(new DefinePersistableEmployee()); // Restore the list using the supplied RWTValPtrDlist ---> // Dlist mapping: Dlist emplist = (Dlist) pstr.restoreObject( TemplateMappings.Dlist(empStreamer)); . . . } }
The output of the complete programs looks like this:
Restored the following list of employees: [[Millie, Dept 1, Mgr: Millie], [Willy, Dept 2, Mgr: Millie], [Billy, Dept 3, Mgr: Millie], [Tillie, Dept 3, Mgr: Billy]] Check that number and graph of objects preserved: Verify: 4 elements... true Verify: emp1 is her own boss... true Verify: emp2 and emp3 have same manager... true Verify: emp3 manages emp4... true
In the previous section, we saw how you must use class PersistableStreamer for isomorphic Tools.h++ classes. Analogously, you must use class CollectableStreamer for any Tools.h++ class marked as Polymorphic in the persistence section of its class reference entry, or for any of your own classes that were made serializable through the macros RWDEFINE_COLLECTABLE or RWDEFINE_NAMED_COLLECTABLE.
An advantage of polymorphic streaming is that you don't have to know the exact type of the objects being streamed. However, you do have to know all the types of objects that might be streamed because, at some point, there must be code that knows how to read and write the states of those objects.
CollectableStreamer accomplishes polymorphic streaming through a registration process. After you create a CollectableStreamer, you use its register() method to notify it of each class that might be streamed. Specifically, you must register objects whose classes implement the DefineCollectable interface.
The DefineCollectable interface interface, like DefinePersistable, includes the create(), saveGuts(), and restoreGuts() methods. It also includes methods that return the C++ class Id as supplied to the macro RWDEFINE_COLLECTABLE, or the C++ string Id as supplied to the macro RWDEFINE_NAMED_COLLECTABLE. Still another method returns the java.lang.Class object for the corresponding Java class. These methods are used during registration by the CollectableStreamer so that later it can automatically call the right DefineCollectable object based on the run-time type of the object being saved or restored.
While the Tools.h++ RWCollectable class provides a compareTo() member function for ordering elements, java.lang.Object does not. To make up for this discrepancy, the DefineCollectable interface includes a method that returns an object com.roguewave.tools.v2-0.Comparator. For classes that don't have meaningful comparison semantics, you can return an instance of com.roguewave.vsj.streamer.CollectableCompare. In Tools.h++, using CollectableCompare is somewhat equivalent to not overriding RWCollectable::compareTo() when you derive your own class from RWCollectable.
This next example, called the Bus example, writes a Bus object from a Java program and reads it into a C++ program. Again, since this code example is quite long, only relevant portions are quoted below.
NOTE: Complete code for this example is located in the examples directory created for your installation of Tools.h++ Professional. The "Examples" chapter in Part V, "Resources," describes the location of that directory, or you can check the online build guide for your installation media.
The Java Bus object contains an int, a String, and two Sets of Strings. On the C++ side, the Bus class derives from RWCollectable and uses the macro RWDEFINE_COLLECTABLE for persistence.
public class Bus { . . . private Set customers_ = new Set(); private Set passengers_; private int busNumber_; private String driver_; }
class Bus : public RWCollectable { RWDECLARE_COLLECTABLE(Bus) private: RWSet customers_; RWSet* passengers_; int busNumber_; RWCString driver_; };
Below, we use a CollectableStreamer and register with it an object whose class implements the DefineCollectable interface specifically for Bus objects. The sample code also takes advantage of some of the provided mappings in com.roguewave.vsj.streamer that map a java.lang.String to a Tools.h++ RWCString, a java.lang.String to a Tools.h++ RWCollectableString, and a com.roguewave.tools.v2-0.Set to a Tools.h++ RWSet.
public class DefineCollectableBus implements DefineCollectable { /** Return a new Bus (to be called from CollectableStreamer) */ public Object create(VirtualInputStream vstr) throws IOException { return new Bus(); } /** * Restore the individual elements of the vector * (to be called from CollectableStreamer). */ public void restoreGuts(Object obj, VirtualInputStream vstr, CollectableStreamer polystr) throws IOException { polystr.register(CollectableMappings.Set); polystr.register(CollectableMappings.CollectableString); Bus bus = (Bus) obj; int busno = vstr.extractInt(); String driver = (String) vstr.restoreObject(SimpleMappings.CString); Set customers = (Set) vstr.restoreObject(polystr); Set passengers = (Set) vstr.restoreObject(polystr); bus.setDriver(driver); bus.setNumber(busno); Enumeration e = customers.elements(); while (e.hasMoreElements()) { bus.addCustomer((String)e.nextElement()); } e = passengers.elements(); while (e.hasMoreElements()) { bus.addPassenger((String)e.nextElement()); } } /** * Save the bus (to be called from CollectableStreamer). */ public void saveGuts(Object obj, VirtualOutputStream vstr, CollectableStreamer polystr) throws IOException { polystr.register(CollectableMappings.Set); polystr.register(CollectableMappings.CollectableString); Bus bus = (Bus) obj; vstr.insertInt(bus.getBusNumber()); vstr.saveObject(bus.getDriver(), SimpleMappings.CString); vstr.saveObject(bus.getCustomers(), polystr); vstr.saveObject(bus.getPassengers(), polystr); } /** * Return the Tools.h++ ClassId of class Bus. (Called by * CollectableStreamer during registration). */ public int getCxxClassId() { return 200; } /** * Return the value signifying the use of a ClassId and * not a StringId. (Called by * CollectableStreamer during registration). */ public String getCxxStringId() { return DefineCollectable.NO_STRINGID; } /** * Compare buses by their bus numbers */ public Comparator getComparator() { return new Comparator() { public int compare(Object x, Object y) { return ((Bus)x).getBusNumber() - ((Bus)y).getBusNumber(); } }; } . . . }
Remember that you must not do anything that would result in a call to restoreObject() from your create() method.
RWDEFINE_COLLECTABLE(Bus, 200) . . . int Bus::compareTo(const RWCollectable* c) const { const Bus* b = (const Bus*)c; if (busNumber_ == b->busNumber_) return 0; return busNumber_ > b->busNumber_ ? 1 : -1; } . . . void Bus::saveGuts(RWvostream& strm) const { strm << busNumber_ << driver_ << customers_; if (passengers_) strm << passengers_; else strm << RWnilCollectable; } void Bus::restoreGuts(RWvistream& strm) { RWCollectable::restoreGuts(strm); strm >> busNumber_ >> driver_ >> customers_; delete passengers_; strm >> passengers_; if (passengers_ == RWnilCollectable) passengers_ = rwnil; }
Finally, it is a simple matter to serialize and restore the Bus object. On the Java side, we instantiate a Bus object, and add passengers and customers to it. Then we open a PortableOutputStream, register a new DefineCollectableBus, and save the Bus to the stream.
static public void main(String[] args) { try { Bus theBus = new Bus(1, "Keysey"); theBus.addPassenger("Jim"); theBus.addPassenger("John"); theBus.addCustomer("Ken"); theBus.addCustomer("Philippe"); PortableOutputStream pstr = new PortableOutputStream(new FileOutputStream("bus.dat")); CollectableStreamer polystreamer = new CollectableStreamer(); polystreamer.register(new DefineCollectableBus()); System.out.println("Saving the bus to a PortableOutputStream..."); pstr.saveObject(theBus, polystreamer); System.out.println("Now run 'bus_in' to read in the bus"); } . . . }
main() { Bus* newBus; if (!RWFile::Exists("bus.dat")) { cout << "Error: You must first run 'java bus_out'" << endl; return 1; } ifstream f("bus.dat"); RWpistream stream(f); cout << "Restoring from an RWpistream..." << endl << endl; . . . return 0; }
Here's what the output looks like when you run the complete Bus example:
> java bus_out # run Java program Saving the bus to a PortableOutputStream... # (output) Now run 'bus_in' to read in the bus: > bus_in # run the C++ program Restoring from an RWpistream... # (output) Bus number 1 has been restored; its driver is Keysey. It has 4 customers and 2 passengers. And they are... Customer @ 0x44ae0: Jim Customer @ 0x44b00: Ken Customer @ 0x44aa0: John Customer @ 0x44ac0: Philippe Passenger @ 0x44ae0: Jim Passenger @ 0x44aa0: John
The addresses above are given to show that the graph of objects has been maintained; for example, the two Johns are the same object.
©Copyright 2000, Rogue Wave Software, Inc.
Contact Rogue Wave about documentation or support issues.