Last month, I discussed event-driven programming in C and wrote a small example that used the keyboard and mouse to capture text screen images into a file on a PC. The program is useful for developing user documentation. You can define a rectangular area of the screen, capture it to disk, and edit it later with a text editor. Then you can print it or merge it into your word processing files.
The example program runs from the DOS command line, which limits its utility. You need to capture screen snapshots on-the-fly while the program you are documenting is running. Most programs will not retain the screen while you exit to DOS to run a screen grabber, and even if they would, the DOS command line would use up some of the screen, possibly scrolling off the part you want to capture. Besides, the business of moving between DOS and your program is a nuisance. Therefore, to capture screen segments from running programs, the screen-grabber program must be memory-resident.
This month's column shows you how to turn the command-line program into a terminate-and-stay-resident (TSR) program. The subject of TSRs has been given comprehensive treatment by now. Many books and magazine articles have dealt with it. I've covered the subject myself in three books. I'm addressing it again because I wanted to complete the program that I started last month and because it would be unfair to dump a lot of code on you without a bit of explanation.
The TSR is a kludge. It exists because the designers of DOS failed to foresee the need for simple task-switching between concurrently resident programs -- not multitasking, mind you, but simple task-switching. The TSR was made possible because those same designers needed to tack a print spooler to the single-user, single-tasking DOS. They put some hooks into DOS to allow their print spooler to run in the background in a way that did not disturb the integrity of a foreground task. They did not publish the details of those hooks, probably because the technique is both fragile and ugly. Other programmers reverse-engineered the DOS print spooler program's code to build early pop-up utility programs such as SideKick. Eventually, the techniques came into common knowledge, and TSRs proliferated. By the way, you can use the Paste operation of SideKick's Notepad to do what the example screen grabber program does, except that SideKick does not use the mouse to describe the part of the screen you want to capture.
Here in a nutshell -- an appropriate place for a discussion of a kludge -- is how a pop-up TSR works. You run a TSR from the DOS command line. It attaches itself to some interrupt vectors and uses a special DOS call to terminate without giving up the memory it occupies. Thus the name, "terminate-and-stay-resident." When you run other programs, DOS loads them into memory above the TSR program. When you press the TSR's hot key, the interrupt service routine (that the TSR attached to the keyboard interrupt vector) intercepts the keystroke and goes through some gyrations that let the TSR program pop up.
A TSR that makes DOS calls cannot pop up at just any old time. DOS is not always able to accommodate that. DOS is not reentrant. If you interrupt a program while it is in the middle of a DOS call and run another program that makes DOS calls, DOS will crash and burn. A TSR must set an indicator that says it wants to pop up and then wait until DOS says it is OK. DOS does that in two ways. DOS maintains something called the INDOS flag, an indicator that tells when DOS is running and cannot be interrupted. As long as the INDOS flag is set, you must not switch to a different program that makes DOS calls. The catch is that the COMMAND.COM program makes a DOS call to read the console. As long as you are sitting at the command line prompt, DOS is running and the INDOS flag is set. If INDOS provided the only way to let you interrupt DOS, no pop up could occur while you were at the command line prompt. To get around that snafu, DOS adds another kludge. While DOS is looping waiting for a keystroke, it periodically sets things up to allow itself to be interrupted. Then it calls interrupt number 0x28. A TSR attaches itself to INT 28 and knows that it is OK to pop itself up when its INT 28 interrupt service routine executes.
To tie all these things together, a TSR attaches the keyboard interrupt to watch for the hot key, the INT 28 interrupt to watch for when DOS will allow itself to run, and the timer interrupt to watch everything. When the TSR's keyboard interrupt service routine sees that you have pressed the hot key, it sets a flag saying so. When the timer interrupt service routine executes -- 18.2 times every second -- it looks at the hot key flag. If the hot key flag is set and the INDOS flag is not, the timer interrupt service routine pops up the TSR. If the INT 28 interrupt service routine executes and the hot flag is set, the INT 28 routine pops up the TSR.
Before popping up, the TSR must switch context from the interrupted program to itself. In an orderly multitasking environment, the operating system handles context switching between tasks. In the TSR kludge, every resident program must do it for itself. One of the reasons that the TSR situation is fragile is that there are different ways to manage context switching; not every program does it the same way, and not every program does a thorough job of it. Here are the items of context that you must switch. You need to change from the interrupted program's stack to that of the TSR; you need to trick DOS into thinking the TSR is its single task by switching its pointer to the running program's Program Segment Prefix (PSP); you need to change the Disk Transfer Address (DTA) from that of the interrupted program to the TSR's DTA; you need to temporarily disable what DOS would do with a critical error and when the user presses Ctrl-Break or Ctrl-C; if the TSR is going to use the mouse, you must save the current mouse context. The interrupted program might be using it, too.
Depending on what the TSR does, you must deal with the video mode as well. If the TSR is smart enough to recognize the current video mode and use it, you do not need to change video context. On the other hand, if the TSR is a text-mode-only program, for example, you need to capture the current mode and, perhaps, the contents of video memory, and change the video mode to the one that the TSR uses. The screen grabber program does not worry about that because it captures text mode screens only and assumes that you would run it only in text mode.
Dealing with PC video modes is no trivial job. If the interrupted program sets modes by directly addressing the CRT controller's registers, for example, you might not be able to determine the current mode, much less save it and switch back to it.
After you've done all that context switching, you can run the TSR. A TSR that executes when INDOS is clear is free to make almost any DOS call. One that executes from its INT 28 interrupt can use most DOS calls above function 0x0c. The functions from 0 to 0x0c are screen and keyboard DOS calls and are still vulnerable to a crash. Not to worry. These functions are ones that most TSRs would not use because of their behavior and performance. Most TSRs will read the keyboard through BIOS and write to the screen with direct video memory writes. TSRs should avoid making DOS memory allocation calls or running other programs from the DOS EXEC function.
When the TSR is done, it must switch the context back to what it was before the program popped up. Note that the program in this and the previous issue will run correctly as a TSR only for versions of DOS greater than 3.0. The DOS functions that get and set the PSP did not work properly in earlier versions of DOS. There are tricks for swapping PSP context in DOS Version 2, and these are described in the books mentioned at the end of this discussion.
Listing One, page 150, is tsr.c, the driver that turns the program into a DOS terminate-and-stay-resident program. It uses some of the Turbo C extensions for reading and writing hardware registers and executing interrupts. Other compilers have other ways of doing these nonstandard operations, so if you use a compiler other than Turbo C, you must port the hardware-specific stuff to the conventions of your compiler. Most compilers have no analogue to the Turbo C stack segment and stack pointer pseudoregisters, so you might need to write an assembly language equivalent to read and change the values in SS and SP. Most PC compilers do have the int86 family of functions with REGS and SREGS structures, and they all implement them in common ways, so that part of your port should be no problem.
You can use this TSR engine to create other TSRs from regular C programs if those programs avoid any console input/output through DOS calls. Three define statements at the beginning of Listing One associate the driver with a TSR program. The KEYMASK and SCANCODE symbols define the hot key, and the tsr_program symbol defines the name of the application function that the driver calls when the user presses the hot key. In this example, the hot key uses a scan code of 52 and a key mask of 8. These values make Alt-period the program's hot key. The program will call the function named copy-scrn when the user presses the hot key. Scan codes and the value of the BIOS shift mask are published in most books on low-level PC programming, including the ones listed at the end of this discussion.
The TSR driver manages all the TSR bother about when to pop up and how to switch contexts between the program that is running and the TSR. The TSR driver includes the main function for the program that it supports. To bypass being a TSR, a program should provide its own main function, one that either calls or is itself the tsr_program function. In fact, that is the easiest way to test a TSR. Because in that configuration it is a regular DOS program, you can test it with a source-level debugger, linking in the TSR driver after the program works properly as a DOS program. The code from last month includes a TSR compile-time conditional statement. When you define that global symbol in the compile, the code changes the name of its main function to tsr_program, the function called by the engine to run the TSR.
There is a lot more to know about TSRs than I have told you here. You can learn more from the books mentioned later, or you can decide not to know much about the subject and use engines such as the one published with this column. The engine published here is not comprehensive, however. You would not use it with a commercially distributed TSR because it would crash under DOS 2.0 and would make funny screens if you popped it up in graphics mode. There are other engines published in books, including some of my own, that have the code to get around some of these problems. There are some commercial libraries that manage the TSR part of your programs. Your best bet is a rugged shareware package named TesSeRact that takes care of most of it in ways that survive in the presence of most other TSRs. You can download TesSeRact from many BBSs and services or you can order it from Innovative Data Concepts at 215-443-9705. Source code is available, too. (I'd plug this package more often if its name was easier to type.)
This is a list of books that address the machinations of the TSR or that discuss issues relevant to the development of TSR programs:
Having just written and published a TSR program, let me crawl out on a spindly limb and say that I think the TSR is and should be an endangered species. Its original justification was that PCs of yore did not support multitasking, and users wanted the convenience of pop-up utility programs. The conventional wisdom persisted that because there are a kazillion of those old 8088-based PCs out there with their paltry 640K, we have a duty to continue to develop software to support them. Why? You can get a 386 with lots of memory for a lot less than the original cost of a 1981 PC with no hard drive and 64K. Tell those folks to use the old software or get off a dollar and upgrade. In the meantime, the multitaskers such as Windows and DesqView solve the thorny TSR problems for those willing to use contemporary hardware. Write a program, any program. Forget about INT 28, INDOS, critical errors, break handlers, context switching, and all that rot. Run that program under DesqView and pop it up whenever you want. And if you happen to pop it up while your communications program is downloading a big file, so what? Everything keeps humming along. And if one of those old-timers can't live without your snazzy new program and won't give up the antique, tell them about the DOS command line.
I hereby resolve to never write another piece on TSRs.
My excursion into event-driven programming and my analysis of TurboVision, Zinc, and Mewel led me to a conclusion. C programmers need an efficient way to put the IBM Systems Application Architecture (SAA) Common User Access (CUA) into their DOS text-mode programs and into programs developed for other, non-PC platforms. If TurboVision, a lovely new part of Turbo Pascal 6.0, finds its way into Turbo C, it will no doubt be a C++ additive because of its strong orientation to classes. Users of the C component of Turbo C++, Turbo C 2.0, or other C compilers will not benefit from TurboVision. The Zinc library is likewise a Turbo C++ product. Mewel is a good solution for C programmers, but only if you are developing for the high-end computers, ones fast enough and with enough memory to support Mewel programs, and only if you want most of the features supported by the Windows CUA interface.
Over the next several months I will be publishing a new "C Programming" column project, which will be a C library that implements a subset of CUA in a text-mode environment. Because all the really good C-oriented puns have already been taken (C-Worthy, C-scape, and so on), I will call the package "D-Flat," which is another way of saying "C-Sharp." If I really wanted to be hip, I'd call it "Five," which is the jazz musician's shorthand for the key of D-flat. I will not use "C-Sharp" itself because there is almost certain to be someone out there with a trademark registration and a lawyer on the payroll. D-Flat sounds safer somehow. Maybe there could be two versions: a small, concise, featureless version called "C-Sharp Minor" and a feature-rich, all-things-to-all-programmers version called "C-Sharp Major." This could get disgusting. I'll stick with D-Flat.
D-Flat will provide the CUA interface in an event-driven architecture with the hardware drivers developed separately. It will support applications windows, child document windows, menu bars, pop down menus, dialog boxes, buttons, edit boxes, list boxes, scroll bars, context-sensitive help, and other CUA things. It will use the C compiler's preprocessor as a resource compiler. The version published here will run on the PC and will compile with as many popular compilers as I can possibly address within the confines of this column and the time I have to give to it. The hardware-dependent and compiler-dependent code will be separate from the rest of the library, and it will be small in relation to the rest of D-Flat.
D-Flat will not clone the Windows' API as Mewel has done. D-Flat's purpose is not to grease the skids on the way to Windows or to provide Windows-to-DOS source code portability. That solution has already been effectively implemented by Mewel. I am making no attempt to get close to the Windows' way of doing things in the development of this software. On the other hand, I've made no conscious effort to avoid it, either. Looking back on the way the code works so far, I can see some occasional Windows influence in the design.
D-Flat was born after I looked for a CUA-like library for an applications program I am writing. The program must be hospitable to most computers including the little laptops. Performance is critical. If the program is slow to load and slow to run, the users will not use it. If that belies the TSR soapbox I mounted a few paragraphs back, so be it. Besides wanting performance, I wanted the benefits of a package that could manage menus, the mouse, dialog boxes, and the like after the fashion of the high-end libraries. I do not suggest that such smaller libraries are not available, and I do not pretend to have made an exhaustive search, but I liked the idea of a package of my own design over which I have complete control, and I liked the notion of publishing it as a project. We will start the project next month with some of the low-level stuff.
The Journal of C Language Translation is a small, quarterly publication that targets developers of C language compilers, interpreters, libraries, and such. Each edition contains nine or ten essays of interest to those who need to understand the ins and outs of C translation so that they can write translating programs or interfacing libraries that comply with the official definition of Standard C. It is no surprise that many of the contributors to the Journal are members of the ANSI X3J11 committee. The audience that the Journal has targeted is, understandably, small, and so the price is steep -- $235 for a one-year subscription of four issues. The irony of this marketing strategy is that it excludes a large segment of its potential market. Programmers not writing compilers could learn a lot about C from the Journal if they could only afford the admission. Some of the contributors are regularly seen elsewhere in print, but it appears to me that they save their best C stuff for the Journal.
Here are some of the subjects they've covered in their first two years: proposed numerical extensions to C; name space pollution; const and volatile type qualifiers; aliasing problems; translating Pascal to C; the so-called "quiet changes" introduced in the Ansi standard definition; translating Fortran to C; variable length arrays; C on a Cray; the weak spots in Standard C; and trigraphs. The authors cover these and many other topics from the perspective of those who helped to frame the standard, those in the best position to recognize and identify its weaknesses and to understand and explain the imponderable.
It might be hard to persuade your boss to cough up 235 bucks for four issues of an inexpensively produced, nonglossy periodical with no ads and a total annual page count of about 250 pages, smaller than a typical $25 C book. But try. If your shop has lots of programmers, maybe you can all share a copy. Address your inquiries to:
Copyright © 1991, Dr. Dobb's JournalTSR Books
TSRynosaurus
D-Flat
The Journal of C Language Translation
The Journal of C Language Translation
2051 Swans Neck Way Reston,
Virginia 22091
703-860-0091
_C PROGRAMMING COLUMN_
by Al Stevens
[LISTING ONE]
/* --------- tsr.c --------- */
/*
* A Terminate and Stay Resident (TSR) engine
*/
#include <dos.h>
#include <stdlib.h>
#include <stdio.h>
#include "mouse.h"
#include "keys.h"
void tsr_program(void);
#define KEYMASK 8
#define SCANCODE 52
extern unsigned _stklen = 1024;
extern unsigned _heaplen = 8192;
/* ------- the interrupt function registers -------- */
typedef struct {
int bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl;
} IREGS;
/* --- vectors ---- */
#define DISK 0x13
#define CTRLBRK 0x1b
#define INT28 0x28
#define CRIT 0x24
#define CTRLC 0x23
#define TIMER 8
#define KYBRD 9
#define DOS 0x21
unsigned highmemory;
/* ------ interrupt vector chains ------ */
static void (interrupt *oldtimer)(void);
static void (interrupt *old28)(void);
static void (interrupt *oldkb)(void);
static void (interrupt *olddisk)(void);
/* ------ ISRs for the TSR ------- */
static void interrupt newtimer(void);
static void interrupt new28(void);
static void interrupt newdisk(IREGS);
static void interrupt newkb(void);
static void interrupt newcrit(IREGS);
static void interrupt newbreak(void);
static unsigned sizeprogram; /* TSR's program size */
unsigned dossegmnt; /* DOS segment address */
unsigned dosbusy; /* offset to InDOS flag */
static int diskflag; /* Disk BIOS busy flag */
unsigned mcbseg; /* address of 1st DOS mcb */
static char far *mydta; /* TSR's DTA */
int hotkeyhit = FALSE;
int tsrss; /* TSR's stack segment */
int tsrsp; /* TSR's stack pointer */
/* -------------- context for the popup ---------------- */
unsigned intpsp; /* Interrupted PSP address */
int running; /* TSR running indicator */
char far *intdta; /* interrupted DTA */
unsigned intsp; /* " stack pointer */
unsigned intss; /* " stack segment */
unsigned ctrl_break; /* Ctrl-Break setting */
void (interrupt *oldcrit)(void);
void (interrupt *oldbreak)(void);
void (interrupt *oldctrlc)(void);
/* ------- local prototypes -------- */
static void resident_psp(void);
static void interrupted_psp(void);
static void popup(void);
void main(void)
{
unsigned es, bx;
/* ---------- compute memory parameters ------------ */
highmemory = _SS + ((_SP + 256) / 16);
/* ------ get address of DOS busy flag ---- */
_AH = 0x34;
geninterrupt(DOS);
dossegmnt = _ES;
dosbusy = _BX;
/* ---- get the seg addr of 1st DOS MCB ---- */
_AH = 0x52;
geninterrupt(DOS);
es = _ES;
bx = _BX;
mcbseg = peek(es, bx-2);
/* ----- get address of resident program's dta ----- */
mydta = getdta();
/* ------------ prepare for residence ------------ */
tsrss = _SS;
tsrsp = _SP;
oldtimer = getvect(TIMER);
old28 = getvect(INT28);
oldkb = getvect(KYBRD);
olddisk = getvect(DISK);
/* ----- attach vectors to resident program ----- */
setvect(KYBRD, newkb);
setvect(INT28, new28);
setvect(DISK, newdisk);
setvect(TIMER, newtimer);
/* ------ compute program size ------- */
sizeprogram = highmemory - _psp + 1;
/* ----- terminate and stay resident ------- */
_DX = sizeprogram;
_AX = 0x3100;
geninterrupt(DOS);
}
/* ---------- break handler ------------ */
static void interrupt newbreak(void)
{
return;
}
/* -------- critical error ISR ---------- */
static void interrupt newcrit(IREGS ir)
{
ir.ax = 0; /* ignore critical errors */
}
/* ------ BIOS disk functions ISR ------- */
static void interrupt newdisk(IREGS ir)
{
diskflag++;
(*olddisk)();
ir.ax = _AX; /* for the register returns */
ir.cx = _CX;
ir.dx = _DX;
ir.es = _ES;
ir.di = _DI;
ir.fl = _FLAGS;
--diskflag;
}
/* ----- keyboard ISR ------ */
static void interrupt newkb(void)
{
static unsigned char kbval;
kbval = inportb(0x60);
if (!hotkeyhit && !running)
if ((peekb(0, 0x417) & 0xf) == KEYMASK)
if (SCANCODE == kbval) {
hotkeyhit = TRUE;
/* --- reset the keyboard ---- */
kbval = inportb(0x61);
outportb(0x61, kbval | 0x80);
outportb(0x61, kbval);
outportb(0x20, 0x20);
return;
}
(*oldkb)();
}
/* ----- timer ISR ------- */
static void interrupt newtimer(void)
{
(*oldtimer)();
if (hotkeyhit && (peekb(dossegmnt, dosbusy) == 0) &&
!diskflag)
popup();
}
/* ----- 0x28 ISR -------- */
static void interrupt new28(void)
{
(*old28)();
if (hotkeyhit)
popup();
}
/* ------ switch psp context from interrupted to TSR ----- */
static void resident_psp(void)
{
intpsp = getpsp();
_AH = 0x50;
_BX = _psp;
geninterrupt(DOS);
}
/* ---- switch psp context from TSR to interrupted ---- */
static void interrupted_psp(void)
{
_BX = intpsp;
_AH = 0x50;
geninterrupt(DOS);
}
/* ------ execute the resident program ------- */
static void popup(void)
{
running = TRUE;
hotkeyhit = FALSE;
intsp = _SP;
intss = _SS;
_SP = tsrsp;
_SS = tsrss;
oldcrit = getvect(CRIT); /* redirect critical err */
oldbreak = getvect(CTRLBRK);
oldctrlc = getvect(CTRLC);
setvect(CRIT, newcrit);
setvect(CTRLBRK, newbreak);
setvect(CTRLC, newbreak);
ctrl_break = getcbrk(); /* get ctrl break setting */
setcbrk(0); /* turn off ctrl break */
intdta = getdta(); /* get interrupted dta */
setdta(mydta); /* set resident dta */
resident_psp(); /* swap psps */
intercept_mouse(); /* intercept the mouse */
/* ------ save the video cursor configuration ------- */
savecursor();
normalcursor();
unhidecursor();
enable();
tsr_program(); /* call the TSR C program */
disable();
/* ----- restore the video cursor configuration ----- */
restorecursor();
restore_mouse(); /* restore the mouse */
interrupted_psp(); /* reset interrupted psp */
setdta(intdta); /* reset interrupted dta */
setvect(CRIT, oldcrit); /* reset critical error */
setvect(CTRLBRK, oldbreak);
setvect(CTRLC, oldctrlc);
setcbrk(ctrl_break); /* reset ctrl break */
disable();
_SP = intsp; /* reset interrupted stack*/
_SS = intss;
running = FALSE; /* reset semaphore */
}