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 5: Using Shared Libraries

This chapter begins with an overview of what libraries are, how to load them, and how to link to the routines that they provide. Then follows a discussion of calling conventions and how to interpret the headers and constants, and finally how to unload a library when no longer using it or to free up memory.

In the second half of this chapter provides a list of common libraries that are included with C64 OS and which you are likely to want to use. Each of these libraries is given its own sub-page with an outline of all the functions provided.

To learn how to write your own library, see Chapter 10: Writing a Shared Library.

Relocatable object files

Before discussing the specifics of a library, we should cover what a relocatable object file is. Typically, when you assemble a piece of code, you have to specify the address where the code will be run from in memory. The code is then assembled to run only from that address. All instructions that use an absolute memory addressing mode have fixed addresses that were computed at assemble-type based upon the start address.

For example:

        * = $c000
        
        lda var1
        sta var2
        
        rts
        
var1    .byte 5
var2    .byte 0

When we assemble this very small program, the LDA instruction is found at $c000, and it is in an absolute addressing mode. The 2-byte argument is an absolute address whence to load the accumulator. That argument will be computed by the assembler as $c007, the address where var1 is found. Similarly, the 2-byte argument of the STA instruction will be computed as $c008.

This prevents us from simply loading this chunk of code to some other place in memory and running it. If this exact code were later loaded to $7000 and run from there, the first LDA would still load from $c007 and the STA would still write to $c008, and the two bytes following the RTS would not be accessed at all.

The C64 OS KERNAL provides the loadreloc routine which implements a simple, efficient, and effective technique for relocating small units of code from 1 to 8 pages in length. The KERNAL documentation of loadreloc provides a technical description of how to create relocatable object files, and links to further discussions about the theory of how the technique works.

In C64 OS, many different software components are assembled as relocatable object code files. These include, drivers, data-type loaders and savers, Toolkit classes, and libraries.

What is a library?

A library is a relocatable object file that provides one or more routines. It is useful for those routines to be in a library because different Applications can access them without having to reimplement them each time. Typically the routines provided by a library are both generally reusable and non-trivial.

For example, toggling the lock status of a file is a specific task that more than one Application or Utility could plausibly wish to perform. It is also a non-trivial task because different device families, which are all supported by C64 OS, require different methods of affecting this change. By providing a common routine via a library, each Application can toggle a file lock in an abstract way without needing to get into the technical details of how it is done.

Using libraries to perform common reusable tasks has several advantages:

  • Reduces the amount of code you have to write
  • Ensures that different Apps perform the task the same way
  • New hardware support can be acquired by an Application when the library is updated
  • Bug fixes and efficiency gains improve all Applications that use the library

What is a shared library?

The difference between a library and a shared library is that one instance of a shared library loaded into memory can be accessed by more than one unrelated program. For example, an Application like File Manager loads and makes use of the directory library (dir.lib), but when the Utilities Utility is loaded at the same time, it too makes use of the directory library. Both of these programs (one Application and one Utility) make calls into the same instances of the directory library.

Because all libraries (i.e., relocatable code objects loaded using the library loading APIs) are shared, it is important that they be written with this in mind. Shared libraries have additional benefits over those of libraries generally:

  • Reduced memory footprint when two processes share a library
  • Shortened load time when a process needs a library that is already loaded

Loading and Unloading Libraries

A library's name must be uniquely identifiable by the first 2 characters and must end with: .lib.r

The library must be installed in either the //os/library/ directory, for global access by multiple processes, or in the root of an Application's bundle directory, for private use.

Libraries are loaded by calling loadlib in the service KERNAL module. The two character code of the library is passed in the X and Y registers, first character in the X register and second character in the Y register. See Appendix III: Shared Library Registry for a list of available libraries, their 2 character codes, and their size in pages. The loadlib routine wraps a call to the lower-level loadreloc routine.

When calling loadlib the size of the library must be passed in the accumulator. Because relocatable objects cannot exceed 8 pages, the size of a library always fits in the low nybble of the accumulator. The high nybble is used to pass in optional flags.

A maximum of 10 shared libraries may be loaded at a time, in a single Fast App Switching bank. If loadlib is called and 10 shared libraries are already loaded, an exception is raised. When a library is loaded from disk its init routine is called and its reference count is set to one. If loadlib is called and the requested library is already loaded, its init routine is called again and its reference count is incremented by 1.

If a library only needs to be initialized once, it will update its own init jump table entry to prevent itself from being initialized subsequent times. Every time loadlib is called, regardless of whether the library is loading from the disk the first time or is already available in memory, the starting page of where the library has been relocated to is returned in the accumulator. All relocated objects are page-aligned. If the accumulator came back as $65, then the library starts at $6500. If the library is, 3 pages long, for example, then that library would be occupying memory from $6500 to $67FF.

Any library that is loaded must at some point be unloaded. With a bit of extra logic to track whether you've loaded a library or not, a library can be lazy loaded. Lazy loading means deferring loading a library until it is actually needed. For example, the File Info Utility has a checkbox to toggle the lock status of a file. The flock.lib can be used to make toggling a file lock trivially easy. The library requires 2 pages of memory and also needs to load 2 blocks from disk. However, flock.lib is not needed at all unless the user clicks the lock checkbox. Since it is entirely possible that the user will open the File Info Utility to rename or copy a file but never toggle the file's lock status, it makes a lot of sense to defer loading flock.lib until the checkbox is clicked.

Lazy loading libraries saves memory and speeds up the load time of your Application or Utility. If your code supports lazy loading a library, you can also unload the library in response to low memory warnings.

Alternatively, some libraries need only be loaded momentarily and can be immediately unloaded after using them for a single call. For example, the utils.lib is loaded, it rebuilds the Utilities menu from the source file at //os/settings/:utilities.m and then it is no longer needed and can be unloaded. The grab.lib takes a screen grab when loaded and thereafter can be immediately unloaded.

When your Utility or Application is quit (or in some cases, when your Toolkit object is deleted, such as instances of TKTArea or TKPlaces) they must unload any library which they loaded.

Libraries are unloaded by calling unldlib in the service KERNAL module. The two character code of the library is again passed in the X and Y registers. The size of the library does not need to be passed in when unloading, however the accumulator is again used to pass optional unloading flags. When a unldlib is called, the library's reference count is decremented by 1. If the reference count is reduced to zero, then the library is in fact unloaded and the memory it occupied is freed.

Linking to libraries

Similarly to the way in which your code must build a KERNAL link table in order to make KERNAL calls, you must also build a link table of the routines you want to access in each library loaded.

The reason for the KERNAL link table is because the size and location of the KERNAL modules gets moved around in each version of C64 OS. The table is dynamically linked to the KERNAL modules each time the Application (or other code) is initalized. The reason why a library link table is necessary is because every time a library is loaded it gets relocated to an available place in memory, but the exact address in memory cannot be known until runtime.

By convention, it is recommended that library link tables be established adjacent to the KERNAL link table, either just before or just after. This is not necessary, but is recommended for good code organization. All of the link tables are conceptually similar; a jump table through which your Application will access executable code provided by the OS.

To gain access to the library's routine constants, the library header can be included using the #inc_h macro. Let's use checksum.lib as an example. A description of all of the routines, their inputs, outputs, and error conditions can be found in the file //os/h/:checksum.t and the constants without the comments which are imported by #inc_h are found in //os/h/:checksum.h.

In order to calculate any CRC checksum it is first necessary to make the appropriate tables. This only has to be done once. Then, before calculating the CRC of some data the initial values of that CRC must be initialized. To compute the running CRC value each byte of data must be passed in, however there is a separate CRC update routine for each type, CRC8, CRC16 and CRC32. For this example, let's say we only want to compute a CRC16. Finally, after inputting all of the data, there is a routine to fetch the CRC checksum as either an int or a string. This final routine also performs some necessary final steps of bit manipulation. Thus, to compute a CRC16, we need access to 4 of the routines provided by checksum.lib.

The checksum.lib link table is built like this:

There are a few things to note. First, like the KERNAL routines, the constants end with an underscore. This allows you to use the same name but without the underscore for the jump table entry. The constants ending with an underscore are always only 1 byte (i.e., have a value less than 256). However, the JMP instruction does not have a zero page address mode. Without using parentheses to specify an indirect JMP(), a normal JMP instruction has only an absolute addressing mode. This ensures that the assembler will always output 3 bytes per jump table entry, with the constant as the low byte and the high byte zero. The 6502 uses little endian byte ordering, so the entries in the table above will assemble as bytes in the following order:

Note that there is no terminator for the table. Each entry must have its high byte set to the page byte returned from loadlib. The following code loads and links the library. This is usually found in the Application's init routine.

Each STA instructions writes the page address of the loaded library into the high byte of an entry in the jump table. This must be repeated for each entry.

To unload a library it is not necessary to clear the high bytes of the jump table, as long as nothing attempts to call any of the library routines after the library has been unloaded. Unloading the library will typically be done in the Application's willquit routine.

Not every library takes the slunload flag while unloading. The checksum library requires it because on unload it needs to free the memory that was allocated for the tables that were created by calling maketab.

Lazy loading libraries

The following is some sample code that shows how you might lazy load a library.

The code to load and link the library is put in its own routine, with a local variable used as a flag for whether the library has already been loaded. With chloaded in place, you are safe to call ldchksum as many times as you want. It will only load it if you haven't already loaded it. You could, for example, put a call to this routine in the callback of clicking a button that will take some action that need these library routines.

You'll still need to unload the library at some point. The following routine should definitely be in the Application's willquit routine, but because this routine can be called whenever you want, it could also be called in response to a low memory warning message.

The carry is used to indicate whether this routine actually attempted to free a resource or not. If this were the only thing your Application could give up in order to free some memory in reponse to a low memory warning, then this routine could be called directly in response to that message. The status of the carry informs the caller whether it should keep requesting memory if it still needs more, or if this is all it's able to get.

Additionally, you could call this at any time to free up memory for your own purposes. Call it also during willquit to ensure the library gets unloaded, and there is no harm done in calling this even if the library isn't currently loaded.


Calling Conventions

The way in which you call the routines found in libraries is exactly the same as you call routines provided by the C64 OS KERNAL, which in turn is modeled on how the C64's KERNAL ROM routines are called.

The routines found in libraries are just like KERNAL routines, except that the library has to be loaded at runtime to have access to them. The decision to put a routine in a library or to put it in the KERNAL is somewhat arbitrary. It is primarily based on the probability that a typical Application will or will not need access to it. The KERNAL is a set of routines that are permanently loaded into memory. A library is a set of routines that can be loaded and unloaded from memory at any time. Therefore, a routine that is likely to be used by all or a great many Applications is a candidate for being in the KERNAL. Use of KERNAL memory space must be conservative though, and so any routine that is specialized in some way is better suited to be found in a library.

An additional benefit of libraries is that 3rd party developers can create them and distribute them. Different Applications that are open at the same time in different Fast App Switching banks can have a different set of libraries loaded in.

The 6502 has no privileged execution mode and the Commodore 64 has no memory management unit. There is no protected memory and no virtual memory. All I/O is memory mapped and although certain regions of memory can be substituted for ROM, I/O or RAM, the PLA is responsible for this mapping, which is controlled by the 6502's built-in port, the processor port, and access to this port is also unprivileged.

What this means is that the KERNAL, drivers, libraries and Application code share an execution mode and all have equal access to the whole machine. From a modern perspective this obviously has certain disadvantages (i.e., security, privacy, crash resilience, etc.) However, the Commodore 64 is what the Commodore 64 is, and its openness and simplicity are also part of its enduring charm.

Library routine input and output parameters

The KERNAL module header files, found in //os/h/, specify the input and output parameters using an arrow notation. System-reserved libraries, those whose names exactly match a KERNAL module, have their routines defined and documented in the bottom half of the corresponding KERNAL module's header file.

For example, memory.lib is a library which conceptually extends the memory KERNAL module. Therefore, in the header file //os/h/:memory.t, first are listed all of the routines in the KERNAL module, followed by a list of all the routines and their documenting comments for the routines found in the memory library.

Everything listed with a right-facing arrow (-> or →) must be set prior to making the call. Everything listed with a left-facing arrow (<- or ←) will be set when the routine returns.

The following is an example from //os/h/:memory.t (recall from Chapter 3: Development Environment → Constants and Includes that *.t files, where they exist, contain the comments and documentation, while their *.h equivalents have been stripped of these to speed up assemble time.)

pgfetch and pgstash have higher indexes than realloc because they are found at the end of the KERNAL memory module, then the comment indicates the start of the memory library. realloc starts at index $03 instead of $00 because the library has its standard link routine at $00.

Following the notation in the comments, realloc takes three input parameters. The memory pool (the starting page number of the pool) is passed in in the accumulator. The pointer to the allocated memory (previously returned by malloc) is passed in a RegPtr. In this example, another parameter is required, memsize. When a parameter is not a processor register (A, Y, X, RegPtr, RegWrd) nor a processor flag (C, Z, etc.) then it is a constant defining some fixed address in workspace memory. Since this is memory.lib, and the header is //os/h/:memory.t the constant is defined by //os/s/:memory.t.

First load the new 16-bit memory length into memsize, then load the registers, then JSR to realloc. The returned parameters are indicated with the left-facing arrow. The carry will be set if an error occurred, indicating the memory could not be reallocated. If an error occurred the returned RegPtr is invalid. If error did not occur, the address of the new allocation is returned in the RegPtr. The original pointer is no longer valid. If the address of the block moved, the memory in the original allocation was moved to the new allocation automatically.

Details, such as described above, are not necessarily covered completely by the comments found in the header file. For a complete description of all behaviors of each library call, consult the library's documentation.


Chapter 5: Using Shared Libraries, Summary

This chapter began with an over of what a library is and what a shared library is. It gave a brief description of relocatable objects. Next it covered how to load and unload libraries, and how to link to the routines you want to use within a library. A technique for lazy loading libraries was discussed. The chapter ended with an explanation of how to make calls and how the documentation works.

The second part, making up by far the bulk of this chapter, contains a detailed discussion of libraries that are included with C64 OS, and a detailed description of each of their calls and how to use them. Libraries are listed alphabetically, and grouped into two sets; common libraries you are likely to use, and less common libraries that serve some special purpose.


Library Reference

As of C64 OS v1.07, there are 25 libraries included with the system. Some are small, offering only one or two routines for special purposes, like toggling a file lock. Others are more complex, such as the cnp.lib with more than 10 routines, and offering important system functionality like network sockets.

Next Section: Library Reference


Next Chapter: Using Toolkit

Table of Contents



This document is subject to revision updates.

Last modified: Jan 24, 2025