Another type of binary often built for AROS is a shared library. First a basic
skeleton will be given of how to build a library; some handy extensions will
follow.
A shared library is built with the %build_module macro with a line like
this:
%build_module mmake=MetaTarget modname=mylib modtype=library files=SourceFiles
This macro can build different AROS module types, like devices, Zune classes,
HIDDs, etc., but in this text the focus is on shared libraries. Therefore,
the assumption is that you specify the modtype=library parameter.
The mmake and the files parameter act the same as for the
%build_prog macro. Additionally to the meta-target MetaTarget and
MetaTarget-quick, also meta-targets MetaTarget-includes,
MetaTarget-includes-quick and MetaTarget-linklib are defined. This
allows to build just a subset of all the files normally generated. They will
most of the time be used to specify dependencies.
For building a shared library, more information is necessary than is given in
the %build_module macro. That extra information is stored in another file
that by default is called mylib.conf when modname=mylib is
specified. This file can contain a lot of information but for brevity only a
minimal example is shown. More information can be found in the
reference section of the %build_module macro. Here is an
example .conf file:
##begin config
version 1.0
##end config
##begin functionlist
void func1(LONG a, LONG b)
int func2(char *s, ULONG a)
##end functionlist
As you can see this is a file with two sections, each section starting with
##begin sectionname and ending with ##end sectionname. The section
config is for providing the information about the library that AROS needs
to use the shared library and for giving options to influence the type of
module that will be built. In-depth discussion can be found in the reference
section. The functionlist section gives a list of functions that will
be included in the library; the list consist of the function prototypes.
The order of the list is important because it will determine the location of
the function in the lookup table. Empty lines are important as well, as an
empty in this list will cause an empty slot in the table. Lines start with one
'#' character are considered comments, and are ignored without causing empty
slots in the library's lookup table.
If all this information is present and you then execute the command make
MetaTarget the following files will be generated ($(AROSDIR) corresponds
with the directory for the binary tree):
- $(AROSDIR)/Libs/mylib.library: the module itself
- several include files in $(AROSDIR)/Development/include, with the main
include file proto/mylib.h. The latter file can be included, for the
function prototypes, in code using the library.
- $(AROSDIR)/Development/lib/libmylib.a: a static link library that can be
linked to other code and that can take care of auto-opening the library
and contains stub functions. When these functions are called it will
redirect to the code in the library using the lookup table in the libbase.
In the example given above, standard C variable type or standard exec types
were used for the arguments of the function. If you want to use your own
types or types defined in other include files, you will need to take extra
steps. This can be done in the cdef section, as shown in this example of
using a extra include file:
##begin config
...
##end config
##begin cdef
#include <exec/semaphores.h>
##end cdef
##begin functionlist
...
BOOL func3(struct SignalSemaphore *sig)
##end functionlist
The lines in the cdef structures are normal C code and they will be included
in the generated include files before the library's function prototypes. You
could also define your own structure like this:
##begin cdef
struct MyStruct
{
...
};
##end cdef
##begin functionlist
...
int func4(struct MyStruct *sig)
##end functionlist
When doing it this way the structure definition will be included in the
generated include files. The recommended way to do this in AROS, however,
is to have a separate header file for the definition, and then include that
header file. One way to do this is to define your own file, named
libraries/mylib.h, with the following contents:
#ifndef __LIBRARIES_MYLIB_H
#define __LIBRARIES_MYLIB_H
struct MyStruct
{
...
};
...
#endif /* __LIBRARIES_MYLIB_H */
This file is then copied as explained in another paragraph and then simply
included by the cdef section:
##begin cdef
#include <libraries/mylib.h>
##end cdef
The functions put into the library vector table up to now were regular C
functions. In the Amiga m68k days the parameters for library functions most
of the time were passed in registers and not on the stack. For backwards
compatibility it's possible to define functions where the arguments are passed
in m68k registers. When your library is compiled for m68k it will use the
specified registers, on other architectures different conventions will be used
either by using registers available on that CPU or by using stack-based
argument passing. Defining a function with m68k registers is done by adding
the registers to the line in the function list and using macros for the header
of the function in the source code. The line in the functionlist looks as
follows:
##begin functionlist
...
ULONG func5(ULONG a, STRPTR b) (D0,A0)
...
##end functionlist
And the function in the source code is defined as follows:
AROS_LH2(ULONG, func5,
AROS_LHA(ULONG, a, D0),
AROS_LHA(STRPTR, b, A0),
struct Library *, MylibBase, 9, Mylib
)
{
AROS_LIBFUNC_INIT
...
AROS_LIBFUNC_EXIT
}
This macro has the name AROS_LHn with n the number of arguments passed to the
function. The macros has the following arguments:
- The function return type
- The name of the function
- The list of function arguments using the AROS_LHA(vartype, varname,
register) macro. vartype is the type of the argument, varname is the name of
the argument and register the m68k register to use. The register is
specified as D0-D7 for numeric arguments and A0-A5 for pointer arguments (A6
and A7 are reserved for other purposes).
- The library base type. When you have not defined your own libbase type as
explained in this paragraph
- The variable for the libbase, which can be used in the function for
accessing the libbase
- The number of the vector in the vector table. For libraries the first
function in the functionlist has number 5, the next 6 and so on. Although
this information is not necessary, because the functionlist in the .conf
already determines this number, it's still required for legacy reasons.
- The base name of the library. If this is not overridden in the config
section of the .conf file it is equal to the name given to the modname
parameter with the first letter capitalized.
On AROS and other Amiga-like systems every shared library has a library base.
The base of a library contains the vector table and some data about the
library used by the OS. It can also be extended with user-defined data.
This can be done by providing your own C struct for the type of the libbase.
There are two config options that let you decide the type of the libbase:
##begin config
...
libbasetype struct MyLibIntBase
libbasetypeextern struct MyLibBase
...
##end config
libbasetype is the type used internal in the library code, this type
also decides how much memory is allocated for the libbase. If this type is
not given struct Library is taken as default. libbasetypeextern is
the type by external programs using your library. Here too, struct Library
is used as the default type. Both the internal and the external
type have to start with a struct Library structure. If an external type
is specified, the first part of the internal type has to be the same as the
external type.
To keep libraries backwards compatible, the external type of a library can not
be changed. Once a version of the library is released into the public, the
only possible modification is too extend the structure. The internal type
can be changed at will, provided all internal code of your library is adapted
to the new internal library structure.
The external type also has be exported to the users of your library. This
is the same as the usage of other non-standard types. On the other hand,
the internal type is not meant to be exported to the users, which is why a
cdefprivate section can be put in the config file. This way the library
initialization code has all the information about your internal type without
having the internal structure publicly exported. A common convention is to
declare your internal structures in mylib_intern.h and then include this in
the cdefprivate section. The mylib_intern.h would then include the
following code:
struct MyLibIntBase
{
struct Library base;
...
};
And the config file the following section:
...
##begin cdefprivate
#include "mylib_intern.h"
##end cdefprivate
...
So far, in each case only one libbase would be created for a library. All
users who opened the library would get a pointer to the same library base.
Sometimes there's an advantage in having data that differs per opener of
the library. This can be accomplished by using a special option in the
config section:
##begin config
...
options peropenerbase
##end config
The use of a base per opener of the library does not make much sense when
not using an extended libbase. Also, currently the only way to pass
the libbase to the functions of the library is to use
m68k register passing.
(Development is under way to also be able to get the libbase in library
functions using the normal C argument passing). You could also add the
libbase as an explicit argument to the function but this is not encouraged.
Note
On AROS the need for extended libbases is much less then on classic
AmigaOS. On classic AmigaOS it was discouraged to use global
variables in the library and to use the libbase for storing
variables. On AROS global variables are handled fine so the
use of an extended libbase is only needed for using a
per-opener libbase.
In some cases, some initialization should be performed when a library is
loaded or when it is opened. For this, the same mechanism as for programs
can be used through the ADD2INIT and ADD2EXIT, as in the next example:
static int InitFunc(void)
{
...
}
static void ExitFunc(void)
{
...
}
ADD2INIT(InitFunc, 0);
ADD2EXIT(ExitFunc, 0);
When this code in added to a source file, the code in InitFunc will
be executed when the library is initialized and the code in ExitFunc when
the library is expunged. The return value of InitFunc indicates success or
failure, with a zero (== FALSE) value indicating a failure to initialize,
and the library will be unloaded again and not be usable. The ExitFunc
should not be able to fail, and thus has no return value.
Often part of the libbase should be initialised, and therefore the methods
discussed above are not appropriate. For libraries, additional ways are
available for adding initialization or clean-up code:
static int InitFunc(struct Library *lh);
ADD2INITLIB(InitFunc, 0);
static int ExpungeFunc(struct Library *lh);
ADD2EXPUNGELIB(ExpungeFunc, 0);
static int OpenFunc(struct Library *lh);
ADD2OPENLIB(OpenFunc, 0);
static void CloseFunc(struct Library *lh);
ADD2CLOSELIB(CloseFunc, 0);
The InitFunc function will be called once during initialization and the
ExpungeFunc once during expunge of the module. OpenFunc and
CloseFunc functions are called respectively every time the module is
opened or closed. InitFunc, ExpungeFunc and OpenFunc return a
value indicating the success of the function. If InitFunc fails, the module
will be expunged, if OpenFunc fails, the opening of the library will fail,
and if ExpungeFunc fails, expunging the library will be delayed. If the
latter happens, the next time the expunge will be tried, again all registered
functions for expunge will be called. This means that if more then one
function is registered and the second function returns 0, the first function
will be called a second time the next time AROS tries to expunge the module.
If you implement an ExpungeFunc that can return a 0, you also have to be
sure that other ExpungeFuncs may be called more then once.
If you look at the ADD2...LIB macros above, you can also see that next to the
function name there is an extra number. This number indicates the priority
to call the function. A InitFunc or OpenFunc with a higher number will
be called after one with a lower number. For CloseFunc and
ExpungeFunc the opposite order is used, e.g. higher numbers are called
before lower numbers. The number is a signed byte, which means it must have
a value from -128 to 127. Usually, this value can be kept at 0.
If a per-opener base is used, a copy will be made of your libbase
every time the module is opened. InitFuncs will be called before the copy
so initialization of values in the libbase will be seen by all the openers.
OpenFuncs are called after the copy of the libbase and changes made to
libbase are thus private to the opener.