AROS Application Development Manual -- Introduction
Index
Avis
This document is not finished! It is highly likely that many parts are
out-of-date, contain incorrect information, or are simply missing
altogether. If you want to help rectify this, please contact us.
This chapter explains how to develop programs that will run on the AROS
platform. It also tells you how to compile them on the different machines
that AROS runs on. It assumes that you have an average knowledge of the C
programming language and of basic concepts like linking.
Below is a program that shows a "Hello, World!" message - a programmer's
tradition since ages (well: 1972). Create a file helloworld.c with the
following contents:
#include <stdio.h>
int main(void)
{
puts("Hello World");
return 0;
}
If you have your own source tree of AROS and compiled AROS there, you can
use the AROS build system to compile programs for AROS. You do this by
putting your program source code somewhere in the AROS tree. First, make a
directory there, assuming you are in the top AROS source directory:
% mkdir local
% mkdir local/helloworld
Put the helloworld.c file there, plus an additional file for the build
instructions named mmakefile.src, with the following contents:
include $(TOP)/config/make.cfg
%build_prog mmake=local-helloworld files=helloworld progname=HelloWorld
In the top AROS source directory you can now build the helloworld program
with the following command:
% make local-helloworld
After doing so, you'll find the compiled program as
local/helloworld/HelloWorld in the binary tree of AROS.
The AROS build system is meant to ease your life when building binaries with
not-trivial dependencies. It is explained in a separate chapter.
Under AROS/hosted you are using a configured version of the Linux GCC.
There is a difference depending on whether you use the compiled version
of AROS (i386-linux-system) or you compile the source yourself:
i386-linux-system
You have to download the package i386-all-sdk. Unpack it, cd into the
created directory and start the included script as root (e.g.
sudo AROS-SDK-Install).
The script asks some questions but you can use the default values. The
next step is to add a path. It depends on the shell you are using how this
can be done.
Assuming you're using the Bash and you have kept the default values for
the paths:
open the file /home/user/.bashrc and add the line PATH=/usr/local/aros-sdk/bin:"${PATH}"
at the end of the file. Type i386-aros-gcc -v in a new shell for a
quick test.
self-compiled
The AROS compiler has the path AROS/bin/linux-i386/tools. Add this path
as shown above. The name of the compiler is i386-linux-aros-gcc.
You can compile the program with the following command from a Linux shell:
% i386-linux-aros-gcc -o helloworld helloworld.c
You will find additional tools in the path of the AROS C compiler:
AROS versions of ld, ranlib, the catalog compiler FlexCat, etc.
Note
If you are using i386-linux-aros-strip you have to add the
parameters --strip-unneeded --remove-section .comment. Otherwise strip
will create corrupt binaries.
You can download a cross-compiler from here.
Advantage over the fake compiler is, that you can additionally compile C++
sources. After installing you should update the sys-includes and the libs
from a recent SDK.
You can download a version of GCC which runs natively under AROS from
Sourceforge.
You need at least the binutils and the core. Also you'll need an AROS SDK.
Unpack them to the same place (for example, sys:ADE). Copy the includes and
libs from the SDK to sys:ADE.
Then you'd need to use the following commands:
path sys:ade/bin add
assign Development: sys:ade
AROS comes with a variety of include files. They are placed in
sys:Development/include. The subdirectory proto contains include files
with function prototypes for the shared libraries. In libraries are headers
with structures and defines. Some of the bigger libraries, like Intuition,
have their own directory with headers.
Shared libraries are the magic that makes AROS work. Every library is a
collection of functions that fulfil a certain purpose. Normally, functions
with a similar purpose are contained in one library. For example all the
basic memory handling functions are contained in exec.library.
Libraries are normally found in the LIBS: directory, but can also be
stored at other places. Some important libraries are not stored as a
separate file, but are contained in the kernel. However, exactly which
libraries are part of the kernel differs between installations, so don't
depend on a specific library being part of the kernel.
Here is a list of some important libraries and their purposes.
You don't have to remember all of these; they will be discussed in detail
later on.
- exec.library is the most important library. It is responsible for
handling the most basic things, like managing tasks (i.e. programs),
memory, libraries and many other things.
- utility.library implements very important mechanisms for "talking" to
libraries: taglists, which will be discussed later in this chapter, and
hooks.
Apart from that, utility contains miscellaneous small utility functions.
- dos.library is responsible for file-handling and some basic I/O
functions. Without dos, AROS would not be able to access files.
- intuition.library handles graphical user interfaces (GUIs). With
Intuition you can create windows and gadgets and handle them
accordingly. Working on top of intuitions are several other libraries,
providing more-sophisticated and more-specialised GUI functions. Examples
are muimaster.library, which implements some more complicated gadgets
and asl.library, which provides file- and other requesters.
- graphics.library contains drawing functions.
- cybergraphics.library extents graphics.library by functions for true-
and high-color bitmaps.
- muimaster.library provides an advanced object-oriented mechanism for
gadgets (a.k.a widgets on other platforms). Therefore it replaces fully
or partially gadtools.library, boopsi.library reqtools.library
asl.library and intuition.library.
- datatypes.library implements an object-oriented interface to all kinds
of multimedia data.
- asl.library handles requesters for fonts, files and screens.
- locale.library makes AROS international. It provides functions for
loading catalogs with translated strings.
- keymap.library translates between keyboard and ANSI codes.
- workbench.library and icon.library contain support functions for the
AROS GUI "Wanderer".
- diskfont.library loads fonts from disk.
- commodities.library is for programs that watch and manipulate the input
event stream, e.g. programs which react to hot keys.
- iffparse.library supports reading and writing of data which is written
in IFF format.
This format is mainly used for configuration data and graphics/music/text.
- bsdsocket.library for network support.
- rexxsupport.library and rexxsyslib.library are useful when you want
to extend your programs with the AREXX macro language.
Some additional libraries for very specific tasks, aren't mentioned here.
The term library usually refers to an object whose purpose is to collect
in a single place functions that may appear in programs more often,
usually with all such functions serving one common purpose. E.g. libraries
to parse configuration files, to handle localization, and other kinds of
tasks which a program might have to perform.
A distinction can be made between link-time libraries and run-time
libraries. The link-time libraries, as the name suggests, are used only at
the program linking stage: the linker collects from the provided libraries
just those functions the program requires and together with the program
links them into one executable.
Run-time libraries, instead, are made available to programs when they are
run or during their execution by special request of the program. In most
systems the run-time libraries are shared between running programs so they
only take up memory for one instance of the library. In such cases the
object is often called a shared library.
Whilst link-time libraries are handled more or less the same way across all
operating systems, since they're independent of the OS itself, run-time
libraries may be handled differently by different OS-s. In AROS, before a
library can be used in a program it has to be opened. This is done by
calling the exec function called OpenLibrary. When a library is
successfully opened a pointer to the so called library base is return. The
library base is a zone of memory that both holds the function vectors and
the library's own data . When libraries are opened they are free to
choose whether their bases will be the same for all instances or whether a
new one will be allocated each time it's opened. When a function of the
library is called, most of the time the library base is passed to the
function so the data in the library base can be accessed from inside the
library . A library can make part or all of the data in the library base
public, by defining a type for the base. Where this is the case you'll find
the type in the include file proto/libname.h. This mechanism is used by a
number of older libraries, but more recent libraries usually keep all their
data private. The only way to change the state of such a library, is through
the use of the available functions.
As already explained in the previous section, libraries have to be opened
before their functions may be used. Additionally, you have to include a
header to make the prototype of the functions known to the code. This
include file is in the proto directory, so if you want to use functions of
dos.library you have to use the following line:
#include <proto/dos.h>
The only library that never has to be opened first is exec.library. Exec
is always open and your compiler knows how to access it. Your compiler or
build environment may additionally open some other libraries for you, so you
don't have to open them manually. Read your compiler's manual to learn about
such a feature. The paragraphs below list which libraries are opened by the
AROS tools, and describe how one opens libraries manually.
The gcc compiler from the AROS SDK auto-opens the following core libraries:
- aros.library
- asl.library
- commodities.library
- cybergraphics.library
- datatypes.library
- diskfont.library
- dos.library
- expansion.library
- gadtools.library
- graphics.library
- icon.library
- iffparse.library
- intuition.library
- keymap.library
- layers.library
- locale.library
- muimaster.library (which is provided by Zune on AROS)
- partition.library
- realtime.library
- utility.library
- workbench.library
You can disable the auto-opening of these library by providing the
-nostdlibs flag to the gcc compiler. For the other libraries provided by
AROS you can use the corresponding link-time library that will take care of
opening the library. So if your programs uses reqtools.library you add
-lreqtools to the gcc command.
Note
To summarise: when using the AROS gcc compiler the usage of shared
libraries becomes very easy and can be done in two steps:
Use an include statement to declare the functions of the library:
#include <proto/reqtools.h>
Add an extra link library if the library is not auto-opened by gcc:
% i386-linux-aros-gcc ... -lreqtools
Auto-opening libraries by the build system is very similar to using the AROS
gcc compiler. Analog to specifying a -l option, you specify the libs you
use in the uselibs parameter to the %build_prog and the
%build_module macro. More information can be found in the build system
tutorial.
To open a library you have to use a function of exec.library:
#include <proto/exec.h>
struct Library *OpenLibrary( STRPTR name, ULONG version );
OpenLibrary() takes two arguments:
- name
points to the name of the library. Normally this is just the plain name,
but this can also be a complete (absolute or relative) path to the
library.
Note
Paths do not work with kernel-based libraries
(i.e. libraries that are included in the kernel).
Use absolute path only, if you know exactly, what you
are doing!
- version
- is the minimal version of the library to open. If the named library is
found, but its version is lower than version, the library will not be
opened, but an error will be returned. Versions are important, because
libraries are supposed to be expandable. Some functions are only
available from a certain version of a library on. For example the
function AllocVec() of exec.library was introduced in version 36 of
the library.
If you try to call this function with lower versions of exec.library
installed, unexpected things will happen (most likely the application
will crash).
The following procedure is used to load the library to open:
First, the name of the library is searched in the list of already loaded
libraries. If this library was loaded into memory before (e.g. by a
different program) and still is there, everything is fine and
OpenLibrary() returns immediately.
Libraries in the kernel are always on the list of loaded libraries.
Note
Comparisons in this list are case sensitive! Be sure to use the
right case in name. Normally all characters in a library name
are lower-case.
If the library was not found in the resident list and a path was supplied
with name, an attempt is made to open the indicated file. If this fails,
OpenLibrary() returns an error.
If instead a plain library-name was given, the library is searched for in
the current directory first. If it's not found there, it is searched for
in the directory LIBS:.
OpenLibrary() returns either a pointer to a structure, describing the
library (struct Library * defined in exec/libraries.h) or NULL,
meaning that opening the library failed for some reason. The resulting
pointer has to be stored for the compiler's use. Normally it is stored in a
variable in the form: <libraryname>Base, e.g. IntuitionBase for the
pointer to intuition.library.
After opening the library, you can use its functions by just calling them
like any other function in your program. But to let your compiler know, what
to do, you have to include the library-specific header-file. This is
normally called proto/<libraryname>.h for C compilers.
When you've finished using the library, you have to close it again to free
the resources it uses. This is done with:
#include <proto/exec.h>
void CloseLibrary( struct Library *base );
CloseLibrary() closes the library pointed to by base. This may
also be NULL, in which case CloseLibrary() does nothing.
The use of libraries will be demonstrated by creating a small graphical
hello-world program. Instead of printing Hello World! to the console, it
will be displayed it in a requester. A function to display a requester is
EasyRequestArgs(), which is a function of intuition.library. Its usage
will not be discussed here; for more information, see the section about
Requesters.
Example usage of libraries:
#include <proto/exec.h> /* OpenLibrary() and CloseLibrary() */
#include <exec/libraries.h> /* struct Library */
#include <dos/dos.h> /* RETURN_OK and RETURN_FAIL */
#include <proto/intuition.h> /* EasyRequestArgs() */
#include <intuition/intuition.h> /* struct EasyStruct */
/* This variable will store the pointer to intuition.library */
struct IntuitionBase *IntuitionBase;
int main(int argc, char *argv[])
{
/* Needed for EasyRequestArgs(). */
struct EasyStruct es = {
sizeof(struct EasyStruct), 0UL,
"Requester", "Hello World!", "Ok"
};
/* First, open intuition.library. Version 36 or better is needed,
because EasyRequestArgs() was introduced in that version of
intuition.library.
*/
IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library", 36);
/* Ccheck that intuition.library was successfully opened.
If it was not, return immediately with an error, as you can't call
a function from the library.
*/
if (!IntuitionBase)
return RETURN_FAIL;
/* After opening intuition.library, you can call EasyRequestArgs(). */
EasyRequestArgs(NULL, &es, NULL, NULL);
/* Finally, close intuition.library again. */
CloseLibrary((struct Library *)IntuitionBase);
return RETURN_OK;
}
Try to compile and run this program. It should present you with a handsome
hello-world requester.
Shared libraries may evolve over time and new features may be introduced.
If a program were to use a feature of a more recent version of the library,
running it on a machine that had an older version of the library would most
likely lead to a crash. Therefore, versioning of libraries was introduced,
so programs can check whether the version of a library is adequate and quit
gracefully or reduce the functionality accordingly if it isn't. On AROS and
Amiga-like systems, the version is determined by a major number and a minor
number (also respectively called version and revision).
A new major number indicates the introduction of new features while an
increase of the minor number just indicates some optimizations and/or
bug fixes, with compatibility. A version of a library is often presented
as major.minor and can be retrieved with the version dos command:
5.System:> version dos.library
dos.library 41.7
When opening a library, you can provide a version number; opening the library
will then fail if the version of the library is lower than this number:
mylibbase = OpenLibrary("my.library", 2);
This will return NULL if only version 1 of my.library is installed. If you
use auto-opening of libraries the library will be opened with the version of
the library used during link time. This version can be overloaded with a
variable named libbasename_version. At the moment the version of dos.library
is 41 and this means that programs compiled will only run on other systems
that have version 41 of dos.library. If you are sure you're only using
features from up to version 36, you can let your program run on these
systems by including the following statement somewhere in your code:
const LONG DOSBase_version = 36;
The consequence for libraries is that they always have to be backwards
compatible: if the version of your library is 41 but the program was
compiled for version 36 it still need to run without problem. Therefore a
function at a certain place in the lookup table always has to perform the
exact same function even for a newer version of the library.
If you really want to change the behaviour of the function with a certain
name you could do that by putting it at another place in the lookup table.
At the old location you put then a compatibility function that is still
compatible with the behaviour in older library versions. For example, in the
first version of AmigaOS the exec function OpenLibrary did not have a
version parameter. In a later version of the OS, a new OpenLibrary function
was introduced that included the version parameter. While the old function was
at position 68, the new function was put at location 92. The function at
position 68 was kept for compatibility, but was renamed to OldOpenLibrary.
The AROS shared libraries have an unique architecture with advantages and
disadvantages. Some aspects will be discussed later in this chapter.
Windows and Unix(-like) systems are usually taken as reference in those
discussions, as for those who port programs the differences are important.
On AROS the dynamic link libraries are relocatable ELF objects. The first
time a library is opened, it is loaded from disk and relocated with the start
address it was loaded to. On AROS and Amiga-like systems, memory is shared
between all code running on the system as a single big memory region. This
approach allows all programs to use the library loaded at the memory it was
loaded to.
Other systems, including Windows and Unix, have a different virtual address
space for each process. Here too the OS tries to load the shared library
only once and it then maps the same library in the address space of each of
the processes using it. The library may thus be located at different
addresses in the different spaces and the OS has to handle this.
Windows will first try to locate the shared library at a single location in
memory and tries to map it to the same memory region in each process that
uses the library. If this is not possible the library will be duplicated
in memory. On most Unix systems this problem is avoided by letting the
compiler generate position independent code, e.g. code that works at any
position in memory without having to relocate the code. Depending on the
architecture this type of code may have less or more impact on the speed of
the generated code.
Programmers that use a higher level language for accessing the functions in
a shared library, will use the name of the function they want to use. When a
microprocessor executes a program, it uses memory addresses to jump to a
certain function. At some point, the name used by the programmer has to be
translated into a memory address.
On the Amiga, the translation happens when the code is compiled or when a
program or module is linked. Every libbase of an AROS library contains a
lookup table for the functions of the library. During compilation (or
linking) the name of a function is translated into a position in this table
where the address of the function can be found . Functions in an AROS
shared library are thus accessed with one level of indirection. Depending on
the CPU architecture this level of indirection may have more or less
influence on the speed of the code. Fortunately, a similar type of
indirection is used for calling virtual functions of C++ classes and because
of this, most modern CPUs are optimized to handle the indirection without
a (big) impact on the speed. As the lookup table is attached to the libbase
it has to be duplicated for libraries that use a per-opener base.
On Windows and Unix-like systems the translation of the name of a function
to an address is done when the program is loaded and linked at run-time with
shared library . When the program is linked at compile time a list of
libraries is put in the executable together with a list of the functions to
be used. These lists are ASCII strings. When the program is then loaded it
will convert the functions names to their addresses (or to a pointer in a
lookup table). First the libraries in the library list are opened,
afterwards each of the functions is looked up in the libraries. Different
mechanisms are used for the lookup of the function names. For example on
Windows, the functions available are put in a sorted array so a binary search
can be performed and on Linux hashes are used to speed up the lookup.
As said in a previous paragraph, AROS shared libraries are only loaded into
memory and initialised once. This also has an impact on the way global and
static variables are handled. You can declare a global variable in the source
code of your library in the following way:
int globvar;
This will create a global variable that is accessible in all parts of the
library. Once the shared library is loaded into memory your variable
will also be located in the memory taken by the library and will always stay
at the same location until the library is unloaded from memory. Static
variables defined in a function are handled in an analog way. This also
means that the code in the library accessing a global variable will always go
to the same location no matter how many times the library is opened or which
program called the library code. Currently, the only way to have a variable
that has a different value per opener of the library is to have a library
with a per-opener base and store the library in this base. Also, global
variables in AROS shared libraries currently can't exported. They can only be
accessed within the library itself; a program using a library can not access
the library's global variables directly. In this respect, variables in AROS
shared libraries are handled differently from variables in link-time
libraries. A global variable defined in a link-time library is also
accessible by the program to which the library is linked and every program
linked with the same link-time library will have its own version of the
global variable.
On Unix, shared libraries were introduced after the link-time libraries were
already heavily used. One of the design goals then, was to make the
behaviour of the shared libraries the same as that of link-time libraries.
Therefore, a copy is made of the variables every time a program opens a
shared library. In this way every program that opens a shared library will
get its own set of the global variables. Also, the global variables of a
shared library are automatically exported from that library, so they can also
be accessed directly in the program using that library.
On Windows, one can choose the behaviour of global variables to be like the
AROS way or the Unix way but by default it is handled in the Unix way.
For porting shared libraries to AROS or Amiga, this different handling of
variables has to be taken into account. Some libraries depend on how
variables are handled in Unix and Windows shared libraries and may be
difficult to port to AROS.
Note
The explanation in this paragraph describes how the handling of data in
shared libraries worked when the text was written.
At that time there was also discussion on how to extend this to also
allow handling similar to the handling done by other library types.
A library can open another library. When a library opens another library it
will get a libbase for that library. This means that a library that has
a per-opener base will return a unique libbase to another library.
When a program opens library 1 with a per-opener base, it will get a libbase
back. If that program then opens library 2, that itself also opens library 1,
then library 2 will get a different base for library 1 than the base the
program itself has for that same library 1. Programmers of libraries with a
per-opener libbase have to take this into account.
As was already discussed before, on Unix and Windows everything is based on
processes. When a program is loaded, a new process is created, every shared
library used in that process will only be dynamically linked once into the
process. This means that a program and shared library that both access a
second shared library will use the same instance of that shared library.
Again this different behavior may make porting shared libraries from
Unix/Windows difficult.
Note
Again, the explanation in this paragraph describes how the handling of
opening shared libraries from a library worked when the text was written.
At that time there was also discussion on how to extend this to also
allow handling similar to the handling done by other library types.
Every library function takes a fixed number of arguments. This poses quite
a problem with complex functions that would need a lot of arguments. To avoid
this problem, so-called taglists were introduced. The header file
utility/tagitem.h contains a structure TagItem, which includes the
members ti_Tag and ti_Data.
A taglist consists of an array of this structure. The size of the list is not
limited. The field ti_Tag is an identifier (often referred to as Tag) that
declares what ti_Data contains. ti_Data is either an integer or a
pointer. It is guaranteed to be at least of the size of a long-word or a
pointer (whichever is bigger).
In every description of a function that uses a tag-list, all possible tags
are listed. Functions have to ignore unknown tags and use defaults for tags
not provided, so taglists are a very flexible way of providing arguments to
a function.
There are some special tags that all functions understand
(defined in utility/tagitem.h):
- TAG_DONE and TAG_END
- Define the end of a taglist. Every taglist must be terminated with
one of them. A following ti_Data must be ignored by the called
function, so it doesn't have to exist in memory.
- TAG_IGNORE
- means that the contents of ti_Data is to be ignored. This tag is
especially useful for conditional inclusion of tags.
- TAG_MORE
- By using this tag, you can link taglists together. ti_Data points to
another taglist. Processing of the current taglist will be stopped and
instead the new one will be processed. This tag also terminates the
current taglist.
- TAG_SKIP
- forces the parser to skip the next ti_Data tags. They will not be
processed.
You may always provide NULL instead of a pointer to a taglist. All
functions must be able to handle NULL pointers. They are equal to taglists
with TAG_DONE as first tag.
A function that requires a taglist is:
#include <proto/intuition.h>
struct Window *OpenWindowTagList
(
struct NewWindow *newwin, struct TagList *taglist
);
This function will be discussed in detail in the
.. FIXME:: chapter about windows.
For now, you just need to know that this function opens a new window. Set
the argument newwin to NULL. The only tags looked at for now are:
Tag |
Description |
Type |
WA_Width |
Width of window in pixels |
UWORD |
WA_Height |
Height of window in pixels |
UWORD |
WA_Title |
Window title |
STRPTR |
Another function needed for your small example is:
#include <proto/intuition.h>
void CloseWindow( struct Window *window );
This function is used to close an opened window.
Now, have a look at another small hello-world-program. This opens a
window, which says "Hello World!" in the title-bar, for two seconds:
#include <proto/exec.h>
#include <exec/libraries.h>
#include <proto/dos.h>
#include <proto/intuition.h>
#include <intuition/intuition.h>
struct DosLibrary *DOSBase;
struct IntuitionBase *IntuitionBase;
int main(int argc, char *argv[])
{
int error = RETURN_OK;
/* You need this for Delay() later on. */
DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 36);
if (DOSBase)
{
IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library", 36);
if (IntuitionBase)
{
struct Window *win;
/* Set up your tags. */
struct TagItem tags[] =
{
{ WA_Width, 100 },
{ WA_Height, 50 },
{ WA_Title, (IPTR)"Hello World!" },
{ TAG_DONE, 0UL }
};
win = OpenWindowTagList(NULL, tags);
if (win)
{
/* Now wait for two seconds, to show the nice
window.
*/
Delay(100);
/* Close your window again. */
CloseWindow(win);
}
CloseLibrary((struct Library *)IntuitionBase);
}
else
error = RETURN_FAIL;
CloseLibrary((struct Library *)DOSBase);
} else
error = RETURN_FAIL;
return error;
}
Of course, this method of setting up the taglist is quite complicated. So for
most functions that use taglists short-cuts are available. The link-library
amiga.lib provides these short-cuts for all internal AROS functions. These
varargs versions can be used like this:
#include <proto/alib.h>
Function( arg1, ..., argn, TAG1, data1, ..., TAG_DONE );
The example above would look like this, using the varargs version of
OpenWindowTagList(), called OpenWindowTags():
[...]
if( IntuitionBase )
{
struct Window *win;
win = OpenWindowTags
(
NULL, WA_Width, 100, WA_Height, 20,
WA_Title, "Hello World!", TAG_DONE
);
)
if( win )
{
[...]
Much easier, isn't it?
"Hello, World!" is not a Museum of Programmer's Talent, so you might wonder
if there is more to AROS than that. Why, yes, of course there is! But this
guide is neither a Programmer's Guide nor a Programmer's Reference Guide.
Such guides might be written in the future, but for now, the best AROS
Programmer's Guides you can find are the books that have been written for
the Amiga, and the best reference for AROS are the AROS Autodocs
(AROS autodocs are descriptions of AROS library functions that are created by
parsing the AROS sources).
The Autodocs are mainly useful to advanced Amiga programmers, though: they
only provide a very short explanation for each function. If you have to
learn AROS programming from the start, you really should try to find that
old Amiga book, or buy the Amiga Developer CD-ROM.
|
|