Copyright (c) Hyperion Entertainment and contributors.

Libraries and Devices

From AmigaOS Documentation Wiki
Revision as of 22:36, 22 April 2013 by Steven Solie (talk | contribs) (→‎Migrating the Source Code)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

Overview

This guide is most specifically directed at programmers wanting to convert library code to AmigaOS 4.x. A (hopefully) comprehensive explanation of the process of converting a library will be presented.

For an introduction to some basic concepts see Exec Libraries.

The Self Pointer and the APICALL Tag

If you look at the standard vector declarations above, you will notice that their first parameter is always a "struct Interface *Self", but we do not specify that Self parameter in the calls. Instead, all method function pointers have a tag APICALL added. That tag identifies them as interface function pointers.

Through this special function attribute, the gcc compiler for AmigaOS 4 will recognize a construct of the form

IExec->CloseLibrary(libBase);

and implicitly (not by preprocessor magic, but through the code generator) transform this into

IExec->CloseLibrary(IExec, libBase);

This saves the user the hassle of writing this, but of course this only works on the AmigaOS 4 compiler. Obviously, any compiler adapted for AmigaOS 4 must implement this mechanism.

Migrating a Library from AmigaOS 3.x

Introduction

Migrating a library from AmigaOS 3.x may or may not be trivial. It all depends on the internals of the library. For a library function itself, the most obvious change is that the library base will not be passed along, but the interface pointer. The interface pointer, however, contains the library base pointer, so if that is required it can still be gotten from the interface. Also, the interface itself may carry along private or instance data.

Interface Definition Files

The first thing to do when starting a new library or migrating an existing one is to write the IDL file . The IDL file is what used to be the FD file in the old system, with a few substantial differences. First of all, the file format is now XML, using a special "library" DTD. The file should have the same base name as the library with the suffix ".xml" added, e.g. exec.xml, expansion.xml and so on. Each IDL file should start like this:

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE library SYSTEM "library.dtd">
<library name="expansion" basename="ExpansionBase">
...
</library>

This declares an XML file and includes the dtd. The third line (<library...>) defines the document's contents, in this case a single library. The "name" attribute specifies the name of the library. A ".library" is implicitly added to this name. The second attribute (basename) specifies the global name of the library base and the name of the base structure (i.e. struct ExpansionBase * in this case). Note that the library base pointer need not be global at all for a function to be called - only the interface pointer is used for that.

The <library> element may contain any number of <include> elements. <include> elements specify include files that need to be present to define all types that are used within the library. The files <exec/interfaces.h> and <exec/types.h> are implicitly added. The name of the include file must be given as contents to the <include> element, and only one such file is allowed per element:

<include>expansion/pci.h</include>

After the <include> element, you should start listing all library interfaces. There can be any number of interfaces. Interfaces are specified using the <interface> element:

<interface name="pci" version="1" flags="protected" struct="PCIIFace" prefix="_pci_">

The following attributes exist:

  • name (required) The name of the interface. This name will be used by an application to request the interface.
  • version (default: 1) The version of the interface. Interface versions start at 1. Multiple interfaces with the same name may be defined if and only if they have different version numbers. Once an interface is published, it must not be changed unless the interface version number is incremented.
  • flags (default: none) The flags to set for this interface. This may be "protected", "private", or "none", with obvious mappings to the interface flags.
  • struct (required) The name of the structure that holds the interface, automatically prefixed by "struct". The example above specifies the struct name to be "PCIIFace", that means that any interface acquired from expansion library by the name of "pci" will have the layout of "struct PCIIFace".
  • prefix (default: "_impl_") The common prefix for all interface functions. It is highly recommended that all interface functions get a command prefix, since some function names might be ambiguous, like Obtain, Release, Expunge and Clone. This prefix is used for the automatic library creation process.

The <interface> element may contain any number of <method> elements. <method> elements describe the methods that an interface provides. Contrary to FD files the method description also contains the return types and parameter types. The following lists the four standard interface methods, which must be first in every interface:

<method name="Obtain" result="uint32"></method>
<method name="Release" result="uint32"></method>
<method name="Expunge" result="void" status="unimplemented"></method>
<method name="Clone" result="struct Interface *" status="unimplemented"></method>

These methods don't have any other parameter but the Self pointer, which is implicit and need not be named. The "name" attribute defines the name of that method, the name under which the method is called when used. Together with the prefix specified in the interface, it forms the name of the function (see the testlib example). The "result" attribute describes what the method returns, which may be "void" if the function does not return anything, or a type like "uint32", or even a complex type like "struct Interface *". Finally, the "status" attribute used by some methods (which defaults to "implemented") specifies if the method actually exists or not. If it doesn't exist, set this to "unimplemented". This will "reserve" a slot in the interface's jump table, but fill it with NULL. Obviously, calling such a function results in Violent Program Termination.

After these initial methods, you need to add a <method> element for each method in the interface. Methods without parameters can be written as above, or in a single tag, as per XML specifications:

<method name="Obtain" result="uint32"/>

If a method has arguments (the most common case), the arguments must be listed as <arg> elements:

<method name="AllocResource" result="struct PCIResourceRange *">
 <arg name="ResType" type="uint32"/>
 <arg name="NumBytes" type="uint32"/>
</method>
 
<method name="FreeResource" result="void">
 <arg name="Resource" type="struct PCIResourceRange *"/>
</method>

These two declarations would result in the following two method prototypes:

struct PCIResourceRange *AllocResource(struct PCIIFace *Self,
 uint32 ResType, uint32 NumBytes);
void FreeResource(struct PCIIFace *Self, struct PCIResource *Resource);

<arg> elements must not have any content, and hence should always be written in the one-tag way.

Remember to close the <interface> element by a trailing </interface>. As was said before, any number of <interface> elements may follow.

Once you have written the interface description, the "idltool" program must be used to generate the required header files. Running idltool with the "--all" option generates all available targets.

Migrating the Source Code

As has been said before, library calls in AmigaOS 4 are arranged into interfaces. There are four interface methods that every interface has. Usually you can use generic versions of these. The idltool will output a complete skeleton for you which makes it much easier to get going.

All functions need to be modified in such a way that the LibBase parameter (if present) is removed, and the new Self parameter needs to be added. For example, consider this function (AmigaOS 3.9 style) from a fictitious library "fubar.library":

ULONG FubarFunc(ULONG param1, ULONG param2, struct FubarBase *libBase)
{
 libBase->FubarCalled++;
 ULONG x = param1 * libBase->FubarFactor / param2;
 return x;
}

The modifications required mostly depend on a few design choices we have to make. The "FubarCalled" variable is part of the library base in the example above but that need not be the case for AmigaOS 4 - we can move this field either to the interface private data or leave it in the library base.

We assume that our interface for the library is called "FubarMainIFace". You may chose any name but it is recommended (though not enforced) that they start with the library name and end with "IFace".

uint32 FubarFunc(struct FubarMainIFace *Self, uint32 param1, uint32 param2)
{

So far, things that have changed is the types are modernized and the removal of the libBase parameter and the addition of the interface pointer. What follows depends on the choice of placement of the FubarCalled and FoobarFactor fields. Let's first assume that we left them in the library base, so we need a pointer to the library. That pointer is always stored in the interface:

struct FubarBase *libBase = (struct FubarBase *)Self->Data.LibBase;
 libBase->FubarCalled++;
 uint32 x = param1 * libBase->FubarFactor / param2;
 return x;
}

Now, if we decided to move these fields into the interface itself, things look a bit different. Generally if you think that data is specific to an interface, and hence specific to a task context, put the fields into the interface. Otherwise leave them in the base. Migrated libraries will probably want to keep them in the base structure, but just in case this is the way to access the data from the interface:

struct FubarMainData *ifData =
 (struct FubarMainData *)((uint32)Self - Self->Data.NegativeSize);
 ifData->FubarCalled++;
 uint32 x = param1 * ifData->FubarFactor / param2;
 return x;
}

This example assumes that your data layout for the interface looks like the struct FubarMainData structure.

Handling 68K

Overview

There are two possible scenarios from where a library might be called: From native code, or from 68k code. The native code calls will invariably go via an interface; the 68k calls will always be base-relative.

This means that for a library to be native-callable, at least one interface must exist. Since this is only some sort of switchboard, we will call these "proxy" interfaces: the interface just redirects calls through the emulator.

This also means that if a native library needs to be callable from 68k, it must have a jump table. Of course, for a 68k program to call a native library, this program must know the library; it is highly unlikely that a 68k program will call the PCI interface of expansion.library directly (i.e. not through an emulation layer) because it will most likely not know that expansion.library offers anything but the "normal" functions.

Native Interfaces to 68K Libraries

A 68k library that needs to be native-callable needs an interface. That interface may be created through two different mechanisms. For a disk-based library, the loader will scan the directory for a file that contains this interface, to be loaded with the library. This file may or may not contain additional code that replaces some of the library vectors.

For resident libraries, this looks a bit different. Resident libraries must have the interface already created in memory. Since these libraries are most likely part of the system itself, these libraries should be treated very much like a native library.

Assume for example that graphics.library would remain 68k. In order to call it from native code, it needs to have an interface (preferably "main"). This is achieved by inserting a ROMTAG with a priority of one lower than the actual library, which constructs the interface and stores it as an interface of Exec, under the name "graphics.interface". When a program attempts to obtain an interface from a non-AmigaOS 4 library, Exec's GetInterface function constructs this name from the library name and returns the pointer to this "proxy" interface.

Using idltool

The utility program idltool can be used to generate libraries as well, in a very convenient manner. All you have to do is provide the XML interface description file and invoke idltool with the command line

idltool -k <interfacefile>.xml

This will create a directory <interfacefile>_files in your current directory that contains a complete skeleton library with a Makefile and a README with additional instructions. Simply typing "make" should build the library. For more information, refer to the documentation that comes with idltool.