Wednesday, September 19, 2012

AdaTutor - Advanced Topics (3)

Ada.Sequential_IO, Ada.Direct_IO, and Ada.Streams.Stream_IO

Ada.Text_IO creates, reads and writes text files that can be typed on the screen or printed.  Ada also provides generic packages Ada.Sequential_IO and Ada.Direct_IO, which create, read, and write binary files.  (In Ada 83, the names don't contain Ada., and Ada 95 accepts the shorter names for compatibility.)

Binary files usually can't be typed or printed, but they tend to be more efficient than text files, because the computer doesn't have to convert numbers between its internal representation and text to read and write binary files.

The full specifications of Ada.Sequential_IO and Ada.Direct_IO are in Annex A.8.1 and A.8.4 of the Ada 95 RM.  They begin as follows:

      -- "(<>)" is omitted in Ada 83.
      type Element_Type(<>) is private;
   -- "Ada." is omitted in Ada 83.
   package Ada.Sequential_IO is

      type Element_Type is private;
   -- "Ada." is omitted in Ada 83.
   package Ada.Direct_IO is

Like Ada.Text_IO, both packages have procedures to Create, Open, and Close files, but the I/O procedures are called Read and Write, rather than Get, Put, Get_Line, and Put_Line.  The first parameter is an object of type File_Type; the second is the item being read or written.  Ada.Sequential_IO always reads and writes sequentially, but Ada.Direct_IO is capable of random access.  In Ada.Direct_IO, an optional third parameter in Read and Write tells the procedure the position in the file to read From or write To; this parameter is sometimes referred to as the "index."  The start of the file is position 1.

Both Ada.Sequential_IO and Ada.Direct_IO can be instantiated for any non-limited type.  In Ada 95, Ada.Sequential_IO can be instantiated for class-wide types.  This means that a file created by Ada.Sequential_IO can be heterogeneous, containing objects of different types belonging to the same class.  However, Ada.Direct_IO can't be instantiated for class-wide types.  Files created by Ada.Direct_IO must be homogeneous (containing objects of one type only), because of the ability to use an index.  (We'll discuss Ada.Streams.Stream_IO shortly.)

Ada.Direct_IO provides a File_Mode of Inout_File as well as the usual In_File and Out_File.  In Ada 95, Ada.Sequential_IO, like Ada.Text_IO, provides an additional File_Mode, Append_File, not present in Ada 83.  Note that Ada.Text_IO and instantiations of Ada.Sequential_IO and Ada.Direct_IO each define their own File_Type, so we can't open a file with one package and then do I/O on it with another.

If you like, you can examine the file ADATU400.ADA for an example of the use of Direct_IO.  (This file is written to compile with either Ada 83 or Ada 95, so it uses the name Direct_IO rather than Ada.Direct_IO.)  AdaTutor creates a subtype for a block of characters and then instantiates Direct_IO for that subtype.  It then opens ADATUTOR.DAT with mode In_File so that it can read blocks of characters by random access.  This enables AdaTutor to find and display any screen quickly.  The preliminary comments in ADATU400.ADA describe the format of the data file ADATUTOR.DAT in detail.

You may also want to examine the files DAT2TXT.ADA and TXT2DAT.ADA, which are much simpler than ADATU400.ADA.  Again, these were written to compile with either Ada 83 or Ada 95.  These two programs are used when installing AdaTutor on non-PC computers.  Their use is described on pages 41-42 of your printed course notes.  They with both Text_IO and Direct_IO, because they access a text file as well as a binary file.  However, to avoid confusion between the two packages, they use neither Text_IO nor the instantiation of Direct_IO.  Dot notation is used instead.

DAT2TXT.ADA and TXT2DAT.ADA could have used Sequential_IO instead of Direct_IO, because they don't do random access.  (In contrast, ADATU400.ADA does random access and requires Direct_IO).  However, the file written by TXT2DAT.ADA is meant to be read by ADATU400.ADA, using an instantiation of Direct_IO, on a non-PC computer.  To avoid any possible incompatibilities between different file types on an unknown system, TXT2DAT.ADA produces the file with an instantiation of Direct_IO, because ADATU400.ADA will use a similar instantiation of Direct_IO to read the file.


Which commented line is illegal?
   with Ada.Text_IO, Ada.Sequential_IO; --
   use Ada.Text_IO, Ada.Sequential_IO;  -- 1
   procedure IO is
      subtype Line is String(1 .. 80);
      type Screen is array(1 .. 24) of Line;
      package Line_IO is new Ada.Sequential_IO(Line); --
      use Line_IO;                                    -- 2
      package Screen_IO is new Ada.Sequential_IO(Screen); --
      use Screen_IO;                                      -- 3
   end IO;

The specification of the Ada 95 package Ada.Streams.Stream_IO is in Annex A.12.1 of the Ada 95 RM.  This package enables us to create a truly heterogenous file.  It's not generic, so all the files it creates are of the same type.  The file modes available are In_File, Out_File, and Append_File.

Suppose we define type Date as before, and we want to create a file containing dates and random-length strings.  We Create a file in the same way as with Ada.Text_IO.  To write an object of a constrained type like Date to the file, we give the name of the type followed by the attribute 'Write.  (Similarly, to read, we use the attribute 'Read.)  The first parameter is of type Stream_Access; the second is the object being read or written.  The first parameter is obtained from the following function in Ada.Streams.Stream_IO:

   function Stream(File : in File_Type) return Stream_Access;

For example if we with and use Ada.Streams.Stream_IO and we have F : File_Type; and D : Date;, we can Create a file with F and then write

   Date'Write(Stream(F), D);

If we use 'Write or 'Read with an unconstrained type, the constraint information is not read or written.  So with unconstrained types, we should use the attributes 'Output and 'Input instead of 'Write and 'Read.  For example, if we want to write the string "Hello" to our file F, we would say

   String'Output(Stream(F), "Hello");

This would first store the string bounds (1 and 5), and then store the five characters of the string.

We can write our own procedures to be called by 'Read, 'Write, 'Input, and 'Output if we want to.  Our procedures can do anything they want - they don't even have to do I/O!  For example:

procedure My_Date_Write(
     Stream : access Ada.Streams.Root_Stream_Type'Class;
     D      : in Date);
  for Date'Write use My_Date_Write;

However, overwriting the standard attributes is normally not recommended.

Here's a program that stores a String, a Date, another String, and another Date in a heterogeneous file:

   with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO;
   procedure Test is
      type Date is ...
      F : File_Type;
      Create(F, Out_File, "STREAM.DAT");
      String'Output(Stream(F), "Ada");
      Date'Write(Stream(F), (12, Dec, 1815));
      String'Output(Stream(F), "United States");
      Date'Write(Stream(F), ( 4, Jul, 1776));
   end Test;

After executing the above, we could Open the file in mode In_File, and then use 'Input, 'Read, 'Input, and 'Read in that order to read the four items back.

Ada.Streams.Stream_IO can do random as well as sequential access.  We can call

   procedure Set_Index(File : in File_Type; To : in Positive_Count);
in Ada.Streams.Stream_IO to set the index before using any of the four attributes mentioned above.  The index of the first byte of the file is 1.

However, we have to know where to set the index, and that can vary from one implementation of Ada 95 to the next.  For example, when we ran the above program Test on a particular Ada 95 system, it created a 50-byte file.  That's because the Ada compiler that we used allows four bytes for each Integer, one byte for each object of an enumeration type like Month_Type, and of course one byte for each Character.  Recall that two Integers are stored before each String (the bounds).

With that particular Ada 95 compiler we could retrieve the second Date stored by adding D : Date; to our declarations and executing

   Open(F, In_File, "STREAM.DAT");
   Set_Index(F, 42);
   Date'Read(Stream(F), D);

However, with Ada 95 compilers that use a different size for Integer, the call to Set_Index would have to be changed.

In contrast, when we instantiate Ada.Direct_IO for type Date, for example, the first Date in the file is always at position 1, the second Date is always at position 2, etc., regardless of the number of bytes per Date.  In this case, the difference between position 1 and position 2 is the length of a Date, not one byte.  If our instantiation of Ada.Direct_IO is called Date_IO, and we declare D : Date; and F : Date_IO.File_Type;, we can write

   Date_IO.Read(File => F, Item => D, From => 2);
and be certain that we have read the second Date in the file.

Thus, using an instantiation of Ada.Direct_IO instead of using Ada.Streams.Stream_IO makes our program more portable, at the expense of requiring the file to be homogeneous.  Ada.Direct_IO has another advantage: mode Inout_File is available with Ada.Direct_IO, but not with Ada.Streams.Stream_IO.

< prev   next >


Anonymous said...

Nice tutorial, very informative.
But I have a question, how can you skip a line in a random access file using Direct_IO?

In other words, to skip a line using Text_IO in the command line you can use New_Line; what is used to skip a line on a file using Direct_IO?

Zhu Qunying said...

What do you mean by "skip a line"? Do you mean adding an end of line character to the output stream, just like New_line does on Text_IO? If that is the case, then simply output a new line character to the stream should do.