Copyright (c) Hyperion Entertainment and contributors.
Difference between revisions of "AmiWest Lesson 2"
Steven Solie (talk | contribs) |
Steven Solie (talk | contribs) |
||
(28 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | = AmiWest Lesson |
+ | = AmiWest Lesson 2: Fundamentals = |
== Basic Types == |
== Basic Types == |
||
Line 57: | Line 57: | ||
| ZERO || none || (BPTR)0 || (BPTR)0 |
| ZERO || none || (BPTR)0 || (BPTR)0 |
||
|} |
|} |
||
+ | |||
+ | == Static versus Dynamic Linking == |
||
+ | |||
+ | The different between static and dynamic linking can best be explained with an example. |
||
+ | |||
+ | <syntaxhighlight> |
||
+ | #include <stdio.h> |
||
+ | |||
+ | int main() |
||
+ | { |
||
+ | printf("Hello, world\n"); |
||
+ | return 0; |
||
+ | } |
||
+ | </syntaxhighlight> |
||
+ | |||
+ | Here is "hello" created using static linking: |
||
+ | |||
+ | gcc -mcrt=clib2 -N -o hello hello.c -Wl,--cref,-M,-Map=hello.map |
||
+ | strip hello |
||
+ | |||
+ | The "hello" executable is roughly 34964 bytes in size. The "hello.map" file contains the linker map which shows you exactly what pieces of code have been pulled in from where to create that executable. |
||
+ | |||
+ | Here is "hello" created using dynamic linking: |
||
+ | |||
+ | gcc -mcrt=newlib -N -o hello hello.c -Wl,--cref,-M,-Map=hello.map |
||
+ | strip hello |
||
+ | |||
+ | The "hello" executable is now 5488 bytes in size and the "hello.map" file is substantially simpler as well. |
||
+ | |||
+ | {{Note|The -N switch is used to work around a feature in the current GCC toolset. The problem has been fixed but a new compiler is not yet generally available. See [[The_Hacking_Way:_Part_1_-_First_Steps|Myth #2: AmigaOS binaries are fat]] for more details.}} |
||
+ | |||
+ | It is important to remember that each program is still essentially the same size. In fact, the dynamically linked program may even be larger. The reason is that the same amount of code is still used. The difference is that the dynamically linked executable is sharing code with other executables. The statically linked executable is not sharing code and thus occupies more disk space. |
||
+ | |||
+ | A statically linked program is a self-contained unit which generally has no extraneous external dependencies. Any dependency problems will show up when linking the executable. |
||
+ | |||
+ | A dynamically linked program requires external libraries in order to function. If any of those libraries are missing or the wrong version, your program will fail at runtime and not when you compile it. |
||
+ | |||
+ | For programmers, it is very important to know where your libraries are coming from and what their limitations are. Just assuming you can mix and match static and dynamic libraries at will is foolish. Always try to understand what each library does and what caveats apply to its usage. |
||
+ | |||
+ | == Libraries and Interfaces == |
||
+ | |||
+ | AmigaOS provides hundreds of functions which are split up into different shared libraries. Each shared library is further split up into different interfaces. Much more detailed information about Libraries and Interfaces can be found in [[Exec_Libraries|Exec Libraries]] |
||
+ | |||
+ | === Opening and Closing === |
||
+ | |||
+ | The following code can be used to open interfaces: |
||
+ | <syntaxhighlight> |
||
+ | struct Interface* try_open_iface_name(CONST_STRPTR libname, uint32 libver, CONST_STRPTR ifacename) |
||
+ | { |
||
+ | struct Library* base = IExec->OpenLibrary(libname, libver); |
||
+ | if ( base != 0 ) { |
||
+ | struct Interface* iface = IExec->GetInterface(base, ifacename, 1, 0); |
||
+ | |||
+ | if ( iface != 0 ) { |
||
+ | return iface; |
||
+ | } |
||
+ | |||
+ | IExec->CloseLibrary(base); |
||
+ | } |
||
+ | |||
+ | return 0; |
||
+ | } |
||
+ | |||
+ | struct Interface* open_iface_name(CONST_STRPTR libname, uint32 libver, CONST_STRPTR ifacename) |
||
+ | { |
||
+ | struct Interface* iface = try_open_iface_name(libname, libver, ifacename); |
||
+ | |||
+ | if ( iface == 0 ) { |
||
+ | IDOS->Printf("Can't open %s version %lu interface %s\n", libname, libver, ifacename); |
||
+ | } |
||
+ | |||
+ | return iface; |
||
+ | } |
||
+ | |||
+ | struct Interface* open_iface(CONST_STRPTR libname, uint32 libver) |
||
+ | { |
||
+ | return open_iface_name(libname, libver, "main"); |
||
+ | } |
||
+ | </syntaxhighlight> |
||
+ | |||
+ | The following code can be used to close an interface and the corresponding library: |
||
+ | <syntaxhighlight> |
||
+ | void close_iface(struct Interface* iface) |
||
+ | { |
||
+ | if ( iface != 0 ) { |
||
+ | struct Library* lib = iface->Data.LibBase; |
||
+ | IExec->DropInterface(iface); |
||
+ | IExec->CloseLibrary(lib); |
||
+ | } |
||
+ | } |
||
+ | </syntaxhighlight> |
||
+ | |||
+ | === Special Syntax Support === |
||
+ | |||
+ | Exec Interfaces are called using a special syntax supported by the GNU GCC compiler included with the SDK: |
||
+ | |||
+ | IExec->DebugPrintF("Hello, world\n"); |
||
+ | |||
+ | Using a regular compiler this would translate into: |
||
+ | |||
+ | IExec->DebugPrintF(IExec, "Hello, world\n"); |
||
+ | |||
+ | Notice how the first argument to the function call is 'hidden' when using the modified GNU GCC compiler. This becomes especially important when trying to understand error messages from the compiler. For example, the following code is invalid: |
||
+ | <syntaxhighlight> |
||
+ | #include <proto/exec.h> |
||
+ | |||
+ | int main() |
||
+ | { |
||
+ | IExec->DebugPrintF(42); |
||
+ | return 0; |
||
+ | } |
||
+ | </syntaxhighlight> |
||
+ | |||
+ | gcc -o args args.c |
||
+ | args.c: In function 'main': |
||
+ | args.c:5: warning: passing argument 2 of 'IExec->DebugPrintF' makes pointer from integer without a cast |
||
+ | |||
+ | Notice how the compiler is referring to "argument 2" even though there is only one argument in the call to IExec->DebugPrintF(). This is a side effect of the 'hidden' IExec argument referred to earlier. |
||
+ | |||
+ | == Tasks and Processes == |
||
+ | |||
+ | AmigaOS has two basic threads of control: Tasks and Processes. The following class diagram illustrates their relationship to each other: |
||
+ | |||
+ | [[File:ProcessTaskClassDiagram.png|frame|center]] |
||
+ | |||
+ | Tasks are created by Exec (exec.library) while Processes are created by DOS (dos.library). A Process includes more overhead than a Task. This is primarily because a Process can access all the facilities offered by DOS such as file handling and input and output streams. |
||
+ | |||
+ | In general, you should always prefer to use a Process instead of a Task. Although a Process may include more overhead, a Process is also simpler to work with. Tasks are generally relegated to low level activities. I will be focusing on the use of Processes in this lesson. |
||
+ | |||
+ | To create a new Process, DOS provides the following function: |
||
+ | <syntaxhighlight> |
||
+ | struct Process *proc = CreateNewProcTags(uint32 Tag1, ...); |
||
+ | </syntaxhighlight> |
||
+ | |||
+ | There are many tags which can be used to customize the way in which the new Process is created which are fully explained in the dos.doc AutoDoc. |
||
+ | |||
+ | Library base pointers (struct Library*) may be global and shared between Processes. Interface pointers must not be shared between Processes unless this is explicitly allowed. The reason for this is because an Exec Interface may allocate resources which are bound to the context of the caller. That means if your parent Process calls GetInterface() then it allocates resources only for that Process. A child Process must call GetInterface() as well to allocate resources for that child Process. |
||
+ | |||
+ | === Synchronization Primitives === |
||
+ | |||
+ | When two or more Tasks and/or Processes want to share common data with each other then synchronization primitives are likely to be required. |
||
+ | |||
+ | * Signals are the most basic way for two processes to communicate with each other. A signal is a single bit which is either 1 or 0. There are 16 user signals available and many system signals. See [[Exec_Signals|Signals]] for more details. |
||
+ | |||
+ | * Message ports are used to transfer messages between two processes. The message can be sent one way or it can be sent and replied. Message data is always shared and not copied so it is critical that message passing protocols are followed. See [[Exec_Messages_and_Ports|Messages and Ports]] for more details. |
||
+ | |||
+ | * Semaphores provide a rich interface for sharing resources between processes. Sempahores can be shared so that multiple processes can wait on a single semaphore simultaneously. See [[Exec_Semaphores|Semaphores]] for more details. |
||
+ | |||
+ | * Mutexes provide a simple high speed interface for sharing resources between processes. Mutexes are exclusive and cannot be shared. Mutexes can also be recursive or not depending on the application. See [[Exec_Mutexes|Mutexes]] for more details. |
||
+ | |||
+ | {{Note|If you are sharing memory between two entities that memory must be marked as MEMF_SHARED. Failure to comply with this requirement will result in problems when AmigaOS starts to enforce MEMF_PRIVATE memory semantics.}} |
||
+ | |||
+ | == Shared Objects == |
||
+ | |||
+ | Shared objects are an idea directly lifted from the Unix world. The main reason shared objects were added to AmigaOS is to enable much simpler porting of complex applications. For example, Amiga Python was implemented using shared objects which makes it very easy to maintain. It is up to each programmer to determine whether they want to use traditional Amiga shared libraries or shared objects for their particular project. For a discussion of the pros and cons see [[The_Right_Tool_for_the_Job_(Shared_Objects)|The Right Tool for the Job]]. |
Latest revision as of 20:56, 5 December 2017
Contents
AmiWest Lesson 2: Fundamentals
Basic Types
It is important to under at least the basic types when programming. The following table summarizes the basic types used in AmigaOS as compared to standard C and C++ types:
Type | Deprecated Type(s) | C | C++ |
---|---|---|---|
uint64 | none | uint64_t | uint64_t |
int64 | none | int64_t | int64_t |
uint32 | ULONG or LONGBITS or CPTR | uint32_t | uint32_t |
int32 | LONG | int32_t | int32_t |
uint16 | UWORD or WORDBITS or USHORT or UCOUNT or RPTR | uint16_t | uint16_t |
int16 | WORD or SHORT or COUNT | int16_t | int16_t |
uint8 | UBYTE or BYTEBITS | char or unsigned char | unsigned char |
int8 | BYTE | signed char | signed char |
STRPTR | none | char* | char* |
CONST STRPTR | n/a | char* const x | char* const x |
CONST_STRPTR | n/a | const char* | const char* |
CONST CONST_STRPTR | n/a | const char* const | const char* const |
APTR | none | void* | void* |
CONST APTR | none | void* const x | void* const x |
CONST_APTR | none | const void* | const void* |
CONST CONST_APTR | none | const void* const | const void* const |
float32 | FLOAT | float | float |
float64 | DOUBLE | double | double |
BOOL | none | int16 | int16 |
TEXT | none | char | char |
NULL | none | 0L | (void*)0L |
BPTR | none | int32_t | int32_t |
BSTR | none | int32_t | int32_t |
ZERO | none | (BPTR)0 | (BPTR)0 |
Static versus Dynamic Linking
The different between static and dynamic linking can best be explained with an example.
#include <stdio.h> int main() { printf("Hello, world\n"); return 0; }
Here is "hello" created using static linking:
gcc -mcrt=clib2 -N -o hello hello.c -Wl,--cref,-M,-Map=hello.map strip hello
The "hello" executable is roughly 34964 bytes in size. The "hello.map" file contains the linker map which shows you exactly what pieces of code have been pulled in from where to create that executable.
Here is "hello" created using dynamic linking:
gcc -mcrt=newlib -N -o hello hello.c -Wl,--cref,-M,-Map=hello.map strip hello
The "hello" executable is now 5488 bytes in size and the "hello.map" file is substantially simpler as well.
Note |
---|
The -N switch is used to work around a feature in the current GCC toolset. The problem has been fixed but a new compiler is not yet generally available. See Myth #2: AmigaOS binaries are fat for more details. |
It is important to remember that each program is still essentially the same size. In fact, the dynamically linked program may even be larger. The reason is that the same amount of code is still used. The difference is that the dynamically linked executable is sharing code with other executables. The statically linked executable is not sharing code and thus occupies more disk space.
A statically linked program is a self-contained unit which generally has no extraneous external dependencies. Any dependency problems will show up when linking the executable.
A dynamically linked program requires external libraries in order to function. If any of those libraries are missing or the wrong version, your program will fail at runtime and not when you compile it.
For programmers, it is very important to know where your libraries are coming from and what their limitations are. Just assuming you can mix and match static and dynamic libraries at will is foolish. Always try to understand what each library does and what caveats apply to its usage.
Libraries and Interfaces
AmigaOS provides hundreds of functions which are split up into different shared libraries. Each shared library is further split up into different interfaces. Much more detailed information about Libraries and Interfaces can be found in Exec Libraries
Opening and Closing
The following code can be used to open interfaces:
struct Interface* try_open_iface_name(CONST_STRPTR libname, uint32 libver, CONST_STRPTR ifacename) { struct Library* base = IExec->OpenLibrary(libname, libver); if ( base != 0 ) { struct Interface* iface = IExec->GetInterface(base, ifacename, 1, 0); if ( iface != 0 ) { return iface; } IExec->CloseLibrary(base); } return 0; } struct Interface* open_iface_name(CONST_STRPTR libname, uint32 libver, CONST_STRPTR ifacename) { struct Interface* iface = try_open_iface_name(libname, libver, ifacename); if ( iface == 0 ) { IDOS->Printf("Can't open %s version %lu interface %s\n", libname, libver, ifacename); } return iface; } struct Interface* open_iface(CONST_STRPTR libname, uint32 libver) { return open_iface_name(libname, libver, "main"); }
The following code can be used to close an interface and the corresponding library:
void close_iface(struct Interface* iface) { if ( iface != 0 ) { struct Library* lib = iface->Data.LibBase; IExec->DropInterface(iface); IExec->CloseLibrary(lib); } }
Special Syntax Support
Exec Interfaces are called using a special syntax supported by the GNU GCC compiler included with the SDK:
IExec->DebugPrintF("Hello, world\n");
Using a regular compiler this would translate into:
IExec->DebugPrintF(IExec, "Hello, world\n");
Notice how the first argument to the function call is 'hidden' when using the modified GNU GCC compiler. This becomes especially important when trying to understand error messages from the compiler. For example, the following code is invalid:
#include <proto/exec.h> int main() { IExec->DebugPrintF(42); return 0; }
gcc -o args args.c args.c: In function 'main': args.c:5: warning: passing argument 2 of 'IExec->DebugPrintF' makes pointer from integer without a cast
Notice how the compiler is referring to "argument 2" even though there is only one argument in the call to IExec->DebugPrintF(). This is a side effect of the 'hidden' IExec argument referred to earlier.
Tasks and Processes
AmigaOS has two basic threads of control: Tasks and Processes. The following class diagram illustrates their relationship to each other:
Tasks are created by Exec (exec.library) while Processes are created by DOS (dos.library). A Process includes more overhead than a Task. This is primarily because a Process can access all the facilities offered by DOS such as file handling and input and output streams.
In general, you should always prefer to use a Process instead of a Task. Although a Process may include more overhead, a Process is also simpler to work with. Tasks are generally relegated to low level activities. I will be focusing on the use of Processes in this lesson.
To create a new Process, DOS provides the following function:
struct Process *proc = CreateNewProcTags(uint32 Tag1, ...);
There are many tags which can be used to customize the way in which the new Process is created which are fully explained in the dos.doc AutoDoc.
Library base pointers (struct Library*) may be global and shared between Processes. Interface pointers must not be shared between Processes unless this is explicitly allowed. The reason for this is because an Exec Interface may allocate resources which are bound to the context of the caller. That means if your parent Process calls GetInterface() then it allocates resources only for that Process. A child Process must call GetInterface() as well to allocate resources for that child Process.
Synchronization Primitives
When two or more Tasks and/or Processes want to share common data with each other then synchronization primitives are likely to be required.
- Signals are the most basic way for two processes to communicate with each other. A signal is a single bit which is either 1 or 0. There are 16 user signals available and many system signals. See Signals for more details.
- Message ports are used to transfer messages between two processes. The message can be sent one way or it can be sent and replied. Message data is always shared and not copied so it is critical that message passing protocols are followed. See Messages and Ports for more details.
- Semaphores provide a rich interface for sharing resources between processes. Sempahores can be shared so that multiple processes can wait on a single semaphore simultaneously. See Semaphores for more details.
- Mutexes provide a simple high speed interface for sharing resources between processes. Mutexes are exclusive and cannot be shared. Mutexes can also be recursive or not depending on the application. See Mutexes for more details.
Note |
---|
If you are sharing memory between two entities that memory must be marked as MEMF_SHARED. Failure to comply with this requirement will result in problems when AmigaOS starts to enforce MEMF_PRIVATE memory semantics. |
Shared objects are an idea directly lifted from the Unix world. The main reason shared objects were added to AmigaOS is to enable much simpler porting of complex applications. For example, Amiga Python was implemented using shared objects which makes it very easy to maintain. It is up to each programmer to determine whether they want to use traditional Amiga shared libraries or shared objects for their particular project. For a discussion of the pros and cons see The Right Tool for the Job.