[Contents] [Index] [Help] [Retrace] [Browse <] [Browse >]

Packet Level I/O under Release 2


by Dale Larson and John Orr


Normally when an application needs to perform I/O using DOS, it uses
functions from the dos.library to open, read, write, and close any
I/O channels.  These functions take care of talking to the underlying
DOS device (see the ``About Devices'' section below), shielding the
programmer from most of the grunt work it takes to carry out the I/O.
These functions are adequate for most I/O needs.

 *********************************************************
 *                                                       *
 * About Devices                                         *
 *                                                       *
 *                                                       *
 * The term ``DOS devices'' is confusing because the     *
 * term ``device'' is a bit overloaded.  There are       *
 * three entities on the Amiga that are referred to as   *
 * a device.  There is the Exec device (for example      *
 * trackdisk.device and console.device) the AmigaDOS     *
 * device (such as DF0: and RAM:), and the logical       *
 * AmigaDOS device name (for example, SYS:, C:, and      *
 * S:).  The Exec level device is basically a subset of  *
 * an Exec library.  Exec devices are described in the   *
 * RKRM: Devices book. The AmigaDOS device is a higher   *
 * level entity than the Exec device (AmigaDOS devices   *
 * often utilize Exec devices).  An AmigaDOS device has  *
 * a process associated with it, normally referred to    *
 * as a handler, that controls the AmigaDOS I/O stream   *
 * via a FileHandle.  The AmigaDOS logical device name   *
 * (better known as an ``assign'') is made to look like  *
 * an AmigaDOS device, but is a higher level entity      *
 * than the AmigaDOS device.  Both the AmigaDOS device   *
 * and the logical device name are referred to by a      *
 * name ending in a colon (':').  The logical device     *
 * can refer to any directory on an AmigaDOS device (as  *
 * long as the AmigaDOS device supports having files).   *
 * The user can change the directory to which logical    *
 * device name refers from a command line. This article  *
 * deals primarily with AmigaDOS devices.                *
 *                                                       *
 *********************************************************

One of the reasons these functions are only adequate is because they
are synchronous.  When an application attempts to transfer data using
one of these functions, the I/O function waits for the entire
transfer to finish before returning.  Ideally, the application should
be able to perform I/O asynchronously, so it can do something else
while the data transfer takes place.

Another reason that these functions are only adequate is because they
don't implement all of the features built into DOS devices.  To
utilize these features, an application has to work on a lower level.
The application has to talk directly to the DOS device.


The Packet Level

When DOS functions talk to devices such as the floppy drive or the
serial port, they talk to a special process called a handler.  Every
DOS device (like CON:, SER:, DF0:, and PIPE:) has a handler process.
The handler process is responsible for receiving and carrying out a
standard set of DOS device commands.  It provides a standard
programming interface to a lower-level I/O software or hardware
entity (such as an Exec device).  The packet interface abstracts the
lower-level entity so that dos.library functions don't have to deal
with a lot of bothersome details such as moving a disk head or
reading serial registers.   The interface to every handler is the
same, so every DOS device operates in the same manner, regardless of
any underlying software or hardware.  Theoretically, to the
dos.library functions, writing to the console handler (CON:) should
be no different than writing to the serial port handler (SER:) or the
DF0: handler.  The handler lets dos.library functions treat all DOS
devices in the same way.

Typically, the handler talks directly to an underlying Exec device.
Two examples are the CON: and the DF0: handlers.  The CON: handler
process talks directly to the console.device.   When trying to access
a floppy in DF0:, the DF0: handler talks directly to the
trackdisk.device.  These handlers accept handler level commands (for
example, ACTION_READ and ACTION_WRITE) and hide any Exec level I/O
from the dos.library functions and subsequently, the application.

In cases like the DF0: handler, which is a special type of handler
called a file system, the handler has to take care of organizing the
lower-level medium into directories and files.  The DF0: handler
takes care of translating directory and file names into terms the
trackdisk.device can understand (disk blocks).  The only requirement
of a handler to be a file system is that the handler must support
files.  A file system does not have to support a directory structure
to be considered a file system.  Handlers which are not file systems
are called stream handlers.

Some handlers do not have any underlying software or hardware.
Handlers such as RAM: have no underlying software or hardware.  These
kinds of handlers take care of implementing everything necessary to
carry out the I/O rather than delegating to an Exec device.

Handlers receive commands through an Exec message port.  Every
handler process has a message port for receiving commands.  A handler
accepts a command in the form of a DosPacket structure (defined in
<dos/dosextens.h>), which is an extension of an Exec Message
structure:

struct DosPacket {
   struct Message *dp_Link;
   struct MsgPort *dp_Port;  /* Reply port for the packet */
                             /* Must be filled in each send. */
   LONG dp_Type;
   LONG dp_Res1;             /* Result #1 */
   LONG dp_Res2;             /* For file system calls this is what would
                              * have been returned by IoErr() */
   LONG dp_Arg1;             /* Argument list */
   LONG dp_Arg2;
   LONG dp_Arg3;
   LONG dp_Arg4;
   LONG dp_Arg5;
   LONG dp_Arg6;
   LONG dp_Arg7;
}; /* DosPacket */

The dp_Type field contains an identifier corresponding to the
command.  The identifiers for each of the standard commands are
defined in <dos/dosextens.h>.  For example, the command to write data
is ACTION_WRITE.  Each packet type has different parameters, which
the programmer supplies in the ``dp_Arg'' fields.

After a handler finishes with a packet, it returns the packet to the
message port in dp_Port.  The handler places return values (including
any error codes) in the result fields dp_Res1 and dp_Res2.  If there
was an error, dp_Res2 contains the corresponding DOS error code.

The packet types are described in the Amiga Mail article ``AmigaDOS
Packet Interface Specification'' on page II-5 and also in The
AmigaDOS Manual, 3rd Edition.  Since its original publication, the
``AmigaDOS Packet Interface Specification'' has been updated numerous
times in Amiga Mail to correct errors.  The information in that
article (plus its errata) should appear in the next edition of The
AmigaDOS Manual.


Finding the Handler

There are various ways to find the address of the target handler's
Message port, which is also called a process identifier by some
documentation.  When working with an open FileHandle, the handler's
port address is in the FileHandle's fh_Type field.  Note that the
dos.library functions normally access a FileHandle using a BPTR, so
to get to the fh_Type field an application has to do something like
this:

    my_handler_port = ((struct FileHandle *)BADDR(FileHandle))->fh_Type);

Because the AmigaDOS device NIL: has no handler process, the fh_Type
field of any of its file handle's will be NULL.  The application must
account for this case.

When working with a device or assign name, an application can get to
the handler's message port by using the dos.library function
GetDeviceProc():

    struct DevProc *GetDeviceProc(UBYTE *dev_name, struct DevProc *prev_devproc);

This function returns a pointer to the following structure:

struct DevProc {
        struct MsgPort *dvp_Port;     /* The handler's Message port     */
        BPTR            dvp_Lock;     /*           (send packets there) */
        ULONG           dvp_Flags;
        struct DosList *dvp_DevNode;  /* DON'T TOUCH OR USE! */
};

This function is intended to improve on the DeviceProc() function as
it also deals with multiple assigns.  GetDeviceProc() must be
countered by FreeDeviceProc().  See the GetDeviceProc() Autodoc for
more details.


The Old Way

Prior to Release 2, sending packets was relatively complicated.  Some
of the early disks from the Fred Fish Library (disks 35, 56, and 66)
contain complete examples of using DOS packets synchronously and
asynchronously.  As of Release 2.04, dos.library contains functions
which make it easier to use DOS packets synchronously and
asynchronously.


Synchronous Packet Calls

Performing synchronous packet I/O is now very simple.  Simply call
the dos.library function DoPkt():

LONG DoPkt(struct MsgPort *handler_port, LONG action_id, LONG arg1,
           LONG arg2, LONG arg3, LONG arg4, LONG arg5);

This function allocates and fills out a DosPacket using the packet
type ('action_id') and arguments ('arg1', 'arg2', etc.) you supply.
This function then sends off the packet to the 'handler_port' and
waits for the packet to return.   DoPkt() returns the value from the
packet's dp_Res1 field.  To get the value from the packet's dp_Res2
field, call the dos.library function IoErr().  Assembly language
programmers can also get dp_Res2 from register D1.


Asynchronous Packet Calls

To do asynchronous packet calls, things are a little more complex,
but they are still much better than they were before V37 (This
subject was partially covered in Martin Taillefer's article, ``Fast
AmigaDOS I/O'', page II-77, from the September/October 1992 issue of
Amiga Mail).  When using a DOS packet asynchronously, there are three
things to do before sending the packet anywhere:

    o Acquire a message port
    o Allocate and initialize the packet
    o Set up the packet's arguments


Acquiring the Message Port

It is possible for an application to use its process message port for
packet transmissions rather than allocating a new one.  It can get to
its Process structure by calling FindTask() with an argument of NULL.
The Process structure has an Exec MsgPort structure embedded in it,
which an application can get to via the pr_MsgPort field.  The bad
thing about using this port is that many system functions also use
it.  Consequently, after sending an asynchronous packet, an
application can not call any system functions that use pr_MsgPort.
This includes most dos.library functions, many C compiler linker
library functions, and many standard I/O functions (i.e., printf()
and scanf()).  The application has to wait for the asynchronous
packet to return before calling such functions.  If the application
sent an asynchronous packet and inadvertently initiated some other
packet level I/O before the asynchronous packet came back, it is
possible to cause an ``unexpected packet received'' system crash if
the asynchronous packet returned at the wrong time.  Also, an
application should remove such a packet from the process message port
using the WaitPkt() function.  This function will take care of any
system idiosyncrasies that the application would otherwise need to
address itself.  This applies to idiosyncrasies that exist now or new
ones that may appear in the future.

As of V37, arguably the best and easiest way to acquire a message
port is to create one with the exec.library call CreateMsgPort().  By
allocating its own message port, the application doesn't have to
worry about any problems with sharing the port.


Allocating and Initializing a Packet

The packet I/O system uses Exec's message passing system, so each
DosPacket must also have an Exec Message structure.  As both
structures are necessary, they have been combined in a single
StandardPacket structure (defined in <dos/dosextens.h>):

struct StandardPacket {
   struct Message   sp_Msg;
   struct DosPacket sp_Pkt;
}; /* StandardPacket */

To make a packet usable, the Exec Message and DosPacket structures
have to be set up to point to each other.  The DosPacket structure is
straightforward about its link to its corresponding message.  The
DosPacket's dp_Link field must point to the packet's Message
structure.  However, the link from the Message to the DosPacket is a
bit obscure.  The Message contains an Exec Node structure which in
turn contains a field called ln_Name.  Although the Node structure
defines ln_Name as a character array, the DOS packet I/O system
instead requires that this field point to the Message's corresponding
DosPacket:

    my_standard_pkt->sp_Msg.mn_Node.ln_Name = (STRPTR)(my_standard_pkt->sp_Pkt);

As of V37, the dos.library function AllocDosObject() is usually the
best way to allocate a StandardPacket.  To allocate one, call:

    mypacket = AllocDosObject(DOS_STDPKT, NULL);

This function takes care of linking the StandardPacket's Message and
DosPacket.  Note that the function call above does not return a
pointer to a StandardPacket.  This function allocates an entire
StandardPacket structure, but it returns a pointer to the
StandardPacket's sp_Pkt field.  To access the sp_Msg portion of the
StandardPacket, use the pointer in the dp_Link field.

Any structure allocated with AllocDosObject() must be freed with
FreeDosObject().  To free 'mypacket' from the above AllocDosObject()
call:

    FreeDosObject(DOS_STDPKT, mypacket);


Filling in Packet Arguments

Before sending an asynchronous packet, an application needs to fill
in its action and arguments.  For example, a packet set up to read
4096 bytes from an open file handle might look something like this:

. . .

#define BUFSIZE 4096

. . .

BPTR myfh;
struct DosPacket *my_pkt;
UBYTE *buffer[BUFSIZE];

        . . .

        my_pkt->dp_Type = ACTION_READ;
        my_pkt->dp_Arg1 = ((struct FileHandle *)BADDR(myfh))->fh_Arg1;
        my_pkt->dp_Arg2 = buffer;
        my_pkt->dp_Arg3 = BUFSIZE;

        . . .


Sending the Asynchronous Packet

To send a packet asynchronously, use the dos.library SendPkt() function:

    SendPkt(struct DosPacket *mypacket,
            struct MsgPort *handlerport, struct MsgPort *replyport)

This function sends 'mypacket' to 'handlerport' and exits without
waiting for the packet to come back.  When the packet returns, it
will arrive at 'replyport'.


Waiting for the Asynchronous Packet

After calling SendPkt(), the application can go about its business,
taking care of some other work while the DOS device handles the
application's packet.  Eventually, the application will have to test
the reply port to see if the packet has come back yet.  It can do
this with WaitPort() or, if the application has to test for more than
just the reply port's signal, it can use Wait().  If the application
doesn't poll its signals too often, it can test its own signal bits
using SetSignal() (see the exec.library Autodocs and the
``Introduction to Exec'' chapter of the Release 2 RKRM: Devices for
more information on how to use these functions).  Be sure to remove
any and all packets from the message port using GetMsg().

If the application used its process message port (pr_MsgPort) as the
reply port, it must use the WaitPkt() function to remove the packet
from the message port.  As mentioned earlier, this function will take
care of any hidden system idiosyncrasies so the application never has
to account for them.  WaitPkt() will not necessarily clear the
process message port's signal bit.  Like SendPkt(), WaitPkt() assumes
any DosPacket it works with is part of a StandardPacket structure.

The dos.library contains a function called AbortPkt() that, at a
glance, looks like it might be useful to an application that needs to
abort a packet (for example, upon receiving a break signal).  This
function, which was introduced in Release 2, is supposed to attempt
to abort a packet that has already been sent to a handler.  If the
abort operation is successful, the aborted packet will arrive at its
original reply port (the same place the packet would have arrived if
it was successful).  Unfortunately, the abort operation will never be
successful, at least not under existing releases of the operating
system.  Currently, this function returns without doing anything (the
most recent release is 3.0 or V39).  In the future when AbortPkt()
does do something, it will assume that any DosPacket it works with is
part of a StandardPacket structure.


Interpreting the Packet Results

Upon returning to the application, for most packets, there will be
result values in dp_Res1 and dp_Res2.  For most packets, if dp_Res1
is DOSTRUE, the packet returned without a problem.  If dp_Res1 is
DOSFALSE, the handler experienced an error with the packet and there
should be an error code in dp_Res2.  The error codes are defined in
<dos/dos.h>.  Note that not all packets follow this error reporting
convention.  See the ``AmigaDOS Packet Interface Specification''
article on page II-5 for more information on how individual packets
work.


Packets without dos.library Functions

Besides offering the ability to do asynchronous I/O, directly using
DOS packets also allows applications to utilize features of certain
handlers that are not available through a dos.library function.

The following packets are not available via a dos.library function as
of Release 3.0:

    ACTION_WRITE_PROTECT    ACTION_DISK_INFO (for console handlers)

If an application needs the feature that these packets provide, the
application has to send the packet to the handler.  The first two
packets are fairly straightforward and are explained in the
``AmigaDOS Packet Interface Specification'' article on page II-5 of
Amiga Mail.

The ACTION_DISK_INFO packet is peculiar because its function changes
depending on the handler.  When sent to a file system handler, it
returns information about its media.  The dos.library function Info()
uses this packet.  The ACTION_DISK_INFO packet has a different
meaning to a console handler.  When a console handler receives this
packet, it returns a pointer to the window of the open console file
handle.  When an application needs this pointer, the only way to get
it as of Release 3.0 is to send the console handler this packet.

There are two commonly used packets that an application could not
call through a dos.library function under 1.3 that now have
corresponding dos.library functions.  The packet to rename a disk,
ACTION_RENAME_DISK, is now available through the dos.library function
Relabel().  Also the ACTION_SCREEN_MODE packet acquired a dos.library
function, SetMode().


About the Examples

Two examples accompany this article.  The first, CompareIO.c, uses
packet level I/O to copy the standard input channel to the standard
output channel (as set up by the standard startup code, c.o).
CompareIO uses both synchronous and asynchronous I/O to perform the
copy and reports the time it takes to do each.  The other example,
InOutCTRL-C.c, also uses packets to copy the standard input channel
to the standard output channel, but it only uses asynchronous I/O.
The second example does a better job checking for a user break.

 CompareIO.c 
 InOutCTRL-C.c