GCOOPE Version 1.0 Generic C-language Object Oriented Programming Environment Version 1.0 by Brian L. Price Released as Public Domain July, 1994. Table of Contents I. Introduction to Objects....................................1 A. Object-ively Defined....................................1 B. Genericity isn't just for canned goods..................2 C. C. Inheritance without genetics.........................3 D. Introducing GCOOPE......................................3 II. The GCOOPE Environment.....................................5 A. Object handles..........................................5 B. Class variables.........................................6 C. Instance Variables......................................7 D. Methods.................................................7 E. Function Dispatching....................................9 1. 1. g is the Name.....................................9 2. The Smalltalk Connection............................11 3. Local method caching................................11 4. Steering the instance...............................13 F. Classes and Pseudo Classes.............................14 G. Flag Classes...........................................15 III. Creating a class definition..............................16 A. Layout of a class definition module...................16 B. Method definition section.............................16 C. Installation routine..................................17 IV. Expansion and Growth......................................18 Appendices A. Appendix A. The GCOOPE USER'S GROUP.......................19 B. Appendix B. About the Author in case you're really bored..20 i GCOOPE Version 1.0 User's Guide By Brian L. Price July, 1994 I. Introduction to Objects A. Object-ively Defined Before diving into GCOOPE, let us take a few moments to examine the concept object. My pocket dictionary provides a starting point with two interesting definitions: 1. Object - Something serving as a focus of attention or ACTION. 2. Object - (Grammer) A noun that receives or is affected by the action of a verb.... An object is first of all a thing, something that exists in a perceptible form, in other words data. That data is the focus of actions, it is changed, translated, or simply accessed by a verb (method). If we consider the verb to be a method for performing an action, and that method is described as a computer algorithm, we have the 'active' portion of the definition. Another way of saying object is active data. Now how do we apply this concept to computer programming? We must first lay out some useful principles. Lets borrow some tried and true programming principles as a start: 1. Modularity - modular code is self contained (at least as much as possible), and it has a well defined interface to the outside world. Since our object concept is already a seperate entity from its surroundings, enforcing the principle of modularity helps to keep it that way. 2. Orthagonality - The set of methods that we define to act upon our object's data should be orthagonal. In other words if you define an add operation, you must define a subtract operation. This goes hand in hand with our first principle. If our set of methods is complete and orthagonal, then there is no need for outside intervention. So our concept of object now consists of a piece of data that can be acted upon by an orthagonal set of methods with a well defined interface, and all this is contained as a seperate entity from its surroundings. Lets call this encapsulation. 1 Now, by itself, our encapsulated object concept isn't more powerful than procedurally written code. (Although it would be easier to debug.) We need a few more concepts to build upon what we already have. B. Genericity isn't just for canned goods. If you have ever been short on cash, or frugal, you have probably purchased a generic product. Something that is generic is the same as any other thing of the same type or class, at least in all the functional ways. A generic item can be substituted for any other item of the same type. What if we develop a set of encapsulated objects, a subset of whose methods are remarkably similar in function, lets say that those methods are math functions. Now if one of those objects has integer data and another has floating point data, an add operation performs the same type of action on both objects. Thus we could say that in this case add is a generic action. If we provide a suitable method for detecting which object should be called depending upon the data type, we can develop a generic function add. It won't matter if its operand is of type float or integer because the correct object method will perform the actual operation. Thus we have the concept of a generic function label. This concept makes our object concept a bit more powerful. Now, wherever we have similar operations performed on different types of data, we can define one label which we can then use for that operation no matter what the actual data type is. We have simplified our interface, since there are fewer function labels required. More importantly, we no longer have to know the exact type or structure of the piece of data we are manipulating, all we have to know is that the operation we want to perform is a valid one for that object. To simplify if the desired action is filling a basket with fruit, do we really care if it gets filled with peaches in place of tangerines? Genericity gives us a degree of data independence. 2 C. C. Inheritance without genetics We now have encapsulated objects with generic methods, what could make this concept more powerful ? In traditional programming we push complexity down the procedure chain. The real work gets done in seperate functions that we define to do a job. This allows us to concentrate on the big picture without being overwhelmed by details. The layout of a program looks like an upside down tree structurally. What if we have written an object to do a simple task and now we want to do a more difficult task that has the simple task as a necessary sub-task. Traditionally, we would call a subroutine. We could do that with our objects, but is there a better way ? Consider the upside down structure tree of the traditional program to be a family tree and the answer is obvious. What if we could define a new object that would automatically gain all the capabilities and data structures of the old object ? This is the concept called inheritance. If we define a child object with the old object as its parent, we can gain all its abilities (methods) through inheritance. Can we do better ? Sure, we can allow multiple ancestory ie. more than one parent, we can allow partial ancestory ie. gaining only a subset of the parent's capabilities, and we can redefine the parent capabilities adding our own structure and actions on top of what we inherit. Inheritance lets us both change and build upon the characteristics of the parent. This concept is made even more powerful by our earlier insistance on encapsulation. The parent can be changed to adapt it to new requirements and as long as the original interface and capabilities are not changed or removed, it makes no difference to the child object. D. Introducing GCOOPE We have reached the point where we have developed a quite powerful and useful concept. Now how do we apply it ? Brute force is always a good first shot, so we could write a program module that would define a data type or structure and methods (ie functions) to perform actions upon it. Well, we'll probably need more than one so we'll have to create and track each.... INSTANCE! Hmm, we have a set of these data regions or instances, and we need only one set of methods to act upon them since they are all members of the same,... CLASS! 3 Okay so we have this set of methods and data structure definitions for a class of object, lets call this the class definition. We need some sort of instance tracking database, lets use a variable to represent each instance. Call it type object, so I guess the variables would be OBJECT HANDLES. There's the first step, now lets incorporate the concept of genericity. We need some way of tracking all these methods for each of the .... GENERIC (shape changing) functions. Heck, we need some way of tracking all these generic function labels. Routing all these generic calls is going to be as bad as running a taxi service in Manhattan, we need a .... DISPATCHER to tell them where to go. Now for inheritance, we'll just add our ancestor's methods and instance memory to our own, of course we need a way to add our own methods that will overwrite any inherited methods for the same generic, and we have to keep track of which instance of the parent class belongs to our own instance,... you know we need a couple of object classes just to do all these things. At this point GCOOPE politely knocks at your door offering its services. GCOOPE has lists to track object classes and instances, it has a powerful function dispatcher and instance tracking mechanism, in addition it addresses problems we haven't even considered yet in this discussion. Topics like default destructors, temporary object deletion, error handling, class definition objects, and more. Best of all GCOOPE is written in and for the C programming language, bare metal power cloaked in an object friendly environment. 4 II. The GCOOPE Environment In this section, we will explore the strengths and weaknesses of the GCOOPE system and its implementation. GCOOPE is a powerful system but there are pitfalls and costs in using it. GCOOPE, like most other real world systems, is a product of choices between often conflicting goals and the resulting system is defined by those design choices. A. Object handles The data type object, defined in both pcstruct.h and GCOOPE31.h, is primarily used as a 'handle' for an object class or instance. The type object is defined in such a way that it is always at least 32 bits in size. It is normally defined as a long integer. For practical purposes, you can consider the object handle to be the object although in actuality it serves as a special type of index/pointer with internal structure members. For the actual details of the object handle structure, consult the T.R.M. (Technical Reference Manual). A secondary use of the data type object is to return values from a method function. When used in this manner, it should be cast as the appropriate data type if it is being used to return something other than an object handle. See the basic class libraries and the gcoope10.h file for assistance macros. 5 B. Class variables Class variables are variables or data structures that are accessible to all instances of that class. They are static variables that are the same for all instances of the class. The structure for a class variable area is defined in the class definition, each class can have at most one class variable structure/area. Another way of stating this is to say that the class variable structure area is a shared memory area private to class members but shared among those same members. All class variables must be accessed as members of the class variable structure. WARNING: In GCOOPE it is possible to access the class variable area of another class. DON'T. This type of practice defeats the purpose of encapsulation. If a change occurs to the structure in the owning class, you are in the same situation that you would be in with C++ friends, or normal C code, namely code re-write and re-compile time! In order to access the class variable area use the public kernel function, getCVptr. Full details of this function is given in the T.R.M. The class variable area is automatically created when the class definition is installed. When a class definition is installing itself as an instance of class 'Class', the size of the class variable structure must be passed as the cvSize parameter. 6 C. Instance Variables Instance variables are variables or data structures that belong to an instance of a class. This is a private static memory area for the instance. Each instance of a class will have a separate instance variable memory area. The structure for an instance variable area is defined in the class definition. Each class may have at most one instance variable structure. All instance variables must be referenced as members of that structure. WARNING: In GCOOPE, it is possible to access the instance memory for another class, or ancestor instance. DON'T. This practice makes your code dependant on the internal structure of those other modules, it defeats the entire purpose of encapsulation. Using this type of practice will result in code that is full of side effects and cross dependancies. To create the instance variable memory area, you must use the public kernel function makeInst. Full details of this function is given in the T.R.M. This function should only be called within the constructor (New) method of a class. It must be called prior to any ancestor constructor calls. To access the instance variable memory area, you use the public kernel function getIVptr. Full details on the use of this function is given in the T.R.M. When a class definition is installing itself as an instance of the class 'Class', the size of the instance variable structure must be passed as the parameter ivSize. D. Methods The generic GCOOPE method prototype is defined as: typedef object (*method)(object,...); A function of type method takes at least one parameter of type object and returns a type object. The first passed parameter must always be an object handle of type object. Additional parameters may be passed. 7 In our method type definition, you may observe the use of the variable argument parameter ... This is handy because we can use that one type method no matter how many parameters are actually required by an actual method. This does present some dangers. First, passing a pointer as a variable argument parameter is like running barefoot through a minefield. Second, passing different data types in a variable argument list is like adding machine gun and mortar fire to that minefield. Unless your name is Superman, you will take serious hits. At first glance, it seems we've put ourselves in a dilemma, we have a powerful abstraction that is too dangerous to use, right? Wrong. Why are you passing pointers to a method in the first place ??? You should be passing object handles! Object handles are much safer because they undergo some error checking and their internal structural integrity is verifiable. In fact, you can add even more error-checking on top of the built-in checks in your class definitions. As for multiple data types, we already have a type object which can be used to pass almost any other data type. Why not use it? If you only pass one parameter in the variable argument field and if you are careful in your design of object class definitions, it is convient and fairly safe to pass a pointer or other data type. However, with multiple parameters, take heed. 1. RULE #1 When writing a method for use by an applications program, if more than one parameter is to be passed in the variable argument portion, always use type object for all variable argument list parameters, and always use an object handle to an object instance rather than any pointers to memory areas. Another solution is to use the experimental strong typing option. See typing.h. The best approach is probably to use a combination of the above advice, judicious use of strong typing and when in doubt be sure and use the return value typedefs. 8 E. Function Dispatching The function dispatching and instance tracking system in GCOOPE version 1.0 is far more sophisticated than in the earlier. Used correctly, they can make your programming chores easier, used incorrectly you will need a good debugger. 1. 1. g is the Name The function g is GCOOPE's function dispatcher, its usage is deceptively simply. Even though complete details are given in the Technical Reference Manual for the GCOOPE kernel, I'll repeat the usage info here. returnObjectValue = g(generic)(objectInstance, [methodParms,...]); Where: returnObjectValue is a return value of type object that is returned from the dispatched method; generic is the generic function value of type generic; objectInstance is the object type object handle of the instance or class being acted upon; and the optional methodParms,.... are the parameters passed to the method. If you want the details of its operation, see the Technical Reference Manual, here, just take for granted that it works as advertised. WARNING: SEE RULE #1 1. Incorrect casting of the return value either in the calling routine or in the method executed. 32 bit values are always passed, however depending on the method, all 32 bits may not be valid. Be sure to clearly specify in the class definitions what values are being returned by their methods. Also, whenever possible all methods with the same generic name should return the same data type. Otherwise you will be using the alternate definition of generic. 9 2. Passing the wrong type in the method parameter list. GCOOPE is a weakly typed system, method parameters are NOT type checked, so stay alert. Wherever possible in your method definitions be sure and validate the recieved parameters. If possible, use the experimental strong typing option. It DOES provide type checking for method parameters, although it is a bit awkward to use. 3. Passing a char or int type as the objectInstance parameter instead of an object type. Can you say "abnormal program termination" ? It cannot be stressed enough, GCOOPE is weakly typed, this is both a blessing and a curse, a strength and a weakness. If you are the type of programmer that commonly expects the compiler to catch or correct your type casting, I strongly suggest that you use the strong typing option and use the return value typedefs. Another point to watch for: with variable argument prototyping such as that used by GCOOPE, it is tempting to define methods for the same generic function that take a different number of parameters or totally different data types as arguments. DON'T. That is asking for trouble. The only exceptions to this are the generic functions New and Err, the definition of these functions require that you use different numbers and types of parameters. Just be careful and don't carry the practice any further than absolutely necessary. TIP: While it is handy to use the same generic function for many similar methods, make sure that the methods truly are similar enough to justify it. Not only are you tempted to use different parameter types or numbers as noted above, but you will also be slowing down your program when you improperly apply methods to generics. generic functions with large numbers of methods are slower in dispatching. 10 2. The Smalltalk Connection If you have previous experience with Smalltalk, GCOOPE will smell like home. There are a few snakes in the garden however. Smalltalk makes regular use of trivial methods, methods such as +. While you may define the equivalent Plus and the like generics in GCOOPE, don't use them where execution speed is important. You will have noticed that GCOOPE methods execute an order of magnitude faster than Smalltalk methods. This is because GCOOPE methods are compiled C code while Smalltalk methods are interpreted code. What may not be so readily apparent is that GCOOPE's function dispatching is very similar to Smalltalk's and so it is not much faster. What happens with 'trivial' methods is that the execution time for function dispatching dwarfs the execution time for the method. In other words, GCOOPE will slow down to near Smalltalk speeds when executing these types of methods. When speed is critical, and you only need to call such a method once or twice, do these 'trivial' methods locally where possible. For another idea see the next section. All in all, the use of Smalltalk techniques is generally a very good idea, GCOOPE is very close in concept to the idea of compiled Smalltalk. They share many of the same strengths and mannerisms. One trap to avoid is forgetting that GCOOPE has more capabilities with regards to inheritance than Smalltalk does, so don't miss out on reaping those benefits. 3. Local method caching If you are going to call the same method more than once or twice in the same statement block or routine, you can drastically decrease the execution time of the dispatching process. Just use the following call: methodToCall=p(generic, instanceObject); Where: methodToCall is a method type variable used to hold the method address; generic is the generic function index; and instanceObject is the object type object handle for the instance. 11 For this technique to apply, instanceObject must remain constant, or must be a non-inherited flag class instance of the same constant class. (flag classes will be explained in detail later, also see the T.R.M.) Also if instanceObject is not a non-inherited flag class, you will have to make the following call prior to the one above: instanceObject = steer(ClassOfMethod, instanceObject); This will set the instanceObject's instance tracking variable to correctly point to the desired instance data. With all the above limitations, this technique will not be used often, it can however be used in some cases to get around the 'trivial' method penalty described in the previous section. PROGRAMMER'S NOTE: The best solution to the need for speed in function dispatching is to rewrite the dispatcher so that it does not make any function calls. Then if writing a platform dependant version, you can drop to assembly and hand-optimize the result of the first step. This is the path I used in tests of PCOOPE version 2.1 and while the dispatcher has changed quite a bit, the new dispatcher should be capable of reaching comparable speeds. (In 2.1 the speed increase was quite remarkable (try >10x) and well worth the effort.) If you have a copy of version 2.1 examine the dispatcher routine for hints. 12 4. Steering the instance In the section above you caught your first glimpse of the function steer. It is used to 'steer' the instance tracking mechanism so that makeInst and getIVptr will work correctly. Normally this is done automatically by the dispatcher. However there are certain instances where you must call steer from within your class definitions. a. RULE #2 The rule for the need for a manual call to steer is: any time you are calling a generic function that will dispatch to a method that is defined in an ancestor of the current class, you must use manual steer -ing. Prime examples of this situation occur in methods for the generics New and Kill (and to a lesser extent in Err). The procedure is fairly simple: retVal=g(generic)(steer(ancestor, instance),[parms]); Where: ancestor is the class object handle for the ancestor class; and instance is the object handle for the current instance. This situation occurs because the current instance is used by the dispatcher to determine what the current class is. If the current class and an ancestor class have methods for the same generic, you could never reach the ancestor method because the dispatcher would call the current class method. The above call 'fools' the dispatcher into believing that the ancestor class is already the current class and so the call goes through. Also see the macro ST() in gcoope10.h. Some commercial products similar in concept to GCOOPE initialize the ancestor classes for you prior to calling the child class's method for New. The problem with this is, that either you end up with dozens of parameters in the call (to pass all the ancestor initialization parms and don't ask me how you track that with multiple ancestory) or you don't have any control over ancestor initialization. Frankly, either case is unacceptable. 13 The above solution isn't what I sought, but it is usable and achieves the desired purpose. The only other solutions I could find added way too much complexity to the kernel code and still didn't solve the occasional problem of calling an ancestor method outside of New or Kill. You would have a bigger, slower function dispatcher, and still need the steer routine! You will also need to use the above technique in your Kill methods wherever they occur, for exactly the same reasons. Another place where you may find yourself in need of the above solution is when you redefine an ancestor method in a way that adds to the inherited method. In other words, where you still need to call the ancestor method. Another way to look at the above is, if you don't know the ancestor class you don't need to call steer. Applications programs should never need it. Performance wise, the extra call doesn't hurt you very much. First because the dispatcher won't have to go through a steer procedure, and second because it will only occur normally in the constructor, destructor, or error handler portion of the class definition. New and Kill methods are one time cost routines per object instance and if your code is calling Err very often something is wrong . F. Classes and Pseudo Classes We have not yet reached the point where we are ready to begin writing class definitions, but in order to understand some of the next section, you need to introduce yourself to some of the concepts. Classes are comprised of an optional class variable structure definition, an optional instance variable structure definition, method definitions, and an installation function. Pseudo classes are classes that cannot be properly inherited, they have no class or instance variable structure definitions, although they may make use of structure definitions defined internally or externally. Pseudo classes may define methods, although they don't have to. They may also define non-generic functions. They must include an installation procedure. 14 An installation procedure installs the class or pseudo class definition into the GCOOPE system. It is responsible for inheritance and genericity. For a class definition, it is also the place where class variables may be initialized. Non-generic functions are simply standard C library type functions instead of generic function methods. Class definitions are where objects are defined, pseudo class definitions interface to the platform and/or hardware. Pseudo class definitions are also used to expand GCOOPE's kernel adding necessary or optional functionality. The pseudo class concept was developed because of a desire to treat kernel expansion in a modular way that could make use of kernel resources used by normal class definition modules. G. Flag Classes The last topic to cover in the GCOOPE environment is the flag class. A flag class uses a portion of the object handle as an index to its class object. This means that the lower 16 bits of the object handle are no longer needed by the GCOOPE system. A flag class uses those 16 bits as its instance variable storage area. Thus, for the overhead of a simple flag check, flag classes can implement classes such as Boolean, Char, Int, etc. with no instance memory being dynamically allocated! For full details of the object handle structure see the T.R.M. A flag class is subject to certain limitations. First, it cannot inherit any class which has instance variables. Second, it must check to see if it is the owner of the instance object handle in each method prior to accessing the instance variable. Third the New method for such a class must be dual purpose. If it is being inherited, it must behave exactly as a normal class. Basically, the flag class must be careful to check whether or not it has been inherited with respect to the current instance. If it has been, it must behave exactly like a normal class. (Note: a flag class's ivSize must be given as sufficient to store its variable type.) Before attempting to create a flag class definition, it is strongly suggested that you study the relevant portions of the T.R.M. closely and also be sure to check the example code provided. 15 III. Creating a class definition A. Layout of a class definition module The first statement in a class definition is #define CLASS . This names the class and becomes the symbol string name for it. The second statement is #include "GCOOPE31.h" and the third statement is object CLASS;. Following those statements should be your structure definitions for the class variable and instance variable structures. These structures are optional. Hint: instead of creating them as a named structure, typedef them as a structure. Next should be the class constructor method definition, followed by any user defined method definitions and by a destructor and error handling method if desired. The final portion of the class definition is the class installation function definition. B. Method definition section The only method you must define is a constructor. It should call makeInst and then call the constructors of each superClass by: p(New)(steer(superClass,instance),[initParms]); Other than that, you're on your own as far as methods go. Just make sure they all fit the following prototype: retValType methodName(object,...);. A few points to remember however, if you dynamically allocate memory within your object: 1. use the kernel memory management shells s_calloc, s_free, s_malloc, and s_realloc. 2. write a method for Kill that frees the memory and calls p(Kill)(steer(superClass, instance)); for each superClass. (immeadiate parent) and ending with p(Kill)(Object, instance);. 16 C. Installation routine The name of the class installation routine MUST be given as the macro CLASS_INSTALL. This insures compatibility with the class installation macros. The first statement after any local variable and pointer declarations is: CLASS = p(New)(Class, sizeof(classVarStruct), sizeof(instanceVarStruct), [SuperClass object handle list], END); This creates the class definition, you may now optionally get a pointer to the class variables area by getCVptr and initialize the class variables. The next step is to add each method, this is done by: addPMthd(CLASS, genericName, methodAdr); You can also block ancestor methods for a generic via rmvPMthd (see T.R.M.). This is useful for ancestor methods which should not be directly called except from a method defined by this class definition. A note here about inheritance, the call to p(New)(Class,...) will setup a description of an instance memory block for instances of this class. That description includes the instance memory blocks for all ancestors. After performing that setup, the class creation method will add ALL the ancestor methods to the current class automatically. The order that you specify superClasses (immediate ancestors) is important. Those given first in the list will take precedence over later entries. Their methods will overwrite any conflicting or overlapping methods for a generic. When you use addPM or rmvPM you will overwrite or remove any listed ancestor method for the generic in question. 17 IV. Expansion and Growth In the short documentation with the PCOOPE version 2.1 release, I foolishly stated that the interface was written in stone and that all expansion and growth would occur through the class definitions. Alas, as so aptly put by the first user's group member, G/PCOOPE isn't ready for that. He was 100% correct. In version 1.0 I have attempted to write the kernel in an expandable manner. That is one reason why the provided code is optimized for little else than debugging. For each target platform, an optimized version of the appropriate kernel module(s) should be created. Low level functions that prove necessary can be added via the pseudo class expansion module route. In the design of this system, I have attempted to provide for the possiblity of future expansion at each level of the system. GCOOPE has potential as an application framework. For that potential to be realized standard, generic, well defined, and expandable interface specifications need to be developed. Available public domain libraries that help to meet these needs should be incorporated as expansion modules. No doubt, some custom code will have to be written. The GUI portion alone will prove quite an undertaking, if GCOOPE is to realize its potential a GUI specification must provide a path to compatibility with Windows, X-windows, and other popular GUI environments. GCOOPE also has potential as an object oriented operating system extension. In this concept, the GCOOPE kernel would be a system resource with instantiated memory. Such an extension would have to include the ability to share dynamically loaded modules. This type of extension to the GCOOPE framework is not as difficult as it might sound at first, GCOOPE could be made available as a process on a Mach 3.0 or QNX microkernel. There would, no doubt, be many additional demands placed upon the GCOOPE system however. As of this writing, GCOOPE is in its infancy, at the very least it serves as an exploratory framework into the next generation of object oriented programming languages, application frameworks, and operating systems, at the most .... you be the judge. Or better yet, join the GCOOPE user's group and join in the expansion and implementation of the GCOOPE system. 18 Appendix A. Appendix A. The GCOOPE USER'S GROUP JOIN THE GCOOPE USER'S GROUP! For a one year membership send check/money order in the amount of $20.00, (made payable to Brian Lee Price) to the address below: Brian Lee Price GCOOPE USERS GROUP RD2 BOX239AA CLEARVILLE, PA. 15535 Please include my middle name to avoid local post office difficulties :). User's group members receive the latest updates to the GCOOPE system, new class libraries, expansion modules, etc. Limited free technical support over the phone is available, and access is granted to a User's group only private limited access bbs, you will be added to the quarterly newsletter mailing list, and other services are available. It is my hope that the user's group members will participate in the growth of the GCOOPE system, expanding its libraries and its capabilities. This way, all user's of the framework will benefit from a degree of co-operation that is often impossible to achieve with a commercial product. 19 Appendix B. Appendix B. About the Author (in case you're really bored) The author is a starving artist type, currently unemployed and living in the mountains of south central Pennsylvania (Hi-technology's equivalent of Bumf---, Egypt). First introduced to computers after a tour in the USAF as a television studio technician in 1982. First programming experience in assembly language on Intertec (who?) SuperBrain II CP/M and Atari 800XL machines. Catching the PC wave, the author worked throughout most of the 80s as a computer technician/consultant ending up working in the computer evaluation laboratory of Entre' computer corporate where he was allowed to play with early networks, CAD/CAM, and the first 386 machines. For the late 80's and early 90's the author was a self employed electronic systems and circuit designer/consultant having some success with early video frame digitizer designs for the Apple Macintosh II before they butchered its expansion slot capacity. The last few years spent working with embedded systems such as traffic control computers and monitors and playing around with computer languages, the author began his programming career in C in 1993. GCOOPE is his first publicly released software package. When not working on the GCOOPE project or his (t)rusty '70 Ford F-150, the author is working with local business and political interests to establish a free public access bbs and Internet link in his home county. Anyone need the above talents for a paid job??? 20