Writing SAS/C subroutines to True BASIC by Paul Castonguay True BASIC allows you to use certain programs written in C as SUBROUTINES or FUNCTIONS from within your BASIC programs. This article describes the use of SAS/C version 5.10B to create such routines. It assumes a knowledge of C and of True BASIC. FUNDAMENTAL CONCEPT To make a C routine accessible as a True BASIC SUBROUTINE or FUNCTION, you compile the C routine as you would any other C program, but when you come to LINK it, you do so in a slightly differently way. Specifically, you pass the routine's object code through the LINKER but without the usual startup code (LIB:c.o in SAS/C). The result is an object file that cannot be executed on its own, but that, with the help of a program called FinalTouch, can be made equivalent to any True BASIC LIBRARY file. C ROUTINE PARAMETERS The most important aspect of this process is the convention used for passing information to and from the two languages. The C routine must contain two parameters, arg and uv. Arg allows access to the argument list written when the routine is invoked from True BASIC, as well as a mechanism for returning a value to True BASIC should the C routine be designed as a FUNCTION. Uv provides access to some important Amiga system variables, like SysBase, IntuitionBase, ... etc, should they be needed. Here is what the ANSI prototype definition of your C routine will look like: void my_routine(long **arg, struct TBUserVector *uv); SPECIAL TRUE BASIC STRUCTURES For your C routine to make sense of the information handed to it from True BASIC, it must know how to interpret it, naturally. This is done by having it #include a header file called TB.h, which contains some related structure template definitions. You will find TB.h in the Assembly drawer of your True BASIC disk. Thus an important part of your C routine is the preprocessor directive: #include "True BASIC:Assembly/TB.h" If you are using a floppy based system you may find it convenient to copy TB.h to your INCLUDE: device (where your SAS/C header files are kept), and then use the following preprocessor directive: #include That way you will not be required to insert the True BASIC disk in a drive unit every time you compile a C routine. However, if your system has a hard drive on which you have installed both True BASIC and SAS/C, it is best to leave TB.h where it is and to refer to it in the first way that I mentioned. When designing programs using a variety of tools, you should keep their respective resources organized and separated. Of course the above reference requires that the logical device name "True BASIC:" be defined on your system to be the location where the True BASIC language is installed. You can have your computer make that definition automatically at boot-up time by placing an instruction in your AmigaDOS Startup-Sequence, or User- Startup file, whichever you prefer. Here is a possible example: Assign "True BASIC:" "DH1:Languages/True BASIC" The above instruction would be used on a system where the True BASIC language was installed in a drawer called "True BASIC", which itself was installed in a drawer called Languages, as shown in the hierarchical diagram below. The quotation marks are needed in the above instruction because of the space used in the name True BASIC. DH1: | | ---> Languages/ | -- |---> SAS_C/ | | | Other languages that you |---> AmigaBASIC/ | may program in. | | |---> Assembly/ | | -- | ---> True BASIC/ | | |---> True BASIC (the language itself) | |---> TBHelp/ | |---> TBLibrary/ | |---> TBDo/ | |---> Assembly/ | |---> UserLib/ | |---> AmigaTools/ | |---> Examples/ | ---> Demos/ ARGUMENT PASSING You can design your C routine to be either a SUBROUTINE or a FUNCTION. If it is a FUNCTION it will return a value to True BASIC. In both cases, values are passed from True BASIC to C through an argument list, just as they are for any True BASIC external SUBROUTINE. Access to these arguments from within the C routine is made possible through the arg parameter, which points to an argument pointer array, an array of addresses, each of which represents one of the arguments. The method of access operates in a fashion similar to how in AmigaDOS a parameter called argv is used to access command line arguments. Thus arg is True BASIC's equivalent to AmigaDOS' argv. Arg is declared as a pointer to a pointer, which in modern parlance is called a handle. Ignore for now the uv parameter. void my_routine(long **arg, struct TBUserVector *uv); Parameter Argument Values in in C array True BASIC ----------- ----------- ------------ _______ __________ | | | | | arg -------->| arg #1 -------> argument #1 passed to |_______| |__________| subroutine | | | arg #2 -------> argument #2 passed to |__________| subroutine Addresses representing each argument appear in the argument array in the same order that they were written in the True BASIC argument list when the SUBROUTINE or FUNCTION was invoked. Arguments can be either character type (string), or integer. Each type is handled differently. Note that floating point numeric values are passed as strings - more about that later. The above diagram depicts the pointer relationships for a SUBROUTINE that contains two arguments. You can design your C language routine to use any number of arguments you it to. Note however that it is your job to keep track of their number and type. After all, it is you who is designing the thing. Your C routine can either read from or write to the various arguments by using addresses obtained from the argument array. Once your C routine is designed, compiled, and converted to True BASIC's LIBRARY format, it will operate successfully only if invoked using the correct number and type of arguments, that is, the number that you designed it to have. Thus, when it comes to using your routine within a True BASIC program, it operates just like any other SUBROUTINE or FUNCTION. If your routine is to be a FUNCTION, then its argument array will contain one extra element for the purpose of returning a value (either integer or string) to True BASIC. It is your responsibility to write your routine's return value to that extra argument, as opposed to relying on Standard C's usual return statement. A return instruction within your C routine causes it to terminate, as you would expect, and program execution to return to True BASIC, but it cannot in the process communicate any value. For True BASIC to receive a return value the C routine must write it to the argument location provided on the argument list. The following diagram depicts the argument array of a typical FUNCTION that accepts two input arguments. We will design such a FUNCTION as a first example. parameter Argument Values in in C array True BASIC ----------- ----------- ------------ _______ __________ | | | | | arg -------->| arg #1 -------> input argument #1 |_______| |__________| | | | arg #2 -------> input argument #2 |__________| | | | arg #3 -------> output value returned to |__________| True BASIC by C routine. INTEGER ARGUMENTS Integer arguments are accessed through a special C structure called TBInt, whose template is defined in the header file TB.h. For each element on the argument array that corresponds to a numeric argument, there will exist in memory a TBInt structure that was generated by True BASIC at the time the C routine was invoked. Here is the template definition of that structure as it appears in TB.h. struct TBInt { short exponent; /* flag */ short integer; /* data */ } TBInt contains two members, exponent and integer. Exponent is a flag that identifies the structure's data as either type integer or type floating point. The integer member contains the data itself. Integers are passed as 16 bit values in 2's complement form, which is equivalent to SAS/C's short integer. Longer integers must be passed using a different method, which is shown later in this document. The diagram below shows a SUBROUTINE that has an integer as its first argument. parameter Argument Argument in C array structure ---------- ----------- ----------- _______ __________ __________________ | | | | | | | arg -------->| arg #1 ------->| struct TBInt | |_______| |__________| |__________________| | | | | | | arg #2 | |exponent| integer | |__________| |________|_________| The exponent value must be -1 for integer data. Other values mean that the data in the integer member is in floating point format. Note however that True BASIC makes no guarantee that the format used internally for its floating point numbers will be the same as that used by your C compiler, or even that it will remain unchanged for future releases of the language. For that reason it is not recommended that floating point numbers be passed directly from True BASIC to C through the TBInt structure. This is in fact the reason why this structure was given the name TBInt in the first place, as opposed to something like TBNum. Floating point values should be passed to C routines as character strings containing their IEEE, double precision values - more about that later. GENERIC C ROUTINE With everything that has been said so far, we are led to the following generic form, or model, of a C routine that is to act as either a SUBROUTINE or FUNCTION to True BASIC. #include "True BASIC:Assembly/TB.h" /* === ANSII prototype definition === */ void routine(long **arg, struct TBUserVector *uv); /* ========== routine body ========== */ void routine(long **arg, struct TBUserVector *uv) { ... ... C code of routine ... } Typically you will add your own code to the body of the above routine, add other C functions to support that routine, and make other #include references to your compiler's header file system. READING THE ARGUMENTS To actually access arguments from within a C routine you must declare a pointer variable for each one and then assign to each the address of its corresponding argument from the argument array. Here is how four integer arguments can be read: void routine(long **arg, struct TBUserVector *uv) { struct TBInt *arg1, *arg2, *arg3, *arg4; arg1 = (struct TBInt *)*arg--; arg2 = (struct TBInt *)*arg--; arg3 = (struct TBInt *)*arg--; arg4 = (struct TBInt *)*arg; Each element of the argument array that represents a numeric argument contains the address of a TBInt structure that was created by True BASIC at the time the SUBROUTINE or FUNCTION was invoked. In the above code, the arg handle reads the first element on the argument array, assigns it to pointer arg1, then decrements to the next element (notice the post decrementing operator). Thus arg1 points to the TBInt structure corresponding to the first argument passed to the SUBROUTINE or FUNCTION from True BASIC. Similarly arg2 points to the second, and so on. You must decide at the time you design your C routine how many arguments it will need. Pointing to the argument structures is only half the job. To obtain actual values you must read the contents of their respective member names. Here is how that it is done in the case of the above integer example: short flag1, flag2, flag3, flag4; short value1, value2, value3, value4; flag1 = arg1->exponent; value1 = arg1->integer flag 2 = arg2->exponent; value2 = arg2->integer flag 3 = arg3->exponent; value3 = arg3->integer flag 4 = arg4->exponent; value4 = arg4->integer It is your responsibility to verify that the exponent member of each integer argument equals -1, which means that the structure's data is in the proper 16 bit integer format, otherwise your C routine could produce confusing results. You will see one way of doing that in the next example. CONSTRUCTING AN INTEGER FUNCTION The best way to make all the above concepts more clear is to look at a completed example. Consider a C routine whose purpose is to accept two integer values and then return their sum. Since we want this routine to return an answer, we will design it as a FUNCTION. Thus, the argument array will contain 3 elements, two to act as INPUT arguments, and one extra one for the return value. All arguments will be integers. Below I show the solution, without comments. See the file MySum.c in the MySum drawer for a fully documented version. parameter Argument Values in in C array True BASIC ----------- ----------- ------------ _______ __________ | | | | | arg -------->| arg #1 -------> input argument #1 |_______| |__________| | | | arg #2 -------> input argument #2 |__________| | | | arg #3 -------> output value returned to |__________| True BASIC by C routine. Sum of #1 and #2 #include #include "True BASIC:Assembly/TB.h" VOID MySum(LONG **arg, struct TBUserVector *uv); VOID MySum(LONG **arg, struct TBUserVector *uv) { struct TBInt *arg1, *arg2, *ans; arg1 = (struct TBint *)*arg--; arg2 = (struct TBint *)*arg--; ans = (struct TBint *)*arg; ans->exponent = -1; if(arg1->exponent == -1 && arg2->exponent == -1) ans->integer = arg1->integer + arg2->integer; else ans->integer = 0; } I #included in the above program to allow me to use the Amiga's uppercase VOID and LONG notation, instead of Standard C's lowercase void and long. Notice how I test the exponent member of both input arguments to make sure they were proper 16 bit integers. This protects the routine from accidentally performing integer operations on data that is not in integer format. In the event that non-integer data is received, the above routine will return a 0. If you have been programming in True BASIC for a while, you may have gotten used to treating all numeric data as the same, never thinking about the possible differences between integer and floating point. Such conveniences were the vision of Kemeny and Kurtz when they invented BASIC in 1964. However, it turns out that for maximum efficiency, integer and floating point data should be stored differently. True BASIC does that for you automatically in your BASIC programs. But in C it's you, the programmer, who must ensure that different types of numeric data are stored and processed correctly, otherwise, garbage in, garbage out. COMPILING THE ROUTINE. The easiest way to show you how to compile the above routine is to give you an lmkfile to accomplish the task. Here it is: PROGRAM = MySum $(PROGRAM)* : $(PROGRAM).o BLink FROM $(PROGRAM).o TO $(PROGRAM) Copy $(PROGRAM) TO $(PROGRAM)* $(PROGRAM).o : $(PROGRAM).c LC1 -b0 -cfistc -oQUAD: $(PROGRAM) LC2 -v -o$(PROGRAM).o QUAD:$(PROGRAM) An lmkfile is a script file that causes a complex compilation sequence to be performed when you execute SAS/C's LMK command. Basically, all you do is enter the above code (there's a copy on disk), save it in your project directory, and then enter LMK on the AmigaDOS command line. The LMK program reads the lmkfile and executes the compilation and AmigaDOS instructions found therein. If you prefer to use icons, and if you have properly set up your project directory (using sascsetup), simply double click SAS/C's build icon to accomplish the same thing. The above lmkfile consists of two groups of instructions. The second group gets executed first. These compile the program MySum.c by invoking SAS/C's first and second pass compiler commands, LC1 and LC2. Note that the program name "MySum" gets substituted for every occurrence of $(PROGRAM) in the file. Section 7 (page U55) of your SAS/C documentation is worthwhile reading if your are unclear about any of this. The important things to be careful about when compiling a routine for use by True BASIC is to use -b0 for absolute 32 bit addressing with the LC1 command, and -v to turn off SAS/C's stack checking with the LC2 command, otherwise your C program will not work as a True BASIC SUBROUTINE or FUNCTION. It may may even cause your computer to crash. The rest of what you see in the lmkfile is recommended, but not necessary. Refer to your SAS/C documentation for details. The other group of instructions in the above lmkfile causes the object code produced by the compiler to get passed through SAS/C's linker, BLink. Even though you are not linking to anything, that is, even though you have not specified any startup code or SAS/C support libraries, you must perform this operation, otherwise your routine will not be in the correct form for execution from True BASIC. Notice how I copy the linked routine to MySum* when I'm done, to conform with True BASIC's naming convention for compiled files. MySum* is soon to become a True BASIC external program unit. If when trying to compile this project all you get is a message saying that the project is already up to date, try: LMK -a You see, the LMK program is smart enough to know, from the dates of the various files whether or not your source code has been modified since the last compilation, and if not to report that compilation is not necessary at this time. However, when copying files on the Amiga the date codes, on which the above operation relies, are destroyed. Are you listening Commodore? So the files you receive may not compile the first time using the LMK command. The -a option forces LMK to perform the compilation, even if it has already been done. THE FINAL TOUCHES The file MySum* is almost ready to be used, but it is missing one thing: the compiled file header which tells True BASIC the routine's name, what kind of routine it is, and what kind of arguments it takes. The True BASIC program FinalTouch*, which you will find in the Assembly drawer of your True BASIC disk, adds this header to the file. To make the following operations convenient, you should copy the FinalTouch* program to your project drawer. Run FinalTouch* from True BASIC's editor and it will ask you for a SUB or DEF statement to define your routine. Just imagine the SUB or DEF line that your program would use if it were written in True BASIC. For MySum this would be: DEF MySum(x,y) Note that if you are like me and prefer to use the key word FUNCTION instead or DEF, too bad. FUNCTION doesn't work as input to the FinalTouch* program. But you can still use the word FUNCTION to declare the routine from within your True BASIC programs, that is, when you actually use it. Getting back to the above entry in FinalTouch*, when you enter the above routine and parameter names, you can use any combination of upper/lowercase letters. The name you give here for your routine will become the one it will be known by when you use it from within your True BASIC programs. That name is important. But it doesn't matter what actual names you enter for the parameters, although you must indicate their correct types (numeric or string). If you do not get them right, either the routine will misbehave, or True BASIC will report an error when you try to invoke it from one of your programs. You might at this point ask if the name you give to your routine in the FinalTouch* program has to be the same as the one you used as a function name within your C source code. The answer is no. The name you use in C is not important, although I always like to make it the same so that I can better identify the purpose of its source code at a later time. Finally the FinalTouch* program asks your for the name of the file containing your linked C routine. In this example you supply the name MySum*, which is the name that was used to save the final copy of the linked file. FinalTouch* will then add the necessary information to that file, converting it into a True BASIC LIBRARY file. RUNNING MYSUM FROM TRUE BASIC Below is a program (called Test_MySum.TRU on your disk) that will exercise the MySum FUNCTION. LIBRARY "MySum*" DECLARE FUNCTION MySum SET MODE "HIGH4" DO INPUT PROMPT "Enter two integers : " : X, Y PRINT STR$(X); " plus "; STR$(Y); " Is "; STR$(MySum(x,y)) LOOP END Notice the LIBRARY instruction refering to the file "MySum*", the file where the MySum FUNCTION resides. Also the DECLARE FUNCTION, something that is required in True BASIC for any external FUNCTION. If this routine had been designed as a SUBROUTINE, which it certainly could have, it would not have to be declared before being used. Run the above program and enter various integer values. You will see that the routine properly returns their sum. Now try a floating point value, that is, a number with a decimal point and a fractional part. This time the MySum FUNCTION returns a 0. It does not perform the addition because it was not programmed to process floating point numbers. Luckily we designed it to recognize such values and return a 0 whenever one was encountered, rather than just return garbage values. However, there are still some bugs with this program if you are patient enough to look for them. Try adding the two integers: 20000 and 20000. Hmm ..., the answer reported is a negative value, - 25536. Also try 5 and 40000. The answer is 0. These unforseen problems result from the fact that the arguments and returned values of the MySum FUNCTION are 16 bit integers in 2's complement form. Returned values that are between 32767 and 65535 are interpreted by True BASIC as negative integers. Arguments that are less than - 32768 or greater than 32767 are not accepted by the C routine because True BASIC reports them (through the exponent member of the TBInt structure) as outside the range of short integers. The above mentioned bugs may or may not be a problem, depending on exactly what you are designing your C routine to do. Obviously if you intended to use MySum as a general purpose routine, you would have to make it more robust. The next example will demonstrate how to incorporate some simple improvements. However, despite its bugs, this example did serve our purpose well by demonstrating the overall procedure for designing integer FUNCTIONS in C. The above test program (Test_MySum.TRU) is very simple. It doesn't even have a quit feature. To quit the program select Stop from True BASIC's Run menu. SUMMARY FOR PRODUCING AND USING C SUBROUTINES AND FUNCTIONS 1 - Design your C language routine using the above described model. 2 - Compile and link it using options shown in the above lmkfile. 3 - Run FinalTouch* on it. 4 - Access it from True BASIC by adding to your program the proper LIBRARY statement, as well as a DECLARE FUNCTION statement if it is a FUNCTION. MAKING SURE YOUR ARGUMENTS ARE INTEGERS The above C example contained code to test whether its input arguments were legitimate integers, thus eliminating the possibility of getting obscure results by it accidentally performing integer operations on data that was actually in floating point format. However, the end result, the FUNCTION returning a 0, might have been what you wanted. As an instructional example, suppose instead you want your routine to convert floating point values to integer and then process them normally. You could do that by using True BASIC's INT() FUNCTION on whatever arguments you pass to the MySum FUNCTION whenever you invoke it. For example, suppose you had numeric variables A and B, but weren't sure if their values were integer or floating point. You could write the following: PRINT "The integer sum of"; A; "and"; B; "is"; MySum(INT(A), INT(B)) ... and you would get what you want. The INT() FUNCTION would guarantee that all arguments passed would be type integer before being sent to the MySum FUNCTION, that is, as long as their values were within the range -32768 to 32767 for 16 bit integeters. The above idea works, but it's a little messy. It requires that you remember to use True BASIC's INT() FUNCTION every time you invoke the MySum FUNCTION. If you accidentally forget, you run the risk of receiving a 0 result if for any reason one of the input arguments is a floating point value. There must be a better way. There is a simple solution to all this, one which also has the advantage of allowing me to demonstrate another capability of True BASIC: how several files, each containing an external FUNCTION or SUBROUTINE, can be combined together into a single LIBRARY file. A DRIVER (SHELL) ROUTINE We can easily solve the above requirement by designing a True BASIC driver FUNCTION (also called a shell) for our C routine, that is, a FUNCTION whose job it is to invoke the C routine, making sure in the process that its arguments are legal integers. Here's a hierarchical diagram: ______________________ | | | True BASIC Program | |______________________| | | | ________________\|/_________________ | | | MySum FUNCTION | |____________________________________| | | | ________\|/________ | | | CSum FUNCTION | |___________________| In the above diagram the C routine, which actually accomplishes the job of adding the integer values, has been renamed to CSum, while the FUNCTION called MySum has been designed to invoke CSum, making sure in the process that its arguments are legal integers. It does this by invoking True BASIC INT() FUNCTIONS for each argument. Thus when you invoke MySum from within your True BASIC program, you don't have to worry about the C routine receiving floating point values. The MySum driver makes sure that all values received by CSum are first converted to integer form. In the event that you feel that the above mechanism seems too complex or wasteful to be useful, consider that C routines are usually designed to accomplish tasks that require a lot of processing, like sorting several thousand numbers, or solving large systems of equations, nothing as trivial as summing two integer values. In typical, real world examples, the actual speed of a program will depend more on the C routine's internal algorithm, and less on the added overhead of a driver FUNCTION and a couple invocations of True BASIC's high level INT() FUNCTION. On the other hand, because C programs are more cryptic and difficult to design than their True BASIC equivalents, you will not want to waste development time using C to solve aspects of your programming problem that represent too small a percentage of the total processing time. Considering all everything, it is most likely that you will want to design those parts of your program that prepare arguments for processing within a driver routine, in BASIC, where such operations are easily programmed, while at the same time solve those parts that require heavy processing in C, where overall program performance will be significantly affected. The smart programmer uses each language where it is most beneficial. It turns out that this idea of one FUNCTION acting as a driver or shell to another occurs over and over again in True BASIC. Note that were it not for True BASIC's ability to invoke any FUNCTION or SUBROUTINE from any other, not just from a main program, as is the case with AmigaBASIC, all this would be impossible. In True BASIC, FUNCTIONS and SUBROUTINES can invoke each other hierarchically in whatever way is required for the solution of your problem. They can even invoke themselves, a process called recursion. It is in programming situations like these that you begin to understand the real meaning of a fully structured language. The files of this example are on disk, in the drawer MySum_CSum. The C routine has been recompiled as CSum* and passed through FinalTouch*. The driver has been designed and saved as CSUM.DRV. Below I show it with its comments removed: EXTERNAL FUNCTION MySum(X,Y) DECLARE FUNCTION CSum LET MySum = CSum(INT(X),INT(Y)) END FUNCTION Note that the name of this FUNCTION is MySum. But wait a minute. How does the above routine access the CSum FUNCTION in the CSum* file? Shouldn't it have a LIBRARY instruction? If I did nothing else, you are correct. I would get an error. True BASIC would not be able to find the CSum FUNCTION because it is in another file, in CSum*. But embedding a LIBRARY statement within a LIBRARY file can lead to other problems. For example, if ever you changed the name of the CSum* file, the MySum FUNCTION would no longer work. So instead of writing a LIBRARY statement inside the MySum FUNCTION, let's combine the two LIBRARY files, CSum.DRV and CSum*, together into one common file, SumLib*. That way we are guaranteed that the driver will always be able to find the C routine. CREATING MULTIPLE ROUTINE LIBRARIES Before proceeding, pre-compile the CSum.DRV file from the last section, saving it as CSum.DRV*. The Assembly drawer of your True BASIC disk contains a program called MakeLib*, whose purpose is to combine several compiled LIBRARY routines into a single file. To make the following operations convenient, you should copy the MakeLib* program into your project drawer, where your CSum.DRV and CSum* files are. Run MakeLib* and it will ask you for the name of the LIBRARY file you want to create, which in this case is SumLib*. If the file already exists it will ask you if you want to append new routines to the existing LIBRARY, or erase it, thus creating a new one completely from scratch. As a first introduction, enter ERASE to start a new file. The program now prompts you for the names of your routines. Enter CSum.DRV* then CSum*, and finally just press return to quit the program. You now have a LIBRARY file that contains two FUNCTIONS, the MySum driver FUNCTION, and the CSum FUNCTION that was written in C. The new True BASIC program called Test_MySum.TRU (on in the MySum_CSum drawer) invokes MySum, just like the previous version, except that now, rather than solve the problem directly, MySum invokes CSum to do so, insuring in the process that CSum receives its arguments in the proper integer form. Below is a hierarchical diagram: ________________________ | | | PROGRAM Test_MySum.TRU | | | | LET Z = MySum(X,Y) | |________________________| | file = Test_MySum.TRU | | | ---------------------|--------------------- | | | | _________________\|/_________________ | | | | | | | FUNCTION MySum | | | | | | | | LET MySum = CSum(INT(X),INT(Y)) | | | |_____________________________________| | | | | | | | | | | | _________\|/________ | | | | | | | FUNCTION CSum | | | | | | | | answer = ... | | | |____________________| | |___________________________________________| file = SumLib* Run this test program and confirm that the single library reference: LIBRARY "SumLib*" ... allows True BASIC to access both MySum (the driver) and CSum (the C routine). The driver program is always able to properly find the CSum FUNCTION, because it is in the same SumLib* file. In fact, once this design in completed, you can begin to forget that this entire operation is actually being accomplished by two routines. As far as you are concerned, the SumLib* LIBRARY gives you access to the MySum FUNCTION, and that's all you need to know. Confirm that if you now enter floating point values, the program responds by first converting them to integer. Note that a change has also been made to the CSum.c routine to detect when the calculated return value is outside the range of 16 bits, and if so to return a zero value, removing the previous bug of returning negative values when the sum exceeds 32767. You might want to check CSum.c to see how that was done. Later in this article I will present how the routine can be designed to return integers of greater range. This example has been presented for instructional purposes. It has allowed me to go through the steps required to produce a reasonably robust True BASIC SUBROUTINE or FUNCTION in C. The C example was kept very simple so as to not over-complicate the procedure. Later I will present some more realistic examples. But first, we must cover a few more concepts. PASSING STRING ARGUMENTS String arguments are handled differently than integers. Strings are accessed through a structure called TBString, whose template is defined in the header file TB.h. For each element on the argument array that corresponds to a string argument, there exists in memory a TBString structure that was generated by True BASIC at the time the routine was invoked. Here is the template definition of that structure as it appears in TB.h. struct TBString { long length; char text[1]; } This structure has two members, the length of the string and the string itself, which is stored within a character array. Note that the template shows a character array of only one element, however in reality the structures that True BASIC will generate for your routines will have arrays of whatever length is required to pass their arguments. The string is not NULL terminated. Be warned that you will have to copy True BASIC strings to local arrays and then add a NULL character yourself before using them in any Standard C library functions, which expect to see it. The diagram below depicts a SUBROUTINE whose first argument is a string: parameter Array of Argument in C pointers structure ----------- ----------- ------------ _______ __________ ___________ _________________ | | | | | | | | | arg ----->| arg #1 ----->! TBString ----->| struct TBString | |_______| |__________| | pointer | |_________________| | | |___________| | | | | arg #2 | | length | text[] | |__________| |________|________| There is a difference here from our previous integer example. The address on the argument array points not directly to a TBString structure, but to a pointer to one. This will make a difference in the code that reads the address of the TBString structure. Below I show how the addresses of four string arguments are read: struct TBString *arg1, *arg2, *arg3, arg4; arg1 = (struct TBString *)**arg--; arg2 = (struct TBString *)**arg--; arg3 = (struct TBString *)**arg--; arg4 = (struct TBString *)**arg; I must de-reference arg twice because the address on the argument array represents not the address of the TBString structure itself, but the address of a pointer to it. Other than that, the code is the same as in the integer case. The result of the above instructions is that arg1 points to the TBString structure corresponding to the first argument, arg2 points to the second, and so on. Accessing the actual data is done by using the length and text member names of the TBString structure corresponding to each argument. A STRING EXAMPLE A good, simple example that demonstrates the reading of a string argument is a FUNCTION to return the ASCII code of the first character of any string. This FUNCTION will be equivalent to True BASIC's internal ORD() FUNCTION. It will have a single input argument of type string, and it will return an integer value. The argument array will therefore have two elements, the first representing the string input argument, and the second the integer FUNCTION return value. Below I show the solution without comments. See the file MyOrd.c in the MyOrd drawer on disk for a fully documented version. parameter Argument Values in in C array True BASIC ----------- ----------- ------------ _______ __________ | | | | | arg -------->| arg #1 -------> input argument #1 |_______| |__________| | | | arg #2 -------> Output value returned to |__________| True BASIC by C routine. ASCII value of first character of #1 #include #include "True BASIC:Assembly/TB.h" VOID MyOrd(LONG **arg, struct TBUserVector *uv); VOID MySum(LONG **arg, struct TBUserVector *uv) { struct TBString *input_arg; struct TBInt *output_val; input_arg = (struct TBString *)**arg--; output_val = (struct TBInt *)*arg; output_val->exponent = -1; if(input_arg->length == 0) output_val = -1; else output_val->integer = input_arg->text[0]; } The length of the input argument is tested to see if a NULL string was received (length = 0), in which case the FUNCTION is designed to return a -1, otherwise the integer value of the first character in the string is returned. Note that the exponent member of the TBInt structure representing the output argument must be made to equal -1, thus informing True BASIC that the value it is receiving is in integer format. The return value itself must be written to the integer member. The compiler options used for this example are exactly the same as in the last one. You can verify that by looking at the lmkfile in the MyOrd drawer on disk. When you run FinalTouch*, enter the function definition: DEF MyOrd(A$) ... and the filename: MyOrd* Run the test program for this example and confirm the operation of this new C routine. Again this example is kept relatively simple in order to be instructional. PASSING STRINGS BACK TO TRUE BASIC Using a string's value is perfectly safe, but changing the value requires care. You must not change a string's length. For that reason, the length of all strings that are to be modified by your C routine must be established on the True BASIC side, before the C routine is invoked. This gives me an opportunity to present one final improvement to our earlier MySum FUNCTION, the ability to return integer values that fall outside the range of 16 bits. RETURNING LONG INTEGERS To overcome the 16 bit limitation on values returned from our previous MySum routine, you will use as an output argument a TBString structure whose text[] array has been set up for a length of 4 bytes on the True BASIC side. In addition, the C routine itself will now be declared in the FinalTouch* program to be of type string. Since we have designed the MySum program with its own True BASIC driver, we can incorporate these changes without the end user even noticing. That is, from the point of view of the True BASIC program that ultimately uses the MySum routine, this new version will be no different than the last. Our changes are invisible. This is one advantage of using a fully structured language. First look at the CSum.c source code in the MySum_CSum2 drawer. The output argument has been redefined as a string. Also, within the body of the routine a LONG pointer is used to write values to the text member of the output TBString structure. In light of the above warning to not change the length of a True BASIC string from within a C routine, it is prudent to verify that the length of the string has been set up correctly on the True BASIC side, and to return gracefully if it has not. Actually, this is not a requirement in our current example because, as you will soon see, the length of the string is guaranteed by the driver. Despite that, I included such error checking in my C code just to demonstrate how it could be done if required. The correct length of the string is 4 bytes. Now look at the CSum.DRV driver program: EXTERNAL FUNCTION MySum(X,Y) DECLARE FUNCTION CSum$ CALL PackB(LongSum$,1,32,0) LET LongSum$ = CSum$(INT(X),INT(Y)) LET MySum = UnPackB(LongSum$,1,32) END FUNCTION CSum returns a 4 byte string which will contain the binary answer. Binary? Yes. True BASIC has an easy to use set of routines, PackB() and UnPackB(), that can read and write binary numbers to and from strings. And, are you ready for this, it is bitwise. That is, you can read and write to individual bit positions in any length string, even very long ones representing graphic data, like IFF pictures. Refer to a Regular Edition Reference Manual for more details about these routines. In the above example, PackB() establishes LongSum$ as a four byte string by initializing it to zero. This string is then used in an assignment instruction with the CSum$() FUNCTION, which modifies its value. Finally, the UnPackB() FUNCTION is used to extract the value, that is, to convert it from binary to True BASIC's internal form. Note that the ZeroString$() function from the {AmigaTools}amiga* library could also have been used to set up the 4 byte NULL string. As with the last MySum example, the two files, CSum* and its driver CSum.DRV* are combined into a common file by using True BASIC's MakeLib* program. This last version of MySum properly returns the integer sum of values entered, even if the sum exceeds the 16 bit limit of the TBInt structure. Note however that I did not modify the input arguments to accept extended values, although you certainly could do that if you wanted to. But chances are, what you really want to do is handle floating point values. That's the next subject. FLOATING POINT NUMBERS I have already hinted that the internal format of floating point numbers in True BASIC is not guaranteed to be the same as that used by your C compiler. At first you may be troubled by that, but there are some very good reasons for it. By having it's own internal format, True BASIC is able to take most advantage of any platform, and as a result its numerical accuracy and speed are very competitive. On the Amiga its accuracy is in the league of SAS/C's double precision, while at the same time it executes faster than AmigaBASIC's single precision. But more importantly, locking down a language's internal format is against some of the principles around which BASIC was originally invented. It was, after all, designed to be a high level language, one that not only protected you, the programmer, from the internal details of how numeric data is stored on your particular model of computer, but also that left the language implementors free to do whatever they deem necessary to obtain the best performance out of your BASIC programs. IEEE DOUBLE PRECISION FORMAT The FUNCTION called NUM$() allows you to convert a numeric value within a one of your programs from True BASIC's internal format to a string containing the same value in standard IEEE, eight-byte, double precision. That representation is the same on many compilers and brands of computers, including SAS/C on the Amiga. The opposite of NUM$() is NUM(), which converts a string containing a number in IEEE double precision format back to True BASIC's internal format. Thus to convert a number to IEEE format: LET MyPi$ = NUM$(3.14159265358981) ... and to convert it back to True BASIC. LET MyPi = NUM(MyPi$) FLOATING POINT ARGUMENTS To pass floating point values as arguments to your C routines you simply convert them first to IEEE format using the NUM$() FUNCTION, and then pass them as strings. Similarly, when receiving floating point results back from your C routines, you use the NUM() FUNCTION to convert them from IEEE to True BASIC's own internal floating point format. A good example that demonstrates the use of floating point data is a FUNCTION to multiply two numbers, which follows in the next section. MYMULT EXAMPLE In a normal C program, floating point mathematical operations are written using standard notation and then carried out by certain library resources of the compiler. For example, in SAS/C you have the choice of several math libraries, lcm.lib, lcmffp.lib, lcm6881.lib, ... etc. Each of these has its advantages. To use any of these resources you specify the one you want when invoking the linker. However, in the particular case of SAS/C some of these resources may also require linkage to the startup code, LIB:c.o, and we don't want that. Startup code is used to produce self standing executable programs, and we want object code that can be invoked as a SUBROUTINE or FUNCTION from True BASIC. For example, if you try to write a C routine that contains floating point mathematical operations and if you try to compile it as we have been doing in this article, that is, without the startup code, but with SAS/C's lcm.lib math library, the linker will report that it cannot resolve certain symbols. In this particular case the unresolved symbols are simply some global valiable names, _FPERR and _SIGFPE, that the library wants to write values to in the event of an error. It is safe to supply these variables yourself, in your own code. Look at the following example, MyMult.c, to see see how this is done. A fully commented version is on disk. #include #include "True BASIC:Assembly/TB.h" int near _FPERR; int near _SIGFPE; VOID MyMult(LONG **arg, struct TBUserVector *uv); VOID MyMult(LONG **arg, struct TBUserVector *uv) { struct TBString *in_one, *in_two, *out; DOUBLE numeric_one, numeric_two, *numeric_out; in_one = (struct TBString *) **arg--; in_two = (struct TBString *) **arg--; out = (struct TBString *) **arg; numeric_one = *((double *)in_one->text); numeric_two = *((double *)in_two->text); numeric_out = (double *)out->text; if(in_one->length != 8 || in_two->length != 8 || out->length != 8) { if(out->length == 8) { *numeric_out = (DOUBLE)0.0; return; } else return; } *numeric_out = numeric_one * numeric_two; } The MyMult.c program is a FUNCTION that accepts two floating point arguments as IEEE format strings and returns their product, also as an IEEE format string. Look at its lmkfile and make sure you understand how the math library, lcm.lib, is specified. The MyMult* file is passed through FinalTouch* and defined as follows: DEF MyMult$(a$,b$) ... that is, as a string type FUNCTION with two string arguments. The Test_MyMult.TRU program demonstrates how the MyMult$ FUNCTION is invoked. As mentioned above, True BASIC's NUM$() and NUM() FUNCTIONS are used to convert the numerical data to and from IEEE double precision floating point format and True BASIC's internal format. Later I will present a drive for this routine, thus allowing it to be used without having to go through the trouble of converting its arguments to IEEE strings every time. 68881 DEDICATED COPROCESSOR CODE If you have an Amiga 3000, or otherwise accelerated machine, you might want your C routines to take advantage of the math coprocessor. The fastest code is that which is compiled using lcm6881.lib. Such code expects to see the coprocessor there. The MyMult2 drawer repeates the last example except that this time your program uses the resources of SAS/C's lcm881.lib. Pay particular attention of the -f8 option used for the LC1 command, as listed in the lmkfile. Without that, your code will not link proplerly. Refer to your SAS/C documentation for direction on how to properly compile programs using different floating point resources. Note also that the reference to the previous _FPERR and _SIGFPE are removed. They are no longer needed. Later in this article I will present a more complex example where you can see first hand the speed of the math coprocessor. Be warned that code which has been linked to SAS/C's lcm6881.lib will crash if executed on a system where the math coprocessor is not present. UNRESOLVED CX... FUNCTIONS If at any time when compiling and linking your c routines you receive unresolved symbol errors with names like _CXS55, _CXM55, ..., beware. These symbols represent math routines (actual functions) that your code is trying to access. Faking these symbols by adding stubs in your own code is dangerous. Chances are you are making a mistake in the way you are compiling and linking your program. For example, if you try to compile the above lcm6881 example while at the same time forgetting the -f8 option for LC1, you will get these kinds of errors. If you ignore them and execute your code anyway, your machine will probably crash. If you consistently get these kinds of errors, you should contact SAS/C product support to find out what you are doing wrong. USING THE AMIGA'S IEEE LIBRARY Compiling your routine using lcm.lib produces a program that does not use the coprocessor. Using lcm6881.lib does, but crashes if the resulting program is executed on a machine where the coprocessor is not installed. The compromise is to compile your program such that it uses the coprocessor if it is installed, but also runs normally if it is not. To do this you use the resources of the Amiga's own math library, "mathieeedoubbas.library", located in the LIBS: directory. The resulting code is not as fast as lcm6881.lib code, but it is faster than lcm.lib code. Your C routines can load that run-time library and use its various mathematical functions directly. It's a little more difficult than having a compiler parse your mathematical expressions for you, but it's not unreasonable. The use of the Amiga's run-time math libraries is explained in Chapter 28 of the "Amiga ROM KERNEL REFERENCE MANUAL: Libraries & Devices", 1990, Addison Wesley. There is some model code on page 564, but before you start, I need to warn you about something else. 2.0 HEADER FILE PROBLEM You may experience an operational problem with SAS/C when compiling code that accesses the Amiga's "mathieeedoubbas.library" and when you have installed your compiler such that it uses the version 2.0 header files. When you compile your code, the second pass compiler, LC2, reports that you are using an incorrect number of arguments in a library call. Also, the resulting code, if linked, does not operate normally. The problem has to do with the version of header files on your system. Many copies of SAS/C version 5.10B and earlier were shipped with version 2.02 of the header files, and most Amigas today use 5.04. You can obtain the latest header files by ordering from Commodore: NATDEV20, 2.0 Native Developer Kit The cost is $20.00 plus $3.00 for shipping. In this particular example the problem can also be resolved by making your compiler use the older 1.3 header files. Note that this does not mean that you must operate your compiler under AmigaDOS 1.3. Nor does it mean that the resulting compiled code will not operate under AmigaDOS 2.0. It simply means that when SAS/C compiles your program, it will use the older, version 1.3 header files instead of the newer, version 2.0 ones. This is a good short term solution, however, if you are doing a reasonable amount of this work you really should get the most up-to-date files. With that said, you should now go to page 564 of the ROM KERNAL MANUAL and experiment with the model program to access the various functions in the IEEE double-precision math library. Write a few test programs in C and link them to SAS/C's startup code, to confirm that you know how to use them. IEEE MYMULT In the MyMult3 drawer you will see our previous MyMult example, but this time it has been redesigned to take advantage of the Amiga's IEEE library. The C routine to accomplish this uses only one function from the IEEE library, IEEEDPMul(). The copy on disk is fully documented. #include #include #include #include "True BASIC:Assembly/TB.h" VOID MyMult(LONG **arg, struct TBUserVector *uv); VOID MyMult(LONG **arg, struct TBUserVector *uv) { struct TBString *in_one, *in_two, *out; DOUBLE numeric_one, numeric_two, *numeric_out; in_one = (struct TBString *) **arg--; in_two = (struct TBString *) **arg--; out = (struct TBString *) **arg; numeric_one = *((double *)in_one->text); numeric_two = *((double *)in_two->text); numeric_out = (double *)out->text; if(in_one->length != 8 || in_two->length != 8 || out->length != 8) { if(out->length == 8) { *numeric_out = (DOUBLE)0.0; return; } else return; } if((MathIeeeDoubBasBase = (struct Library *) OpenLibrary("mathieeedoubbas.library",0))) { *numeric_out = IEEEDPMul(numeric_one, numeric_two); CloseLibrary(MathIeeeDoubBasBase); } else *numeric_out = 0.0; } /* End of MyMult() */ Remember that floating point values are passed from True BASIC to C as strings containing their values in IEEE format. Addresses for each argument are read from the argument array and assigned to TBString pointers. Then the numeric data itself is assigned to floating point variables. Note that a pointer is defined for the numeric data of the output variable. Following that, the Amiga's IEEE math library is opened and the IEEEDPMul() function is invoked using as arguments the the two input floating point numbers. The value returned from IEEEDPMul() is then assigned to the output argument via its floating point pointer. Voila! Note that the arguments received are tested for proper length (8 bytes for ieee format). A wrong length would indicate that an incorrect argument has been passed from True BASIC. Also, in the usual Amiga style, a safe value is returned in the event that the IEEE run-time library refuses to open. And finally, the mathieeedoubbas.library is closed before returning to True BASIC. COMPILING AND FINALTOUCH As with previous examples, the lmkfile contains all compiling information. When FinalTouch* is run, enter the following function definition: def MyMult$(a$,b$) That is, define MyMult$() as a string type FUNCTION that takes two strings as input arguments. TESTING MyMult The following Test_MyMult.TRU program exercises this example: LIBRARY "MyMult*" DECLARE FUNCTION MyMult$ LET C$ = NUM$(0) DO INPUT PROMPT "Enter two values: ": X, Y LET A$ = NUM$(X) LET B$ = NUM$(Y) LET C$ = MyMult$(A$,B$) PRINT "The result is"; NUM(C$) END Note that the version on disk demonstrates the USING$() FUNCTION to format the output. A DRIVER FOR MyMult As often happens when you design a routine in C, you need to perform some preparation of its arguments before invoking it, preparation that makes your routine much more difficult to use than it should be. Things like initialing the output variable, or converting arguments and return values between different floating point formats are not only tedious to do, they are often difficult to remember. Even the FUNCTION type of MyMult$(), a string, does not seem intuitive here. After all, why should a FUNCTION that multiplies numbers return a string? What is needed here is a driver program to perform these details automatically, thus making the routine easier to use. That has been done for you in the drawer called MyMult_CMult. In a way similar to our first MySum example, the C routine has been renamed and a True BASIC driver designed to ensure that the C routine gets properly invoked. The driver is of type numeric, and it accepts numeric arguments, just as you would expect from such a FUNCTION. Note that the C routine and its driver have been combined into one common file, MultLib*. Below I show the Test_MyMult.TRU program from that drawer, along with a hierarchical diagram of the entire project: LIBRARY "MyMult*" DECLARE FUNCTION MyMult DO INPUT PROMPT "Enter two values: ": X,Y PRINT X; "multiplied by"; Y; "is"; MyMult(X,Y) LOOP END _________________________ | | file = Test_MyMult.TRU | PROGRAM Test_MyMult.TRU | | | | LET Z = MyMult(X,Y) | |_________________________| | | | ________________________|________________________ | | |file = MultLib* | ____________________\|/____________________ | | | | | | | FUNCTION MyMult | | | | | | | | LET MyMult = NUM(CMult(NUM$(X),NUM$(Y))) | | | |___________________________________________| | | | | | | | | | | | ____________\|/___________ | | | | | | | FUNCTION CMult | | | | | | | | numeric_out = ... | | | |__________________________| | |_________________________________________________| I'm sure you'll agree that the use of MyMult is now much more intuitive. WHAT GOOD IS IT? Having now seen all the steps required to design True BASIC SUBROUTINES and FUNCTIONS in C, you are probably wondering how much benefit you can expect from doing all of this. The examples seen so far have been too simple to be even worth the trouble of breaking open a C compiler. In fact, a program that uses the above MyMult FUNCTION will actually execute more slowly than if it multiplied the same numbers directly within True BASIC. Where all this really becomes valuable is when part of your program requires a lot of processing power. In the remainder of this article I present a more realistic example that will allow you to see first hand how much speed improvement you can expect to gain from such routines. It will also demonstrate more techniques that you will find valuable in designing your own routines. FRACTAL EXAMPLE The generation of fractal images (strange mathematically generated shapes) is a new activity that has grown out of the proliferation of personal computers. Typically they require a lot of floating point processing. Although the algorithm to produce them typically involves only a few different types of operations: the squaring, the addition, and the calculation of the absolute value of complex numbers, these operations need to be performed thousands or even millions of times to produce a single image. Often people use programs specially written to experiment with the generation of fractal images, much as a child uses a kaleidoscope to view interesting patterns. However, to really profit from the experience, one should write their own program to generate a fractal. In the drawer called Fractal you will find a program, Fractal.TRU, which is written totally in True BASIC. It generates a fractal image that is part of the now famous Mandelbrot set. You might want to run the example to see what it does. Then inspect the code to see if, with the help of the explanation below, you can figure out how it works. This Fractal.TRU program will be the reference program against which two others, Fractal2.TRU and Fractal3.TRU, which contain C routines to accomplish the work, will be compared. THE MANDELBROT FUNCTION Most of the execution time of the program is spent in the FUNCTION called Mandelbrot. This routine repetitively squares a complex number, adds a complex constant to it, measures its absolute value, and then finally checks to see if the result meets certain conditions. Depending on the result, execution will either return to the FUNCTION's calling program, or loop again to square the number another time. A complex number has two parts, called a real part and an imaginary part: Z = A + Bi ... where i equals the square root of -1. A is the real part, B is the imaginary part. To multiply two complex numbers requires the following operations: A + Bi C + Di ------------- ADi + BDi2 AC + BCi --------------------- AC + (AD+BC)i + BDi2 And, since i equals the square root of -1, the above expression becomes: (AC - BD) + (AD + BC)i To add two complex numbers is easier: A + Bi C + Di ------------- (A+C) + (B+D)i The calculation of a fractal requires the iteration (repetitive calculation each time using the previous answer) of the following equation: (Z(n))^2 = (Z(n-1))^2 + C ... where Z and C are complex numbers. The value of Z, a complex number, is changed by squareing it and adding the complex constant C. Z(n) represents the current value of Z, Z(n-1) represents its value calculated in the previous iteration of the equation. During the first itteration, Z(n-1) = 0 + 0i, that is, it's zero. The C term is a complex constant that is passed to the FUNCTION from the calling program. It usually represents the coordinates of a single point in the image that is being calculated - more about that in a minute. The value of C is added to Z^2 each iteration, but itself is not changed by the above equation. As the value of Z is changed by successive iterations of the above equation, its absolute value is checked. Note the absolute value of a complex number: ___________ |A + Bi| = \/ A^2 + B^2 When the absolute value of Z exceeds 2, the FUNCTION returns to the calling program, reporting the number of times that the equation was iterated. Actually, if you look at the code you will see that it doesn't actually bother to calculate the square root of A^2+B^2, it simply checks to see if Z^2 exceeds 4. That trick saves some processing time. In essence what is happening here is that a complex number is being repetitively squared and added to a constant to see how it grows (absolute value gets larger). If it's value grows beyond 2, the routine quits and reports to the calling program the number of iterations it took to grow to that value. But what if it never grows? Good question. If left unchecked it could iterate forever. For that reason a second condition is checked every iteration, and that is whether the number of iterations has exceeded some predetermined amount, MaxI. Look in the Mandelbrot FUNCTION and see if you can identify these above operations. MAIN PROGRAM This is the controlling part of the program. First it scales the screen such that each pixel represents a complex number. Horizontal coordinates represent the real part, vertical coordinates the complex. Thus the screen becomes part of a complex plane where each pixel represents a point in that plane, a single complex number. Next the program sets up a color table, an array, called KolorTable, that contains register numbers of the screen colors available in the present screen mode arranged in a certain order. Ignore for now what criteria was used to set up this table. The meat of the program is a double nested loop that scans the entire screen. That is, it sequentially visits each and every pixel on the screen. At each location it calls the Mandelbrot FUNCTION, passing to it the complex number representing the current pixel, and a value for MaxI, to control the maximum number of times that the Mandelbrot FUNCTION will be allowed to iterate. Thus the C that the Mandelbrot FUNCTION receives is really nothing more than the coordinate position of a pixel expressed as a complex number. The Mandelbrot FUNCTION then does it job of iterating until either the absolute value of Z exceeds 2, or until the maximum permissible number of iterations are completed. The actual number of iterations for each pixel (value of C) is called the escape velocity, which in this example is an integer in the range 0 through 500, and is returned to the main program. Now here is the key. The escape velocity is used to determine what color each pixel will be illuminated by choosing a color register that has been previously stored at a corresponding position on the KolorTable array. Said another way, the excape velocity of each pixel is used as an index to choose a color from the KolorTable array. FOR Y = YBottom TO YTop+dy/2 STEP dy FOR X = XLeft TO XRight+dx/2 STEP dx SET COLOR KolorTable(Mandelbrot(X, Y, MaxI)) PLOT X,Y NEXT X NEXT Y Notice the SET COLOR instruction that uses the value returned from Mandelbrot as an index to choose a register color from the previously initialized KolorTable array. Each element of the KolorTable array contains an number from 0 to 3, one out of a possible four color register numbers in the present screen mode. Now obviously some insight was needed to set up the colors, otherwise the image would be nothing more than a bunch of random colored dots. The trick is to set up KolorTable in ranges corresponding to certain escape velocities. The first range is a single value, MaxI. The MaxI position of the array is the highest (500) and is assigned color register 2, black. As a result, all pixels which cause the Mandelbrot FUNCTION to return an escape velocity of 500 will get colored black. The next range, 70 through 499 is assigned color register 3. Thus, all pixels that cause the Mandelbrot FUNCTION to return an escape velocity between 70 through 499 will get colored orange. The next range 26 through 69 is assigned register 0, the background color. The next, 19 through 24, register 1, white. Velocities in the range 0 through 18 are assigned alternately between registers 2 and 3 by using the True BASIC's MOD() FUNCTION. To experiment with the Mandelbrot set and find new images you simply choose different coordinates from the complex plane that the Mandelbrot set is known to occupy, -2.4 to +0.8 horizontally, -1.2 to +1.2 vertically. In fact, entering those coordinates as screen scaling in the above program will allow you to see the entire Mandelbrot set. My example showed only one small portion of it. To view different portions of the Mandelbrot set, using different magnifications, simply choose different coordinates values. You will also find that making adjustments to the KolorTable will allow you to produce different artistic effects, depending on what actual coordinates you are using. And of course, you can experiment by using a screen mode having more colors. The higher the value of MaxI, the more accurately the fractal will be calculated, and the more time it will take. Typically, the fractal program will take a few hours to several days to calculate. WHY BASIC If generating fractals is such a time intensive activity, why do it in BASIC at all? To answer that you should look at the different parts of the program and ask yourself how difficult they would be to design in a faster development language like C. Setting up a screen mode is done in one line in True BASIC. The equivalent C code must open the Amiga's intuition.library, initialize members of a NewWindow structure, call an OpenWindow function, and verify its success. And the window that you get is not even scaled, that is, its coordinates are expressed in pixel numbers, not Cartesian coordinates in which your complex numbers need to be expressed. If you want that feature you have to design your own scaling functions. It becomes immediately obvious to anyone doing this kind of work that True BASIC provides the interactive environment needed for mathematical experimentation, which is what playing with fractals really is. Yet once a program is designed, one is alway tempted to try to improve its efficiency, and that's where a C FUNCTION or SUBROUTINE comes in. Given the right kind of problem, a C routine can make quite a difference to the execution of your BASIC program, while at the same time not destroying its readability. Once designed, it can be used by any other True BASIC program. Thus you can design LIBRARY files to handle certain standard problems and market them to other people who need to use them. FRACTALLIB* In the Fractal drawer you will find the C source code C_Mandelbrot_Calculator.c, which together with a driver FUNCTION replace the operation of the Mandelbrot FUNCTION of Fractal.TRU. The hierarchical diagram below shows the relationships between these routines: ______________________ | | | True BASIC Program | |______________________| | | | | file = FractalLib* --------------------|--------------------- | | | | ________________\|/_________________ | | | | | | | Mandelbrot FUNCTION | | | |____________________________________| | | | | | | | | | | | ________________\|/_________________ | | | | | | | C_Mandelbrot_Calculator FUNCTION | | | |____________________________________| | | | |__________________________________________| The two routines have been placed in the file FractalLib*, from which they can be invoked by calling the FUNCTION Mandelbrot(). FRACTAL2, USING IEEE LIBRARY Look at the source code of C_Mandelbrot_Calculator.c, to see if you can identify the operations previously discussed in this article. There are a number of issues here that you will find helpful in the design of your own subroutines. Notice that the string input parameters, which represent the real and imaginary parts of the complex constant in IEEE floating point format, are not verified within the C routine. I do not need to because the driver routine invokes True BASIC's NUM$() FUNCTION for each one before invoking the C_Mandelbrot_Calculator FUNCTION, thus guaranteeing their correct form. On the other hand, the MaxI input parameter must be verified. If a value greater than 32767 is passed, the numerical format received from True BASIC will be incorrect. Perhaps you are questioning the opening and closing of the mathieeedoubbas.library for every invocation of this routine. Doesn't that take an excessive amount of time. No, not really. If you design your routine to leave the library open, you will not save a significant amount of time. Besides, if your routine leaves the library open, you then impart on the person using it the responsibility of closing it. Look at the math functions called by the iteration loop and verify that you understand the purpose of each. Compare the code to the equivalent Mandelbrot FUNCTION in Fractal.TRU, which is written in True BASIC. The name of this C routine, C_Mandelbrot_Calculator, is made excessively long on purpose to reduce the chances that you will accidentally use this same name yourself in one of your own projects, thus creating a conflict. Since it is invoked by its driver, not directly by you, the excessively long name does not represent any inconvenience. FRACTAL3, USING MATH COPROCESSOR This version is equivalent to the last except that it has been linked to SAS/C's lcm6881.lib math library. This version will operate only on a machine that is equipped with a math coprocessor. In fact, if you try to run it on one that doesn't have one, the program will cause your computer to crash. LAST EXAMPLE, A SORT Perhaps the previous example did not demonstrate enough difference between C and BASIC to make such a design worth your while. That's because the calculation of fractals is so floating point intensive that speed differences become more a function of which floating point library you use, than which language. More representative of the differences between C and BASIC is the sorting of numbers. In the sort directory you will find two demonstration programs. The first, Test_CSort.tru, sorts two different lists of 10,000 numbers arranged in in reverse order and uses two different sort algorithms: Bubble and Combsort. The C routines that do the work have been grouped together in the Sort_Lib* file. You will find their source code in the same directory for your inspection. The demonstration takes about 20 minutes to complete on an Amiga 3000. Note that using the Combsort, 10,000 floating point numbers can be sorted in only 8.64 seconds. In contrast, the Test_TBSort.tru program executes a bubble sort algorithm on the same integer data used by Test_CSort.tru and takes overnight to execute.