The C64 OS Programmer's Guide is being written

This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.

Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.

C64 OS PROGRAMMER'S GUIDE


Chapter 4: Using the KERNAL → File (module)

Thin abstraction of the KERNAL ROM and CBM DOS

The KERNAL ROM's routines only operate on channels to devices, and reading or writing single bytes to and from those channels. The File KERNAL module does not do away with the KERNAL ROM, and it does not reimplement any part of the DOS found on each storage device. It is instead a thin wrapper on the KERNAL ROM's concept of logical file numbers, device numbers, secondary addresses, the C64's input and output channel, and the CHKIN/CHKOUT, CHRIN/CHROUT pairs of routines.

The File KERNAL routines operate on C64 OS file reference structures. There are two representations of a C64 OS file reference: A memory structure, and a serialized string. The routine frefcvt can be used to convert the in-memory structure to the serialized string format, or vice versa.

The in-memory structure must be page aligned and each component is found at a standard offset within the page. The string format is designed to be human readable, reminiscent of the standard path format pioneered by CMD and used by IDE64 and SD2IEC. The string format is useful for being written to config files, for being displayed on screen, or for being taken as input from the user.

File Reference (in-memory structure)
Constant Offset Size Notes
frefdev 0 1 Device # (1 to 30)
frefpart 1 1 Partition # (1 to 255)
freflfn 2 1 Logical File Number. 0 = File not open.
frefblks 3 2 16-bit (little endian) block size of the file on disk.
frefname 5 17 16 character filename, null terminated.
frefpath 22 234 Absolute path from root of partition, null terminated.

The path is the only component that is of arbitrary length, which is why it comes at the end of the structure. This allows it to be appended into the free space at the end of the page, up to a maximum of 234 characters.

Saving memory

In order to save memory, if it is known that the path of a given file reference will not exceed some maximum length (e.g., 40 characters,) then the remaining space in the page may be used for other purposes.

Having other data stored after the file reference structure in the page will not interfere with the ability of the File routines to use the file reference. They stop at the null terminator of the path component. Just beware, if you append to the path, it may overwrite whatever else is stored later in the page.

A valid path must begin with two slashes, which means root directory. Each additional subdirectory, including the final subdirectory, must end with a slash.

INVALID:

os/c64tools/
/os/c64tools/
//os/c64tools

VALID:

//os/c64tools/

File Reference (serialized string format)

Each of the components is separated by a full colon. The components are, device number, partition number, filename, path.

8:10:PRGAlias Creator://os/c64tools/

The above example is a file reference to the file called "PRGAlias Creator", which is found on device 8, partition 10, at the absolute path "//os/c64tools/".

The path must be valid, and is the same path format used for the in-memory file reference. The serialized file reference cannot be used to open a file, therefore it does not have any representation of the logical file number or the block size of the file. A serialized file reference has to first be converted to an in-memory structure before it can be used to open a file. When a KERNAL or library routine says that they take a pointer to a file reference, it is always to a file reference in-memory structure, unless otherwise explicitly noted.

File-based Clipboard

The C64 OS universal clipboard is based on the file system. While it is technically possible to read from and write to the clipboard using just file routines, a series of clipboard-related routines are provided as a convenience. These are a thin wrapper on fopen, fread, fwrite, and fclose.


finit

Purpose Initialize the drive to work with a file reference.
Module offset 0
Communication registers A, X, Y
Stack requirements 11 bytes +
Zero page usage $1c, $1d, $61, $62, $63, $64, $65, $66, $67, $68, $ba
Registers affected A, X, Y
Input parameters RegPtr → Pointer to file reference.
Output parameters C ← Set on error.
A ← Error code.
X ← Current device's Command Channel's LFN.
currentdv ← Set to FileRef's device number.

NOTE: The KERNAL ROM must be patched in to call finit.

Description: Prepares the drive to access the file specified by the file reference by initializing the drive's defaults. Sets the C64's current device, represented by currentdv. Changes the device's default partition. Changes the device's current path. Checks for errors in the process of changing partition and path, and updates the status bar. Returns the error state in the carry.

The routine returns in the X register the logical file number to the command channel to the new current device. The KERNAL ROM routines CHKIN and CHKOUT both take the logical file number in the X register. This allows you to call finit on a file reference and then immediately issue a command to that device, relative to the partition and path where the file is found, without needing to explicitly know any of the numbers involved.

The following example shows how to rename a file represented by a fileref, by lowercasing its name.


ferror

Purpose Read the current device's error channel.
Module offset 3
Communication registers A
Stack requirements 11 bytes +
Zero page usage $61, $62, $63, $64, $65, $66, $67, $68, $ba
Registers affected A, X, Y
Input parameters currentdv → The current device number.
Output parameters A ← The current device's drive status code.

NOTE: The KERNAL ROM must be patched in to call ferror.

Description: This routine is used to read the current device's drive status code. It doesn't take any input parameters, but uses the workspace variable currentdv, which is set by finit and fopen.

Ferror clears the current device's error channel, and thus stops a drive's error LED from blinking after an error occurred. The drive's status is saved in workspace memory and the status bar is told to update by calling updstat in the Service KERNAL module.

The drive's status code is returned in the accumulator. By CBM DOS convention, codes 0 to 19 are not errors. All codes 20 and above are errors. Ferror should be called anytime after you send some command to the drive that could fail.

The following example is a slight modification on the example shown above for finit. It shows how to rename a file represented by a fileref, by lowercasing its name. However, the drive's media could be write protected, or the directory could already contain another file with the new name, etc. Because the rename command could fail, ferror should be called. If an error occurred, the original name is restored. The user can review the error message in the status bar.


Opening and Closing, Reading and Writing Files

The following four routines form a group that are almost always used together. The typical pattern is to fopen a file by file reference, fread some data out of the file to a memory buffer, then fclose the file. Or, fopen a file by file reference, fwrite a memory buffer of data to the file, then fclose the file.

Due to the importance and usefulness of this group of four routines, they can be called directly from a Utility, with the KERNAL patched out. These routines automatically patch the KERNAL in and back out for you. This is not the case for finit or ferror. Before calling finit or ferror, the KERNAL must be patched in.

The mouse pointer is automatically killed and automatically restored to its previous state at the beginning and end of fread and fwrite. If your code will make a series of separate calls to fread or fwrite, it is recommended to manually kill the mouse first, and restore it manually at the end, to prevent it from being enabled and disabled between calls to fread or fwrite.

fopen

Purpose Open a file by file reference for read or write.
Module offset 6
Communication registers A, X, Y
Stack requirements 14 bytes +
Zero page usage $1c, $1d, $61, $62, $63, $64, $65, $66, $67, $68, $ba
Registers affected A
Input parameters RegPtr → File reference to file to open.
A → File Open Flags
Output parameters RegPtr ← File reference to the same input file.
C → Set on error
A → Error code

Description: This routine is used to open a file by file reference. The file can be opened for read, write, overwrite or append, depending on the file open flags. Load a RegPtr to a file reference and put the file open flags in the accumulator, then call fopen. The RegPtr is maintained so that it can be passed directly into the next call, either fread, fwrite or fclose. If an error occurred, the carry is returned set and the error code is in the accumulator.

Auto-allocation of Logical File Numbers

The KERNAL ROM maintains a table of 10 open files. It connects a logical file number (LFN) to a corresponding device number and secondary address. Traditionally, the KERNAL ROM requires you to choose which logical file number to use when opening a file.

C64 OS features auto-allocation of logical file numbers. A file reference holds the LFN assigned to this file. If the LFN is 0, that means the file is not open and no LFN has been assigned. Upon opening a file with fopen, the first (i.e., lowest) available logical file number is selected automatically. The LFN is written into the file reference structure, at offset freflfn. This byte serves to identify the open connection at the KERNAL ROM level, but it also serves as a flag to indicate that the file is open.

When an open file reference is closed with fclose, the process is reveresed. The low-level connection to the device is closed, freeing up one channel on that device. The logical file number is released back into the pool for auto-allocation. The freflfn property of the file reference is set back to zero, indicating that the file is closed.

When fopen is called, it automatically calls finit internally. fopen explicitly patches the KERNAL ROM in, and restores the KERNAL ROM state upon return, allowing this routine to be called directly from a Utility.

Following are the set of file open flags to specify the mode and options. These are bit flags which are OR'd together. Not every combination is valid, see the notes in the table for their usage.

Constant Value Direction Notes
ff_r %00000001 input Open an existing file for read.
ff_s %00000010 input May only be combined with ff_r, retrieve the file's block size.
ff_w %00000100 output Create and open a new file for write. Default file type: SEQ
ff_a %00001000 output Open an existing file for append. File must already exist.
ff_o %00010000 output Optional flag to combine with ff_w. If the file already exists it will be overwritten.
%00100000 reserved
ff_p %01000000 output Optional flag to combine with ff_w. Creates the file as PRG instead of SEQ.
%10000000 reserved


fread

Purpose Read data from an open file reference to a memory buffer.
Module offset 9
Communication registers A, X, Y
Stack requirements 8 bytes +
Zero page usage $1c, $1d, $1e, $1f, $20, $21, $90, $ba
Registers affected A
Input parameters RegPtr → File reference to file fopen'd for read.
a1 → .Word, pointer to memory buffer.
a2 → .Word, length of data to read.
Output parameters RegPtr ← File reference to file fopen'd for read.
This routine raises exceptions.

Description: This routine is used to read data of a fixed length from a file, that has previously been opened for read by fopen, into a memory buffer. Load the opened file reference into a RegPtr and call fread. This routine takes two inline arguments. The first is a word, a pointer to the memory buffer into which to read the data from the file. The second is another word, a 16-bit length of data to read.

There are no checks to confirm that the buffer is valid or has enough allocated space to hold the length of data that is read. It is therefore necessary to explicitely allocated a buffer of the necessary size, and to not read a data length greater than the allocation size.

The length to read is a maximum. If the end of the file is reached before reading in the specifed length, fread returns without modifying the remainder of the buffer.

Fread does not return anything, except that it maintains the RegPtr to the file reference. This can be used to make multiple calls to fread in a row without needing to reload the RegPtr to the file reference between calls. This routine may raises exceptions.

Mix-and-match fread with chrin

As discussed earlier, fopen and fread are a thin layer on top of the KERNAL ROM's underlying system of logical file numbers, device numbers, secondary addresses, and the CHKIN and CHRIN routines.

It is, therefore, not only possible but intentional by design to combine the use of CHKIN/CHRIN with fread. Each call to fread uses the logical file number found in the file reference to perform a CHKIN. It then performs a 16-bit loop over the length specified, and repeatedly calls CHRIN, writing each byte to the next address in the memory buffer.

It is safe, and often desirable, to read the LFN from the file reference and manually call CHKIN. You can then call CHRIN to read individual characters from the open file. Calls to fread and manual reads with calls to CHKIN/CHRIN/CLRCHN can be interleaved. However, closing the file should be performed with fclose in order to update the file reference and perform other low-level steps.

Below is an example of reading 16 bytes into a static buffer using fread. Followed by a manual read of 2 bytes into two other static variables.



fwrite

Purpose Write data to an open file reference from a memory buffer.
Module offset 12
Communication registers A, X, Y
Stack requirements 8 bytes +
Zero page usage $1c, $1d, $1e, $1f, $20, $21, $90, $ba
Registers affected A
Input parameters RegPtr → File reference to file fopen'd for write or append.
a1 → .Word, pointer to memory buffer.
a2 → .Word, length of data to write.
Output parameters RegPtr ← File reference to file fopen'd for write or append.
This routine raises exceptions.

Description: This routine is used to write data of a fixed length to a file, that has previously been opened for write or append by fopen, from a memory buffer. Load the opened file reference into a RegPtr and call fwrite. This routine takes two inline arguments. The first is a word, a pointer to the memory buffer from which to write the data to the file. The second is another word, a 16-bit length of data to write.

There are no checks to confirm that the buffer, from which the data is being written to the file, is valid.

Fwrite does not return anything, except that it maintains the RegPtr to the file reference. This can be used to make multiple calls to fwrite in a row without needing to reload the RegPtr to the file reference between calls. This routine may raises exceptions.

Mix-and-match fwrite with chrout

As discussed earlier, fopen and fwrite are a thin layer on top of the KERNAL ROM's underlying system of logical file numbers, device numbers, secondary addresses, and the CHKOUT and CHROUT routines.

It is, therefore, not only possible but intentional by design to combine the use of CHKOUT/CHROUT with fwrite. Each call to fwrite uses the logical file number found in the file reference to perform a CHKOUT. It then performs a 16-bit loop over the length specified, and repeatedly calls CHROUT, writing each byte from the buffer out to the file.

It is safe, and often desirable, to read the LFN from the file reference and manually call CHKOUT. You can then call CHROUT to write individual characters to the open file. Calls to fwrite and manual writes with calls to CHKOUT/CHROUT/CLRCHN can be interleaved. However, closing the file should be performed with fclose in order to update the file reference and perform other low-level steps.

Below is an example of write 16 bytes from a static buffer using fwrite. Followed by a manual write of 2 bytes from two other static variables. Fopen has been opened with ff_w for write, as well as ff_o for overwrite. If the file does not yet exist, it will be created. If it already exists, it will be overwritten.



fclose

Purpose Close a previously opened file by file reference.
Module offset 15
Communication registers X, Y
Stack requirements 8 bytes +
Zero page usage $1c, $1d
Registers affected A, X, Y
Input parameters RegPtr → File reference to file previously opened with fopen.
Output parameters None

Description: This routine is used to close a file that was previously opened using fopen. Call fclose by passing a RegPtr to the file reference that was passed to fopen. The file on the device is closed. The channel used on the device is freed to be used again. The logical file number is freed so it can be used again. The logical file number in the file reference is zeroed to indicate that this file is closed. The KERNAL ROM routine clrchn is called to restore the C64's default channels.

frefcvt

Purpose Convert a file reference from string to struct or vice versa.
Module offset 18
Communication registers X, Y
Stack requirements 8 bytes
Zero page usage $61, $62, $63, $64, $fb, $fc, $fd, $fe
Registers affected A, X, Y
Input parameters C → Set to serialize.
RegPtr → Pointer to file reference in-memory structure

C → Clear to unserialize.
RegPtr → Pointer to serialized file reference string
Output parameters If C was passed in Set:
RegPtr ← Pointer to serialized file reference string

If C was passed in Clear:
RegPtr ← Pointer to file reference in-memory structure

This routine raises exceptions.

Description: This routine is used to convert a file reference between the in-memory structure format and the serialized string format. A pointer to the source format is passed as a RegPtr, and a RegPtr is returned to the target format. The carry is used to indicate whether you are performing a serialization or an unserialization.

A serialized file reference string must be null terminated.

One page of memory is allocated by the system. When this routine returns a pointer to that new memory allocation, it becomes owned by the process that called frefcvt. It is the responsibility of the process that called frefcvt to ensure that this memory is later deallocated, or memory will be leaked. If the one page of memory cannot be allocated, an exception is raised.

Below is an example of how convert a file reference string to the in-memory format, use it, and then deallocate its memory page. This is only an example, the file reference string is hardcoded to device 8, partition 10, with a path of "//text files/". This kind of hardcoding should be avoided in practice.




File-Based Universal Clipboard

C64 OS provides KERNAL-level support for a universal clipboard. Any process may write data to the clipboard, assigning a type and subtype when the data is written. Data already stored on the clipboard is automatically overwritten. Clipboard writes usually happen when the user performs a cut or copy operation. If data is cut, it is the responsibility of the process to remove from its own context the data that was written to the clipboard. This functionality is built-in to some Toolkit classes, such as the TKInput text field.

Any process may read from the clipboard. This usually happens when the user performs a paste operation. Generally speaking, any process that can read from the clipboard will check the current type, subtype and length of data on the clipboard, and determine if the data is of a type that it can read in. If the data type is not compatible, the process should reject the paste, or preferably pre-emptively prevent the user's ability to paste, such as by disabling a menu option or a button.

Workspace memory variables hold the current type, subtype and size of the clipboard. The data content of the clipboard is held in the file "//os/clipboard/:0.d". The .d extension is used for generic data of any type. The "0" filename is to allow a multi-clipboard manager to maintain multiple numbered clipboards. When the clipboard is written to, the data is written to the 0.d file and the type, subtype and size are written to "//os/clipboard/:0.t". During bootup C64 OS reads the type, subtype and clipboard size from 0.t into workspace memory. Thus, the contents of the clipboard are preserved between changing Applications and between rebooting C64 OS and power cycles of the machine.

The following clipboard workspace variables are defined by //os/s/:file.s. C64 OS data types and subtypes are defined by //os/s/:type.s.

Constant Value Notes
c_dtype $246 C64 OS Data Type
c_dstype $247 C64 OS Data Subtype
c_dsize $248-$249 Clipboard data size. 16-bit little endian.

See: Appendices → Data Types for a list of C64 OS data types and subtypes.
See also: C64 OS User's Guide Appendices → III. Data Types.

Both the Application and the Utility are sent an asynchronous clipboard changed message (mc_cpbd) whenever the clipboard contents change. This is used, for example, by the Places Utility. When Places is open, it is listening for clipboard changed messages. In response to the message, it checks if the new clipboard type/subtype is text/fref. If it is, and the cursor is in one of the text fields, it clears the text field and replaces its contents with the new file reference string from the clipboard. This is just an example of how Applications and Utilties can respond automatically when something is cut or copied to the clipboard.


clipin

Purpose Read the clipboard into a buffer.
Module offset 21
Communication registers A, X, Y
Stack requirements 12 bytes +
Zero page usage $1c, $1d, $1e, $1f, $20, $21, $90, $ba
Registers affected A, X, Y
Input parameters RegPtr → Pointer to buffer to read into.
A → Maximum length to read. 0 = No limit.
Output parameters None.

Description: This routine is used to read the contents of the clipboard into a buffer in the most convenient way possible. A pointer to a memory buffer into which the data will be read is passed in a RegPtr. The accumulator passes the amount of data to be read, or 0 to read the entire contents of the clipboard. Because the accumulator is 8-bit, this method can only be used to read 1 to 255 bytes, or the entire clipboard.

It is strongly recommended that you check and confirm the data type and subtype before reading the data in with clipin. It is also strongly recommended that you confirm the data size before using clipin with the no limit option. There are no checks that the data size will not overflow the buffer, or even the entire memory contents of the C64.

Clipin is a convenient wrapper; Its functionality is basic and limited, but easy to use. If you need more control, use copen, cread and cclose.