Copyright (c) Hyperion Entertainment and contributors.
Programming AmigaOS 4: Exec - The Kernel
This article was adapted from Amiga Future magazine's series on developing for AmigaOS....
We've installed the SDK from the previous installment, so hopefully you've got the appropriate development environment ready, we can take off now - you can take that literally since without Exec nothing would be running.
But before we start coding we'll need some basic background knowledge.
Contents
Interface replaces jump table
While the 68k shared libraries were only required to be opened by OpenLibrary(), AmigaOS 4 puts another interface into play. Libraries utilising multiple interfaces include Exec (MMU and debug), expansion (pci and _pcidev_), and application (application and prefsobjects). Most have only a "main" interface with all the functions that previously were called from the jump table and replace these. In theory, this makes it possible to introduce new interface versions with more parameters to present functions or different return values. The sample code "OpenInterface.c" shows you how to open a shared library and its interface. Exec and DOS are already opened by the launch code and all their functions are immediately available. In practice, you can make your life even easier by telling the linker to open all the other required libraries via "-lauto". The only drawback is that you can't require a specific version. This you can accomplish by letting your main function check whether the required version is available. That may be necessary if you use very new functions and want to make sure that the user has the latest system updates installed already. Otherwise the program will crash when using the new functions.
/* OpenInterface.c * gcc -o OpenInterface OpenInterface.c */ #include <proto/exec.h> #include <proto/dos.h> struct Library *IntuitionBase; struct IntuitionIFace *IIntuition; int main(int argc, char *argv[]) { if((IntuitionBase = IExec->OpenLibrary("intuition.library", 50))) { if((IIntuition = (struct IntuitionIFace *) IExec->GetInterface(IntuitionBase, "main", 1, NULL))) { /* Library and Interface are open to use */ IDOS->Printf("Intuition interface open.\n"); IExec->DropInterface((struct Interface *)IIntuition); } IExec->CloseLibrary((struct Library *)IntuitionBase); } return 0; }
Next to the familiar basic functions Obtain, Release, and Expunge, a fourth, previously unused Clone function has been defined. It's intended for holding several instances of an interface in memory (e.g. if you need it to store program specific data).
The interface changes the syntax for function calls. OpenLibrary() becomes IExec->OpenLibrary(). This has two advantages: the very same function name can be used in different libraries and you can see at once which library is used. Accordingly, you'll have to include the <proto/exec.h> headers for the compiler to know Exec's functions.
As the jump tables are dropped SetFunction() has become meaningless. With it you could patch into a call and check or adjust values or simply log them. For AmigaOS 4 libraries this is replaced by SetMethod(). Both shouldn't be used in normal applications.
Two worlds
If you want to compile old sources for AmigaOS 4 you may have to do some modifications first. In any case you need to include the proto headers while previously sometimes clibs were used. Some structures were changed slightly and need to be replaced by the new ones. Most of the required changes will be to replace the function calls. There's also an alternative: by setting the __USE_INLINE___ define you can tell the compiler (or rather the preprocessor) to take care of adapting the interface syntax. This may even be necessary when you'd like to provide your code for AmigaOS 3.x and 4.x simultaneously. You'll need to compile twice with the corresponding compilers but you can use a single source code to create both versions. This is probably preferred, compared to having two separate sources that you'd have to keep current in parallel. So, even if you want to introduce new features that rely on AmigaOS 4 you can restrict the code block by "#ifdef __amigaos4__". That block will then only be used by the current gcc. The sample "CheckForOS4.c" shows you how to create a PPC and a 68k program from a single code.
/* CheckForOS4.c * gcc -o CheckForOS4 CheckForOS4.c -D __USE_INLINE__ * 68k compatible for compiling: * mcppc CheckForOS4.c -o CheckForOS4_68k */ #include <exec/execbase.h> #include <stdio.h> extern struct ExecBase *SysBase; int main() { int ret; if(SysBase->LibNode.lib_Version == 50) { printf("AmigaOS 4 (PreRelease) is running !\n"); ret = 0; } else if(SysBase->LibNode.lib_Version >= 51 && SysBase->LibNode.lib_Version <= 52) { printf("AmigaOS 4.0 is running !\n"); ret = 0; } else if(SysBase->LibNode.lib_Version >= 53) { printf("AmigaOS 4.1 or better is running !\n"); ret = 0; } else { printf("AmigaOS before AmigaOS 4 is running\n"); ret = 5; } return ret; }
This wiki contains three documents with important information about programming for AmigaOS 4. Migration Guide specifically addresses changes in AmigaOS 4 (interfaces, compatibility). Everything on memory management and memory type attributes can be found in Exec Memory Allocation and Libraries and Devices is about programming libraries and devices under AmigaOS 4.
AmigaOS 4 programs for PPC processors are created in ELF format. These programs can't be run on older AmigaOS versions (up to Workbench 3.9) and also not on the (Win)UAE emulator. Conversely, old programs in 68k code are run through an emulation mode in AmigaOS 4. It invisibly translates the code to PPC and executes it. This permits a high execution speed in contrast to simulating a 68xxx CPU and interpreting the code. The custom chips of the classic machines are not emulated, programs accessing these directly won't work.
You can find the c-lib functions from stdio.h/stdlib.h/strings.h etc. in the "clib" link library. Starting with AmigaOS 4, these are replaced by "newlib" which is largely identical in function. Without specifying options, "newlib" is used for linking. Alternatively, you can choose the required library by using the link options -mcrt=newlib or -mcrt=clib2. In theory, you could also do without these libraries and use file functions like fopen() or printf() from the dos.library and use string functions from utility.library.
68k processor functions dropped
The special functions directly controlling 68k CPU functions have been completely dropped from exec.library. These include AllocTrap() and FreeTrap() for reserving or releasing processor trap vectors. Additionally, GetCC() for detecting condition codes, SetSR() for setting the CPU status registers and SuperState() for switching to supervisor mode are not possible any more. For normal applications these functions are irrelevant and shouldn't pose any problem when updating your old sources.
Hardware and processor
If you need to know on which hardware platform your program is running there are various ways to inquire. On the one hand there are specific libraries that only exist on some hardware. For instance, "a1ide.device" only exists on Eyetech's AmigaOne machines. The UAE emulator can be recognized by its "uaegfx.card" library. A much safer and easier way is to use expansion.library. The function IExpansion->GetMachineInfo() provides you with various information about the hardware as you can see in the "WhichHardware.c" sample code. The code can also be compiled for 68k, so you can also test it on a 68k powered machine.
/* WhichHardware.c * * gcc -o WhichHardware WhichHardware.c -D __USE_INLINE__ -D __USE_BASETYPE__ * 68k compatible for compiling: * mcppc WhichHardware.c -o WhichHardware_68k */ #include <exec/execbase.h> #include <utility/tagitem.h> #include <proto/exec.h> #ifdef __amigaos4__ #include <proto/expansion.h> #endif #include <proto/dos.h> #include <string.h> extern struct ExecBase *SysBase; #ifdef __amigaos4__ struct ExpansionBase *ExpansionBase; struct ExpansionIFace *IExpansion; #endif int main() { BOOL foundamithlon = FALSE; struct Node *node; STRPTR machinestr = NULL; Disable(); for(node = SysBase->ResourceList.lh_Head; node->ln_Succ != NULL; node = node->ln_Succ) { if(strcmp(node->ln_Name,"amithlon.resource") == 0) { foundamithlon = TRUE; break; } } Enable(); struct Library *uaegfxBase; if((uaegfxBase = OpenLibrary("uaegfx.card",0))) CloseLibrary(uaegfxBase); if(SysBase->LibNode.lib_Version >= 50) { #ifdef __amigaos4__ if((ExpansionBase = (struct ExpansionBase *) OpenLibrary("expansion.library", 50))) { if((IExpansion = (struct ExpansionIFace *) GetInterface((struct Library *) ExpansionBase, "main", 1, NULL))) { GetMachineInfoTags(GMIT_MachineString, &machinestr, TAG_END); DropInterface((struct Interface *) IExpansion); } CloseLibrary((struct Library *) ExpansionBase); } #else machinestr = ""; #endif } else { } if(machinestr) { Printf("AmigaOS 4 enviroment: %s\n",machinestr); } else if(foundamithlon) { Printf("Amithlon enviroment.\n"); } else if(uaegfxBase) { Printf("UAE enviroment.\n"); } else { Printf("Old classic Amiga.\n"); } return 0; }
While we're talking about CPUs: you can also differentiate the various processors that the different versions of AmigaOS run on. For the earlier 68k CPUs a single bit from "ExecBase->AttnFlags" was enough to distinguish a 68000 from a 68030 or a 68060. Since bits tend to run out eventually and it's only a very rough method there's also a function to get the processor's name string. You can call IExec->GetCPUInfoTags() with GCIT_xxx tags (from exec/exectags.h). As the function was introduced with AmigaOS 4 this program can never run on older versions. See the complete example "DeterminateProcessor.c" for details.
/* DetermineProcessor.c * * gcc -o DetermineProcessor DetermineProcessor.c */ #include <exec/exectags.h> #include <proto/exec.h> #include <proto/dos.h> int main() { uint32 family, model, l1cache, l2cache, l3cache, unit; uint64 speed; IExec->GetCPUInfoTags(GCIT_Family,&family, GCIT_Model,&model, GCIT_ProcessorSpeed,&speed, GCIT_VectorUnit,&unit, GCIT_L1CacheSize,&l1cache, GCIT_L2CacheSize,&l2cache, GCIT_L3CacheSize,&l3cache, TAG_DONE); IDOS->PutStr("CPU Family: "); switch(family) { case CPUFAMILY_60X: IDOS->PutStr("PowerPC 60x\n"); break; case CPUFAMILY_7X0: IDOS->PutStr("G3 PowerPC 7x0\n"); break; case CPUFAMILY_74XX: IDOS->PutStr("G4 PowerPC 74xx\n"); break; case CPUFAMILY_4XX: IDOS->PutStr("PowerPC 4xx\n"); break; case CPUFAMILY_PA6T: IDOS->PutStr("Semi PowerPC\n"); break; case CPUFAMILY_E300: IDOS->PutStr("E300 PowerPC\n"); break; case CPUFAMILY_E5500: IDOS->PutStr("E5500 PowerPC\n"); break; default: IDOS->PutStr("unknown\n"); break; } IDOS->PutStr("CPU Model: "); switch(model) { case CPUTYPE_PPC603E: IDOS->PutStr("PPC 603 E\n"); break; case CPUTYPE_PPC604E: IDOS->PutStr("PPC 604 E\n"); break; case CPUTYPE_PPC750CXE: IDOS->PutStr("PPC 750 CXE\n"); break; case CPUTYPE_PPC750FX: IDOS->PutStr("PPC 750 FX\n"); break; case CPUTYPE_PPC750GX: IDOS->PutStr("PPC GX\n"); break; case CPUTYPE_PPC7410: IDOS->PutStr("PPC 7410\n"); break; case CPUTYPE_PPC74XX_VGER: IDOS->PutStr("PPC 74xx (Vger type)\n"); break; case CPUTYPE_PPC74XX_APOLLO: IDOS->PutStr("PPC 74xx (Apollo type)\n"); break; case CPUTYPE_PPC405LP: IDOS->PutStr("PPC 405 LP\n"); break; case CPUTYPE_PPC405EP: IDOS->PutStr("PPC 405 EP\n"); break; case CPUTYPE_PPC405GP: IDOS->PutStr("PPC 405 GP\n"); break; case CPUTYPE_PPC405GPR: IDOS->PutStr("PPC 405 GPR\n"); break; case CPUTYPE_PPC440EP: IDOS->PutStr("PPC 440 EP\n"); break; case CPUTYPE_PPC440GP: IDOS->PutStr("PPC 440 GP\n"); break; case CPUTYPE_PPC440GX: IDOS->PutStr("PPC 440 GX\n"); break; case CPUTYPE_PPC440SX: IDOS->PutStr("PPC 440 SX\n"); break; case CPUTYPE_PPC440SP: IDOS->PutStr("PPC 440 CP\n"); break; case CPUTYPE_PA6T_1682M: IDOS->PutStr("PPC PA6T\n"); break; case CPUTYPE_PPC460EX: IDOS->PutStr("PPC 460 EX\n"); break; case CPUTYPE_PPC5121E: IDOS->PutStr("PPC 5121 E\n"); break; case CPUTYPE_P50XX: IDOS->PutStr("PPC P50XX\n"); break; default: IDOS->PutStr("unknown\n"); } IDOS->PutStr("CPU Vector Unit: "); switch(unit) { case VECTORTYPE_ALTIVEC: IDOS->PutStr("AltiVec\n"); break; default: IDOS->PutStr("none\n"); } IDOS->PutStr("CPU Speed: "); IDOS->Printf("%lld Mhz\n",speed); if(l1cache) IDOS->Printf("L1CacheSize: %ld kBytes\n",l1cache/1024); if(l2cache) IDOS->Printf("L2CacheSize: %ld kBytes\n",l2cache/1024); if(l3cache) IDOS->Printf("L3CacheSize: %ld kBytes\n",l3cache/1024); UBYTE *smodel, *sversion; IExec->GetCPUInfoTags( GCIT_ModelString, &smodel, GCIT_VersionString, &sversion, TAG_DONE); IDOS->Printf("CPU: %s V%s\n", smodel, sversion); return 0; }
Memories galore
Generally, the newer machines have lots of memory, so you're much less likely to run out of RAM when running several programs simultaneously, or working with large databases. Nevertheless, AmigaOS provides virtual memory for which it uses "external" memory on the hard-disk, completely transparent to an application. If a program tries to allocate memory that the system can't provide from physical RAM, the OS tries to write swappable but currently unused memory to disk and thus to free up sufficient RAM. Generally, AmigaOS can only manage up to 2 GBytes of memory (the physical limit of a 32-bit operating system). So, if you already have 2 GB of physical RAM there's no point in setting up virtual memory on your disk. The OS won't be able to make any use of it (at least not in its current version). The earlier distinction between chip and fast RAM obviously doesn't exist any more, even though both flags still exist for compatibility reasons. The graphics card's memory is only used by the graphics driver internally, it has no relation to chip mem. The old method to determine the remaining available memory by calling IExec->AvailMem(MEMF_TOTAL) is meaningless. Virtual memory alone can provide more memory than the function can handle.
The functions IExec->AllocMem() and IExec->AllocVec() are replaced by Exec->AllocVecTags() which is more flexible and expandable by using tag lists and provides greater functionality. You select the required memory type through AVT_Type (exec/exectags.h). You need to distinguish between MEMF_PRIVATE (the default) - which will only be visible to your own program and not to other processes, MEMF_SHARED for shared memory and MEMF_EXECUTABLE for running program code in. The latter is protected against changes and causes a GrimReaper hit when written to. Self-modifying code that was occasionally used earlier can't be used any more. The user can be sure that a program can't be damaged from another "rogue" process so easily any more. While AmigaOS 4 comes with rudimentary protection mechanisms, the open concept and shared memory areas can't completely prevent a faulty program from writing into other processes' memory but as far as possible this is avoided.
The allocated memory area can be initialised with an arbitrary value (AVT_ClearWithValue) or prevented from being swapped out (AVT_Lock). Otherwise, it's possible for the OS to swap the memory out to disk when running low on memory. The program itself doesn't notice anything of this since all memory is restored automatically when accessed. The user may notice a short delay while the process is halted and the system swaps in the accessed memory. Additionally, AVT_NoExpunge should be noted. When set to TRUE no cleanup runs will be used to remove unused libraries from RAM on low memory conditions.
There is also the brand new concept of "named memory" in AmigaOS 4 from V51 on. IExec->AllocNamedMemory() takes the type and size of the desired memory block and additionally a name. If there's already a memory area with the same name it fails and returns NULL. When successful, another process can look for this memory with IExec->FindNamedMemory() and access it. This memory must be deallocated by IExec->FreeNamedMemory() and on no account by FreeVec() or FreeMem()! The name consists of the namespace and the name itself; the namespace may be empty or "resident". See the "MemoryExamples.c" sample for the various functions for memory management.
/* MemoryExamples.c * * gcc -o MemoryExamples MemoryExamples.c */ #include <proto/exec.h> #include <proto/dos.h> CONST_STRPTR getErrStr(uint32 err) { static STRPTR errstr; switch(err) { case ANMERROR_NOERROR: errstr = "no error"; break; case ANMERROR_NOMEMORY: errstr = "no free memory"; break; case ANMERROR_DUPLICATENAME: errstr = "duplicate name"; break; case ANMERROR_PARAMETER: errstr = "missing parameter"; break; default: errstr = "unknown error"; break; } return errstr; } int main() { uint32 total, error, newsize; uint8 *adr, *adr2; total = IExec->AvailMem(MEMF_TOTAL); IDOS->Printf("Memory total: %ld MByte\n",total / 1024 / 1024); IDOS->PutStr("\n"); if((adr = (UBYTE*) IExec->AllocVecTags(262144, AVT_Type,MEMF_SHARED, AVT_ClearWithValue,0, TAG_END))) { IDOS->Printf("256 kByte memory allocated.\n"); IExec->FreeVec(adr); } else IDOS->Printf("256 kByte memory allocation failed.\n"); IDOS->PutStr("\n"); if((adr = (UBYTE*) IExec->AllocNamedMemoryTags(1024,"myapp","mem_space_name", ANMT_Error,&error, TAG_END))) { IDOS->Printf("Named memory allocation at $%lx\n",adr); if((adr2 = (UBYTE*) IExec->AllocNamedMemoryTags(1024,"myapp","mem_space_name", ANMT_Error,&error, TAG_END))) { IDOS->Printf("System error !\n"); } else IDOS->Printf("Named memory failed: %s\n",getErrStr(error)); if((adr2 = (UBYTE*) IExec->FindNamedMemory("myapp", "mem_space_name"))) { IDOS->Printf("Find named memory at $%lx\n",adr2); } IExec->FreeNamedMemory("myapp", "mem_space_name"); } else IDOS->Printf("Allocation named memory failed: %s\n",getErrStr(error)); IDOS->PutStr("\n"); return 0; }
Protection mechanisms
The new mutex functions in V52 (AmigaOS 4.0) provide an alternative mechanism to semaphores to prevent access collisions for shared data structures or to synchronize access. A mutex object is created by IExec->AllocSysObject(ASOMUTEX_Recursive) and destroyed by IExec->FreeSysObject(). IExec->MutexLock() grants access to it. As required, the process is blocked until the lock is available. Alternatively, you can try to access the lock with IExec->MutexAttempt() which returns an error when not free without blocking the process. When done you need to release the lock by MutexRelease().
Debugging
For debugging your programs the common method is to output special messages with kprintf() (requires linking the program with -ldebug) or IExec->DebugPrintF(). Both are sent to the serial interface and can be read from another machine with a terminal program. Sashimi allows you to capture the output and redirect it to another local window. If the whole system crashes due to the bugs you won't be seeing the output however. You may be more successful with a second computer so you can review the latest output. A simple use of this is shown in "PrintDebug.c".
/* PrintDebug.c * * gcc -o PrintDebug PrintDebug.c */ #include <proto/exec.h> int main() { IExec->DebugPrintF("\n >>> Test Debug - Output ! <<<\n\n"); return 0; }
Replacements
The link library amiga.lib contains many convenient functions. Many of them were moved to exec.library, e.g. all functions for managing lists. New functions like IExec->AllocSysObject() were also added to create structures. Only by this method can you make sure that sufficient memory for a structure is allocated, especially if it contains additional management data that may not be public. Other methods/functions are obsolete and shouldn't be used any more, the new functions are more convenient and more flexible anyway (e.g. CreatePool() or MakeLibrary().
New data types
AmigaOS 4 also introduces new names for data types in in exec/types.h. Ultimately, it's a matter of taste which notation you use. The new names are already in use in the current SDK, so you should know which types they replace. There's a brand new type for 64-bit integers that had no official name previously. This type can be used in normal code just like a 32-bit integer.
See Fundamental Types for a table listing the new types available.
Authors
Written by Michael Christoph and Nils Petersen
Copyright (c) 2013 Michael Christoph