AmigaDOS Buffered I/O¶
Besides the unbuffered Read()/Write()/Seek() calls, which hand every request to the handler and cost a task switch each time, dos.library offers a family of buffered I/O functions: FRead(), FWrite(), FGetC(), FPutC(), FGets(), FPuts(), UnGetC(), Flush(), SetVBuf() and the formatted printers VFPrintf() and VFWritef(). They collect data in a buffer attached to the file handle and talk to the handler only in larger chunks. They resemble ANSI-C stdio, but their error reporting is quite different and several of them had version-specific defects. This article collects the rules and the pitfalls from §5.6–5.8 of the 2024 ROM Kernel Reference Manual.
The buffered functions always existed inside AmigaDOS, but before V36 only BCPL programs could reach them through the Global Vector. Since the V36 rewrite they are part of the dos.library interface. One difference from ANSI-C stands out: the file handle is the first argument, not the last (FPuts(fh, str) versus fputs(str, fp)).
The file handle buffer¶
- Every file handle can do buffered I/O. A buffer is attached as soon as a buffered function is first used. Its default size is 204 bytes.
- In V45 and earlier the buffered functions were built on single-byte primitives and had a large overhead (the NDK autodoc notes that
FRead()/FWrite()were a series ofFGetC()/FPutC()calls). V47 redesigned them and they are now much faster. - The default buffer is small. The manual recommends enlarging it with
SetVBuf()to 4096 bytes, the block size of modern hard drives. - The buffer lives in the
FileHandlefieldsfh_Buf,fh_Posandfh_End; see DOS String Streams for the structure and for building a handle over a memory buffer.
Note: the NDK 3.2 autodoc for
SetVBuf()states that the default and minimum buffer size is 208 bytes.
Function summary¶
Prototypes as in NDK 3.2 clib/dos_protos.h; all are V36 except SetVBuf(), which only works from V39.
| Function | Prototype | Result on error | IoErr() |
|---|---|---|---|
FRead 2.0+ |
LONG FRead(BPTR fh, APTR block, ULONG blocklen, ULONG number) |
fewer complete records, never -1 | only set when the handler is called |
FWrite 2.0+ |
LONG FWrite(BPTR fh, CONST_APTR block, ULONG blocklen, ULONG number) |
fewer complete records, never -1 | only set when the handler is called |
FGetC 2.0+ |
LONG FGetC(BPTR fh) |
ENDSTREAMCH (-1), same as EOF |
set on refill |
FGets 2.0+ |
STRPTR FGets(BPTR fh, STRPTR buf, ULONG buflen) |
NULL, same as EOF |
set on refill |
UnGetC 2.0+ |
LONG UnGetC(BPTR fh, LONG character) |
0 |
not set |
FPutC 2.0+ |
LONG FPutC(BPTR fh, LONG ch) |
ENDSTREAMCH (-1) |
only when the buffer is written out |
FPuts 2.0+ |
LONG FPuts(BPTR fh, CONST_STRPTR str) |
ENDSTREAMCH (-1); 0 = success |
only when the buffer is written out |
PutStr 2.0+ |
LONG PutStr(CONST_STRPTR str) |
-1; 0 = success |
only when the buffer is written out |
WriteChars 2.0+ |
LONG WriteChars(CONST_STRPTR buf, ULONG buflen) |
fewer characters, never -1 | only when the buffer is written out |
Flush 2.0+ |
LONG Flush(BPTR fh) |
always DOSTRUE |
set if the handler fails |
SetVBuf 2.0+ |
LONG SetVBuf(BPTR fh, STRPTR buff, LONG type, LONG size) |
non-zero; 0 = success |
only on out-of-memory |
VFPrintf 2.0+ |
LONG VFPrintf(BPTR fh, CONST_STRPTR format, CONST_APTR argarray) |
-1 |
error code |
ENDSTREAMCH (-1) and the BUF_* modes are in dos/stdio.h.
Error detection: the SetIoErr(0) pattern¶
The buffered functions only touch IoErr() when they actually call the handler, i.e. when the buffer has to be refilled or written out. A request satisfied entirely from the buffer leaves IoErr() as it was, and several functions have no error return value at all. The reliable pattern is therefore:
SetIoErr(0)before the call (or before a loop of calls);- make the calls;
- if the result is short,
NULLorENDSTREAMCH, look atIoErr():0means end of file, anything else is an error.
#include <dos/dos.h>
#include <dos/stdio.h>
#include <proto/dos.h>
/* Copy everything from one open file to another through the buffers.
Returns DOSTRUE on success; IoErr() holds the reason on failure. */
LONG CopyBuffered(BPTR from, BPTR to)
{
UBYTE buf[512]; /* plain C buffer: no alignment needed */
LONG n;
SetIoErr(0);
while ((n = FRead(from, buf, 1, sizeof(buf))) > 0) {
if (FWrite(to, buf, 1, n) != n)
return DOSFALSE; /* IoErr() set by the failed write-out */
}
if (IoErr() != 0) /* n == 0: EOF or error? */
return DOSFALSE;
Flush(to); /* always returns DOSTRUE ... */
return (IoErr() == 0) ? DOSTRUE : DOSFALSE; /* ... so check IoErr() */
}
Record I/O: FRead and FWrite¶
FRead() reads number records of blocklen bytes each; FWrite() writes them. The record size has no special meaning: they transfer blocklen × number bytes but return only the number of complete records. An incomplete last record is not counted. To read or write a plain block of bytes, set blocklen to 1.
- Neither returns
-1on error, and neither clearsIoErr()first: use the pattern above. - V47 "burst" mode.
FRead()first takes what it can from the buffer; if more bytes remain than the buffer holds, the handler is called to read directly into the caller's buffer.FWrite()first tops up a partially filled buffer, then writes a large remainder in one handler call, bypassing the buffer; small remainders go into the buffer. - Before V47,
FRead()was slow; see FGet() for a faster inlinable reader.
Writing characters and strings¶
FPutC(fh, ch)returns the character orENDSTREAMCH. Pass aLONGin the range 0–255.FPuts(fh, str)writes a NUL-terminated string without the NUL. It returns0on success,ENDSTREAMCHon error.PutStr(str)isFPuts(Output(), str); no newline is appended. Returns0or-1.WriteChars(buf, len)isFWrite(Output(), buf, 1, len)and returns the number of characters written. It does not return-1on error, and setsIoErr()only when the buffer is written out; callSetIoErr(0)first.
Note: the NDK 3.2 autodoc for
WriteChars()states that a result of -1 (EOF) indicates an error.
SetVBuf: buffer size and mode¶
LONG SetVBuf(BPTR fh, STRPTR buff, LONG type, LONG size); /* V39 */
sizeis the buffer size in bytes; smaller values are rounded up to 204.buff == NULL: dos.library allocates the buffer and frees it onClose()or when the buffer is changed again.buff != NULL: your own buffer, longword-aligned. It is never freed by dos.library.typeis one of:
| Mode | Value | Writing | Reading |
|---|---|---|---|
BUF_LINE 2.0+ |
0 | Flush on line feed, carriage return or NUL, only if the file is interactive; otherwise like BUF_FULL |
Fills the whole buffer |
BUF_FULL 2.0+ |
1 | Flush only when the buffer overflows or on Flush() |
Fills the whole buffer |
BUF_NONE 2.0+ |
2 | Every character goes straight to the file | No buffering |
Pitfalls:
- The result is inverted:
0means success, non-zero means failure (out of memory, bad mode, bad handle).IoErr()is set only for out-of-memory. - Before V39 it did nothing and always returned
0, although it was documented since V36. - It crashes on
NIL:handles in all versions. Checkfh_Typefirst. - It cannot change the buffer of
Input()of a Shell command; see below.
Note: the NDK 3.2 autodoc for
SetVBuf()states that a size of -1 changes only the buffering mode.
/* Give a freshly opened file a 4 KB buffer. */
static void BigBuffer(BPTR file)
{
struct FileHandle *fh = BADDR(file);
if (fh->fh_Type != NULL) /* never on NIL: */
if (SetVBuf(file, NULL, BUF_FULL, 4096) != 0) {
/* failed: keep the default buffer */
}
}
Flush¶
LONG Flush(BPTR fh); /* V36 */
Flush() synchronizes the file with the buffer. Pending written data is pushed out. If data was read and unread bytes remain in the buffer, they are dropped and dos.library seeks back to the position of the last byte actually consumed.
- The result is always
DOSTRUE; it says nothing about errors. UseSetIoErr(0),Flush(), thenIoErr(). - Call
Flush()when switching from writing to reading the same handle, or back. The buffer logic cannot handle this case by itself. - Call
Flush()when switching from buffered to unbuffered I/O (Read()/Write()) on the same handle.Seek()flushes implicitly and is safe to mix.
Note: the NDK 3.2 autodoc for
Flush()states that before V37 it returned a random value, and that V36 and V37 did not properly flush handles on which no buffered I/O had been done (redirected command input,NP_Arguments,SelectInput()beforeRunCommand()), fixed in V39; the workaround isFGetC(),UnGetC(), thenFlush().
Reading characters and lines¶
FGetC¶
Returns the next byte (0–255) or ENDSTREAMCH for both EOF and error; distinguish them with SetIoErr(0) and IoErr().
V36 bug: once FGetC() had returned EOF, it kept returning ENDSTREAMCH even if new data arrived on an interactive stream. Fixed in V37.
FGets¶
STRPTR FGets(BPTR fh, STRPTR buf, ULONG buflen); /* V36 */
Reads until a line feed (which is kept in the buffer), EOF, an error, or buflen - 1 characters; the result is always NUL-terminated. Returns buf, or NULL on EOF or error (again: SetIoErr(0) first, then IoErr()).
- Before V39 it overflowed the buffer by one byte when a line did not fit: the NUL went one byte past
buflen. Passbuffer size - 1asbuflenwhen you must run on V36–V38. buflenis unsigned: a negative value becomes a huge length and letsFGets()overflow the buffer.
Note: the NDK 3.2 autodoc for
FGets()states that from V39 a length of 0 was treated as 4294967295 characters, and that dos 47.27 makesbuflen == 0returnNULLwithIoErr()0 without touching the buffer.
UBYTE line[256];
SetIoErr(0);
while (FGets(fh, line, sizeof(line) - 1)) { /* -1: pre-V39 workaround */
/* process one line (or one chunk of an overlong line) */
}
if (IoErr() != 0) {
/* read error, not EOF */
}
UnGetC¶
UnGetC(fh, ch) pushes ch back so the next read returns it. With ch == -1 the last character read is pushed back; if the last read hit EOF or an error, UnGetC(fh, -1) pushes back the EOF condition. Only one character is guaranteed to be pushed back after each read. Returns non-zero on success, 0 on failure.
V36 bug: pushing back an EOF did not work. Fixed in V37.
UnGetC(fh, -1) right after Open() is also the cheapest way to make sure a handle has a read buffer; the helpers below depend on it.
The standard input of Shell commands¶
Input() of a Shell command is a special case:
- Its buffer holds the command line arguments, where
ReadArgs()picks them up. A buffered read fromInput()therefore returns the arguments first, not the stream data. - That buffer was installed by hand (the string stream technique), so a plain
Flush()is not necessarily enough when the original stream was unbuffered. Convert the stream to buffered mode first, then flush:
FGetC(Input()); /* convert to buffered mode */
Flush(Input()); /* release the command line buffer */
SetVBuf()cannot change the buffer size ofInput()in any version, becauseRunCommand()deposits the arguments in a buffer it cannot replace.- Do not use
Input()for output. When the Shell has redirected standard input, output written to it is lost (the manual considers this a defect that went unnoticed).
Convenience macros¶
dos/stdio.h defines:
#define ReadChar() FGetC(Input())
#define WriteChar(c) FPutC(Output(),(c))
#define UnReadChar(c) UnGetC(Input(),(c))
#define ReadChars(buf,num) FRead(Input(),(buf),1,(num))
#define ReadLn(buf,len) FGets(Input(),(buf),(len))
#define WriteStr(s) FPuts(Output(),(s))
#define VWritef(format,argv) VFWritef(Output(),(format),(argv))
Each call costs an extra Input()/Output() call. In a loop, fetch the handle once and call the functions directly.
FSkip and FGet: buffer-level helpers¶
Seek() can be mixed with buffered I/O, but it always flushes and loses the buffer. The manual (§5.7.4–5.7.5) gives two helpers that work on the buffer fields directly.
FSkip() skips forwards inside the buffer when possible and falls back to Seek() otherwise:
LONG FSkip(BPTR file, LONG skip)
{
struct FileHandle *fh = BADDR(file);
if (fh->fh_Pos >= 0 && fh->fh_End > 0) { /* buffer present? */
LONG newpos = fh->fh_Pos + skip;
if (newpos >= 0 && newpos < fh->fh_End) {
fh->fh_Pos = newpos; /* stay in the buffer */
return DOSTRUE;
}
}
skip += fh->fh_Pos - fh->fh_End; /* account for unread bytes */
fh->fh_Pos = -1; /* mark buffer absent so */
fh->fh_End = -1; /* Seek() won't Flush() */
if (Seek(file, skip, OFFSET_CURRENT) != -1)
return DOSTRUE;
return DOSFALSE;
}
FGet() copies directly from a partially filled buffer and calls FRead() (which also refills the buffer) only when needed. It is mainly useful before V47, where FRead() is slow, and can be inlined:
#include <string.h>
LONG FGet(BPTR f, void *buf, LONG size)
{
struct FileHandle *cis = BADDR(f);
if (cis->fh_Pos > 0) {
LONG end = cis->fh_Pos + size;
if (end < cis->fh_End) {
memcpy(buf, (UBYTE *)BADDR(cis->fh_Buf) + cis->fh_Pos, (size_t)size);
cis->fh_Pos = end;
return size;
}
}
return FRead(f, buf, 1, size);
}
Both require the buffer to exist. Initialize it immediately after opening; any buffered read works, UnGetC() is the cheapest:
BPTR file = Open(filename, MODE_OLDFILE);
if (file)
UnGetC(file, -1); /* set up the read buffer */
As with all buffered functions, IoErr() is only updated when the handler has to supply data.
Formatted output¶
VFPrintf, FPrintf, VPrintf, Printf¶
LONG VFPrintf(BPTR fh, CONST_STRPTR format, CONST_APTR argarray); /* V36 */
LONG FPrintf(BPTR fh, CONST_STRPTR format, ...);
LONG VPrintf(CONST_STRPTR format, CONST_APTR argarray); /* to Output() */
LONG Printf(CONST_STRPTR format, ...);
They format like exec's RawDoFmt() and write through the file handle buffer. They return the number of characters written, or -1 with an error code in IoErr().
- 16-bit integer model.
%d,%u,%xand%ctake 16-bit arguments regardless of the compiler. With a 32-bitintcompiler use%ld,%lu,%lxand%lc, or the output is garbage. Drop thelonly with a 16-bitintcompiler. %stakes a C string (limit it with a precision, e.g.%.30s);%b(V36) takes a BPTR to a BSTR;%luexists from V37.RawDoFmt()is patched bylocale.library, so theFormatString()extensions of locale become available to these functions too.
LONG args[2];
args[0] = (LONG)"RAM:";
args[1] = 42;
VFPrintf(Output(), "%s holds %ld files\n", args);
Printf("%s holds %ld files\n", "RAM:", 42L); /* varargs form */
VFWritef and FWritef: BCPL style¶
VOID VFWritef(BPTR fh, CONST_STRPTR format, CONST LONG *argarray); /* V36 */
VOID FWritef(BPTR fh, CONST_STRPTR format, ...);
These exist for legacy BCPL programs (they are also a Global Vector entry, where they take BSTRs). They are not patched by locale.library; new code should use VFPrintf(). Width characters x are a single character: 0–9 are widths 0–9, A–Z are widths 10 onwards.
| Directive | Output |
|---|---|
%S |
NUL-terminated string |
%Tx |
string left-justified in a field of width x; not truncated |
%C |
single character (32-bit code point) |
%Ox |
octal, at most x digits, zero-padded, truncated if too long |
%Xx |
hexadecimal, at most x digits, zero-padded, truncated if too long |
%Ix |
signed decimal, space-padded to x, not truncated |
%N |
signed decimal, no width |
%Ux |
unsigned decimal, space-padded to x, not truncated |
%$ |
skip the next argument |
%% |
a literal % |
Note: the NDK 3.2
clib/dos_protos.hdeclaresVFWritef()andFWritef()as returningVOID; the manual documents aLONGcount.
Version bugs at a glance¶
| Function | Versions | Defect | Workaround |
|---|---|---|---|
FGetC() 2.0+ |
V36 | Returns EOF forever after the first EOF | Require V37 |
FGets() 2.0+ |
< V39 | Writes the NUL one byte past the buffer | Pass size - 1 |
UnGetC() 2.0+ |
V36 | Cannot push back EOF | Require V37 |
SetVBuf() 2.0+ |
< V39 | Not implemented, always returns 0 | Live with 204-byte buffers |
SetVBuf(), ExamineFH() |
all | Crash on NIL: handles |
Check fh_Type != NULL |
SetVBuf() 2.0+ |
all | Cannot resize Input() of a Shell command |
None |
Flush() 2.0+ |
all | Always returns DOSTRUE |
SetIoErr(0) + IoErr() |
FRead()/FWrite() |
≤ V45 | Byte-by-byte, slow | FGet() helper, large buffer |
See Also¶
- dos.library Reference
- DOS String Streams
- Interactive Files and Handler Detection
- DOS Error Reporting
- AmigaDOS Error Codes
- BPTR, BSTR, and BCPL Legacy
- AmigaDOS Paths and File Names
Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), §2.6.2, §5.4.3, §5.6–5.8 (pp. 10–11, 35, 40–53); NDK 3.2 R4 dos.doc (FRead, FWrite, FGets, Flush, SetVBuf, WriteChars, VFPrintf), dos/stdio.h, dos/dosextens.h, clib/dos_protos.h Updated: 2026-11-13