Copyright (c) Hyperion Entertainment and contributors.

AmiWest Lesson 1

From AmigaOS Documentation Wiki
Jump to navigation Jump to search

AmiWest Lesson 1: Coding Basics

Program Startup

There are two primary environments in which a user can start applications in AmigaOS - by Shell console or by Workbench. Each method comes with it's own way of sending the application its starting arguments, data or options.

With the Workbench, the user can either double-click the application's icon, shift click the app and some other icon(s) or double-click an icon that has this application as its "default tool".

In the Shell console, the user usually enters the name of the program and ends the command line with arguments, options and/or a filename.

In either case, the application typically uses and responds to the files, options and/or arguments that were involved in starting it. A file is opened and/or the program runs as configured.

How did your application start?

Following standard ISO C, AmigaOS applications always start using the standard argc and argv parameters:

int main(int argc, char **argv)

The argc variable indicates the number of arguments received by the application. The first argument (argv[0]) contains the program name. Any following command line arguments are stored in following argv array elements (argv[1], argv[2], ...).

AmigaOS provides special startup code that fills in the argc and argv parameters for you. The reason for the special startup code is twofold: to setup the standard C library and to support launching programs from Workbench or the Shell.

In AmigaOS, if argc' is equal to zero that means the application was started from the Workbench. If it is non-zero then it means the application was started from the Shell console.

While the ISO C method works for applications started from the Shell, it does not directly support an application started from the Workbench. AmigaOS provides ways for handling both Workbench start-up as well as a more sophisticated way of dealing with Shell start-up and arguments.

Starting from the Console: the ISO C way

For the sake of ISO C standard conformance, an AmigaOS application can use the standard argv array variables to read the arguments passed to the app.

After determining the argument count (argc) is greater than zero, then that number of arguments can be accessed and printed out as follows:

    for (int argNo=0; argNo < argc; ++argNo)
    {
        // print CLI arguments
        printf("   argument #%d = >%s<\n", argNo, argv[argNo]);
    }

This method then leaves the parsing and testing of the arguments to the application and its developer. Many 3rd party libraries are available to parse the arguments such as Getopt. Since we are focusing on AmigaOS-specific programming they won't be discussed any further.

Starting from the Console: the AmigaOS way

AmigaOS provides an alternate way for applications to parse arguments from a Shell command line using the ReadArgs function. In its most common use, this function will parse command line arguments according to a "template" defined by the application. If the user follows the command name with a question mark, this function will print the application's commands template and then allow the user type in their arguments. If the user fails to provide a valid command line, then ReadArgs lets the application know and the application can then exit gracefully.

Configuring ReadArgs

The ReadArgs function is called with the following syntax:

    struct RDArgs *result = IDOS->ReadArgs(CONST_STRPTR template, int32 *array, struct RDArgs *rdargs);

The first "template" parameter is a pointer to a string that defines the application's keywords, settings and "toggles" along with the following "modifiers":

  • /S - Switch - indicates keyword string is a boolean setting
  • /K - Keyword - indicates keyword string must appear before a setting
  • /N - Number - indicates a setting must be an integer
  • /T - Toggle - indicates keyword must be followed with a boolean setting (ON, OFF, etc)
  • /A - Required - indicates keyword must be followed with a value
  • /F - Rest of line - indicates the rest of the line isn't parsed and taken as one setting
  • /M - Multiple strings - indicates following strings except for keywords are settings

Here is an code example of how such a "template" definition could look like:

    CONST_STRPTR argTemplate = "TEST/K,NUMBER/K/N,FILE";

This template allows the user to enter the following optional settings for an application:

  • the TEST keyword followed with a value.
  • the NUMBER keyword follwed with a numeric value.
  • the FILE keyword and a filename.

The second parameter in the ReadArgs function is an array of int32 pointers. If the ReadArgs function finds valid arguments, the respective array elements will point to any valid user input for those parameters (in template order). To start with, those pointers can be defined and set to NULL values like this:

    int32 argArray[] =
    {
        (int32)NULL,
        (int32)NULL,
        (int32)NULL
    };

The final parameter of ReadArgs function is typically NULL unless the application wants to set some default values (see the DOS/ReadArgs autodocs for more information on this).

Calling ReadArgs

Once run, the ReadArgs function will return either a pointer to a RDArgs structure if it succesfully parsed valid arguments or NULL if inadequate input was provided by the user. If the ReadArgs function was successful, then each entry of the second parameter (the argArray defined above) will point to any valid input for that slot in the template.

It is the developer's choice how best to deal with inadequate input from the user (ReadArgs returns a NULL value). At the very lest the application should gracefully let the user know the command line arguments were not accepted.

Using ReadArgs Results

For example, with the above code, the first entry (argArray[0]) would point to any valid input that followed "TEXT" keyword, if it was in the command line. This value could be printed as follows:

    if ((int32 *)argArray[0] != NULL)
        IDOS->Printf("     TEXT = %s\n", (STRPTR)argArray[0]);

If the second "NUMBER" keyword was used with a numeric value, its value could be obtained from the second arguments array entry as follows:

    int32 intArg;
    if ((int32 *)argArray[1] != NULL)
    {
        intArg = *(int32 *)argArray[1];
        IDOS->Printf("           NUMBER = %ld\n", intArg);
    }

Cleaning Up After ReadArgs

If ReadArgs successfuly parsed input from the command line, the FreeArgs function must be called using the resulting pointer from ReadArgs (rdargs in the above example). This will release any memory used by ReadArgs to store the user's input, as follows:

    IDOS->FreeArgs(rdargs);

Starting from the Workbench: Getting File Names

As discussed above, if the user starts an application from the Workbench, AmigaOS indicates this by setting the argc variable to ZERO. AmigaOS also uses the argv variable to send a pointer to a WBStartup structure that contains more information on the user's actions.

There are three ways the user could have started an application that will be reflected by the contents of the WBStartup structure;

  • The user double-clicked on the application's icon - In which case the WBStartup structure will indicate the program's name and "lock" its directory location on disk.
  • The user double-clicked on "project" file's icon that had this application set as its "tool" - In addition to the app's information, the WBStartup structure will indicate the project file's name and "lock" the project file's directory.
  • The user selected one (or more) "project" files and shift-double-clicked on the application's icon - The WBStartup structure will have a file name and directory "lock" for each of the project files in addition to the application's information.

In all these cases, when the application starts, it must goes though a few steps to determine if any files were selected with it. If the application also wants to read any settings attached to those files - as "Tool Types" within their icons - the application has to go through a few more steps. These steps start with reading the WBStartup structure passed to the app.

Accessing the WBStartup structure

When started by the Workbench, an application is passed a pointer to the WBStartUp' structure in the standard argv varable. After checking the argc variable is equal to zero (to see that we were started by the Workbench), the WBStartUp can be obtained like this:

    if (argc = 0)
    {
        struct WBStartup *wbs = (struct WBStartup *)argv;
    }

The WBStartup structure is based on the AmigaOS Message structure and it has the following format:

    struct WBStartup
    {
        struct Message      sm_Message;     /* a standard message structure */
        struct MsgPort *    sm_Process;     /* the process descriptor for you */
        BPTR                sm_Segment;     /* a descriptor for your code */
        int32               sm_NumArgs;     /* the number of elements in ArgList */
        char *              sm_ToolWindow;  /* reserved for future use */
        struct WBArg *      sm_ArgList;     /* the arguments themselves */
    };

Under typical circumstances the sm_NumArgs and sm_ArgList are the only elements of the structure that need to be accessed for program start up. The number of arguments will always be at least one, with the first entry representing the application itself. Any number of arguments above one reflects the number of "project" files or icons the user selected when the application was started.

The sm_ArgList will be an array of WBArg structures where the number of elements corresponds to the number of application & data files selected by the user (relected by the sm_NumArgs variable above). The WBArg structure containing the arguments has the following format:

    struct WBArg
    {
        BPTR   wa_Lock; /* a lock descriptor */
        BYTE * wa_Name; /* a string relative to that lock */
    };

The wa_Lock contains a pointer to an AmigaOS "lock" on the directry containing that argument's application or data file. For each of the files (the application or project files) passed to the application from the Workbench, a "lock" is applied to that directory. This prevents the directory of the application or any of the selected data files ("projects") from being disturbed or deleted while they are in use.

The wa_Name contains a string pointer to the name of the application or data file. This is a pointer to the name of the file itself and doesn't include the pathname for the directory is stored in.

The names of the application and any selected files could be printed out with the following code excerpt:

    uint8 NoArgs, argNo;
    NoArgs = wbs->sm_NumArgs;
    printf("     number of args received = %ld\n",NoArgs);
    for (argNo=0; argNo<NoArgs; ++argNo)
        printf("     arg %d name =  >%s<\n",argNo,(STRPTR)wbs->sm_ArgList[argNo].wa_Name);

Below we will see how to obtain more information from the selected files and how to get full path names for each. When the application finishes running, AmigaOS will release the "locks" on the directories refered to be the WBStatUp entries.

WorkBench StartUp: Examining Icons & Tooltypes

Whereas options might be given to a program in the Shell console in the form of argument in command line, Workbench programs frequently keep such settings within the application's icon, which is stored in a companion .info file. Specifically, each icon has the ability to store "Tool Types" text strings within the icon's .info file. While such settings can be almost any form of text, the standard format is to have a SETTING, equals sign and a value, like these:

    TEST=verbose
    NUMBER=23

These "Tool Type" settings are usually stored within the application's icon, but such settings can be also stored in most any icon files in AmigaOS. As such, an application can also look for such information within each project's icon file.

For an application to access any information that might be stored in its icon's .info file or in those of the "Projects" passed to the application, the application needs to go through a few more steps (after those above).

Getting to Each Icon's Directory

The easiest way to access any of the files sent by the Workbench is to use the sm_ArgList entry provided for each. This is done by first confirming a valid directory was found and "locked" for us by the Workbench. Then we call the CurrentDir function to switch the application to that directory, like this:

    if (wbs->sm_ArgList[argNo].wa_Lock)
    {
        int32 oldDir = -1;
        oldDir = IDOS->CurrentDir(wbs->sm_ArgList[argNo].wa_Lock);
    }

As you can see, this function returns a pointer to the old directory when we switch to the new directory location. This is very important, since the Workbench also maintains a directory lock based on the application's current directory. Before an application finishes, it needs to call the CurrentDir function again and switch back to the application's original location so it can be "unlocked" when the app quits - like this:

    IDOS->CurrentDir(oldDir);

Getting Icon Information

Once the application has "moved" to the directory of the icon file to be examined, we can use the GetDiskObjectNew function to get information from the icon. This function is simply called using the same wa_Name element from the sm_ArgList structure used above, as in this code excerpt:

    struct DiskObject *dobj;
    dobj = IIcon->GetDiskObjectNew(wbs->sm_ArgList[argNo].wa_Name);
    if(dobj)
    {
        printf("         Opened disk object.\n");
    }

The GetDiskObjectNew function automatically appends the ".info" necessary to open the selected file's icon. If there is no .info file for the selected file, this function will automatically use the information from "default icon" for the selected file type. For example, if the file "test.jpg" has no "test.jpg.info" icon file, the "def_jpeg.info" will be used.

If an application needs to explore icon information for a file not in the current directory of the app, the GetDiskObjectNew function will also accept the name of a file including a full path. But as we will see below, this does involve the application going through the overhead of allocating a string sufficient to store the full file & path name.

If succesful, the GetDiskObjectNew function returns a pointer to a DiskObject structure that provides the following information for that .info file and icon:

struct DiskObject
    {
    UWORD              do_Magic;       /* magic number at start of file */
    UWORD              do_Version;     /* so we can change structure    */
    struct Gadget      do_Gadget;      /* a copy of in core gadget      */
    UBYTE              do_Type;
    char              *do_DefaultTool;
    char             **do_ToolTypes;
    LONG               do_CurrentX;
    LONG               do_CurrentY;
    struct DrawerData *do_DrawerData;
    char              *do_ToolWindow;  /* only applies to tools */
    LONG               do_StackSize;   /* only applies to tools */
    };

As you can see, the DiskObject structure presents most of the information conveyed by an AmigaOS icon. The do_DefaultTool is the setting that is used to designate whichever application will be used when a "Project" icon is double-clicked. The do_ToolTypes element points to an array of strings containing the icon's "Tool Types".

Checking for Our Tooltypes

AmigaOS provides another function to simplify checking the Tool Types for values an application is interested in - the FindToolType function. It is simply called with the pointer to the do_ToolTypes array and the desired setting string. This function returns either a pointer to the string with that ToolType setting or NULL if the ToolType wasn't found. The following code would check for our "TEST" ToolType and print out any results found.

    STRPTR TTarg = NULL;
    TTarg = IIcon->FindToolType(dobj->do_ToolTypes, "TEST");
    if (TTarg)
        printf("              TEST found set to = >%s<\n",TTarg);

With all the above code, an application can check each of the elements of the sm_ArgList sent by the Workbench to check any application Tool Types as well as any of the Projects sent to the app.

Workbench StartUp: Getting Path & File Names

So far the above code has relied on using the WBStartUp structure and sm_ArgList sub-structure to obtain the necessary information on the application and any Project files selected. This has avoided directly dealing with the full file and path names of those files. If the full file & path name is required, the application can get the full path name by examining the directory "lock" of each sm_ArgList entry. This is done by calling the DOS NameFromLock function with another multi-step process.

First we must allocate a string in which to put the full name, then call NameFromLock to get the path name from the "Lock" and finally we must free the allocated memory after we're done with it. The following code excerpt would obtain the path to the application (always the first entry in sm_ArgList):

    int32 StringLen = 2048;
    // Alloc memory for pathname string
    filename = (STRPTR) IExec->AllocVecTags(StringLen,
            AVT_Type, MEMF_PRIVATE,
            TAG_DONE);
    if (filename)
    {
        if (wbs->sm_ArgList[0].wa_Lock)
            if (IDOS->NameFromLock(wbs->sm_ArgList[0].wa_Lock,filename,StringLen) != 0)
                printf("    app path name = >%s<\n",filename);
        // deallocate pathname string memory
        IExec->FreeVec(filename);
    }

As you can see, we allocate a large string size for the path name - 2048 characters - to provide room for potentially long and/or heavily nested path names. If that string is not long enough, the NameFromLock function will fail and return a NULL value. The developer can use their judgement in deciding how likely this error might be and how much effort to put into handling it - whether to have the application politely handle and notify the user of the error or whether to wrap this function in some loop that kept allocating largers string sizes until NameFromLock succeeds.

Obviously, there are few levels of error trapping in this code: to make sure we were able to allocate our string (filename), to make sure there is a valid directory lock to use and finally to make sure the path name could be obtained from the lock. Regardless of how unlikely failure might be, within any application these tests should occur and be "wrapped" in proper and graceful error handling and feedback.

Getting The Full Name

Once we have the path name, we can combine that with the name of the file (from the wa_Name element of the sm_ArgList structure). AmigaOS provides the AddPart function which also resolves AmigaOS separator characters ("/" or ":") to create a valid and complete file pathname, like this:

    if (IDOS->AddPart(filename,wbs->sm_ArgList[argNo].wa_Name,StringLen) != 0)
        printf("         full path = >%s<\n",filename);

The resulting combined path and file name should be suitable for use by the application - to open or act upon the file selected by the user.

Bear in mind, we are still working within the fixed size of the "filename" string, that must contain both the path name as well as the file name. Like before, we use a error trapping to check the combined file and path names fit into the string length we've defined. Again, should there be a failure, provide feedback and gracefully manage the situation.

Example Program Source Code

The following is an example program to demonstrate the above concepts in starting an application from Shell and the Workbench.

From Shell, the user can enter arguments or enter the "?" to see the template prompt before entering arguments. Commented out within the code is a working example of using the simple ANSI method of reading command line arguments.

From the Workbench, this example will print out all the above information found on all the arguments passed by the Workbench. Besides the application's information, the printed information will reflected selected icons or Projects calling this app as a default Tool.


/*
************************************************************
**
** Created by:	CodeBench 0.26 (04.01.2012)
**
** Project:		PJS-OS4ex-StartUp
**
** Version: 	4
**
** File:		This an example program of the various AmigaOS
**				ways of starting an application - from CLI or
**				Workbench - and dealing with the options.
**
** Date: 		25-08-2012 18:21:44
**
************************************************************
*/
 
#include <string.h>
#include <stdio.h>
#include <exec/execbase.h>
#include <workbench/startup.h>
 
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/icon.h>
 
// declare & initialize empty LONG words array to be filled with pointers to CLI arguments received
int32 argArray[] =
{
	(int32)NULL,
	(int32)NULL,
	(int32)NULL
};
 
// declare the number of possible arguments (the size of the argArray above)
uint8 noArgs = 3;
 
// declare CLI options template string with these modifiers:
//		/S - Switch - indicates keyword string is a boolean setting
//		/K - Keyword - indicates keyword string must appear before a setting
//		/N - Number - indicates a setting must be an integer
//		/T - Toggle - indicates keyword must be followed with a boolean setting (ON, OFF, etc)
//		/A - Required - indicates keyword must be followed with a value
//		/F - Rest of line - indicates the rest of the line isn't parsed and taken as one setting
//		/M - Multiple strings - indicates following strings except for keywords are settings
//
CONST_STRPTR argTemplate = "TEST/K,NUMBER/K/N,FILE";
 
uint8 returnCode = RETURN_OK;
 
// Starting program
int main(int argc,char **argv)
{
	uint8 argNo;
 
	printf("AmigaOS StartUp Example\n");
 
	// Check if program was started from CLI or Workbench based on value of "argc"
	if (argc > 0)
	{
		printf("Started from CLI.\n");
 
		// declare struct pointer to receive CLI options
		struct RDArgs *rdargs;
 
		// read CLI args (using AmigaOS method)
		rdargs=IDOS->ReadArgs(argTemplate,argArray,NULL);
 
		// were CLI arguments sucessfully read?
		if (rdargs)
		{
			int32 intArg;
 
			// loop through possible options & settings and print them
			for (argNo=0; argNo<noArgs; ++argNo)
			{
				// print readarg arguments
				printf("     arg %d = %s\n",argNo,(STRPTR)argArray[argNo]);
				if ((int32 *)argArray[argNo] != NULL)
				{
					intArg = *(int32 *)argArray[argNo];
					printf("           # = >%ld<\n",intArg);
				}
			}
 
			// free memory used by ReadArgs and data
	        IDOS->FreeArgs(rdargs);
		}
		else
			printf("    ERROR: Incorrect Values received by ReadArgs\n");
 
		//read args using ANSI method
		/*
		for (argNo=0; argNo<argc; ++argNo)
		{
			// print CLI arguments
			printf("   argument #%d = >%s<\n",argNo,argv[argNo]);
		}
		*/
	}
	else
	{
		printf("Started from Workbench.\n");
 
		// set pointer to WBStartUp structure using argv contents
		struct WBStartup *wbs = (struct WBStartup *)argv;
 
		// set a length for path names (to be added to file name length)
		int32 MaxPathLen = 1792;
		// set a length for file names (to be added to path name length)
		int32 MaxFileLen = 256;
 
		// working variable
		uint8 NoArgs;
		STRPTR filename = NULL;
		STRPTR TTarg = NULL;
		int32 oldDir = -1;
 
		// print out number of arguments (first one is this program)
		NoArgs = wbs->sm_NumArgs;
		printf("     number of args received = %d\n",NoArgs);
 
		// Alloc memory for pathname string
		filename = (STRPTR) IExec->AllocVecTags(MaxPathLen+MaxFileLen,
			AVT_Type, MEMF_PRIVATE,
			TAG_DONE);
 
		// Alloc disk object pointer for icon information
		struct DiskObject *dobj;
 
		if (filename)
		{
			// loop through WB arguments getting path names
			for (argNo=0; argNo<NoArgs; ++argNo)
			{
				printf("     arg %d name =  >%s<\n",argNo,(STRPTR)wbs->sm_ArgList[argNo].wa_Name);
 
				if (wbs->sm_ArgList[argNo].wa_Lock)
				{
					printf("         file locked.\n");
 
					// CD to project file's dir
					oldDir = IDOS->CurrentDir(wbs->sm_ArgList[argNo].wa_Lock);
 
					// get disk object on argument's file
					dobj = IIcon->GetDiskObjectNew(wbs->sm_ArgList[argNo].wa_Name);
					if(dobj)
					{
						printf("         Opened disk object.\n");
 
						// check for "TEST" keyword
						TTarg = IIcon->FindToolType(dobj->do_ToolTypes, "TEST");
						if (TTarg)
							printf("              TEST found set to = >%s<\n",TTarg);
						else
							printf("              TEST tooltype not found.\n");
 
						// check for "NUMBER" keyword
						TTarg = IIcon->FindToolType(dobj->do_ToolTypes, "NUMBER");
						if (TTarg)
							printf("              NUMBER found set to = >%s<\n",TTarg);
						else
							printf("              NUMBER tooltype not found.\n");
 
						// free disk object structure & memory
						IIcon->FreeDiskObject(dobj);
					}
					else
					{
						// print why we couldn't get icon information
						int32 MYerror, MYerrLen;
						MYerror = IDOS->IoErr();
						printf("     ERROR: Unable to open disk object.\n");
						printf("              number = >%ld<\n",MYerror);
						MYerrLen = IDOS->Fault(MYerror,NULL,filename,80);
						printf("              text = >%s<\n",filename);
						returnCode = RETURN_ERROR;
					}
 
					// CD back to previous dir
					IDOS->CurrentDir(oldDir);
 
					// Indicate what type of file the WBArg is refering to
					if (argNo == 0)
						printf("         entry = application.\n");
					else
					{
						// check item type
						if (strlen(wbs->sm_ArgList[argNo].wa_Name) > 0)
							printf("         entry = file.\n");
						else
							printf("         entry = device or directory.\n");
					}
 
					// Get the path and full path/filename of the file
					if (IDOS->NameFromLock(wbs->sm_ArgList[argNo].wa_Lock,filename,MaxPathLen) != 0)
					{
						printf("         path name = >%s<\n",filename);
 
						if (IDOS->AddPart(filename,wbs->sm_ArgList[argNo].wa_Name,MaxPathLen+MaxFileLen) != 0)
							printf("         full path = >%s<\n",filename);
						else
						{
							printf("     ERROR: full name is larger than %ld\n",(MaxPathLen+MaxFileLen));
							returnCode = RETURN_WARN;
						}
					}
					else
					{
						printf("     ERROR: path name is larger than %ld\n",MaxPathLen);
						returnCode = RETURN_WARN;
					}
 
				}
				else
				{
					printf("     ERROR: file handle could not be locked.\n");
					returnCode = RETURN_ERROR;
				}
			}
 
			// deallocate pathname string memory
			IExec->FreeVec(filename);
		}
		else
		{
			printf("     ERROR: unable to allocate memory for string!\n");
			returnCode = RETURN_ERROR;
		}
	}
 
	printf("GOODBYE!\n");
	return (returnCode);
}