Standard library: Files and streams
Ada provides different approaches for file input/output (I/O):
Text I/O, which supports file I/O in text format, including the display of information on the console.
Sequential I/O, which supports file I/O in binary format written in a sequential fashion for a specific data type.
Direct I/O, which supports file I/O in binary format for a specific data type, but also supporting access to any position of a file.
Stream I/O, which supports I/O of information for multiple data types, including objects of unbounded types, using files in binary format.
This table presents a summary of the features we've just seen:
File I/O option |
Format |
Random access |
Data types |
---|---|---|---|
Text I/O |
text |
string type |
|
Sequential I/O |
binary |
single type |
|
Direct I/O |
binary |
✓ |
single type |
Stream I/O |
binary |
✓ |
multiple types |
In the following sections, we discuss details about these I/O approaches.
Text I/O
In most parts of this course, we used the Put_Line
procedure to display
information on the console. However, this procedure also accepts a
File_Type
parameter. For example, you can select between standard
output and standard error by setting this parameter explicitly:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Std_Text_Out is begin Put_Line (Standard_Output, "Hello World #1"); Put_Line (Standard_Error, "Hello World #2"); end Show_Std_Text_Out;
You can also use this parameter to write information to any text file. To
create a new file for writing, use the Create
procedure, which
initializes a File_Type
element that you can later pass to Put_Line
(instead of, e.g., Standard_Output
). After you finish writing
information, you can close the file by calling the Close
procedure.
You use a similar method to read information from a text file. However,
when opening the file, you must specify that it's an input file
(In_File
) instead of an output file. Also, instead of calling the
Put_Line
procedure, you call the Get_Line
function to read
information from the file.
Let's see an example that writes information into a new text file and then reads it back from the same file:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Simple_Text_File_IO is F : File_Type; File_Name : constant String := "simple.txt"; begin Create (F, Out_File, File_Name); Put_Line (F, "Hello World #1"); Put_Line (F, "Hello World #2"); Put_Line (F, "Hello World #3"); Close (F); Open (F, In_File, File_Name); while not End_Of_File (F) loop Put_Line (Get_Line (F)); end loop; Close (F); end Show_Simple_Text_File_IO;---- run info:
In addition to the Create
and Close
procedures, the standard
library also includes a Reset
procedure, which, as the name implies,
resets (erases) all the information from the file. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Text_File_Reset is F : File_Type; File_Name : constant String := "simple.txt"; begin Create (F, Out_File, File_Name); Put_Line (F, "Hello World #1"); Reset (F); Put_Line (F, "Hello World #2"); Close (F); Open (F, In_File, File_Name); while not End_Of_File (F) loop Put_Line (Get_Line (F)); end loop; Close (F); end Show_Text_File_Reset;---- run info:
By running this program, we notice that, although we've written the first
string ("Hello World #1"
) to the file, it has been erased because of the
call to Reset
.
In addition to opening a file for reading or writing, you can also open an
existing file and append to it. Do this by calling the Open
procedure
with the Append_File
option.
When calling the Open
procedure, an exception is raised if the
specified file isn't found. Therefore, you should handle exceptions in
that context. The following example deletes a file and then tries to open
the same file for reading:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Text_File_Input_Except is F : File_Type; File_Name : constant String := "simple.txt"; begin -- Open output file and delete it Create (F, Out_File, File_Name); Delete (F); -- Try to open deleted file Open (F, In_File, File_Name); Close (F); exception when Name_Error => Put_Line ("File does not exist"); when others => Put_Line ("Error while processing input file"); end Show_Text_File_Input_Except;---- run info:
In this example, we create the file by calling Create
and then
delete it by calling Delete
. After the call to Delete
, we can
no longer use the File_Type
element. After deleting the file, we
try to open the non-existent file, which raises a Name_Error
exception.
Sequential I/O
The previous section presented details about text file I/O. Here, we
discuss doing file I/O in binary format. The first package we'll explore is
the Ada.Sequential_IO
package. Because this package is a generic
package, you need to instantiate it for the data type you want to use for
file I/O. Once you've done that, you can use the same procedures we've seen
in the previous section: Create
, Open
, Close
, Reset
and
Delete
. However, instead of calling the Get_Line
and Put_Line
procedures, you'd call the Read
and Write
procedures.
In the following example, we instantiate the Ada.Sequential_IO
package for floating-point types:
with Ada.Text_IO; with Ada.Sequential_IO; procedure Show_Seq_Float_IO is package Float_IO is new Ada.Sequential_IO (Float); use Float_IO; F : Float_IO.File_Type; File_Name : constant String := "float_file.bin"; begin Create (F, Out_File, File_Name); Write (F, 1.5); Write (F, 2.4); Write (F, 6.7); Close (F); declare Value : Float; begin Open (F, In_File, File_Name); while not End_Of_File (F) loop Read (F, Value); Ada.Text_IO.Put_Line (Float'Image (Value)); end loop; Close (F); end; end Show_Seq_Float_IO;---- run info:
We use the same approach to read and write complex information. The following example uses a record that includes a Boolean and a floating-point value:
with Ada.Text_IO; with Ada.Sequential_IO; procedure Show_Seq_Rec_IO is type Num_Info is record Valid : Boolean := False; Value : Float; end record; procedure Put_Line (N : Num_Info) is begin if N.Valid then Ada.Text_IO.Put_Line ("(ok, " & Float'Image (N.Value) & ")"); else Ada.Text_IO.Put_Line ("(not ok, -----------)"); end if; end Put_Line; package Num_Info_IO is new Ada.Sequential_IO (Num_Info); use Num_Info_IO; F : Num_Info_IO.File_Type; File_Name : constant String := "float_file.bin"; begin Create (F, Out_File, File_Name); Write (F, (True, 1.5)); Write (F, (False, 2.4)); Write (F, (True, 6.7)); Close (F); declare Value : Num_Info; begin Open (F, In_File, File_Name); while not End_Of_File (F) loop Read (F, Value); Put_Line (Value); end loop; Close (F); end; end Show_Seq_Rec_IO;---- run info:
As the example shows, we can use the same approach we used for
floating-point types to perform file I/O for this record. Once we
instantiate the Ada.Sequential_IO
package for the record type, file
I/O operations are performed the same way.
Direct I/O
Direct I/O is available in the Ada.Direct_IO
package. This mechanism
is similar to the sequential I/O approach just presented, but allows us to
access any position in the file. The package instantiation and most
operations are very similar to sequential I/O. To rewrite the
Show_Seq_Float_IO
application presented in the previous section to use
the Ada.Direct_IO
package, we just need to replace the instances of
the Ada.Sequential_IO
package by the Ada.Direct_IO
package. This is the new source code:
with Ada.Text_IO; with Ada.Direct_IO; procedure Show_Dir_Float_IO is package Float_IO is new Ada.Direct_IO (Float); use Float_IO; F : Float_IO.File_Type; File_Name : constant String := "float_file.bin"; begin Create (F, Out_File, File_Name); Write (F, 1.5); Write (F, 2.4); Write (F, 6.7); Close (F); declare Value : Float; begin Open (F, In_File, File_Name); while not End_Of_File (F) loop Read (F, Value); Ada.Text_IO.Put_Line (Float'Image (Value)); end loop; Close (F); end; end Show_Dir_Float_IO;---- run info:
Unlike sequential I/O, direct I/O allows you to access any position in
the file. However, it doesn't offer an option to append information to
a file. Instead, it provides an Inout_File
mode allowing reading
and writing to a file via the same File_Type
element.
To access any position in the file, call the Set_Index
procedure to set
the new position / index. You can use the Index
function to retrieve
the current index. Let's see an example:
with Ada.Text_IO; with Ada.Direct_IO; procedure Show_Dir_Float_In_Out_File is package Float_IO is new Ada.Direct_IO (Float); use Float_IO; F : Float_IO.File_Type; File_Name : constant String := "float_file.bin"; begin -- Open file for input / output Create (F, Inout_File, File_Name); Write (F, 1.5); Write (F, 2.4); Write (F, 6.7); -- Set index to previous position -- and overwrite value Set_Index (F, Index (F) - 1); Write (F, 7.7); declare Value : Float; begin -- Set index to start of file Set_Index (F, 1); while not End_Of_File (F) loop Read (F, Value); Ada.Text_IO.Put_Line (Float'Image (Value)); end loop; Close (F); end; end Show_Dir_Float_In_Out_File;---- run info:
By running this example, we see that the file contains 7.7
, rather than
the previous 6.7
that we wrote. We overwrote the value by changing the
index to the previous position before doing another write.
In this example we used the Inout_File
mode. Using that mode, we just
changed the index back to the initial position before reading from the file
(Set_Index (F, 1)
) instead of closing the file and reopening it for
reading.
Stream I/O
All the previous approaches for file I/O in binary format (sequential and direct I/O) are specific for a single data type (the one we instantiate them with). You can use these approaches to write objects of a single data type that may be an array or record (potentially with many fields), but if you need to create and process files that include different data types, or any objects of an unbounded type, these approaches are not sufficient. Instead, you should use stream I/O.
Stream I/O shares some similarities with the previous approaches. We still
use the Create
, Open
and Close
procedures. However, instead of
accessing the file directly via a File_Type
element, you use a
Stream_Access
element. To read and write information, you use the
'Read
or 'Write
attributes of the data types you're reading
or writing.
Let's look at a version of the Show_Dir_Float_IO
procedure from the
previous section that makes use of stream I/O instead of direct I/O:
with Ada.Text_IO; with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO; procedure Show_Float_Stream is F : File_Type; S : Stream_Access; File_Name : constant String := "float_file.bin"; begin Create (F, Out_File, File_Name); S := Stream (F); Float'Write (S, 1.5); Float'Write (S, 2.4); Float'Write (S, 6.7); Close (F); declare Value : Float; begin Open (F, In_File, File_Name); S := Stream (F); while not End_Of_File (F) loop Float'Read (S, Value); Ada.Text_IO.Put_Line (Float'Image (Value)); end loop; Close (F); end; end Show_Float_Stream;---- run info:
After the call to Create
, we retrieve the corresponding
Stream_Access
element by calling the Stream
function. We then
use this stream to write information to the file via the 'Write
attribute of the Float
type. After closing the file and
reopening it for reading, we again retrieve the corresponding
Stream_Access
element and processed to read information from the
file via the 'Read
attribute of the Float
type.
You can use streams to create and process files containing different data
types within the same file. You can also read and write unbounded data
types such as strings. However, when using unbounded data types you must
call the 'Input
and 'Output
attributes of the unbounded data
type: these attributes write information about bounds or discriminants in
addition to the object's actual data.
The following example shows file I/O that mixes both strings of different lengths and floating-point values:
with Ada.Text_IO; with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO; procedure Show_String_Stream is F : File_Type; S : Stream_Access; File_Name : constant String := "float_file.bin"; procedure Output (S : Stream_Access; FV : Float; SV : String) is begin String'Output (S, SV); Float'Output (S, FV); end Output; procedure Input_Display (S : Stream_Access) is SV : String := String'Input (S); FV : Float := Float'Input (S); begin Ada.Text_IO.Put_Line (Float'Image (FV) & " --- " & SV); end Input_Display; begin Create (F, Out_File, File_Name); S := Stream (F); Output (S, 1.5, "Hi!!"); Output (S, 2.4, "Hello world!"); Output (S, 6.7, "Something longer here..."); Close (F); Open (F, In_File, File_Name); S := Stream (F); while not End_Of_File (F) loop Input_Display (S); end loop; Close (F); end Show_String_Stream;---- run info:
When you use Stream I/O, no information is written into the file indicating the type of the data that you wrote. If a file contains data from different types, you must reference types in the same order when reading a file as when you wrote it. If not, the information you get will be corrupted. Unfortunately, strong data typing doesn't help you in this case. Writing simple procedures for file I/O (as in the example above) may help ensuring that the file format is consistent.
Like direct I/O, stream I/O support also allows you to access any location in the file. However, when doing so, you need to be extremely careful that the position of the new index is consistent with the data types you're expecting.