For Amiga World Tech Journal. Nov 6, 1990. Set your editor's TAB width to 8 characters (this file contains ASCII graphics). From: Jim Fiore dissidents 730 Dawes Avenue Utica, NY 13502 (315) 797-0343 Shared Libraries for the Lazy by Jim Fiore, dissidents BIX: jfiore If you've done any significant programming on the Amiga, you are no doubt familiar with the concept of a shared library. Shared libraries are an integral part of the Amiga operating system. In essence, a shared library is a collection of routines which any application can utilize. Since the Amiga uses a multi-tasking operating system, it makes sense to offer library support as part of the operating system, rather than relying on typical link libraries. By doing so, applications using the functions do not require duplicates of the code. This saves system memory for other purposes (ie, other tasks, or perhaps larger data files in ram). Any collection of routines which might be used by several programs should be considered for conversion into a library. Classic examples of system libraries include the Intuition and Graphics libraries. In this way, graphics and user interface code is shared among several applications. You may also wish to break up a larger program into a series of libraries instead of using overlays. In this way, libraries can be loaded as needed, keeping the core of the program relatively small. Finally, libraries can be a good way of sharing function capabilities with other people without sharing source code, or requiring a specific language or compiler. Given the pervasive use of libraries and their myriad advantages, learning how to create your own libraries is a useful skill. There have been a few techniques and examples shown in various places over the past few years. Personally, I have always found them lacking in a few specific areas. It seemed that you either had to maintain a large number of files in order to create the library, or the scheme was very much tied into a particular language or compiler. In this article, we'll look at what is perhaps the simplest way of creating a library. It doesn't require a lot of maintenance, and it can be used with a variety of languages, including assembly and C (both Manx and Lattice). All you will need to create (and ever modify in the future) is the functions of interest, and a function description file (sometimes referred to as ".fd files"). Our example will turn a set of ordinary C language functions into a shared library using the Manx C compiler, but you could just as easily use the Lattice compiler or assembly language. Before we look at the example, it will help if we understand the structure of a shared library. A library may be broken into four main parts: a Library Node, a function jump table (often referred to as a vector table), the set of functions, and the global data for the library. In memory, a library looks something like this: ---------------------------- ------>| | | | Functions | | --->| | | | ---------------------------- | | | | ---------------------------- | ----| | -------| Vector Table | | | ---------------------------- <---- The Library Base address | | | Library structure | | | ---------------------------- | | | Library data | | | ---------------------------- Let's look at the Library structure first. A Library structure is defined as follows: struct Library { struct Node lib_Node; UBYTE lib_Flags; UBYTE lib_pad; UWORD lib_NegSize; UWORD lib_PosSize; UWORD lib_Version; UWORD lib_Revision; APTR lib_IdString; ULONG lib_Sum; UWORD lib_OpenCnt; }; The Flags field is used by Exec to keep track of what is happening with the library. The NegSize and PosSize fields indicate the size (in bytes) of the library, on either side of the library base. The Version and Revision fields are used to indicate future changes and updates to the library. The IdString is a pointer to a null-terminated ASCII string giving further information about the library. Sum is the library checksum which is used by Exec to ensure library integrity. The OpenCnt field holds the number of tasks which have opened the library so far. Every time a task calls OpenLibrary() for this library, this field is incremented. Every time the complementary CloseLibrary() function is called, this field is decremented. If the Open Count is zero, Exec may remove the library in order to free up ram. The other major item of interest is the vector table. This is comprised of a group of 6 byte entries, one entry for each function in the library. Each entry consists of a jump instruction (2 bytes) followed by the absolute address of the function being called. Each function call then, is a multiple of 6 bytes behind the library base. For example, the seventh function in the table would be at location LibraryBase-42. These 6 bytes multiples are known as Library Vector Offsets, or LVOs, for short. Along with the normal application functions, there are four mandatory functions which all shared libraries must have. Consequently, the first application function is always the fifth one in the list, at position 30 (referred to as the Bias, in a .fd file). The four special functions are: Open, Close, Expunge and Reserved. The The Open and Close functions are called for each OpenLibrary() and CloseLibrary() call. The Expunge routine is used for final cleanup if the Open Count has reached zero and Exec has decided to remove the library. The Reserved function is for future use. Presently, all it should do is return 0. In order to properly load a library, Exec needs a RomTag structure. In assembly, a RomTag looks something like this: RomTag: dc.w $4AFC ;Voodoo magic RomTag word. dc.l RomTag dc.l endRom dc.b NO_AUTO_INIT ;Auto-initialize, or not. dc.b VERSION ;Library version, as used in OpenLibrary(). dc.b NT_LIBRARY ;Type. This is a Library. dc.b PRIORITY dc.l Lib_Name ;Pointer to Name string. dc.l Lib_Id ;Pointer to ID string. dc.l Lib_Startup ;Pointer to initialization routine. endRom Normally, Priority is 0. NT_LIBRARY is defined as 9, and we'll be using a non auto-initialized library (0) since this saves space and reduces load time. If you were building a library from scratch, besides the functions of interest, you would need to create, compile, assemble and link the RomTag, the Library structure, the function table, the initialization routine, and the Open, Close, and Expunge routines. Some of this may be reused from library to library, but there is still a reasonable amount of work involved. To circumvent this, you can use the LibTool utility. LibTool was created by Jeff Glatt of dissidents. (LibTool and its associated documentation and examples are copyrighted, but are freely redistributable. In other words, feel free to use it at any time, or give it to friends. You just can't sell it to anyone for profit.) LibTool will create all of the auxiliary items you'll need for your library (including pragmas and header files) from a single .fd file that you create. Using LibTool, you'll only need to make two files in order to create a library: the functions of interest, and the .fd file. At this point, it's time to consider the code which will be turned into the library functions. There are a few rules which you should remember. We'll be using C, but these items are pertinent to any language. First, your code should be re-entrant. This means that it does not make use of global variables. All variables should either be held on the stack, or should be allocated as needed. The reason for this is that it is possible for several tasks to open and use a library simultaneously. If two tasks are calling the same function at about the same time, it is quite possible for one task to alter a global variable before the other task is finished with it. The results are chaotic at best. In contrast, it is perfectly acceptable to use global constants. Since constants are not altered by a function, there is no problem with one task stepping on another task. Examples of acceptable constants would be fixed strings and look-up tables (such as a sine wave table). The second item to remember is that if your library needs to use the functions contained in other libraries, these other libraries must be opened (and eventually closed) from within your library. For example, you might wish to create a DrawBox() function based on the system functions Move() and Draw(). Since these two functions reside in the graphics library, graphics must be opened as part of your library's initialization. Graphics will then be closed as part of your library's expunge routine. The final item is that you should not use printf() style functions from within the library. The library will not have access to stdin, stdout, or stderr. The easy way around this is to have library functions which return error codes, which can then be interpreted by the calling program. For our example, we're going to make a library which implements rectangular to polar and polar to rectangular conversions. The library will consist of four functions: Mag(), which takes the real and imaginary rectangular components and returns the polar vector's magnitude; Ang(), which takes the rectangular components and returns the vector's angle in degrees; Real(), which takes the polar magnitude and angle in degrees, and returns the real rectangular component, and Imaj() which takes the same polar arguments and returns the imaginary rectangular component. I chose these functions because they're reasonably useful, and fortunately, very quick to code. In any case, the you don't have to do much typing since all of the example code is found on disk. These functions require the use of floating point math. We have several choices. For the sake of expediency, we'll use Motorola fast floating point. In order to make our functions, we'll need routines found in mathffp.library and mathtrans.library. Consequently, we have to open these two libraries as part of our initialization, and we need to close them as part of the expunge "clean up". If you look at the library code, AWlib.c, you can see our four short functions. We also have two other functions, myInit() and myFree(). myInit() will be called as part of the initialization routine. It returns a BOOL value (TRUE or FALSE). All it does is open the required math libraries. If it can't open the libraries, myInit() returns FALSE. This will prevent our library from loading. myFree() is called as part of the expunge routine. It simply closes the libraries which were initially opened. If we needed to perform a certain amount of work each time the library was opened or closed, we could add our own custom functions there, as well. Much of the drudgery of creating the library is going to be taken care of by LibTool, in conjunction with a specialized function description file. LibTool will create a library startup module which will contain the RomTag, the Library structure, the function table, the four mandatory functions, wedges into the various routines (eg, the references to our myInit() and myFree() functions), the proper version and revision numbers, strings, and the like. Also, this module will automatically open Exec, Dos, Intuition, and Graphics for you, since they are used so often (and are most likely already present in the system). Therefore, if you're only calling functions from these system libraries, chances are that you won't even need Init() and Free() routines. To aid in the construction of your applications which call your new library, LibTool can also create a header file, pragma statements, and C "glue" routines. The glue routines are used to pull C arguments off of the stack and stuff them into the proper registers for the library. It is possible to create libraries with LibTool that expect arguments on the stack. The resulting glue is smaller for C applications, but this does make it harder to access the library from other languages. Of course, the whole issue of glue routines is bypassed if pragmas are used. By using pragmas, the C compiler will move the arguments into the registers, and no glue is needed. The .fd file is an extension of the ordinary .fd files which most Amiga programmers are familiar with. LibTool recognizes extra commands which allow it to create the complete Library Startup module. Here is our .fd file: ##base AWBase *the name of our base used by C glue code and C PRAGMAS ##name AW *the name of our library (ie, aw.library) ##vers 1 *version # ##revs 0 *revision # ##init myInit *to be called once (upon loading the lib) ##expu myFree *to be called once (upon expunging the lib) ##libid Amiga World TJ lib (ver 1.0) ##bias 30 *first function is always at an offset of -30 from lib base *Here are all of the lib functions callable by an application *They all return doubles ##ret double Mag( real, imaj ) Ang( real, imaj ) Real( mag, ang ) Imaj( mag, ang ) ##end Notice that virtually everything you need to describe your library (and update it in the future) is found in this one file. Each specialized item is given its own command. Of particular interest are the ##init and ##expu commands which are followed by the names of our initialization and expunge routines. Commands are available for the previously mentioned Open and Close routines, if required (##open, ##clos). Using Manx 5.0 from the CLI, you make the library as follows: LibTool -cmho glue.asm AWlib.fd This creates the library startup code (AWlib.src) using C style syntax (ie, it prepends an underscore to the function names), the C header file (AWlib.h) for the applications, and a C application glue file (glue.asm) which will be assembled, and then linked with the application. as -cd -o LibStart.o AWlib.src This assembles the library startup module. Note that we are using large code and data so that we don't have to worry about saving register A4, as you would with the small model. cc -mcd0b -ff AWlib.c Here, we compile the library code, again using the large model. We are suppressing the standard Manx startup (.begin), and using fast floating point math. ln -o libs:AW.library LibStart.o AWlib.o -lmfl -lcl Finally, the library is created by linking together the needed parts. It is VERY important that LibStart.o be the first object module in the list. The applications which call the library are compiled and linked in the standard manner. If you are using glue files (as we are here), glue.asm must be assembled and then linked with the application program. Our application, AWlib_app.c, simply opens the library, and calls each of the functions in turn. As mentioned earlier, LibTool can be used with the Lattice compiler, and with assembly language development systems. It can also be used for other purposes, including the creation of BASIC .bmap files, and in the construction of devices. Further details concerning LibTool are found on the disk, along with other examples. Hopefully, this little foray has enticed you into creating some of your own shared libraries, and eased the programming burden as well. Have fun.