Copyright (c) Hyperion Entertainment and contributors.

Basic Input and Output Programming

From AmigaOS Documentation Wiki
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.

Standard Command Line Parsing

AmigaDOS includes standard command line parsing. The ReadArgs() routine is the heart of this feature:

struct RDArgs* rda = IDOS->ReadArgs(CONST_STRPTR argtemplate, int32 *argarray, struct RDArgs *rdargs);

The first argument, argtemplate, is a C-style string that describes program options settable from the command line. Each option should be a full, descriptive name (for example "Quick" not "Q"). Each option can be prepended by an abbreviation of the form "abbrev=option" ("Q=Quick"). The argtemplate options are delimited by commas. Each option can also be followed by modifiers that specify characteristics of individual options. The valid modifiers are:

/S - Switch
This is considered a boolean variable, and will be set if the option name appears in the command line. The entry is the boolean (0 for not set, non-zero for set).
In a command line, if the argtemplate called for "Connect/S", the user would have to include the option name when calling command "TestApp" like this: TestApp Connect.
/K - Keyword
This means that the option will not be filled unless the keyword appears. For example, if the template is "Name/K", then unless "Name=<string>" or "Name <string>" appears in the command line, Name will not be filled.
If your application "TestApp" used the above argtemplate, valid input might look like this: TestApp Name=JoeUser or TestApp Name JoeUser.
/N - Number
This parameter is considered a decimal number, and will be converted by ReadArgs(). If a number specified is invalid, ReadArgs() will fail. The entry will be a pointer to the longword number or NULL (this is how you know if a number was specified).
If your "TestApp" command's argtemplate simply called for "/N", the user could just enter this: TestApp 5.
Frequently, a numeric parameter is combined with a keyword to make things a bit more user friendly. For example "Port/K/N" would require the user provide the keyword and number, like this: TestApp Port 23 or TestApp Port=23.
/T - Toggle
This is similar to a switch (/S), but causes the boolean value to toggle.
/A - Required
This keyword tells ReadArgs() to fail if this option is not specified in the command line.
/F - Rest of line
If this is specified, the entire rest of the line is taken as the parameter for the option, even if other option keywords appear in it.
/M - Multiple
This means the option will take any number of arguments, returning them as an array of pointers. Any arguments not considered to be part of another option will be added to this option. Only one /M should appear in a template. Example: for a template "Dir/M,All/S" the command line "foo bar all qwe" will set the boolean "all", and return an array consisting of "foo", "bar" and "qwe". The entry in the array will be a pointer to an array of string pointers, the last of which will be NULL.

There is an interaction between /M parameters and /A parameters. If there are unfilled /A parameters after parsing, ReadArgs() will grab strings from the end of a previous /M parameter list to fill the /As. This is used for things like Copy ("From/A/M,To/A").

ReadArgs()'s second argument, argarray, is an array of LONGs used by ReadArgs() to store the values of the command line arguments. Before passing this array to ReadArgs(), a program must either set the array entries to reasonable default values or clear them.

If it is successful, ReadArgs() returns a pointer to a RDArgs structure (from <dos/rdargs.h>). ReadArgs() uses this structure internally to control its operation. It is possible to pass ReadArgs() a custom RDArgs structure (myrda in the ReadArgs() prototype above). For most applications, myrda will be NULL, because most applications do not need to control ReadArgs().

struct RDArgs
{
    struct CSource RDA_Source;  /* Select input source */
    LONG           RDA_DAList;  /* PRIVATE */
    UBYTE*         RDA_Buffer;  /* Optional string parsing space */
    LONG           RDA_BufSiz;  /* Size of RDA_Buffer (0..n) */
    UBYTE*         RDA_ExtHelp; /* Optional extended help */
    LONG           RDA_Flags;   /* Flags for any required control */
};

Any successful call to ReadArgs() (even those that use a custom RDArgs structure) must be complemented with a call to FreeArgs():

void FreeArgs(struct RDArgs* rda);

where rda is the RDArgs structure used by ReadArgs().

An application can use a custom RDArgs structure to provide an alternate command line source, an alternate temporary storage buffer, or an extended help string. The custom RDArgs structure must be allocated with AllocDosObject() and deallocated with FreeDosObject(). See the autodocs for more details on these functions.

The RDArgs.RDA_Source field is used to supply ReadArgs() with an alternate command line to parse. ReadArgs() will use it only if the RDA_Source fields are filled in. The CSource structure (from <dos/rdargs.h>) is as follows:

struct CSource
{
    UBYTE* CS_Buffer;
    LONG   CS_Length;
    LONG   CS_CurChr;
};

where CS_Buffer is the command line to parse, CS_Length is the length of CS_Buffer, and CS_CurChr is the position in CS_Buffer from which ReadArgs() should begin its parsing. Normally CS_CurChr is initialized to zero. Note that currently the buffer must end with a newline (\\n).

RDA_DAList is private and must be set to NULL before ReadArgs() uses this structure.

The RDA_Buffer and RDA_BufSiz fields allow an application to supply a fixed-size buffer in which to store parsed data. This allows the application to pre-allocate a buffer rather than requiring ReadArgs() to allocate buffer space. If either RDA_Buffer or RDA_BufSiz is NULL, ReadArgs() assumes the application has not supplied a buffer.

RDA_ExtHelp is a text string which ReadArgs() displays if the user asks for additional help. The user asks for additional help by typing a question mark when ReadArgs() prompts the user for input (which normally happens only when he or she types a question mark as the only argument on the command line).

RDA_Flags is a bit field used to toggle certain options of ReadArgs(). Currently, only one option is implemented, RDAF_NOPROMPT. When set, RDAF_NOPROMPT prevents ReadArgs() from prompting the user.

The following code, ReadArgs.c, uses a custom RDArgs structure to pass a command line to ReadArgs().

/* ReadArgs.c */
 
#include <dos/dos.h>
#include <dos/rdargs.h>
#include <proto/dos.h>
 
UBYTE* vers = "\\0$VER: ReadArgs 1.0";
 
#define TEMPLATE   "S=SourceFiles/A/M,D=DebugLevel/K/N,L=Link/S"
#define OPT_SOURCE 0
#define OPT_DEBUG  1
#define OPT_LINK   2
#define OPT_COUNT  3
 
/* The array of LONGs where ReadArgs() will store the data from the
command line arguments. C guarantees that all the array entries will be
set to zero.
*/
int32 result[OPT_COUNT];
 
/* My custom RDArgs */
struct RDArgs* myrda;
 
uint32 StrLen(CONST_STRPTR);
 
int main()
{
    uint16 x;
    char** sourcefiles;
 
    /* Need to ask DOS for a RDArgs structure */
    if (myrda = (struct RDArgs *) IDOS->AllocDosObject(DOS_RDARGS, NULL))
    {   /* set up my parameters for ReadArgs() */
 
        /* use the following command line */
        myrda->RDA_Source.CS_Buffer = "file1 file2 file3 D=1 Link file4 file5\\n";
        myrda->RDA_Source.CS_Length = (int32) StrLen(myrda->RDASource.CS_Buffer);
 
        /* parse my command line */
        if (IDOS->ReadArgs(TEMPLATE, result, myrda))
        {   /* start printing out the results */
 
            /* We don't need to check if there is a value in
            result[OPT_SOURCE] because the ReadArgs() template
            requires (using the /A modifier) that there be
            file names, so ReadArgs() will either fill in
            a value or ReadArgs() will fail
            */
 
            sourcefiles = (UBYTE **) result[OPT_SOURCE];
            /* VPrintf() is a lot like Printf() except it's in
            arguments are referenced from an
            array rather than being extracted from the stack.
            */
            VPrintf("Files specified:\\n", NULL);
            for (x = 0; sourcefiles[x]; x++)
                IDOS->VPrintf("\\t%s\\n", (int32 *) &sourcefiles[x]);
 
            /* Is there something in the "DebugLevel" option?
            If there is, print it.
            */
            if (result[OPT_DEBUG])
                IDOS->VPrintf("Debugging Level = %ld\\n", (int32 *) result[OPT_DEBUG]);
 
            /* If the link toggle was present, say something about it. */
            if (result[OPT_LINK])
                IDOS->VPrintf("Linking...\\n", NULL);
            IDOS->FreeArgs(myrda);
        }
        IDOS->FreeDosObject(DOS_RDARGS, myrda);
   }
 
   return 0;
}
 
uint32 StrLen(CONST_STRPTR string)
{
    uint32 x = 0;
 
    while (string[x++]);
    return(x);
}