Showing posts with label Roger Donnay. Show all posts
Showing posts with label Roger Donnay. Show all posts

Monday, January 26, 2009

Back to the Future by Roger Donnay [14 July 1996]

This thought provoking article by Roger Donnay, the author of dCLIP(tm) was originally posted on comp.lang.clipper by Andi Jahja of Indonesia. To make it more interesting, I have also added Terry Carmen's rebuttal of certain sections also posted on 29 June 1996. Just received an e-mail on 13th July 1996 from Roger giving his consent to publish this article.

Back to the Future by Roger Donnay


BACK TO THE FUTURE

Like all other forward thinking programmers, I have been watching the horizon anxiously for the last several years. While staying firmly in the Clipper arena, I've done work in Windows via products like VO and VB and have played around with FoxPro and dBase for Windows. But my vision has been foggy. For the first time in many years, I've been groping for my direction. I've noticed that we've all been eyeing each other for clues. The leaders have been following. Microsoft seems to hold our future in its hands. We are bleeting like sheep. But I recently saw beneath the sheepskins. We are not followers! We have been hit in the back of the head with a big stick and sheepskins have been thrown over us. We're in a state of amnesia. It is inevitable that we will "come to" and remember wer'e not prey after all. Before our collective coma, programmers were the leaders, the discoverers, the thought pioneers. We must shake off our myopia quickly lest we all be led to slaughter. To regain our vision and perspective, we must step back and see the larger human picture, not just the technical parts we've been focusing on. This bigger picture presented itself to me recently at the 1995 CGDN Clipper/VO Conference in The Netherlands. On the last day of the conference, I was asked, in a panel discussion, to comment on the future of software development. The other panelists made predictions that included statements about Windows, SQL, OLE-2, multi-media, OOPS, and basically a universe that will have little or no resemblance to that in which most systems developers make their living today. I was fascinated by the technical nature of the panelists predictions and how they described this new world in detail as though it were a well-understood fact that it had to evolve by some pre-written script. It reminded me of the Popular Science magazines I used to read as a teenager. I recall reading how the world would automatically adapt to and accept all modern technology. There would be no resistance to change because those who promoted change did so for our own good with the goal of creating a better world for all mankind. Basically, the message we received was this:

"We'll build more factories with the latest robotic technology, put our unemployed to work, which will allow us to increase the goods and services in our economy and our money supply so we can pay higher wages and have more money for scientific research. We'll focus on creating new robotic technology so we can free up workers to do other things, like space technology research, and mining the asteroid belt so we can create spaceships that move faster than the speed of light to mine the universe. We'll invent a matter transformation machine so we can have unlimited resources without leaving the planet. Factories will be totally run by machines. Everyone will be guaranteed a job. Minimum wage will be 100 thousand dollars a year. College and Health Care will be free. We'll have unlimited money for education, college will be more user friendly, and if you can understand how an engine works you can be a physicist. Eventually, we'll colonize the universe and our new will make disease and death obsolete."

Many of us who believed the above predictions also believed that these changes would occur within our own life-time. As I listened to each person on the discussion panel, I was starting to realize that what was being described was a changing software and information universe that mirrored the above predictions for our changing physical and social universe:

"We'll build an information highway with the latest computer, software and communications technology, which will put our unemployed to work and allow us to increase the number of computers and logged on users. We'll focus on creating libraries of common sub-system objects to free up programmers to do other things like designing desktops and reports. We'll create a single standard for common user interface, a common language and a common protocol to make it possible for every single human being to be "computer-literate". No one will need to go to school to learn how to use the new computers. College will become obsolete. All the knowledge in the universe will exist on the information superhighway and access to this information will be faster than the speed of light. Anyone who understands how to use Windows will be guaranteed a job as a programmer. Eventually, we'll colonize the human mind and our new information system will make human error obsolete because the operating system will guide us through the depths of cyberspace."

Will this new world be created and maintained by a small number of monopolistic giants who will continue to make the rules while software developers and users happily comply and adapt to the "new paradigm"?

Not likely.

Ok, now that I have vented my emotions and basically put myself at risk of being labeled as another "paranoid programmer", let's be a little more realistic and pragmatic and view this changing world in a way that will give us some tools to better handle the transition for ourselves and our customers.

A BODY IN MOTION

At the CDGN I made the following statement:

"Operating Systems and Languages have a tendency to hang on much longer than we think they will. I predict that there will be a Clipper 5.4, 5.5, and even 6.0."

I made this prediction because of what I know about the process of change. As an Electronics Engineer, I spent as many years developing hardware as I did developing software and became acutely aware of how easy it is to evolve technology when human interface and compatability is NOT involved and how hard it is to evolve technology when human interface or compatability IS involved. For example, the evolution of hard-disk technology, memory technology and integrated circuit technology has leaped forward year after year, whereas the basic computer display and keyboard are not much different than those I was designing back in the late 70's.

Those of us who were around when IBM first introduced the PC in 1981 were amazed at how fast CP/M died after MS-DOS was introduced. This "instant death" phenomena led many to believe that DOS would die just as quickly after the introduction of Windows. PC Magazine predicted the death of DOS in 1988 yet it is still going strong 7 years later. There are several reasons why CP/M died so quickly but probably the main reason was because MS-DOS was almost a clone of CP/M but written for a different processor that was being mass produced and could access 10 times the memory. MS-DOS was NOT a new paradigm. In fact, converting CP/M applications to DOS was so simple that it required simply purchasing a cross-assembler or cross-compiler to convert 8080 code to 8086 code.

After the release of Visual Objects, many Clipper product distributors were gearing up for the death of Clipper by replacing advertising space for their Clipper products and training with advertising space for new Visual Objects add-ons and training. What happened next was not what was expected. Not only were the distributors dissatisfied with the poor performance in sales of VO add-ons but they were surprised at how Clipper add-ons continued to sell at the same rate even though advertising space had diminished. What they failed to take into account was that there is a "momentum" in the Clipper world that cannot be simply shifted to the VO world. Maybe they thought that this momentum would gravitate to VO in the same way that the momentum in the dBASE-III world easily shifted to FoxPro. Well, in retrospect it is easy to see that FoxPro does everything as well or better than dBASE and with no required "paradigm shift", whereas VO cannot even handle the simplest of Clipper applications.

Clipper is a body that has been in motion for 10 years, and being DOS-based, also brings with it the collective weight of 70 million DOS users. This momentum is not only impossible to stop but very difficult to even slow down.

THE NEW PARADIGM

At one time or another, most system developers have been confronted with a new paradigm that requires they take a different view of their world. Assembler programmers were confronted with C, C programmers were confronted with C++, mainframe programmers were confronted with microcomputers, procedural programmers were confronted with OOPS and event-driven programming. Windows is a new paradigm that offers great new opportunities for some programmers, and great new obstacles to others. The key to success in adapting applications to the new paradigm is not only knowing "how" an application should be converted but also knowing "if" and "when" it should be converted.

Even though many new paradigms bring excitement and opportunity, they also are a paradox, because while being pulled by customers to support the new methods, the same customers will resist using the methods they were pleading for. Why? Because people have difficulty adapting to change. Systems developers are people too. I try to balance my view of the future by looking at the past and watching human behavior. It is very unlikely that humans will behave any differently in the next 100 years than they did in the past 100 years just as it is unlikely that software developers will behave much differently in the next 5 years than they did in the past 5 years. We will continue to have the same prejudices, religions, fears, political alliances, learning disabilities, and overall dysfunction that has always kept us from fulfilling our vision of the "perfect world".

One of my customers is struggling with trying to decide on a development strategy for the next 2 years. He has a very powerful, data-driven, Clipper application that has evolved for nearly ten years. His business is growing faster than ever and he has never lost a sale to a competing Windows product, yet he is being pushed and pulled to convert his application to Windows. He said to me the other day: "I hear that in 2 years all DOS applications will be converted to Windows." I asked him if there was anything his application could do under Windows that it can't do under DOS and his answer was the same answer I get from most developers. They all say that their customers are asking for the following features:

1. Want to navigate the application with a graphical mouse. 2. Want consistent pull-down menus and better dialogue screens. 3. Want to suspend operation at any time and call up a different program, or the same program in another window to perform different operations. 4. Want to minimize the application window to the desktop with the click of the mouse. 5. Want to move the application window to any position on the desktop with the mouse. 6. Want to run other Windows programs without needing to quit the application. 7. Want to run the application in the background to handle tasks like indexing files, backups, etc. 8. Want to navigate data-entry screens with the mouse. 9. Want to run the application in a Windows memory environment.

What many Clipper developers (and their customers) do not realize is that 5 of the 9 requests listed above are already supported by Windows 3.1 and OS/2 in their "Windowed DOS boxes". The other requests are simply a matter of adding mouse support and better menus to their existing applications and linking in protected mode. This can normally be accomplished in a few weeks or a few months with the help of third-party libraries and DOS extenders for Clipper.

Developers can give their customers the features they want without the huge risk and development cost associated with converting an application to Windows. Eventually, as Windows development environments evolve and the tools and languages become more stable, it may be practical and desirable to complete the tranformation to run as a Windows application rather than in a DOS box window. The developer I mentioned above was wise enough to take my advice and spend a lot less time writing a new system and little more time educating his customers. He tells his customers: "You don't take a boat to Denver, and you don't take a car to China."

TOUGH LOVE

In the early 60's, manufacturing giants who made home appliances, hi-fi equipment and televisions tried to "push" a new paradigm for the home onto American consumers. The fallout from this debacle was a product which became known as the TV/Stereo Console. The objective was to create a product that would supply complete home entertainment in a neat package that was also a nice piece of furniture for the living room. It was quickly accepted that this was the wave of the future so these products gained immediate acceptance despite the fact that they were inferior in performance to systems that consisted of separate components. Such products no longer exist because the consumer "wised up".

In the 90's, Clipper programmers are being "pushed" by other developers to convert their legacy applications to Windows or to even drop support for these applications completely and move over to the new paradigm by finding new customers or by dragging the old customers, kicking and screaming, into the new world. True, some of the force comes from "pull" by customers who have already adapted to the new world but much of the customer pull comes from a misunderstanding of the nature of the new paradigm and how it affects the way we do our work. As each legacy application is converted to Windows (at enormous cost), the customer will wonder why he paid so much money to create an application that is less productive and less reliable than the Clipper application that served him so well for so many years. Eventually, the users will start to understand why they will be better served by programmers who can help them build a "hybrid" of new paradigm applications like word-processing and desktop publishing and old paradigm applications like database management and data-entry. This hybrid environment will consist of "bridges" between DOS, Windows, OS/2, UNIX and others. Software developers have a responsibility to their customers. We talked them into buying computers and software with the promise that we would always be there for them. We need to be honest with them and ourselves and resist the temptation to move them into an environment that we cannot support, that we do not believe in, or cannot return the investment of time and money. I once had a sign over the door in my office that read: "49 out of 50 people will tell you why something can't be done. 1 out of 50 will show you how it can be done." Clipper developers are unique. We are the 1 in 50. That's why we use Clipper instead of dBASE or FoxPro. We know when to use Windows, when to use OS/2 and when to use DOS. If your customer says he won't use a software product that isn't a Windows application, then I suggest you turn him over to a VB or FoxPro programmer and don't look back. Wait for VO. It isn't quite ready for most of us, but when it is, it will be great. Like the prodigal son, your customer will come home and all will be forgiven.

RIDING THE SMALL WAVES

Software developers often get in trouble because they cannot hear what their customer is saying or they respond to the rhetoric that the customer has picked up from the magazines and other developers rather than the real meaning of their message. Most of the time, customers want small changes, not sweeping changes to their working environment and their applications. Windows is such a major paradigm shift that simply converting Clipper applications to Windows is not possible. Computer Associates promised that this would be possible via their new product, Visual Objects (VO) but after the dust settled we all discovered that if we want to write Windows applications we had better start from scratch because the old application code is just not compatible. I fell victim to this kind of thinking myself and even wrote a white paper called "Migrating Clipper applications to Visual Objects". I wrote the paper while attempting to convert my Clipper library, and even though much of the code conversion was possible, the end result was an application that wasn't nearly as good as the original application running in a DOS box. I deluded myself and some of my customers into believing it was possible even though, deep in my soul, I knew it was not practical and would be a waste of time. I believe in Windows and I believe in Visual Objects. Windows is the operating system of the future and Visual Objects is the development language of the future. Both products are magnificent in concept and deserve to be highly successful. Some day, they will come together along with other third-party tools to give us the ability to create database applications that will soar above anything that can be accomplished in DOS. I don't believe, however, that the operating systems, the languages, the development tools, or the users are ready for the complete migration to Windows. Database applications require a much more stable environment than word- processing, desktop publishing, games, or graphics applications. Never has there been a less stable, or more complicated environment than what has been served up to us in this new paradigm. The big appeal for Windows is the promise that it will give us user- interface standards and connectivity standards that will make it possible for anyone, without training, to use a computer. It's understandable, if your'e developing an operating system for use in the home why you would set this as an objective. What isn't understandable is why we would want to establish this as the model for business. Business has never had any problem adapting to computer systems that require user- training. On the contrary, the most popular business applications, like Word Perfect and Lotus 123, were not at all user-friendly and sold millions of copies of after-market books and an entire industry was developed to help train the users.

When asked to predict the future I can't control my urge to get more philosophical than technical. My favorite philosphers are those who tell us how things "will be" rather than tell us how things "should be"., i.e. "Future Shock". In our business, we must not only be sensitive to future shock in our customers, but also to the effect of "Revision Shock" or we can find ourselves quickly out of business. MicroPro, the developers of WordStar, were the first in the microcomputer software business to be dealt this painful truth and they didn't survive. There are people who will always be looking for the "big bucks" or the "big thrill" by riding the leading edge of the technology wave. It takes a lot of skill and hard work to do this successfully. Most of us have been on this leading edge once or twice in our lives and have either succeeded or failed greatly. Others have jumped on the wave because of a fear of being left behind with nowhere to go. Those of us who have been around for awhile know that each technology wave is followed closely by smaller waves that are much less turbulent yet offer the same amount of opportunities. Experienced programmers and consultants have also discovered that it is better to respond to "pull" from their customers than to "push" from their suppliers. You should ask yourself "How much of my development efforts are being influenced by other software developers and how much is being influenced by my customers?" Here's an excerpt from a letter I received from Jan Blum, one of my customers whose Clipper application I have been maintaining for the past year. She says:

"It is a principle of nature that before and after every expansion, there is a contraction. The market and the society are the same. We've been expanding for a long time in the software and hardware world. Those of us in business are contracting for a minute now. We've spent a lot of money and are analyzing our return and catching our breath. We are not ready for more changes that don't work. We are feeling more cautious. I'm not near as ready to buy the latest, the newest, the hottest, the fastest. I don't envy those that do/can. I want something that is solid, not flashy. There is a little anger, a little fear, a sense of having been a guinea pig for the market. I'm contracting. I'll push forward again. But it will be AFTER a pause and I'll be wiser this time."

UNDERSTANDING OUR NICHE
The world of software development is so much different today than it was 10 years ago. For one, the "niche markets" are much larger than they were because the overall computer market has grown by an enormous factor. Each vertical market or niche market has a life of its own. These markets do not react to vaporware announcements and are much easier to predict and service than the larger horizontal markets. Clipper is an example of a niche market that is still very much active on an international scale and will continue to be active for many more years. There are several reasons why I am making this prediction:

1. To many Clipper developers and database application users, Windows database applications have not yet made a strong enough case that they provide a better solution than a well-written Clipper application running in a Windows or OS/2 "DOS box".

2. DOS and Clipper are much more mature and stable than both the Windows operating system and Windows development platforms.

3. A Clipper developer with 5-10 years of xBase programming experience can develop a robust database application much quicker than a Windows developer with only 1-3 years of experience.

4. Most large Clipper applications under DOS load and run faster than most Windows applications with the same functionality.

5. Clipper applications will run on older hardware.

6. There are hundreds of thousands of Clipper legacy applications being used in businesses every day. It is far easier to upgrade an existing application with new features and a new look that use improvements in DOS (like Windows and OS/2 DOS boxes) than to convert it to Windows.

7. Converting a Clipper application to a Windows application is nearly impossible without completely rewriting the application.

Both Windows 95 and OS/2 have introduced a better DOS than DOS. This will give new life to Clipper legacy applications and make them work and look better without needing to change any code. Now it is possible to upgrade a Clipper application with full CUA compliance and mouse-support by simply using an add-on library or upgrading to Clipper 5.3 and a few minor code modifications. The new "better DOS-boxes" will give you a graphical mouse, sizable windows, user-selectable fonts, multi-user support and better memory-management.

THE NEW POLITICAL LANDSCAPE
The conventional wisdom in the software world is basically an understanding that Windows is a "black-hole" that will swallow up everything else including DOS, OS/2 and maybe even UNIX. Programmers have resigned themselves to the fact that one way or another they will be working for Microsoft or will have to go back to the farm. This new wave of thinking comes from the enormous influence of MicroSoft and their desire to control our workplaces and eventually our homes. It seems to me that many Clipper developers are accepting this as some kind of "fate" rather than seeing it as an "opportunity". Possibly this attitude comes from the fact that most products on the shelves at your local computer store say "MicroSoft" on the label and every PC comes preloaded with MS-DOS and Windows. There are those of us who have no problem living in a world in which a single monopolistic giant tells us "the way things ought to be" and will adapt very well to this new system. I can only predict that this may be the way of the near future (next 5 years) but is certainly not the way of the 21st century.

Who would have ever thought that Big Blue (IBM) would be the underdog in this new "race for cyberspace", but I'm suddenly finding myself rooting for them with a vengeance. Why? Because we need choices. We all became Clipper programmers because Clipper was a much better choice than the "more popular" dBASE. MicroSoft's monopolistic practices are crying out for U.S. government invervention and it is very likely that a MicroSoft "break-up" will occur before the end of the century that will lead to the "De-Installation of Windows". IBM is betting heavily that this will happen and have invested a lot of money in OS/2. Several million developers and users are also believers and have jumped on the OS/2 platform. In fact, the publishers of OS/2 Professional Magazine refuse to install a single Windows product on their computers.

CONCLUSION

The more some things change, the more others tend to stay the same. Would you rather read a book or constantly gaze at a computer screen? Is a push-button transmission really better than a stick-shift? Will a mouse replace a keyboard for data-entry? When will the world adopt the metric system? The future has some interesting surprises in store for us, but fortunately, the Gaia principle will keep us from self-destructing. I look forward to writing my final article about Clipper just before my retirement in the year 2035.

Biography

Roger Donnay is the president and founder of Donnay Software Designs, a Boise, Idaho, USA firm specializing in the development of programming tools, dynamic-linking systems, and database management systems for Clipper.

Roger is the author of dCLIP (tm), a DBMS library, IDE library, dynamic-link system and tutoring system for Clipper and Time-Cube (tm), an appointment scheduler for Clipper.

Send your comments to:

DONNAY Software Designs P.O. Box 16330 Boise, ID 83715-6221

Voice - 208-331-2516 Fax - 208-331-2621 BBS - 208-331-2623 CIS - 73227,1225



Rebuttal by Terry Carmen

Subject: Re: Back To Future

Date: 29 Jun 1996 15:12:41 GMT

From: tpcarmen@servtech.com (Terry Carmen)

Organization: Meadowbrook Software

Newsgroups: comp.lang.clipper

In article <4r2878$8t3@server2.rad.net.id>, Andi Jahja says... > >I Would like to Share The Following Article of Roger Donnay:

Nice aritcle, but he's missing the point.

I don't especially care if my customers want a DOS app, or a Windows app or a Unix app or something to run on an HP-41 calculator. If they have a need (real or perceived) and money, that's all the justification that's necessary.

Unless they want something that simply won't work as they expect it to, it's not my responsibility or (or even option) to "reeducate" them.

Donnay says "If your customer says he won't use a software product that isn't a Windows application, then I suggest you turn him over to a VB or FoxPro programmer and don't look back."

"Let them eat cake"? Send them over here, I'm thinking of buying a new car and would like to pay cash.

He also says "Wait for VO. It isn't quite ready for most of us, but when it is, it will be great. Like the prodigal son, your customer will come home and all will be forgiven."

Not likely.

The effort required to get back a lost customer is almost the same as is required to find a new customer (sometimes more, sometimes less, depending on what they think of you).

Once they have found that you can't (or worse can, but won't) fill their needs, they'll be gone like a puff of smoke in a hurricane.

The dark reality is that, for whatever reason, Windows comes on pretty much every PC built, and people are conditioned to want it.

It makes them feel good if they can buy software that does what they want and runs under Windows.

When they can't buy software that does what they want, the only reasons they come up with are:

[1] They're using the wrong O/S

or

[2] Their software vendor is incompetent

The answer isn't #1, since Bill Gates is a billionaire and is by definition rich and sucessful, and they do not blame themselves because they have purchased the same thing millions of other people have.

Guess which one they pick?

It's much easier (and more profitable and usually better) to give people what they want, instead of what you think they should have.

If they say "Windows" and you say "DOS", there's a good chance they'll say "goodbye."

Terry

Programming Techniques for Memory Management (Part 2) by Roger Donnay

This section written by Roger Donnay is the second part of CA-Clipper 5.x Memory Management by Roger Donnay & Jud Cole.


The Affect of Databases on Memory Usage

Many programmers have taken an affection to "data-driven" programming to speed up the development of custom applications. Since CA-Clipper evolved as an X-Base type language, the common approach to data-driven programming is to create an "engine" or "kernel" .EXE program that retrieves "custom" information about the application from a set of database files. The more sophisticated the system, the more data files are required for configuring the application. It is important to understand the impact of databases on memory usage when designing applications that use many databases.

When a database is opened with the USE command, all the field names are placed into the public symbol table. This allows database field names to be used in expressions in the same manner as memvars. Because this symbol table is PUBLIC, the field names will remain in the symbol table, even after the database is closed. CA-Clipper employs no mechanism to remove symbols from the symbol table, only to add them. As each database is opened the symbols are added, thereby reducing the remaining available root memory ( MEMORY(0) ). It is conceivable that an application could run out of memory if many databases are opened and closed. Fortunately, symbols of the same name are "reused" in the symbol table, so if you open and close the same database many times, the symbol memory space will not be reduced. Keeping this in mind, it is a good practice to use databases with fields of the same name.

To improve speed operation, some programmers will open data-dictionary databases at the start-up of the program, load the custom program configuration into arrays, then close the databases. This action will reduce the amount of memory needed for data structures and file buffers and lower the number of DOS handles required, but the symbols will still be added to the symbol table even though they may never be accessed by the program. A data-driven application in which the databases are used only during the start-up of the application could be re-designed to convert the database information to a text file to generate a "runtime" version of the application that will load the array(s) from a text file rather than databases, thereby eliminating the symbol-table problem.

Declare FIELDS in your Compiled Codes

To prevent the need to add Field Names to the symbol table at runtime, it is always a good idea to declare fields to the Compiler by using the FIELD statement in your source code. This will insure that the symbol table memory is allocated at the time the application is linked into an executable program rather than during the running of the program.

Managing Code Segment Size ( C/ASM)

If you are writing code in C or Assembly, then the linker's overlay manager must bring an entire code segment into memory at one time. If you have many large code segments, then the size of the overlay pool may need to be increased. Normally, the requirements of the application dictate the size of the code segments, but it should be noted that a little extra time in optimizing the size of your code will usually pay off if you have a large library of routines.

CA-Clipper-compiled P-Code is loaded in fixed-size pages so this is not a requirement when programming in CA-Clipper.

Reducing RDD Memory

Try to use as few database drivers in your application as is necessary. For example, if you choose to use the DBFCDX driver for compatability with with FoxPro files, then make sure that your application does not link in the DBFNTX (default) driver unless you must have .DBT or .NTX compatability. This is accomplished by modifying the source code in the RDDSYS.PRG that is included with CA-Clipper.

Using the "Garbage Collector"

A variety of memory problems can be caused by memory "fragmentation". Fragmentation can be caused when symbols are added to the symbol table or other "locked" memory is allocated before other "unlocked" memory segments can be released. Database languages require that the symbol table be non-contiguous and not pre-allocated but instead must be allowed to "grow" during the application. This "late binding" is the essence and power behind data-driven engines (like many Clipper applications), unfortunately this kind of flexibility can cause symbols and work-area structures to be added haphazardly thruout the conventional memory space. MEMORY(0) can report that there is plenty of memory available but after the program runs for while it exists in many small segments rather that the single large segment that was available at the start of the application.

When a large segment of memory is required by the application and is not available, Clipper has to try to move around segments that are not "locked" so it can get sufficient memory for the routine that's being called. This is "garbage collection". Unfortunately, symbol memory and other memory allocation are "locked" and cannot be moved. Applications should take into consideration reducing the amount of fixed heap, stack, and VMM memory that gets allocated during the running of the program, and also to insure that garbage collection is forced on a methodical basis to prevent the fragmention.

CA-Clipper uses a technique called "scavenging" in its memory garbage collector. The garbage collector is automatically invoked during writes to the screen and "wait" states while waiting for keyboard input. Programs that have routines which have few screen-write or input routines can fragment memory badly particularly if a lot of file opening and closing, reindexing, appending, etc. is going on with little operator interaction. It is recommended that you put calls to Devout("") in your code to invoke the garbage collector in the event that you experience "Conventional Memory Exhausted", "Memory Low", or "Stack Eval" errors. My own experience with this method has not always given me desirable results, however, and I have found that placing calls to MEMORY(-1) in strategic locations in my code, such as just before opening databases, or creating large arrays actually produces better results. This method is not supported by CA or many other developers so use this recommendation with caution. Another popular method of perfoming garbage collection is to use the FT_IDLE() function from the public domain NANFORUM TOOLKIT available on many BBS's including the CLIPPER forum of COMPUSERVE. Garbage collection should be forced especially before opening databases and indexes.

Managing DGROUP Memory

There is an area of memory in CA-Clipper, as in all large model languages, referred to as DGROUP. This 64k region of low (root) memory is also called the DS (Data Segment) because it is pointed to by the DS and SS registers. Data is often stored in this area because it can be accessed faster than data stored elsewhere. DGROUP is essentially a NEAR CALL area because accessing information in this area is accomplished by a one-word address rather than the more common 2-word addressing required to access other data areas.

Assembly-language programmers like to store data in the DGROUP area to improve performance of their application. Data in this area doesn't get overlayed and it is insured that it will always be accessible regardless of the state of the application. Interrupt handlers always store data in DGROUP to insure that the data will be accessible in the event of an interrupt. CA-Clipper stores it's stacks and static values in DGROUP.

DGROUP is structured like this.

direction of growth | --> <-- |

+--------------+-----------+------------+---------+----------------+ | C/ASM static | cpu stack | eval stack | "space" | Clipper static | +--------------+-----------+------------+---------+----------------+

The eval stack is where local and private variable VALUEs are stored. The CA-Clipper static area is where CA-Clipper static VALUES are stored. ITEMs generated by programs using ITEM.API also create VALUE entries in the CA-Clipper static area. Each VALUE entry uses 14 bytes.

If the eval stack grows into the CA-Clipper static area (or vice versa), you get a UE 667 (stack fault) error.

In low memory situations, the VMM will allocate "space" to the conventional memory pool. After this happens, if the eval stack grows into the allocated space, you get a UE 668 error. Likewise, if the CA-Clipper static area needs to grow, you get a UE 669 error.

When the eval stack or CA-Clipper static area expands, and later retracts, the system maintains "watermarks" to indicate the farthest expansion of them. The VMM allocates only the space between the "watermarks". So one way to control things is to make sure the "watermarks" are set to allow your program to execute normally.

There is very little that a CA-Clipper programmer can do to resolve DGROUP problems other than to avoid using third-party products and/or C/ASM code that uses SMALL MODEL rather than LARGE MODEL programming techniques and replace LOCAL variables with LOCAL arrays. CA-Clipper is a LARGE MODEL programming language and it is recommended in the CA-Clipper API that extensions to the language should also be compiled as LARGE MODEL. Unfortunately, many C/ASM programmers develop libraries designed for speed performance rather than memory performance. I refer to these products as "DGROUP Hogs". These libraries will consume so much of the C/ASM static space that there will be literally nothing left to run the application. If you find that you cannot limit the usage of third-party libraries, then and alternative solution is to use one or more of the methods described below under "Managing the EVAL Stack".

There are several methods to determine how much DGROUP a library uses. See next page:

Monitor memory with //Info

When you start your CA-Clipper application with the //INFO option, CA-Clipper reports the condition of memory at the start of the application. The reports looks like this:

DS=4F4E:0000 DS avail=30KB OS avail=255KB EMM avail=960KB

DS= is the starting address of the DGROUP segment.

DS avail=KB reports the amount of DGROUP available.

OS avail=KB reports the amount of conventional memory available.

EMM avail=KB report the amount of expanded memory allocated to the current application.

When the DS avail (DGROUP) is less than 15k at the start-up of an application, some large applications could experience stack eval errors at runtime. This number is arbitrary and your application may actually run just fine with less available DGROUP, however it is important that you monitor DS especially if your application uses third-party libraries or lots of LOCAL and STATIC memvars. It's always a good idea to make note of the amount of DS avail in your current application before and after you add new functions or libraries to the application. If you suddenly notice a dramatic decrease in DS avail after adding new code, then you should take note that you may be using a function from a library that uses excessive DGROUP.

Create a .MAP file

Rtlink and Blinker support a MAP= command that creates a map file with information about segment usage. This map file will show you which routines use DGROUP and exactly how much they consume.

Managing the EVAL Stack

If your application uses up a lot of DGROUP memory, then you may be required to re-visit your code and either reduce the hit on the EVAL stack or expand the size of the EVAL stack. In my opinion, it is quite easy to resolve these problems with minor changes to source code.

Use LOCAL ARRAYS instead of LOCAL VARIABLES

Every time a new function is called, all LOCAL variables are "pushed" onto the EVAL stack. They are "popped" off the stack when returning to the calling program. If a procedure or function is called recursively, then it can be quite easy to blow up the eval stack with a 667 Stack Eval Error. For example, I had this problem in one of my larger applications due to the fact that programs were deeply nested via calls to a common set of menu functions. I found that my main menu function could blow up the stack if called recursively as few as 5 times. Each time this function was called, 85 LOCAL variables were pushed on to the eval stack. I completely eliminated the problem with about 2 hours of programming and debugging by replacing the 85 separate LOCAL variables with 1 LOCAL array. A LOCAL array uses the same amount of stack space as any other LOCAL variable, therefore I reduced the amount of values pushed on the stack from 85 to 1. After doing this, I could not blow up the stack even if I called the menu system recursively more than 30 times. This was accomplished quite easily with very minor code changes. Here is an example of the BEFORE and AFTER code.

BEFORE:

FUNCTION DC_MenuMain( )

LOCAL cColor, cMenuScrn, nInkey, cTempStr, nTempNum, nItemLen,; cOldColor, nItems, nMsgLen, nHotStart, cBoxColor, ; cMenuColor, cHotColor, cSelColor, cBuffer, nRow, nCol, ; lMouseHit, nSaveRow, nSaveCol, lMessage, cGrayColor, ; nMouseStat, nStRow, nStCol, nEnRow, nEnCol, lMouVisible, ; nPass, aSubMenu, aSubBlock, nElement, aMenuItems, ; aHotKeys, aMenuBlocks, aSubItems, aBlockItems, cType, ; nStart, cTitle, lBar, lReturnVal, lShadow, aMouseKeys, ....

AFTER:

FUNCTION DC_MenuMain( )

LOCAL aMenu := Array(85)

#define cColor aMenu[1] ; #define cMenuScrn aMenu[2]

#define nInkey aMenu[3] ; #define cTempStr aMenu[4]

#define nTempNum aMenu[5] ; #define nItemLen aMenu[6]

#define cOldColor aMenu[7] ; #define nItems aMenu[8]

#define nMsgLen aMenu[9] ; #define nItems aMenu[10]

#define nHotStart aMenu[11] ; #define cBoxColor aMenu[12]

#define cMenuColor aMenu[13] ; #define cHotColor aMenu[14]

#define cSelColor aMenu[15] ; #define cBuffer aMenu[16]

#define nRow aMenu[17] ; #define nCol aMenu[18]

#define lMouseHit aMenu[19] ; #define nSaveRow aMenu[20]

#define nSaveCol aMenu[21] ; #define lMessage aMenu[22]

#define cGrayColor aMenu[23] ; #define nMouseStat aMenu[24]

#define nStRow aMenu[25] ; #define nStCol aMenu[26]

#define nEnRow aMenu[27] ; #define nEnCol aMenu[28]

#define lMouVisible aMenu[29] ; #define nPass aMenu[30]

#define aSubMenu aMenu[31] ; #define aSubBlock aMenu[32]

#define nElement aMenu[33] ; #define aMenuItems aMenu[34]

#define aHotKeys aMenu[35] ; #define aMenuBlocks aMenu[36]

#define aSubItems aMenu[37] ; #define aBlockItems aMenu[38]

#define cType aMenu[39] ; #

Programming Techniques for Memory Management (Part 1) by Roger Donnay

This section written by Roger Donnay is the second part of CA-Clipper 5.x Memory Management by Roger Donnay & Jud Cole.


How you write your CA-Clipper code will greatly determine how the application uses memory. Once you understand and become grounded in the basics you will discover that memory management will become second nature to your programming style. You should never write code or use any third-party library without a basic understanding of the impact on memory usage, otherwise you can paint yourself into a memory-deficient corner that can not be easily undone.

Virtual Memory

CA-Clipper includes an automatic memory manager referred to as the Virtual Memory Manager or VMM. There is very little the CA-Clipper programmer needs to know about the VMM system other than it requires EMS memory or available disk space for the creation of swap files. After CA-Clipper uses up the available EMS for VMM, it will start creating swap files. In a network situation with no local hard drive or ram drive, this can slow down the application, therefore it is recommended that you set up your environment to allocate from 500k to 1 meg of EMS for average Clipper applications. You may monitor CA-Clipper's usage of EMS during your application by reporting the remaining EMS with the MEMORY(4) function. If the number reported falls below 100, then it is recommended that you increase available EMS by several hundred K.

If no EMS is available, then Virtual memory can be allocated from XMS memory by using a Third-party product named ClipXMS. See the section titled DOS Extenders for more information on this product.

Conventional Memory

The CA-Clipper free-pool (also sometimes referred to as the fixed-heap) is always allocated from conventional memory. I have found, from experience, that applications will perform poorly or run the risk of running out of memory completely if the amount of free-pool deteriorates to less than 50k. You can count on the fact that your application will have less free-pool available after running for awhile than it has at start-up time. The CA-Clipper MEMORY(0) function can be used to monitor the amount of memory remaining in the fixed heap. Opening databases has the most serious long-term affect on the use of conventional memory. See the section titled "The Affects of Databases on Memory Usage" for more details. To give the CA-Clipper application more conventional memory, you must start at DOS with ample conventional memory. After you load device drivers, network drivers, mouse drivers, TSR's, etc. your environment just may not have sufficient conventional memory remaining to run your Clipper application. The new generation of expanded memory managers are designed to not only create EMS/XMS memory but they also will load drivers and TSR's into the upper memory blocks (UMB) area of memory above 640k so they will not use up valuable conventional memory. I have used QEMM and 386MAX in the past to accomplish this task but have recently found that, on some systems, the MEMMAKER.EXE utility and the EMM386 driver supplied with DOS 6.x are adequate in creating a better memory environment.

SYMBOL Management

For a dynamic-overlay manager to work effectively, it is important that it load overlays at the smallest possible code-segment level, i.e., the procedure/function rather than the entire object. This requires managing the symbol table separately from the code and data, therefore, these linkers place the symbol table into the root memory area and eachfunction into a separate overlay segment.

Symbols cannot be overlayed or swapped to VMM, therefore it is important that you program in a manner consistent with producing the smallest number of symbols in your CA-Clipper-compiled objects. Here are some tips for reducing the symbol table size in your applications.

Use Constants instead of Memvars

All PRIVATE, PUBLIC and STATIC CA-Clipper memory variables are treated as "symbols". Refrain from using a memory variable if a constant is sufficient. For example, an unnecessary symbol can be eliminated by changing the code:

nEscapeKey := 27

DO WHILE INKEY() # nEscapeKey

* CA-Clipper code

ENDDO

to:

DO WHILE INKEY() # 27

* CA-Clipper code

ENDDO

or:

#define K_ESC 27

DO WHILE INKEY() # K_ESC

* Ca-Clipper code

ENDDO

Use Arrays Instead of Memvars

Every different CA-Clipper PRIVATE, PUBLIC, or STATIC memvar name creates a "symbol", whereas an array name creates only ONE symbol. The following example shows how to save considerable memory in a CA-Clipper application by reducing the symbol count with an array.

This code produces 5 symbols:

PRIVATE cName := customer->name

PRIVATE cAddress := customer->address

PRIVATE cCity := customer->city

PRIVATE cState := customer->state

PRIVATE cZip = customer->zip

@ 1,1 SAY 'Name ' GET cName

@ 2,1 SAY 'Address' GET cAddress

@ 3,1 SAY 'City ' GET cCity

@ 4,1 SAY 'State ' GET cState

@ 5,1 SAY 'Zip ' GET cZip

READ

This code produces 1 symbol:

PRIVATE aGets[5]

aGets[1] := customer->name

aGets[2] := customer->address

aGets[3] := customer->city

aGets[4] := customer->state

aGets[5] := customer->zip

@ 1,1 SAY 'Name ' GET aGets[1]

@ 2,1 SAY 'Address' GET aGets[2]

@ 3,1 SAY 'City ' GET aGets[3]

@ 4,1 SAY 'State ' GET aGets[4]

@ 5,1 SAY 'Zip ' GET aGets[5]

READ

Some programmers choose memvars over arrays because it makes their source code more readable. Source code that refers to hundreds of array elements can look very cryptic and can be hard to maintain. To reconcile this problem, use the CA-Clipper pre-processor to create a "symbolic" reference to each array element like in the following example.

STATIC aGets[5]

#define cNAME aGets[1]

#define cADDRESS aGets[2]

#define cCITY aGets[3]

#define cSTATE aGets[4]

#define cZIP aGets[5]

cNAME := customer->name

cADDRESS := customer->address

cCITY := customer->city

cSTATE := customer->state

cZIP := customer->zip

@ 1,1 SAY 'Name ' GET cNAME

@ 2,1 SAY 'Address' GET cADDRESS

@ 3,1 SAY 'City ' GET cCITY

@ 4,1 SAY 'State ' GET cSTATE

@ 5,1 SAY 'Zip ' GET cZIP

READ

Use the Same Name Memvars whenever possible

Again, every "different" PUBLIC, PRIVATE or STATIC CA-Clipper memvar in a module creates a symbol. If an object contains several procedures, use the same name for memvars even though they may not perform the same or similar functions. For example, procedure A and procedure B both need 5 memvars. If procedure A declares its memvars with 5 unique names and procedure B declares its memvars with 5 unique names, then 10 symbols are used in the linked application. To eliminate 5 symbols, make sure that procedure B assigns the same name to the memvars as procedure A. This is not possible of course, if the memvars need to be PUBLIC to both procedures and perform different functions, only if they are PRIVATE.

Use Complex Expressions instead of Memvars

The following three lines of code represents the method that most CA-Clipper programmers choose to accomplish most programming tasks. It makes sense to code this way for readability and debugging, but if you are writing a very large application, the complex expression technique can save some memory. The following three lines of code will read the disk file READ.ME into a memvar named cReadFile, save the changed code into a file named cEditFile, then write the changed code back to the disk file READ.ME.

cReadFile := MEMOREAD('READ.ME')

cEditFile := MEMOEDIT( cReadFile )

MEMOWRIT('READ.ME', cEditFile )

These three lines of code can be replaced by one complex expression which uses no symbols at all.

MEMOWRIT("READ.ME",MEMOEDIT(MEMOREAD("READ.ME")))

An additional advantage to coding this way is that less free-pool or VMM memory is used because the process of temporarily storing the text in cReadFile and cEditFile is completely eliminated. If you find that creating complex expressions such as this are unreadable and hard to maintain then use the CA-Clipper pre-processor to accomplish the same task as follows:

#define cReadFile MEMOREAD('READ.ME')

#define cEditFile MEMOEDIT(cReadFile)

MEMOWRIT('READ.ME',cEditFile)

The above code is nearly as readable as the original three lines of code but will not create any variables at compile time. Try compiling this code with your CA-Clipper compiler and use the /P switch to write the pre-processed code to a .PPO file, then look at the .PPO file to see what is actually compiled.

Use LOCALS Instead of PRIVATES

Sometimes it is just not possible or practical to write code without using symbols, so if you find yourself in this situation, CA-Clipper provides the feature of "LOCALIZING" symbols to the code segment which is currently being executed rather than placing the symbol in the main symbol table. LOCAL symbols are effectively "overlayed" because they are treated as part of the code segment rather than given a place in the main symbol table.

Not only does this save valuable memory but it also improves speed performance because the public symbol table does not need to be searched each time a LOCAL symbol is referenced in your code. Of course, if the symbol you are referencing is needed for the entire application or is used in a macro, then it must be declared as PRIVATE or PUBLIC. Symbols which are not declared at all are automatically assumed to be PRIVATE, so make sure you use the LOCAL declaration for all symbols in your code which you do not want to end up in the main symbol table. In the previous code example, the 5 PRIVATE memvars consume 110 bytes of memory, the single PRIVATE array consumes 22 bytes of memory, whereas 5 LOCAL declarations would consume 0 bytes of memory.

Use STATIC functions instead of PUBLIC functions

Every PUBLIC function and procedure name will occupy 22 bytes of the symbol table. Don't make functions and/or procedures PUBLIC unless they need to be called from anywhere within your application. STATIC procedures and functions are called only from within the source code module in which they are declared thereby eliminating the need to place the function name into the public symbol table.

Use PSUEDO functions or CONSTANTS instead of PUBLIC functions

A Psuedo-function is a function that does not really exist in the compiled code but exists only in your source code. Many programmers will use a large number of functions with different names even when each function may do something very similar like a conversion or a table lookup. I have seen a lot of code that looks like this:

SetColor( blueonwhite() + "," + redongreen() + "," + black() )

FUNCTION blueonwhite

RETURN 'B/W'

FUNCTION redongreen

RETURN 'R/G'

FUNCTION black

RETURN 'N/N'

Programmers will use this technique to make their code easy to read and maintain without understanding how this can bloat the size of the application. Probably the best technique to replace the above code would be to #define constants for each as follows:

SetColor( BLUEONWHITE+ "," + REDONGREEN + "," + BLACK )

#define BLUEONWHITE "B/W"

#define REDONGREEN "R/G"

#define BLACK "N/N"

Your existing source code may have "many" references to public functions and you don't want to risk changing all the function names to constants, or maybe the functions return something a little more complex than can be handled by a simple constant. In this case, you can #translate the functions into psuedo-functions as follows:

BEFORE:

FUNCTION BLOBget ( nPointer, nStart, nCount )

RETURN dbInfo( BLOB_GET, { nStart, nCount } )

FUNCTION BLOBput ( nPointer, xBlob )

RETURN dbInfo( BLOB_PUT, { nPointer, xBlob } )

FUNCTION BLOBExport( nPointer, cTargetFile, lMode )

RETURN dbInfo( BLOB_EXPORT, { nPointer, cTargetFile, lMode } )

AFTER:

#xTranslate BLOBget( , , ) => ;

dbInfo( BLOBGET, { , , } )

#xTranslate BLOBput( , ) => ;

dbInfo( BLOBPUT, { , } )

#xTranslate BLOBExport( , , ) => ;

dbInfo( BLOBEXPORT, { , , } )

In the BEFORE example above, three PUBLIC functions were created simply to provide a better way to call the same dbInfo() function, whereas in the AFTER example, no PUBLIC functions were needed to accomplish the exact same task. Not only did we save 3 symbol table entries but we also eliminated 3 LOCAL variables that get pushed onto the EVAL stack. Of course it must be remembered that since psuedo-functions don't actually exist they cannot be used in macros or index keys.

Don't use STATICs or PRIVATEs to pass parameters

Many CA-Clipper programmers will assign a variable as PRIVATE or STATIC so it can be accessed and changed within multiple procedures in the same source code module. Variables should be STATIC only if their value needs to be maintained throughout the program, not for the convenience of eliminating the need to pass parameters. By using the pass-by-reference symbol "@" you can change the value of LOCAL variables anywhere within your calling program as shown by the following example.

LOCAL cName, cAddress

MyFunction( @cName, @cAddress )

Return( { cName, cAddress } )

STATIC FUNCTION MyFunction ( cName, cAddress )

cName := "CA-Clipper"

cAddress := "New York, NY"

Return( nil)

The "Myth" of PUBLICs verses STATICs

There is no memory advantage to using a STATIC variable in lieu of a PUBLIC variable. Both types of variables use up permanent space in conventional memory. A STATIC variable will use space in DGROUP, whereas a PUBLIC variable will use space in the Symbol Table. In fact, there are situations in which a good case can be made for using a PUBLIC array instead of a STATIC array. Take the example of a system-wide color system in which the colors are defined in an array. If the array is STATIC, then extracting the information from the array requires a call to a PUBLIC function which exists in the same source code module as the array definition. This would actually add more symbols to the symbol table and take more processor time than if the color array were defined as PUBLIC thereby allowing direct access to the array without the need for the public function. A STATIC array may be a wiser choice in cases where the application is a library which is incorporated into another CA-Clipper application, thereby eliminating any possibility of symbol conflicts with the application.

Linking Techniques for Memory Management (Part 2) by Roger Donnay

This section written by Roger Donnay is the third part of CA-Clipper 5.x Memory Management by Roger Donnay & Jud Cole.

Overlaying Library Modules
The MODULE command provides the important capability of overlaying the larger modules of the CA-Clipper libraries or any third-party library. With previous versions of these linkers, the overlaying of libraries was restricted to ALL modules in a library or NO modules in a library. This is ok for libraries which were designed for overlaying, but in most cases it is simply not practical to overlay an entire library due to speed performance problems. There are many modules in the CA-Clipper libraries which can be overlayed without crashing the program, yet runtime performance can be disastrous. Determining the proper "set" of modules and the optimum "overlay pool size" is simply a matter of trial and error. For example, overlaying the MACRO module in one application may have very little affect on speed performance because of the design of the program and it's probably negligible.

The MODULE command is very useful in that it allows your linker to place "modules" from libraries into overlays. You can organize your projects by placing all your C/ASM objects in libraries with a library manager such as Microsoft's LIB.EXE then by using the SECTION MODULE command in your link file you can place any module into any overlay area.

Rtlink:

LIB mylib

BEGINAREA

SECTION MODULE myfileA,myfileB

SECTION MODULE myfileC

SECTION MODULE myfileD

ENDAREA

The MODULE command in RTLINK requires that the exists in one of the declared libraries and that the name is "unique" to that module otherwise the command will be ignored and the module will be linked into the root memory area. This is not a problem with the CA-Clipper libraries because the modules are given unique names.

BLINKER supports the MODULE command differently and, in my opinion, better than RTLINK. Blinker allows you to define a specific module from a specific library to be linked into the application, thus you don't need to worry about the module being unique to one library. For example, if you have two libraries, both containing an ERRORSYS module, you can decide which ERRORSYS you want linked into the program.

Blinker:

LIB mylib

BEGINAREA

LIB grump

MODULE errorsys FROM grump

MODULE myfileA,myfileB

MODULE myfileC

MODULE myfileD

ENDAREA

Determining the Module Names in a Library

Every ".OBJect" file placed into a library is also assigned a "module" name. This module name is stored in the "COMENT" record of the object in the library. The "module" name of an object is assigned by the compiler of the object and is usually given the name of the original source code. The CA-Clipper compiler assigns the source code file name to the module name however and many other compilers will assign the name of the SOURCE file as the module name including drive letters and directories. For example, let's say we want to place the Funcky FINDATTR() function into an overlay. First, we must figure out what "module" name was assigned to the FINDATTR() function. There are library manager utility programs which can help you figure this out, but I'm going to show you how to do it simply with your linker. During VERBOSE linking the "module" name of each object being linked into the program is displayed on the screen as follows:

FUNCKY50.LIB(C_MAXCHO) <- CA-Clipper code FUNCKY50.LIB(C_PUTKEY) <- CA-Clipper code FUNCKY15.LIB(findattr.C) <- "C" code FUNCKY15.LIB(finddate.C) <- "C" code FUNCKY15.LIB(findfirs.C) <- "C" code FUNCKY15.LIB(chrfound.ASM) <- "Assembly" code FUNCKY15.LIB(strcente.ASM) <- "Assembly" code You must make sure that you insert the command VERBOSE into your link script file to insure that this information is displayed. Next, when you perform the link, make sure route the display output to a file so you can have the information in text file format: RTLINK @MYPROG > MYPROG.TXT

MYPROG.TXT will contain the complete list of modules linked into your program. Now, we must take this information and use it to develop an overlay strategy. The name of each module linked into the program is shown (in parenthesis) and must be referred to "exactly" as it is spelled. For example, to place the function FINDATTR() into an overlay, we must first determine the "module" name which would contain the FINDATTR() function. Fortunately, the FUNCKY15 library uses the same prefix name as the actual function, so in perusing the list of linked objects in MYPROG.TXT we find a module named "findattr.c". To place this module in an overlay section, simply use the command:

SECTION MODULE findattr.c

BLINKER's MODULE command is more useful than RTLINK's because it searches both the THEADR record and the COMENT record for the name of each module. This means that you don't need to reference the MODULE name as precisely with Blinker as with RTLINK. For example, if the module name in a library is FINDATTR.C, BLINKER will overlay it by with the following command: MODULE FINDATTR, because it will determine that what you are trying to overlay is FINDATTR.OBJ.

Overlaying the CA-Clipper Libraries

Overlaying the CA-Clipper libraries is accomplished quite differently with each linker, therefore I have included examples for each linker.

Overlaying Code with RTLINK

RTLINK uses Static reloadable overlays with and the MODULE command to overlay much of the CA-Clipper libraries and thereby reduce memory usage in your CA-Clipper-5.x applications. The below link script example creates an overlay area for overlaying the larger modules in the CA-Clipper libraries which are not likely to call each other recursively, therefore you will probably notice very little difference in speed performance yet you will get up to 50K more memory overhead depending on how much of the CA-Clipper libraries your application uses.

You may get a "warning" message during link time if your application does not call one of the modules referenced in an overlay area. In the event this happens, simply remove that module from the link file. For example, if you are not using MEMOEDIT() in your application remove the line:

SECTION MODULE D:\CC\EDIT\MEMOEDIT.C

FI

LIB

OUTPUT

RELOAD FAR 200

# Create overlay area 1

BEGINAREA

#1

SECTION MODULE D:\CC\EDIT\MEMOEDIT.C

MODULE D:\CC\MEMO\MEMOTRAN.C

MODULE D:\CC\MEMO\MEMOREAD.C

MODULE D:\CC\MEMO\MEMOWRIT.C

MODULE D:\CC\MEMO\MEMOLINE.C

MODULE D:\CC\MEMO\MLCOUNT.C

MODULE D:\CC\MEMO\MLPOS.C

#2

SECTION MODULE D:\CC\STR\IS.C

MODULE D:\CC\MEMO\HARDCR.C

MODULE D:\CC\STR\PADL.C

MODULE D:\CC\STR\PADC.C

MODULE D:\CC\STR\PADR.C

MODULE D:\CC\ARRAY\ASCAN.C

MODULE D:\CC\ARRAY\ASORT.C

MODULE D:\CC\ARRAY\DIRECTRY.C

MODULE D:\CC\BASE\AEVAL.C

MODULE D:\CC\BASE\ACOPY.C

MODULE D:\CC\BASE\ADEL.C

MODULE D:\CC\BASE\AINS.C

MODULE D:\CC\BASE\ATAIL.C

MODULE D:\CC\MISCX\COPYFILE.C

MODULE D:\CC\MISCX\TYPEFILE.C

MODULE D:\CC\TERM\SCROLL.C

MODULE D:\CC\MISCX\GETE.C

MODULE D:\CC\MISCX\DISKSPAC.C

#3

SECTION MODULE D:\CC\SDF\SDF1.C

MODULE D:\CC\SDF\SDF0.C

MODULE D:\CC\SDF\SDFDYN.ASM

MODULE D:\CC\SDF\DLM1.C

MODULE D:\CC\SDF\DLM0.C

MODULE D:\CC\SDF\DELIMDYN.ASM

MODULE D:\CC\EDIT\ACHOICE.C

MODULE D:\CC\DBCMD\DBCREATE.C

MODULE D:\CC\DBCMD\DBJUNCT.C

MODULE D:\CC\DBCMD\DBSTRUCT.C

#4

SECTION MODULE D:\CC\DBF\NET.C

MODULE D:\CC\TERM\ACCEPT.C

MODULE D:\CC\SUPPORT\OSDATE.ASM

#5 SECTION MODULE D:\CC\BASE\FGET.C

MODULE D:\CC\TERM\OLDBOX.C

MODULE D:\CC\TERM\OLDCLEAR.C

MODULE D:\CC\INITEXIT\RUN.C

MODULE D:\CC\BASE\SEND.C

MODULE D:\CC\DBCMD\JOINLIST.C

MODULE D:\CC\NUSASCII\SORTBLOC.C

MODULE D:\CC\DBF\SORTOF.C

ENDAREA

# Create overlay area 2

BEGINAREA

#6

SECTION MODULE D:\CC\EDIT\TBROWSE0.C

#7

SECTION MODULE D:\CC\EDIT\TBROWSE1.C

#8

SECTION MODULE D:\CC\MISCX\EXAMPLEA.ASM

MODULE D:\CC\DBF\DBF0.C

MODULE D:\CC\DBF\DTX0.C

MODULE D:\CC\INITEXIT\INITEXIT.C

MODULE D:\CC\SUPPORT\TXOPEN.C

MODULE D:\CC\TERM\BOX.C

MODULE D:\CC\STR\STRTRAN.C

ENDAREA

When placing modules into overlays, you must use extreme care to not overlay functions which are called by "interrupts" or may be called "recursively". Code which is called by interrupts is called by a direct vector rather than an overlay vector. This can cause computer lockup if the code segment is not currently in memory. Code which is called recursively would be something like special string handling routines that might be called in a do..while loop. If this function were placed in an overlay, you may experience considerable slowing and/or disk access.

Many third-party library developers now distribute their product in two libraries: a "resident" library and an "overlayable" library. Most of the work has already been done for you in determining which modules can be overlayed. If the module is part of the "resident" library, don't make any attempt to try to overlay it or you will experience runtime problems. Also, don't make an attempt to overlay "CA-Clipper code" because this code is automatically overlayed into "dynamic-pages".

A file named MEM52.LNK is provided on the diskette. This is a standard link-script for Rtlink that can save approximately 50k of conventional memory by just calling it from your existing link-script.

Overlaying Code with BLINKER

Overlaying the CA-Clipper libraries with Blinker is simpler than Rtlink because only one overlay area needs to be defined for all overlayable modules, files and libraries. Like Rtlink, Blinker will automatically overlay any code in any library that is compiled with the CA-Clipper compiler.

BLINKER OVERLAY OPSIZE 50

BLINKER PROCEDURE DEPTH 50

FI

LIB

OUTPUT

BEGINAREA

FILE

ALLOCATE

# Modules from CLIPPER.LIB

MOD accept,acopy,adel,aeval,ains,appinit,atail,box,cmem,date

MOD dbcmd2,dbcmd3,dbcmd4,dbcmd5,dbcreate,dbf0,dbfdyn,dbjunct

MOD dbstruct,dtx0,dtx1,dtxdyn,dynina,errsys0,errsys1,fget

MOD getenv,gets0,gets1,gets2,gx,joinlist,lupdate,memory

MOD mrelease,msave,net,oldbox,oldclear,philes,run,saverest

MOD sdf0,sdf1,seq,setcurs,sortbloc,startsym,tb,txopen,vall

MOD vblock,vdb,vdbg,version,vmacro,vnone,vops,vpict,vterm

MOD workarea,xmacro

ALLOCATE extend

ENDAREA

LIB clipper, terminal ,dbfntx

There are a standard set of link-scripts included with Blinker that make it very simple to overlay the Clipper 5.2 libraries. They are as follows:

CL520MIN.LNK - Used to overlay the smallest portion of the Clipper libraries for maximum speed performance but minimum memory performance.

CL520MID.LNK - Used to overlay a moderate portion of the Clipper libraries for moderate speed performance and moderate memory performance..

CL520MAX.LNK - Used to overlay the maximum allowable portion of the Clipper libraries for minimum speed performance by maximum memory performance.

These are sub link script files that can be simply included in your master link script file like so:

BLINKER OVERLAY OPSIZE 50

BLINKER PROCEDURE DEPTH 50

FI

LIB

OUTPUT

@CL520MAX.LNK

Blinker has become the standard for linking Clipper applications, so much of the third-party community also will include "ready-to-go" link-scripts with their libraries to make it easy to add their library to your application. For example, if you are already using a Blinker link script and decide to add the Six Driver (RDD) to your application, you would simply add one line of code to your link-script:

Linking Techniques for Memory Management (Part 1) by Roger Donnay

This section written by Roger Donnay is the third part of CA-Clipper 5.x Memory Management by Roger Donnay & Jud Cole. If you like to have the whole book in .WRI format, click here to visit Roger's Site

The Role of Linkers in developing large applications has taken a new turn in the past few years. In addition to providing segment-linking, they are now also expected to provide the best "memory image" possible. This means that the linker must be sophisticated in it's management of code segments during the running of an application. The physical size of a CA-Clipper application is no longer representative of it's memory image, because the linker "overlays" the code segments to use the smallest possible amount of memory. Some type of code (such as CA-Clipper P-CODE) is easy to overlay while other types (ASM) may be more difficult. Applications that are almost all CA-Clipper-compiled code will usually produce the best memory image regardless of the physical size of the .EXE.

The subjects of this book are the four most commonly-used linkers available on the market today for linking CA-Clipper 5.2 and 5.3 applications. They are:

RTLINK - A "Real Mode" CA-Clipper-5.2 specific linker developed by Pocketsoft, Inc. This is the linker that is bundled with CA-Clipper 5.2.

BLINKER - A "Real Mode" Third-Party linker published by Blink, Inc. This is the linker that is bundled with CA-Clipper 5.3. A "Dual-Mode" version of Blinker is also available from Blink, Inc.

EXOSPACE - A "Protected Mode" linker developed by CA and sold as an add-on to Clipper 5.2. This linker is bundled with Clipper 5.3.

CAUSEWAY - A third-party "Protected Mode" linker developed by Michael Devore for use with Clipper 5.2 and 5.3.

The CA-Clipper compiler has the responsiblity of converting source code to "machine-readable" code. This .OBJect code can not be "executed" by the machine however until it is "linked" to other .OBJect code segments in the CA-Clipper libraries. This is the responsibility of the linker. Linking or "fixing up" references made by your code to external variables and functions is necessary to insure that your compiled program will address or call other programs, including functions in the CA-Clipper libraries and DOS functions.

A linker's basic job is to combine objects which have been compiled separately. Objects contain symbols of three types:

a. PUBLIC symbols - those symbols which are callable from other objects

b. STATIC symbols - those symbols which are callable only from within the same object

c. UNDEFINED symbols - symbols which exist in other objects and are referenced or used by this object ((also referred to as EXTERNAL symbols).

DYNAMIC Overlaying of CA-Clipper Code

Very large CA-Clipper applications can consist of more code than can be loaded into the available amount of DOS memory and may not be able to execute due to insufficient memory. CA-Clipper 5.x has a "Dynamic Overlay Manager" actually built into the CA-Clipper libraries that helps prevent this problem by loading the code into memory only as it is needed by the application that is running. The linker helps prepare the application to use this capability of CA-Clipper 5.2 by splitting the CA-Clipper-compiled application code into fixed-size "pages" that share a pre-allocated area of memory. These pages of code are then written to a seperate .OVL file or to the end of the .EXE file as determined by your linker options. When a procedure or function is called, the "dynamic-overlay manager" first checks to see if it is in memory. If it is, then it jumps to the called function. If it is not in memory, it loads the function from disk into any "free" location available in the overlay pool. If no free space can be found in the overlay pool, the overlay manager discards pages that have not been called recently to free space to load the called procedure.

The larger the overlay pool, the less often the overlay manager needs to load from disk and the faster your program will run. The size of the dynamic overlay pool needed for your application is automatically determined by CA-Clipper.

The great thing about CA-Clipper 5.x's dynamic overlaying system is that you, the programmer, don't need to do anything to create overlays. It is all done automatically by the linker. Only CA-Clipper-compiled modules, not C/ASM modules, are overlayed "dynamically", although since much of the CA-Clipper libraries has been compiled in CA-Clipper, even large portions of the CA-Clipper libraries will be dynamically overlayed.

Application libraries and third-party libraries written in CA-Clipper will also be automatically overlayed by RTLINK and BLINKER. In many cases it is more beneficial to write functions in CA-Clipper (using LOCAL memvars) than in C or ASM because it will not increase the executable memory model. Even CA-Clipper code that has been linked into to Pre-link libraries will be dynamically overlayed.

INTERNAL/EXTERNAL Overlays

INTERNAL overlays are dynamic pages which are written to the end of the .EXEcutable program file. During the running of the application, the dynamic overlay manager reads the disk and loads dynamic pages from this file. This is the default overlay method of RTLINK and BLINKER and it is often advantageous to use internal overlays to reduce the number of file handles being opened by your application. In some cases however, you may find that using multiple overlay files will increase the speed performance of your application because extracting code from several small files which are all opened at the same time is often faster that extracting code from one large file.

EXTERNAL overlays are sets of dynamic pages which are written to a seperately designated file more commonly referred to as an overlay file. The positional command /DYNAMIC[:] or the freeformat command DYNAMIC INTO will tell RTLINK to write the dynamic pages to a file named . You may wish to create a seperate overlay file when your application .EXE is too large to fit on a distribution disk.

Examples:

Rtlink> DYNAMIC INTO myprog.ovl Blinker> SECTION INTO myprog.ovl

Rtlink> DYNAMIC INTO myprogs.ovl MYPROG1, MYPROG2 DYNAMIC INTO myfuncs.ovl MYFUNC1, MYFUNC2 Blinker> SECTION INTO myprogs.ovl MYPROG1, MYPROG2 SECTION INTO myfuncs.ovl MYFUNC1, MYFUNC2

DYNAMIC Overlaying of C / ASM Code

BLINKER also provides the ability to dynamically overlay code written in C or Assembly. This is the most common code found in many third-party libraries. RTLINK also provides for overlaying of C/ASM but it is more complex and requires understanding more about the code being overlayed. See STATIC OVERLAYS and RELOADABLE OVERLAYS for more information on overlaying C/ASM code with RTLINK.

In the previous section, we discussed that application code compiled with the CA-Clipper compiler is dynamically overlayed using the overlay manager built inside the CA-Clipper libraries. This CA-Clipper-specific internal overlay manager is incapable of overlaying C/ASM code, so it is the responsibility of the linker to provide this capability. Each linker has it's own methods for overlaying this kind of code. BLINKER uses an overlay pool that is allocated at the start of the application. The size of this pool is determined at link-time. The larger the overlay pool, the faster the application will run because overlays will not be reloaded into memory from disk so often.

The dynamic-overlay capabilities of BLINKER require that the objects being overlayed are "well-behaved". After much experimentation with our own libraries and many third-party libraries, it is now more clear what constitutes well-behaved modules:

a. Well-behaved code uses the CA-Clipper EXTEND API registers or the stack for parameter passing and does not use undocumented features of CA-Clipper.

b. All CA-Clipper-compiled .OBJects are dynamically-overlayable.

Modules which CANNOT be dynamically-overlaid are:

a. Routines which handle interrupts.

b. Modules in which the DATA area is changed during runtime rather than allocating memory in the root area.

Dynamic overlaying of third-party libraries can be an aggravating learning experience if you don't get support from the third party vendor in trying to accomplish this task. Most of the third-party vendors now break their libraries into separate overlayable and non-overlayable .LIB files to make the task much easier.

Once you have determined which .OBJ files and/or .LIB files are acceptable for overlaying, the task of telling the linker to do this is quite simple. A BLINKER link-script will look similar to an RTLINK script except there is always only one (1) overlay area because all code uses the same overlay "pool". Example of a BLINKER script for overlaying C/ASM:

# Allocate 50k to the overlay pool

BLINKER OVERLAY OPSIZE 50

BEGINAREA

# CA-Clipper-compiled application code

FILE myprog1.obj, myprog2.obj

# C/ASM files

FILE ccode.obj, asmcode.obj

# C/ASM libraries

LIB ccode.lib, asmcode.lib

ENDAREA

Using EXPANDED or UMB RAM for OVERLAYS

BLINKER provides a several very useful feature for increasing the amount of available memory for CA-Clipper applications, i.e., the use of Expanded (EMS) or Upper Memory Blocks (UMB).

EMS Memory

Many computers have EMS memory. EMS memory managers such as QEMM, 386MAX, and DOS's own EMM386 create a memory area above conventional memory referred to as the "page frame". This 64k block of memory is mapped to an area of ram that can execute 8086 code. Blinker takes advantage of this feature by allocating the 64k EMS page frame rather than conventional DOS memory for their runtime overlay pool. This frees an additional 64k of memory for use by CA-Clipper because the overlays are loaded into the page frame rather than conventional ram. If no EMS is present when the application starts, then the overlay pool will be established in normal (conventional) ram. To enable this feature, use the command:

BLINKER OVERLAY PAGEFRAME ON

UMB Memory

Expanded memory managers will also create an area between the 640k and 1 meg memory address called UMB (upper memory blocks). This is the area where you would normally load your memory-resident programs such as DOS, network-drivers, mouse-drivers, etc. If your enviroment gives you about 30k - 60k of contiguous memory available in the UMB area, Blinker will load the overlay pool into the UMB area rather than conventional area, thus freeing more conventional memory for your CA-Clipper application. To enable this feature, use the Blinker command:

BLINKER OVERLAY UMB ON

Note: In today's modern environments, this UMB area is usually consumed by so many TSR's that the possiblity of finding a contiguous block larger than 30k is virtually impossible, so it recommended that you do not rely on the availability of this memory.

STATIC Overlays

STATIC overlays are supported by RTLINK only. They provide a method of overlaying C and ASM code which cannot be overlayed by the dynamic-overlay manager. C and ASM code is not as easily relocatable as CA-Clipper compiled PCODE (pseudo-code) therefore it cannot be overlayed "dynamically". A function which is dynamically loaded and unloaded from a pool is usually found in different places in memory at different times during the running of the program. When a C or assembly routine is called by an application linked with RTLINK it must be loaded in memory at the same address every time. This is accomplished by designating overlay AREAS where many different modules will occupy the same memory space, but only one-at-a-time. Designing Static overlays requires a more thorough understanding of the structure of your program. Static overlay segments usually look like this in your link file:

# area 1

BEGINAREA

SECTION FILE A

SECTION FILE B

SECTION FILE C

ENDAREA

# area 2

BEGINAREA

SECTION FILE D

SECTION FILE E

SECTION FILE F

ENDAREA

You must be sure that you properly place your objects in the overlay segments to prevent computer "lock-up" or "crashing" at runtime. In the above example, any procedure or function in FILE A may call any procedure or function in FILE D, E or F because they are in different areas, however if a function in FILE A calls a procedure or function in FILE B, then your program will crash as soon as the program returns to FILE A because FILE A was removed from memory to load FILE B.

Overlay management with "static overlays" is the most time-consuming because it requires extensive analysis of the structure of the program to insure that the modules are placed properly in the overlay areas.

RELOADABLE Overlays

The following overlaying technique should be applied at your own risk because it is an "unsupported" and "undocumented" feature of the RTLINK linker supplied with CA-Clipper-5.2. I feel however, that it is important to cover this subject because without an understanding of how to use this feature it may be impossible to develop certain types of CA-Clipper applications without resorting to a third-party linker.

I started RELOADING modules from the CA-Clipper libraries years ago using RTLINK and the Summer 87 version of Clipper, so when I received my copy of CA-Clipper-5.2, the first thing I attempted to do was to overlay some of the larger C/ASM modules in the CA-Clipper libraries using the RELOAD command in my linker script file. The CA-Clipper development team had the foresight to insure that their C and ASM compiler assigned a UNIQUE name to each CA-Clipper C/ASM object in the CA-Clipper libraries, thus allowing, you, the CA-Clipper programmer to use both the RELOAD command and MODULE command in your .LNK script files.

So what are "Reloadable Overlays"? These are a form of STATIC overlay in which segments of code are placed into overlay sections that occupy the same memory space at runtime, however the "calling module" is automatically "reloaded" into memory when returning from the "called module".

Reloadable overlay segments usually look like this in your link file:

RELOAD FAR 200

# area 1

BEGINAREA

SECTION FILE A

SECTION FILE B

SECTION FILE C

ENDAREA

# area 2

BEGINAREA

SECTION FILE D

SECTION FILE E

SECTION FILE F

ENDAREA

Not all CA-Clipper applications are going to need to use these reloadable static overlays, but if your application relies heavily on third-party libraries written in C/ASM, then it is recommended that you learn this technique.

The main advantage of "reloadable overlays" over conventional "static overlays" is that you are never in danger of "lockup" in the event that a procedure or function in FILE A calls a function or procedure in FILE B because FILE A will be reloaded into memory on return from FILE B. Make sure when you use the RELOAD command that you always use it as follows:

RELOAD FAR

where the is the amount of memory in hex bytes to use for saving addresses. My experience is that most applications will run just fine with a stack size of 200. If your application does recursive nesting, however, you may need to increase the stack size. Recursive nesting is described as follows: Procedure A calls Procedure B which calls Procedure A which calls Procedure B, etc, etc, etc. Each time a procedure is called, its return address is "pushed" onto the stack, and each time you return to the calling procedure the address is "popped" from the stack. In the above example, if this recursive condition were allowed to continue for a large number of iterations, the stack would be overrun and the program may crash.

Although your application will not crash using RELOAD, it may slow down a bit if overlay segments are not structured properly, because each time an overlay is reloaded the application must go to disk. In the above example if a function in FILE A repetitively calls a function in FILE B then your application will be very "disk intensive" and will run slowly. If a function in FILE A repetitively calls a function in FILE D there will be no slowing at all because both modules will remain in memory.

Overlay management of C/ASM code with "reloadable overlays" is less time-consuming and more reliable than simple conventional "static overlays" and provides an additional advantage of allowing more modules to be overlayed, thereby saving additional memory usage.

Welcome to Clipper... Clipper... Clipper


In 1997, then using Delphi 3, I had already created 32-bits Windows applications for HRIS, ERP and CRM. In 2007, using Ruby on Rails, an AJAX powered CRM site running on Apache & MySQL was created and I am now using Visual Studio .Net 2008 to create web-based projects and Delphi 7 for Win32 applications using SQL2005 & DBFCDX.

So, why then am I reviving the Original Clipper... Clipper... Clipper via a Blog as CA-Clipper is a programming language for the DOS world ? Believe it or not, there are still some clients using my mission-critical CA-Clipper applications for DOS installed in the late 80's and up to the mid 90's. This is testimony to CA-Clipper's robustness as a language :-)

With the widespread introduction of Windows 7 64-bits as the standard O/S for new Windows based PCs & Notebooks, CA-Clipper EXE simply will not work and it has become imperative for Clipper programmers to migrate immediately to Harbour to build 32/64 bits EXEs

Since 28th January 2009, this blog has been read by 134,389 (10/3/11 - 39,277) unique visitors (of which 45,151 (10/3/11 - 13,929) are returning visitors) from 103 countries and 1,574 cities & towns in Europe (37; 764 cities), North America (3; 373 cities) , Central America & Caribeans (6; 13 cities), South America(10; 226 cities), Africa & Middle-East (12; 44 cities) , Asia-Pacific (21; 175 cities). So, obviously Clipper is Alive & Well : -)


TIA & Enjoy ! (10th October 2012, 11:05; 13th November 2015)


Original Welcome Page for Clipper... Clipper... Clipper

This is the original Welcome Page for Clipper... Clipper... Clipper, which I am republishing for historical and sentimental reasons. The only changes that I have made was to fix all the broken links. BTW, the counter from counter.digits.com is still working :-)

Welcome to Chee Chong Hwa's Malaysian WWW web site which is dedicated to Clipperheads throughout the world.

This site started out as a teeny-weeny section of Who the heck is Chee Chong Hwa ? and has graduated into a full blown web site of more than 140 pages (actually hundreds of A4 size pages) ! This is due to its growing popularity and tremendous encouragements from visiting Clipperheads from 100 countries worldwide, from North America, Central America, Caribbean, South America, Europe, Middle-East, Africa and Asia-Pacific. Thanx Clipperheads, you all made this happen !


What is Clipper ?

You may ask, what is this Clipper stuff ? Could Clipper be something to do with sailing as it is the name of a very fast sailing American ship in the 19th century ?

Well, Clipper or to be precise, CA-Clipper is the premier PC-Software development tool for DOS. It was first developed by Nantucket Corporation initially as a compiler for dBase3+ programs. Since then, CA-Clipper has evolved away from its x-base roots with the introduction of lexical scoping & pre-defined objects like TBrowse. As at today, the most stable version ofClipper is 5.2e while the latest version, 5.3a was introduced on 21 May 1996.

As at 11th November, 1996, an unofficial 5.3a fixes file was made available by Jo French. See the About CA-Clipper 5.3a section for more details. BTW, Jo French uploaded the revised 5.3a fixes file on 20th November, 1996.

Latest News

The latest news is that CA has finally released the long-awaited 5.3b patch on 21 May, 1997.

For 5.3b users, you must a take a look at Jo French's comments on unfixed bugs in 5.3b.

BTW, have you used Click ? If you're a serious Clipperprogrammer and need an excellent code formatter, Click is a natural choice. How to get it ? Simple, access Phil Barnett's site via my Cool Clipper Sites.

32-bits Clipper for Windows ?

Have you tried Xbase ++ ? Well, I have and compared to Delphi (my current Windows programming tool of choice), I'm still sticking to Delphi.

Anyway, you should visit the Alaska Home Page. Give it a chance and then draw your own conclusions !.

The Harbour Project

Is this the future of Xbase ? Take a look at at the Harbour Project

You are Visitor # ...

According to counter.digits.com, you are visitor since 3 June 1996.

If you like or dislike what you see on this website, please drop me a line by clicking the email button at the bottom of this page or better still, by filling out the form in my guest book. If you are not sure what to write,click here to take a look at what other Clipperheads have to say.