AmigaDOS is the only part of the Amiga ROM operating system which was not created in-house by the original Amiga developers. It was an adaptation of parts of the Tripos operating system, which would include the Amiga file system, ram-handler, the AmigaDOS kernel, a library of functions which interface the kernel with the dos.library and, lastly, the CLI shell.
The Tripos (the name stands for "Trivially portable operating system") operating system was written in BCPL, which is one of the precursors of the 'C' programming language. The AmigaDOS kernel code was built on top of a portability layer written in assembly language, with the portable BCPL functions on top. The Amiga file system, ram-handler and the shell would directly access the AmigaDOS kernel code, whereas Amiga software written in 'C' or assembly language would make use of the dos.library interface instead.
Although the AmigaDOS kernel as well as the Amiga file system, ram-handler, etc. would eventually be rewritten in 'C' and assembly language code for Kickstart 2.0, the legacy of the use of BCPL still defines how developers use the data structures and the APIs of dos.library.
BCPL as a precursor to 'C' is a much simpler language by design. It only has a single data type, the "word". In AmigaDOS, the word is a signed 32-bit integer, as defined by the LONG data type. The address space of the operating system is defined in terms of the "word". BCPL does not discriminate between integers and memory addresses, there is no type checking performed. A memory address is represented by a BCPL pointer, which is basically the 32-bit address divided by the size of the "word", i.e. divided by 4. Compound data structures in BCPL look more like arrays of integers with named array offsets. Strings consist of a single length byte followed by as many characters as indicated by the length byte. This limits the strings to a maximum of 255 characters.
You will find the BPTR and BSTR types in both the dos.library API and its data structures. Furthermore, you will find that some API functions require that the 32-bit adress of a parameter submitted must translate into a valid BPTR. As mentioned above, this means that the 32-bit address must be LONG-aligned, i.e. it must be divisible by 4. Such requirements are part of the legacy of the AmigaDOS and they exist to this day.
- LONG
-
The original implementation language used for AmigaDOS uses a single scalar data type, which, in terms of the Amiga 'C' and assembly language header files, is the LONG: a signed 32-bit integer.
- BPTR
-
Dynamic data structures reference other data structures through pointers, which are indistinguishable from 32-bit integer values. In Amiga data structures, a BCPL pointer can be identified by the BPTR type, as defined by the 'C' and assembly language header files. The 'C' header files provide the BADDR() and MKBADDR() macros to translate between 'C' and BCPL pointers:
APTR c_pointer;
BPTR bcpl_pointer;
c_pointer = BADDR(bcpl_pointer);
bcpl_pointer = MKBADDR(c_pointer);
- Note:
-
The MKBADDR() macro does not verify if the 'C' pointer can be converted into a BCPL pointer. The conversion will drop the two least significant bits of the 32-bit pointer address.
Make sure that c_pointer == BADDR(MKBADDR(c_pointer)) holds!
- BSTR
-
Strings are stored with a leading length byte, followed by as many characters as indicated. For example, the string "SYS" would be stored as the 'C' array 'UBYTE s[4];' as follows:
s[0] = 3, s[1] = 'S', s[2] = 'Y', s[3] = 'S'
There is also a variant of this string type, known as a NUL-terminated BCPL string, which can be used both in terms of BCPL and as a standard NUL-terminated 'C' string. Stored as the 'C' array 'UBYTE s[5];' it would look as follows:
s[0] = 4, s[1] = 'S', s[2] = 'Y', s[3] = 'S', s[4] = '\0'
- Note:
-
This peculiar type of NUL-terminated BCPL string, which counts the terminating '\0' as part of the string, is typically created by expansion.library/MakeDosNode as part of the Autoboot process (V36). You should rarely encounter it, but you ought to be aware that this form exists. Never assume that a BCPL string only contains printable characters!
In Amiga data structures, a BCPL pointer to a BCPL string can be identified by the BSTR type, as defined by the 'C' and assembly language header files.
AMIGADOS PATH NAME SYNTAX
AmigaDOS file systems make use of device/volume, directory, file and link names.
Devices are, for example, floppy disks (df0) or hard disk partitions (dh0, dh1, etc.). Devices such as floppy disk drives may contain removable media. Such media can be moved from one disk drive to another. Regardless of where it is present, the storage medium can be identified by its volume name.
Whenever you use the Workbench, you will see the volume names of the media which may be accessed, as represented by disk icons. You can rename (or "relabel") volumes to suit your needs. However, volume names do not have to be unique. Device names, however, are always unique. If you know that a disk is present in drive df0, then you can access it through its device name rather than its volume name. The shell "
Info" command will always show you the list of devices along with the names of the volumes present in the respective devices.
Volumes contain directories, files and links. These are called "directory entries". Directories in turn can contain further directories, files and links.
The dos.library functions such as
Open(),
Lock(),
CreateDir(),
Rename(),
Relabel(), etc. all expect the names of the devices and directory entries to be accessed or modified.
The names of volumes, devices and directory entries must not contain the ":" and "/" characters. These characters are used to string them together into path names. Note that "/" does not just serve as a path separator character, it is also used for accessing the parent of a directory. Unlike the UNIX file system semantics, for example, "/" is not equivalent to "//". You need the dos.library functions which parse and assemble path and directory names (
AddPart(),
PathPart() and
FilePart()) to process path names robustly. At this time of writing, dos.library lacks a function which parses path names, though.
A typical path would be
df0:s/startup-sequence
This path identifies the device to search (df0), the name of a directory to look into (s) and the name of a file inside that directory (startup-sequence). The ":" following the device name "df0" indicates that this is a device or volume rather than a directory. The "/" between the "s" and "startup-sequence" names indicates that "s" is likely the name of a directory and "startup-sequence" is a directory entry of "s".
AmigaDOS breaks down path names into a device part and a path part. The device part includes everything from the beginning of the path up to the first ":" character, but not the ":" itself. The path part is the remainder of the path which follows the first ":" character. Path names can be absolute (they include a reference to the root directory of the file system) or relative to the current directory of the Process accessing the path. Note that AmigaOS does not forbid the use of the ":" character in the path part of a path name, file systems will consider it illegal and reject such a path.
Device part and path part may be empty, e.g. "df0:" is a valid path, so is ":s/startup-sequence", and so is "s/startup-sequence". If both the device part and the path part are empty, you get "", which refers to the current directory of the Process.
The difference between "df0:", ":s/startup-sequence" and the "s/startup-sequence" paths is in the use of the ":". The ":" always indicates a reference to the root directory of a file system. Hence, "df0:" refers to the root directory of the "df0" device. ":s/startup-sequence" is a reference to the root directory relative to the current directory of the Process accessing it. "s/startup-sequence" is a reference relative to the current directory of the Process.
The current directory of a Process is represented by a
Lock, established by the parent Process which created the child Process. The current directory is changed with the
CurrentDir() function. Because the length of a path name is limited to 255 characters, it is only through the use of
Lock() and
CurrentDir() that you may access deeply nested directories.
The "/" is used as a separator for directories and directory entries, such as in "devs/keymaps". This is not its only function, though. A leading "/" in a path name is interpreted as a reference to the respective parent directory. Hence, with "df0:devs/keymaps" being the current directory, accessing "/" will find the "devs" directory. By extension, accessing "//" will find the root directory. If you will, the leading "/" is the AmigaDOS equivalent to the UNIX "..". Note that the root directory of a volume has no parent directory, i.e. you cannot move up from any root directory to a higher level directory of volumes.
AmigaDOS path names are always interpreted relative to the current directory of a Process. To access the current directory, you use the empty path name "", which is equivalent to the UNIX ".". If a path name contains ":", then everything which precedes the ":" will be used as the device or volume name of the path. If nothing precedes the ":", then this will be interpreted as a reference to the root directory of the volume associated with the current directory. Likewise, a path name which begins with "/" will be interpreted as a reference to the parent of the current directory.
Data structures and memory alignment
A select few dos.library functions expect 'C' and assembly language code to pass a 32-bit address of a parameter to be suitable as a BCPL pointer. This includes, for example, the
Info(),
Examine() and
ExNext() functions.
The 32-bit address will be converted into a BCPL pointer before it is submitted to the Amiga file system which will fill in the data structure contents.
The conversion of the address into a BCPL pointer carries a risk which is easily overlooked: the two least significant bits of the 32-bit address will be dropped.
Unless you are certain that these bits are zeroes, you may end up causing the data to be stored at the wrong address and corrupting memory. You cannot expect
Info(),
Examine() and
ExNext() to verify if the 32-bit address of the parameters provided are properly aligned.
One way to avoid this risk is to allocate memory for the respective data structure by means of
exec.library/AllocMem or using the
AllocDosObject() function, e.g.
AllocDosObject(DOS_FIB, NULL) for a 'struct FileInfoBlock'.
Integer range
The only scalar type which AmigaDOS "knows" is the signed 32-bit integer, or LONG as defined by the 'C' and assembly language header files.
The maximum size of a file is expressed as a signed 32-bit integer, which limits the accessible size to 2,147,483,647 bytes (about 2 GBytes). Trouble is, most of the software which uses data structures and function parameters which feature LONG values does not verify that the size or offset given is in range.
A file system may support file sizes in excess of 2 GByte, but it cannot consistently indicate the total size. It will likely come out as a negative number. This in turn presents a problem for the
Seek() and
SetFileSize() functions as well as the information produced by the
Examine(),
ExNext() and
ExAll() functions. The 'C' runtime library of an Amiga 'C' compiler builds upon these functions and cannot be expected to validate the range of the size and offset information provided.
Generally, file sizes of up to 2,147,483,647 bytes should be reasonably safe to use, but beyond this point you can expect undefined behaviour, even software failure. For example, a file which is larger than 4,294,967,295 bytes will produce an integer overflow, transforming 4,294,967,296 bytes stored into 0 bytes stored. How the file system handles this is unpredictable.
You are best advised to verify that reading from a file will not accidentally read from a "negative" file position. Likewise, writing to a file should avoid creating a file whose size exceeds 2,147,483,647 bytes. Keep in mind that the unsigned 32-bit integer 2147483648 is equivalent to the signed integer value -2147483648, which is what the file system will try to use, and likely fail.
Beyond the 2 GByte mark
Seek() position and offset values will become ambiguous, rendering random access unreliable. While some file systems may permit you to keep reading/writing sequentially beyond the 2,147,483,647 byte mark, you cannot expect this to work consistently or robustly.
Strings
A BCPL string cannot contain more than a maximum of 255 characters and, for NUL-terminated BCPL strings, that number is even smaller (254 characters).
Because those dos.library functions which make use of strings are bound by these limitations, you will have to watch out for longer strings to be silently truncated if they are longer than 255 characters.
This silent truncation can lead to accidental corruption, e.g. by deleting the wrong file, truncating the wrong file or causing a file to be renamed to "vanish" into a subdirectory.
Boolean values
A small set of dos.library API functions return success or failure as a 32-bit integer value, e.g.
DeleteFile(),
Rename(),
Examine(),
Info() and
IsInteractive().
Because of the Tripos/BCPL legacy, a boolean value returned by a file system or the AmigaDOS kernel is not the same as in the 'C' programming language. The <dos/dos.h> header file defines the AmigaDOS versions of TRUE and FALSE as the DOSTRUE and DOSFALSE constants.
While DOSFALSE is the same as FALSE (both are 0), DOSTRUE is -1, but TRUE is 1. As a software developer you should avoid comparing success/failure indications against the TRUE/DOSTRUE values. Assume that whatever is not the same as FALSE will be TRUE.
The use of DOSTRUE is reserved for dos.library functions which have been documented as returning that value, and it is also advisable to return DOSTRUE as an indication of success if you are implementing an AmigaDOS file system or handler.
Path names will be silently truncated if too long, bugs included
Beyond the size limitations implied by using BCPL strings, there are further limitations on how an Amiga file system or ram-handler will handle path, directory and file names whose length exceeds a length limit as defined by the respective file system.
The original Amiga file system would limit the names of directories, files and volumes to 30 characters (the operating system does its part by limiting the names of devices and assigns to 30 characters). While the file system would at least check if a volume name was too long and deny the attempt to change it, a file or directory name longer than 30 characters would be silently truncated. If you were to copy two files named "supercalifragilisticexpialidocious-one" and "supercalifragilisticexpialidocious-two" to a disk, you would discover that only a single file "supercalifragilisticexpialidoc" was created, and it contains only what was stored in the last of these files copied.
Path names are silently truncated if the path length exceeds 255 characters. This is the case for for Kickstart 1.x (V32-V34) path names as well as other 'C' strings which need to be converted into BCPL strings.
Due to implementation changes in Kickstart 2.0, path names exceeding 255 characters would be truncated to an empty string instead. This appears to be a bug and not a deliberate feature change. It affects all 'C' to BCPL string conversions performed by dos.library alike.
As with truncating file and directory names, the consequences of this silent truncation can trigger various side effects, unexpected and/or destructive. Note that even where the documentation stresses that the length of the 'C' string path name is not subject to truncation to BCPL limitations, the file system receiving it may still end up truncating it.
The only option to work around the maximum path length limitation is to make use of
Lock() and
CurrentDir() to access deeply-nested directories whose full path length exceeds 255 characters.
File and volume name character encoding
The Amiga shipped with built-in support for the ECMA-94 standard, which later became ISO/IEC 8859. Specifically, the Amiga implemented the ISO Latin-1 Western European character encoding scheme (ISO 8859-1).
Under this encoding scheme, the characters in the ranges 0..31 and 127..159 are considered "unprintable" control characters and the Amiga file system will not allow them to be used in file and directory names.
This restriction has consequences for the "portability" of files and directories which originate from operating systems which make use of these "unprintable" character ranges, such as Mac OS Roman (used by the Apple Macintosh between 1984-2006 until Unicode and the UTF-8 and UTF-16 encodings took its place) and UTF-8. If you must copy such files to the Amiga, you may have to pick a different name for the copy.
Processes may call dos.library functions, but plain Tasks may not
With few exceptions, dos.library functions expect that the caller will be a Process and not "just" a Task. Put another way, dos.library does not verify if the caller is a Process and there will be negative repercussions if it is not.
The dos.library interfaces its API with the AmigaDOS kernel by means of transforming the API parameters into a message which is sent to a file system which then acts upon the message and returns the message when finished. Passing this message (defined as a 'struct DosPacket' in the <dos/dosextens.h> and "dos/dosextens.i" header files) back and forth requires that the caller is a Process.
A Process is an extension of the exec.library Task, which features a MsgPort (message port). Because this is part of the Process data structure, it is not necessary to allocate memory for a temporary MsgPort, initialize it as needed, send a message to a file system, etc. within the dos.library API.
Lacking this built-in MsgPort, calling a dos.library function from a Task almost certainly will end up corrupting memory. Functions which will set an error code upon failure, to be retrieved with
IoErr() by the caller, will end up corrupting memory, too, unless the caller is a Process. For example, this will happen for the
AddPart() function if the caller is only a Task.
Assume that a Task cannot call a dos.library function unless it is specifically documented as permitting a Task to call it!
File systems and handlers may not call dos.library functions, even indirectly, and can cause the system to freeze
You can build your own file system or handler (the subject is beyond the scope of this introduction and even the dos.library Autodocs in general), which brings its own challenges and responsibilities with it. A file system or handler is, generally, not free to call dos.library functions which directly or indirectly result in messages being sent to the MsgPort of the file system or handler.
An AmigaDOS file system or handler is a Process, and the dos.library interacts with it by passing messages to it, which have to be acted upon. There are two related challenges here.
The first challenge results from the MsgPort of the file system or handler being used by dos.library functions such as
Read() as well as well as by regular DosPacket messages arriving. The order in which these DosPacket messages arrive may overlap with, for example, the
Read() operation waiting for its ACTION_READ DosPacket to be processed and replied. dos.library may find that the expected DosPacket is not what arrived on the MsgPort and trigger an AN_AsyncPkt alert, which will cause the file system or handler to hang. This likely will have immediate repercussions, e.g. Workbench will freeze, because it regularly sends ACTION_DISK_INFO queries to all file system devices. If just one single file system fails to respond to the query, Workbench will wait forever.
The second challenge is in avoiding trouble due to indirect dependencies. Opening a disk-based library or device is usually safe because the operating system delegates this operation to a Process which will find and, if necessary, load the requested library or device into memory, then initialize it. While this takes place, the file system or handler will wait and, if successful, call the respective library or device open function. If that open function, however, involves opening and reading a a file (e.g. from "ENV:sys/icontrol.prefs"), you get the same results as if your file system or handler had called
Read() directly. You cannot predict what will happen within the respective library or device open function.
While there are strategies to avoid such troubles, such as delegating the responsibility for opening disk-based libraries devices to a separate Process spawned just for this purpose, the complexity of a workaround quickly increases.
If possible, avoid opening disk-based libraries and devices altogether from within a file system or handler.
Performance
Most of the dos.library API functions involve interfacing with file systems, sending them messages to perform certain operations, waiting for these messages to come back, once the respective file system has completed the requested operation.
A file system processes each message and the associated operation at its own pace while the Process which sent the message is blocked, waiting for the message to be returned. The exec Task scheduling, which Processes are subject to (being extended Tasks), which normally would allow a Task to be in control of the CPU more often (higher Task priority) or less often (lower Task priority) will have very little practical effect. The file system processing the message is unaware of any priority attached to the message. You cannot make the file system complete its operations faster even if you wanted to.
This design makes it impossible for a Process to complete a file system operation in less than two Task scheduling intervals, which accounts for about 0.12s (NTSC) or 0.16s (PAL).
This file system message exchange acts as a "leveler", making both high and low priority Processes share the CPU much more fairly than their respective priorities would otherwise suggest.
Because a minimum Task switching delay cannot be avoided, you should try to optimize file system operations which become more efficient the more data you may read or write. For example, calling
Write(file, &string[i], 1) for a single character ten times will always be much "slower" than calling
Write(file, string, 10) once.
Stack size
The Amiga operating system documentation for Tasks and, by extension, Processes, suggests a minimum stack size which is both safe to call operating system functions as well as being useful for local variable storage.
As early as 1985 (Kickstart 1.0, V30), the minimum useful stack size for a Task, which can call operating system functions, was documented as 1,000 bytes. However, the minimum useful stack size for both shell commands and Workbench tools was documented as 4,000 bytes.
It is crucial to have sufficient stack size available because the operating system does not detect stack overflow or underflow conditions. Neither of these will end well.
The Kickstart 1.0-1.3 dos.library requires at least 1,500 bytes of stack space to be available, in addition to the bare minimum of 1,000 bytes. This is because of the interface between the dos.library functions and the underlying Tripos kernel code, which "borrowed" 1,500 bytes in order to safely call a kernel function.
The recommended figure of 4,000 bytes used to be a reasonable balance between conserving available RAM and insuring that the operating system functions could be called without corrupting the caller's stack. While the dos.library in Kickstart 2.0 (V36) removed the 1,500 byte stack space "tax" for calling an AmigaDOS library function, you are well advised to plan ahead and choose your program's default stack size wisely in advance. You may also want to verify if the program is prone to exceed its stack size limits. The kind of memory corruption which stack overflows produces is among the hardest to detect and defend against.
Check the dos.library documentation for specific notes on minimum stack size requirements, such as are given for the pattern matching functions
ParsePattern(),
MatchPattern(), etc.
The
exec.library/StackSwap() function provides control over the user stack your Task or Process uses. It allows you to prepare a sufficiently large user stack prior to calling a function which would otherwise present a problem with a much smaller stack size.
dos.library function parameters are rarely ever validated
Unless documented otherwise, you should assume that dos.library functions such as
Open() or
Close() will not check for invalid parameters, even for those comparatively easily-identified as being invalid (e.g. a NULL pointer instead of a pointer to a file name). Furthermore, invalid parameters are bound to be forwarded as part of the messages through which dos.library communicates with file systems. An invalid parameter value may wreak havoc at the file system level, with the potential for data corruption or even crashing the file system.
You are advised to perform your own validation of the parameters you pass to dos.library functions. In general, the older dos.library versions prior to V36 can be expected to be more "brittle" than the later versions and to be easier to crash.
Documentation
The original "AmigaDOS developer's manual" for Kickstart 1.0 was released in 1985. The dos.library API documentation was covered briefly, in a form not unlike the dos.library Autodocs you are reading now. In general, it lacked context such as how the data structures involved would look like, what respective type a function parameter would have and which constraints would exist. Crucially, it lacked even pointers where to go for more information, such as the 'C' language header files which used to be just <libraries/dos.h> and <libraries/dosextens.h>. To make matters worse, the header files available to 'C' programmers did not necessarily match the API implementation.
While much has changed for dos.library and its documentation since 1985, both its API, its data structures, its behaviour and limitations remain underdocumented and, sadly, at times misleading. As a word of warning: do not assume that data structures as defined in the header files, as they used to be in 1985-1988, are reliably documented. Structure members may be misnamed, may be carry-overs from Tripos, or may have no purpose at all.
Assumptions made in older documentation may no longer hold for Kickstart 2.0 and beyond. For example, the original AmigaDOS documentation for Kickstart 1.0-1.3 would state that directory scanning with the
Examine() and
ExNext() functions would return directory entries which were either files or directories. This is no longer true for Kickstart 2.0 due to the introduction of soft links. Unless you check for the directory entry type and identify an entry as being a soft link, it will be mistaken for a directory.
If your knowledge of prior dos.library features and behaviour is built upon familiarity with the Kickstart 1.0-1.3 AmigaDOS documentation, be sure to reread the Autodocs relevant for your work to find out about possible pitfalls which now exist and how to avoid these.
Different AmigaDOS versions have different constraints and features
Over the course of more than three decades, the underpinnings of AmigaDOS and also the feature sets offered by dos.library and file systems evolved at their own respective pace. While you can expect older dos.library versions to offer fewer features than more recent versions, you cannot necessarily expect the same from file systems.
For dos.library the documentation, including this one, refers to the various library versions by their number. For example, V30 is the Kickstart 1.0 dos.library; V32 = Kickstart 1.1, V33 = Kickstart 1.2, V34 = Kickstart 1.3, V36 = Kickstart 2.0, V37 = Kickstart 2.04, V39 = Kickstart 3.0 and V40 = Kickstart 3.1.
The most significant changes happened between Kickstart 1.3 and Kickstart 2.0 which replaced the Tripos underpinnings with 'C' and assembly language code. Further features were introduced in Kickstart 2.04 and 3.0, as well as bugs which later needed to be fixed. The documentation will point this out by referencing the respective dos.library version in context, e.g. V36. If the description of a dos.library function features a (V36) hint, it means that dos.library version 36 or higher is required to use this function and attempting to do so on prior versions is not supported, with undefined behaviour to follow.
It is much harder to learn which features (and bugs) are present at the file system level. By design, the message exchange between a file system and a Process requires that the file system validates the commands it receives. If an operation cannot be performed because the file system does not implement it or cannot make use of the parameters given, it should indicate failure and provide an error code which explains why it could not comply.
The limitation within this design is in that you cannot safely determine in advance whether a file system will feature a documented bug, which could be avoided. As a precaution, you may have to choose between using a documented feature in spite of it not working consistently, or to avoid it altogether. For example, very few Amiga file systems support advisory record locking through the
LockRecord()/
UnLockRecord(), etc. functions. If your software depends upon a feature like this, you may have to find an alternative approach.
Because the Amiga mass storage media may contain specific file system code associated with individual partitions of a disk, you cannot even make assumptions about the level of feature support in a file system. While the operating system ROM may contain the V40 FastFileSystem (as found in Kickstart 3.1), a partition may be mounted using the older V37 FastFileSystem, bugs included.