Gem #19: XML streaming of Ada objects
by Pascal Obry —EDF R&D
Let's get started…
Since Ada 95 it has been possible to stream any object. Using 'Input/'Output
or 'Read/'Write
attributes, any object (tagged or not) can be streamed using a binary representation. This means that objects can be written into a file or sent over a socket, for example.
Let's take a simple object hierarchy to illustrate this feature. We'll have a Point (x and y coordinate) and a Pixel (a Point with a color).
package Object is type Point is tagged record X, Y : Float; end record; type Color_Name is (Red, Green, Blue); type Pixel is new Point with record Color : Color_Name; end record; end Object;
When writing a Point or a Pixel the first bytes in the stream are the tag external representation then the object's attribute values.
declare File : File_Type; P : Point'Class := ...; begin Create (File, Out_File, "streamed.data"); Point'Class'Output (Text_Streams.Stream (File), P); Close (File); end;
The stream will contain something like (where is the character hexadecimal code):
<01> <00> <00> <00> <0C> <00> <00> <00> O B J E C T . P I X E L <9A> <99> <99> <3f> <66> <66> <06> <41> <00>
The tag is an important part as it will be used to be able to create the proper object instance out of the stream.
P := constant Point'Class := Point'Class'Input (Text_Streams.Stream (File));
All is well! No, there is a little missing feature. There is no way to control how the external tag is streamed. In fact, it is a string and the bounds (lower and upper) are first output into the stream. These bounds are plain numbers written in binary.
In the above example we have the four first bytes for lower bound (equal to 1) and the four following bytes for the upper bound (equal to 12) then the twelve bytes for the external tag full name OBJECT.PIXEL.
In Ada 95 there is no way to stream a textual representation of objects!
But the good news is... Ada 2005 can do this. Ada 2005 goes further by adding support to control finely the external representation of any objects. This means that it is now possible to create a textual representation of such an object using the 'Class'Input
and 'Class'Output
attributes.
Let's put in place the missing pieces.
First the 'Read
and 'Write
attributes to output or read the XML representation of a Point or a Pixel.
with Ada.Streams; package Object is type Point is ... procedure Read (S : access Root_Stream_Type'Class; O : out Point); for Point'Read use Read; procedure Write (S : access Root_Stream_Type'Class; O : in Point); for Point'Write use Write; type Pixel is ... procedure Read (S : access Root_Stream_Type'Class; O : out Pixel); for Pixel'Read use Read; procedure Write (S : access Root_Stream_Type'Class; O : in Pixel); for Pixel'Write use Write;
The Read
routines could be implemented using a full featured XML parser like XML/Ada. For conciseness, we will use two very simple XML oriented routines:
procedure Skip_Tag (S : access Ada.Streams.Root_Stream_Type'Class; Ending : in Character := '>'); -- Skip the next tag on stream S, returns when Ending is found function Get_Value (S : access Ada.Streams.Root_Stream_Type'Class) return String; -- Returns the current value read on stream S
Using those routines the 'Read
and 'Write
implementation are straightforward. Here is the implementation for a Point:
procedure Read (S : access Root_Stream_Type'Class; O : out Point) is begin Skip_Tag (S); O.X := Float'Value (Get_Value (S)); Skip_Tag (S, ASCII.LF); Skip_Tag (S); O.Y := Float'Value (Get_Value (S)); Skip_Tag (S, ASCII.LF); end Read; procedure Write (S : access Root_Stream_Type'Class; O : in Point) is begin String'Write (S, " <x>" & Float'Image (O.X) & "</x>" & ASCII.LF); String'Write (S, " <y>" & Float'Image (O.Y) & "</y>" & ASCII.LF); end Write;
The last missing piece is the handing of the tag. We want the tag to be simply: <point> and <pixel> (no bound and just the name of the object instead of the full name prefixed by the enclosing package name). To set the proper tag name we use the External_Tag
attribute:
package Object is type Point is ... for Point'External_Tag use "point"; type Pixel is ... for Pixel'External_Tag use "pixel";
Then we want to plug in our own XML oriented implementation of the 'Class'Input
and 'Class'Output
attributes. This is necessary only for the root type Point:
package Object is type Point is ... for Point'External_Tag use "point"; procedure Class_Output (S : access Ada.Streams.Root_Stream_Type'Class; O : in Point'Class); for Point'Class'Output use Class_Output; function Class_Input (S : access Ada.Streams.Root_Stream_Type'Class) return Point'Class; for Point'Class'Input use Class_Input;
The Class_Output
routine must output the opening XML tag, output the object itself and then the closing XML tag. Quite simple to do; the following is the commented code:
procedure Class_Output (S : access Ada.Streams.Root_Stream_Type'Class; O : in Point'Class) is begin -- Write the opening tag Character'Write (S, '<'); String'Write (S, Ada.Tags.External_Tag (O'Tag)); String'Write (S, '>' & ASCII.LF); -- Write the object, dispatching call to Point/Pixel'Write Point'Output (S, O); -- Write the closing tag Character'Write (S, '<'); Character'Write (S, '/'); String'Write (S, Ada.Tags.External_Tag (O'Tag)); String'Write (S, '>' & ASCII.LF); end Class_Output;
And now the final part using Ada.Tags.Generic_Dispatching_Constructor
which will create an object out of a stream given the object's tag. This must do the exact opposite of the Class_Output
routine. The opening XML tag is read, then the object using Generic_Dispatching_Constructor
and finally the closing XML tag.
function Class_Input (S : access Ada.Streams.Root_Stream_Type'Class) return Point'Class is function Dispatching_Input is new Ada.Tags.Generic_Dispatching_Constructor (T => Point, Parameters => Ada.Streams.Root_Stream_Type'Class, Constructor => Point'Input); Input : String (1 .. 20); Input_Len : Natural := 0; begin -- On the stream we have , we want to get "tag_name" -- Read first character, must be '<' Character'Read (S, Input (1)); if Input (1) /= '<' then raise Ada.Tags.Tag_Error with "starting with " & Input (1); end if; -- Read the tag name Input_Len := 0; for I in Input'range loop Character'Read (S, Input (I)); Input_Len := I; exit when Input (I) = '>'; end loop; -- Check ending tag if Input (Input_Len) /= '>' or else Input_Len <= 1 then -- Empty tag raise Ada.Tags.Tag_Error with "empty tag"; else Input_Len := Input_Len - 1; end if; declare External_Tag : constant String := Input (1 .. Input_Len); O : constant Point'Class := Dispatching_Input (Ada.Tags.Internal_Tag (External_Tag), S); -- Dispatches to appropriate Point/Pixel'Input depending on -- the tag name. begin -- Skip closing object tag Skip_Tag (S); Skip_Tag (S, ASCII.LF); return O; end; end Class_Input;
At this point the code shown at the start will still work without modification. The fact that the object is streamed using an XML representation is transparent to the users of the Object package.
As a final note, for conciseness, the code as-is does not output conformant XML documents as there is no XML header and there are multiple root nodes. This is left as an exercise to the reader.
Related Source Code
Ada Gems example files are distributed by AdaCore and may be used or modified for any purpose without restrictions.
gem_19.ada