Copyright (c) Hyperion Entertainment and contributors.

Basic Input and Output Programming

From AmigaOS Documentation Wiki
Revision as of 23:06, 22 April 2014 by Steven Solie (talk | contribs)
Jump to navigation Jump to search

Basic Input and Output Programming

This section covers the basics of dos.library I/O functions. Many C compilers supply their own standard I/O functions which differ from the dos.library routines described here. See your compiler manual for information on its standard I/O functions.

The original dos.library was written in BCPL, a precursor to the C programming language. Although dos.library is now written in C, remnants of BCPL remain to keep dos.library backwards compatible. One of these is the BCPL pointer or BPTR.

BCPL only thinks in 32-bit longwords. When BCPL thinks about an individual memory address, it thinks of a four byte wide quantity rather than a single byte quantity (like the CPU), so when BCPL think of memory address 2, it thinks of the second (after the zeroth and the first) set of four bytes (what the CPU thinks of as addresses 8 through 11).

Because AmigaDOS uses BPTRs, programming certain areas of AmigaDOS will require converting normal addresses to and from BPTRs. To convert a normal address (which must be longword aligned) to a BPTR, divide it by four (>> 2). The dos.h include file contains macros to convert between the two address formats. Note that, because a BPTR refers to a longword (a 32-bit address), anything it addresses must be longword alignment.

Another BCPL remnant is the BCPL string, or BSTR. A BSTR is a BPTR to a BCPL string. The first byte of a BCPL string contains the length of the string. The remaining bytes make up the actual characters of the string.

One of the basic features of AmigaDOS is file input and output. To perform file I/O, DOS requires something called a file handle. A file handle is what DOS uses to identify and keep track of an open file.

Another thing that DOS uses when reading and writing files is a Lock. AmigaDOS uses a Lock to "lock" a file, or to prevent two (or more) processes from manipulating a file at the same time. While a file is locked, other processes cannot make changes to it.

There are two types of locks, shared locks and exclusive locks. A shared lock normally is used for read-only access to a file. There can be many shared locks on a file at any given time. The purpose of a shared lock is to prevent another process from writing to a file while your program is reading it. An exclusive lock is used for write access to a file. While any locks exist on a file (shared or exclusive), no one can create a new exclusive lock on the file.

To open a file (and obtain a file handle to it), use the dos.library's Open() function:

BPTR myfilehandle = IDOS->Open(UBYTE* filename, LONG accessMode);

where myfilehandle is a BPTR to a FileHandle structure, filename is a C string naming the file to open (relative to the current directory), and accessMode is either MODE_OLDFILE, MODE_NEWFILE or MODE_READWRITE. MODE_OLDFILE opens an existing file for reading or writing. In this mode, DOS creates a read lock on the file (which changes to a write lock while you are trying to write to the file). Attempting to open a nonexistent file in this mode causes the Open() to fail (returning a NULL). MODE_NEWFILE opens a file for reading and writing, and will delete the file if it already exists. In this mode, DOS creates an exclusive lock on the file. MODE_READWRITE opens a file (with a shared lock) for reading and writing but will not delete the file if it already exists.

The Read() and Write() functions are used to read and write blocks of data to and from a buffer:

LONG actualcount = IDOS->Read (BPTR filehandle, APTR buffer, LONG length);
LONG actualcount = IDOS->Write(BPTR filehandle, APTR buffer, LONG length);

Normally, these functions are used for reading and writing large blocks of data as they are not very efficient for small reads and writes. Buffered reading and writing routines are much more efficient for small blocks of data. They are discussed later.

The Read() and Write() calls take the same arguments: a file handle for the file in question, a buffer that holds the data, and a count of how many bytes to read or write. Both functions return the actual number of bytes read or written. If Read() returns a zero, then no characters were read and the current file position is at the end of the file. If either function returns -1, an error occurred (use the dos.library function IoErr() to get the code of the most recent DOS error).

DOS maintains a current position for open files. DOS increments a file's current position for every byte it reads or writes. Read()s and Write()s are relative to the current position, so if you open an already existing file and you want to write additional data tot it, you must make sure that the current file position is at the end of the file. When Open() first opens a file, the file's current position is at the beginning of the file.

Using the Seek() function, a program can move the current file position:

LONG oldposition = IDOS->Seek(BPTR filehandle, LONG fileposition, LONG offset_from_where);

Seek()'s fileposition field is the new file position, which is relative to the offset. The offset (offset_from_where) is one of the following:

   OFFSET_END                 the fileposition (which should be zero
                              here) is relative to the end of the file.
   OFFSET_CURRENT             the fileposition is relative to the current
                              file position.
   OFFSET_BEGINNING           the fileposition is relative to the
                              beginning of the file.

Seek() returns the file position before the Seek() occurred, or a -1 to indicate an error.

To close an open file handle, use dos.library's Close() function:

LONG returnvalue = IDOS->Close(BPTR file);

Close() returns either DOSTRUE (-1) for success or DOSFALSE (0) for failure.

The following is a very simple example of basic file I/O.

// RW.C
 
/* This program opens the file "s:startup-sequence" and
copies it to "ram:qwe".
*/
 
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <proto/dos.h>
 
/* For most applications, the buffer size below is way too small.
For most programming purposes, the buffer should be MUCH larger
to make reading and writing more efficient.
*/
#define BUFSIZE 256
 
TEXT* vers = "\\0$VER: RW 1.0";
 
uint8 buffer[BUFSIZE];
 
int main()
{
    BPTR myfile, startup;
    LONG count, actual;
 
    if (myfile = IDOS->Open("ram:qwe", MODE_NEWFILE))
    {   if (startup = IDOS->Open("s:startup-sequence", MODE_OLDFILE))
        {   count = 1;
            /* keep writing until we hit the end of the file
            or an error occurs. */
            while ((actual = IDOS->Read(startup, buffer, BUFSIZE)) && (count > 0))
                count = IDOS->Write(myfile, buffer, actual);
 
            if (actual < 0) IDOS->Printf("Error while reading\\n");
            if (count < 0) IDOS->Printf("Error while writing\\n");
            IDOS->Close(startup);
        }
        IDOS->Close(myfile);
    }
}

Using File Handles

Each AmigaDOS device has a process associated with it called a handler. The handler process is used by AmigaDOS to talk to Exec devices. A handler is responsible for processing a standard set of commands that AmigaDOS sends when it needs to use a device. AmigaDOS can use handlers for things such as reading and writing files, writing to the console, or sending output to a printer. DH0:, RAM:, CON: and SER: each are controlled with their respective handler process.

One particular type of handler is the file handler. A file handler is used to maintain a filing system (ie. files and directories) on a particular device. The handler process responsible for DF0: is one example of a file handler. This handle allows AmigaDOS to use the Exec device trackdisk.device. If you try to read a file from DF0:, DOS sends a read request to DF0:'s handler process. DF0:'s handler interprets the read command and extracts the file data from some place on the disk. DOS does not know anything about how the underlying device works, it just asks the file handler for data and the handler supplies it. This scheme makes adding new AmigaDOS devices to the system relatively easy.

Although writing handlers is beyond the scope of this section, using them is not. The handler makes it possible to think about various forms of I/O as file I/O. For example, using the Open() routine, it is possible to open a console window and write to it as if it was a file:

BPTR consoleFH = IDOS->Open("CON:20/20/500/100/Console", MODE_NEWFILE);

This opens a console window on the Workbench screen. You can write directly to the console window and read directly from it using the file handle returned by the Open() call (note that when reading from a CON: window file handle, the user must hit RETURN before the data can be read).

Every process has a standard input and standard output file handle associated with it. For Shell-based programs, the standard I/O handles are normally the Shell's console window. It is possible to redirect the standard I/O for a program started from the Shell using the < (input), > (output) and *> (error output) redirection operators. With these the user can, for example, redirect output from a Shell-based program to a file, or to PRT:. The dos.library routines Input(), Output() and ErrorOutput() return the current input, output and error output file handles, respectively:

BPTR inputFH = IDOS->Input(void);
BPTR outputFH = IDOS->Output(void);
BPTR errorOutputFH = IDOS->ErrorOutput(void);

SelectInput(), SelectOutput() and SelectErrorOutput() allow a program to change the current standard I/O handles:

BPTR oldinputFH = IDOS->SelectInput(BPTR newinputFH);
BPTR oldoutputFH = IDOS->SelectOutput(BPTR newoutputFH);
BPTR olderroroutFH = IDOS->SelectErrorOutput(BPTR newerroroutFH);

where newinputFH/newoutputFH/newerroroutFH is an open, valid file handle, and oldinputFH/oldoutputFH/olderroroutFH is the previous standard input/output/error output handle. Do not carelessly discard the old file handle as it is still valid and will have to be closed or reinstated eventually.

Buffered I/O

The buffered I/O routines improve the performance of small reads and writes by reducing the overhead involved in reading and writing small blocks of data.

The buffered I/O equivalents to Read() and Write() are:

   LONG actualblocks = FRead(BPTR fh, APTR buffer, ULONG blocklength,
       ULONG numblocks);
   LONG actualblocks = FWrite(BPTR fh, APTR buffer, ULONG blocklength,
       ULONG numblocks);

These two functions are similar to their unbuffered counterparts, but their arguments differ slightly. Instead of requiring a number of bytes to read or write, FRead() and FWrite() will read/write a number (numblocks) of blocks. Each block is blocklength bytes long. These functions return the number of blocks actually written or read (or zero if EOF is read). If there is an error, both functions return the number of blocks written or read, but the number will differ from the number of blocks requested.

When switching back and forth between buffered to unbuffered I/O, you must flush the file buffer using the FFlush() routine:

   void FFlush(BPTR fh);

Currently, DOS flushes the buffer when it is full, or when someone writes a \\n, \\0, \\r or \\12. When using buffered I/O on your original standard input file handle, you must Flush() before reading any data.

There are some routines for buffered reading and writing single characters and strings:

   LONG FGetC(BPTR fh);
   UBYTE* FGets(BPTR fh, UBYTE* buf, ULONG buflen);
   LONG FPuts(BPTR fh, UBYTE* str);
   void FPutC(BPTR fh, ULONG ch);
   LONG WriteChars(UBYTE* buf, unsigned long buflen);
   LONG PutStr(UBYTE* str);
   LONG UnGetC(BPTR fh, long character);

See the Autodocs for more information on these and other related functions.

There are also some buffered I/O functions for writing formatted data to a file handle:

   LONG VFPrintf(BPTR fh, UBYTE* formatstring, LONG* argarray);
   LONG VPrintf(UBYTE* formatstring, LONG* argarray);

where formatstring is a C-style string that contains a printf()-like formatting template with the following supported % options:

 %flags][width.limit][length]type
 flags  - only one allowed. '-' specifies left justification.
 width  - field width. If the first character is a '0', the field will be
          padded with leading 0s.
        - must follow the field width, if specified.
 limit  - maximum number of characters to output from a string (only valid
          for %s).
 length - size of input data defaults to WORD for types d, x and c; 'l'
          changes this to LONG (32-bit).
 type   - supported types are:
          b   - BSTR; data is 32-bit BPTR to byte count followed by a byte
                string, or NULL terminated byte string. A NULL BPTR is
                treated as an empty string. (Added in V36 Exec).
          d   - decimal.
          x   - hexadecimal.
          s   - string; a 32-bit pointer to a NULL-terminated byte string.
                In V36, a NULL pointer is treated as an empty string.
          c   - character.

The argarray is a pointer to an array of arguments corresponding to the entries in the formatting template.

The only difference between these two functions is that VPrintf() writes to the current standard output file handle and VFPrintf() writes to the file handle you supply.