With C in TPF - NOV 1992
by Dan Evans

The portability of C provides an opportunity to unit test TPF programs under operating systems other than TPF. The two obvious candidates are VM/CMS on a mainframe, and MS-DOS on a PC. We begin with a general framework for unit testing which look likes:

\=main(int argc, char **argv)
{
InitializeTPF(argc, argv);
InitializeApplication(argc, argv);
ExecuteApplication();
ReviewApplication();
PostMortem(parameters);
}

The function InitializeTPF() is a standard function to be used by all unit testers. It sets up a TPF environment, which mainly consists of a simulated ECB. As simulation use expands, this function will be enhanced to handle more simulated features. PostMortem() at the end of the simulation is another standard function. It will do such things as display core blocks not released or records still held. It also attends to host system requirements such as closing simulated files. It's parameters select optional activities such as printing sections of the ECB. The other three functions will be written by the unit tester. InitializeApplication() will set up the environment expected by the application. For example, the application may expect to receive control with certain control blocks on certain ECB levels, or with various ECB work area fields set. InitializeApplication() will do this. ReviewApplication() will display application dependent results which show the behavior of the application. InitializeTPF(), InitializeApplication(), ReviewApplication(), and PostMortem() are all support functions which set the stage for the real purpose of the program: testing the application which is invoked by ExecuteApplication().

In order to provide an interesting piece of the simulation in one article, I am going to jump directly into a specific simulation of one TPF service, GETFC. In TPF, this service returns the file address of an available record in a specific pool. Prisym/C, which was designed with portability in mind, defines the getfc() C macro as:

/*
* get a new pool file record address on a level with an allocated a block
*
* level - a TPF symbolic level on which the record address will be placed
* id - the address of the 2 byte pool ID
* expr - a pointer to an integer return code location
*/
#define getfc(level,id,expr) \
_asmargs(*(unsigned short *)id);\
#asm\
L 2,0(,1)\
GETFC level,,ID=(R2),BLOCK=YES,ERROR=YES\
BC 1,*+10\
XR 0,0\
B *+8\
XR 0,0\
BCTR 0,0\
#endasm\
*(expr) = (int)_asmrc();

To invoke the getfc() service, use a statement such as:

getfc(L4, &pool_id, &return_code);

If the symbol L4 was previously defined as:

#ifndef _TPF_
#define L4 4
#endif

then, when the program is compiled under any system which does not define _TPF_, the first argument will be a 4, and the second and third arguments will be pointers to the appropriate types. The Prisym/C/TPF compiler defines _TPF_, so when the program is compiled for TPF, getfc() will be expanded into the TPF macro GETFC, and the symbol L4 will be passed through the C macro to become the first parameter to GETFC. The getfc() service therefore remains syntactically the same in TPF and non-TPF compilations. In other words, it ports easily. It should be clear that the prototype for the getfc() simulation function is:

getfc(int level, short *pool_id, int *ret_code) .

In the simulation environment, a pool will be a file of fixed length records of the appropriate size, 381, 1055, or 4095 bytes. The first record will be reserved as a bit map to track allocated pool records. In a small pool, this provides 8 times 381 simulated pool records. Naturally, if this is not enough, more records can be allocated to the bit map. This definition should work in either CMS or MS-DOS. The file address of a pool record will be the byte offset of the record in the pool file. This value is returned by the C function ftell(). However, a TPF file address has a different structure, and if a program is sensitive to the actual fields of a file address, this simulation will not be sufficient. Fortunately, most TPF programs use file addresses as black box keys to read and write records, without knowing internal structure of the file address. For these programs, our simulation is adequate.

The only file information passed to getfc() is the two byte pool ID. The file simulator must be able to determine all the necessary file access information from this. As you might guess, some conventions and a table are needed. We will store file information in a file table structure. Each entry in the table stores a file's record identifier which for the purposes of the simulation are two printable characters. The table also stores the file's type and block size. For fixed files, the maximum number of records is saved. For pool files, there is a pointer to the block allocation bit map. The host operating system's file handle is also saved in the table. The maximum number of table entries is defined by the FTBLSZ preprocessor constant. We will build tTe host system's name for each file from the record ID followed by the string ".TIO".

#define FTBLSZ 10
static struct ftbl
{
char *recordID;
unsigned blksz;
enum Filetype ftyp;
FILE *handle;
unsigned maxrecs;
char *map;
} ftbl[FTBLSZ] = {{"CD", 381, PoolFile}, {"X2", 4095, FixedFile}};
enum Filetype {PoolFile, FixedFile};

The function which manages simulated fixed and pool files is TPFfile(). When TPFfile() is called, it first searches the file table to see if it knows the file identifier. Since the identifier is not a C string, a C string is made in local storage so that the function strcmp() can be used. If the ID is found and the file is open, the function returns a pointer to the file table entry. The interesting things happen if the file is not yet open.

#define RECORD_ID_LENGTH 2
static struct ftbl *TPFfile(char *ky, enum Filetype ftyp)
{
char name[RECORD_ID_LENGTH + 1];
int i, len;
struct ftbl *fp;
len = RECORD_ID_LENGTH;
memcpy(name, ky, len);
name[len] = '\0';
for (i = 0, fp = ftbl; i < FTBLSZ; ++i, ++fp)
if (strcmp(name, fp->recordID) == 0)
break;
if (i == FTBLSZ)
{
fprintf(stderr, "cannot find record ID %s\n", fp->recordID);
return NULL;
}
if (fp->handle != NULL)
if (ftyp != fp->ftyp)
{
fprintf(stderr, "incorrect file type for record ID %s\n", fp->recordID);
return NULL;
}
else
return fp;
fp->filename = (char *)malloc(len + 5);
strcpy(fp->filename, name);
strcat(fp->filename, ".TIO");
if ((fp->handle = fopen(fp->filename, "r+b")) == NULL)
{
if (ftyp == FixedFile)
{
fprintf(stderr, "cannot find fixed file %s\n", fp->filename);
return NULL;
}
if ((fp->handle = fopen(fp->filename, "w+b")) == NULL)
{
fprintf(stderr, "cannot open new pool file %s\n", fp->filename);
return NULL;
}
fp->map = (char *)malloc(fp->blksz);
memset(fp->map, 0, fp->blksz);
fp->map[0] = 0x80;
if (fwrite(fp->map, fp->blksz, 1, fp->handle) < 1)
{
fprintf(stderr, "error writing map for pool file %s\n", fp->filename);
return NULL;
}
}
else
{
if (ftyp == FixedFile)
{
fp->map = NULL;
fseek(fp->handle, 0L, SEEK_END);
fp->maxrecs = (ftell(fp->handle) / fp->blksz);
fseek(fp->handle, 0L, SEEK_SET);
}
else
{
fp->map = (char *)malloc(fp->blksz);
fp->maxrecs = 0;
if (fread(fp->map, fp->blksz, 1, fp->handle) < 1)
{
fprintf(stderr, "error reading map for pool file %s\n", fp->filename);
return NULL;
}
}
}
return fp;
}

A host system file name is created by appending the string ".TIO" to the ID. Then, a trial open is tried. By convention, a fixed file must exist, so if the open fails on a fixed file, an error is returned. If the open succeeds, the number of records in the file is calculated and saved in the file table. A pool file which does not exist is created. A bit map is allocated with the first bit set, and the map is written to the file. There is no maximum number of records for a pool file. If the pool file exists, the bit map is allocated and read. A pointer to the bit map is saved in the file table entry. Finally, the pointer to the table entry is returned. Any error results in a message and a NULL pointer return.

Now that we have the details of file management under control, we can turn to the getfc() simulation function itself.

/*
* file address reference word
*/
struct farw
{
short record_id;
char record_cc;
char reserved;
int file_address;
};
getfc(int lev, char *fil, int *rtn)
{
struct ftbl *fp;
struct farw *fr;
int i;
if ((fp = TPFfile(fil, PoolFile)) == NULL)
{
fprintf(stderr, "error occurred in Getfc\n");
*rtn = -1;
return;
}
if ((i = allocate_pool_record(fp)) == 0)
{
fprintf(stderr, "Getfc - pool file size exceeded\n");
*rtn = -1;
return;
}
fr = &_ecb.ce1fa0 + lev;
fr->file_address = i * fp->blksz + 1;
getccmax(lev, fp->blksz);
*rtn = 0;
return;
}

The simulated getfc() calls TPFfile() to get the file table entry. Any error results in a message and a -1 return code. If all is well, the function allocate_pool_record() is called to get a new pool record. In the interest of space, this function is left up to the reader. It should search the pool file's bit map, which can be located from the table entry, looking for a 0 bit which indicates a free record. The bit should be set to 1 and the map block should be rewritten to the first record of the simulated file. Any error should produce a zero return code. Zero is not a valid bit number since the zero'th bit represents the map itself.

The file address reference word for level 0 is assumed to be declared as ce1fa0. Adding the requested level to this address produces a pointer to the requested level. The file address is then the block number times the block size. We add 1 to the file address so that a valid file address is always non-zero, although this is only applicable to simulated fixed files. Pool files always have non-zero file addresses because of the bit map stored as the first block. The last action performed by getfc() allocates a block of the proper size on the proper storage level. The function getccmax() is a simulation of the TPF macro GETCC level,SIZE=(reg). The GETCC services are easily simulated. The only interesting issue is guaranteeing a 4K boundary when a 4K block is requested. Again, the actual code is left up to the reader. getfc() finally returns a code of 0 when it completes successfully.

The above code should work with any C compiler in any host system, with the exception of some older versions of fopen() which do not accept the "b" option. In this case, it can be omitted. The TPFfile() function should also work with fixed file symbolic identifiers as long as the first two characters are unique. If the record ID's used in the TPF system are binary, the file table can be modified to accommodate them. A new file name convention will need to be invented. The actual TPF GETFC service interface for users who do not have Prisym/C may be slightly different. In the worst case, this can be handled by conditional compilation.

Simulation is a powerful tool which can improve TPF productivity and reduce development cost. It is not even necessary to buy a simulation package, as most functions which can be simulated are as simple as getfc(). Of course, simulation cannot handle all the complexities of a real system, but it can help get code to the system test stage more rapidly. Skeptical users are invited to contact the author at 516-367-6776 for more information. I will look into some other aspects of simulation in future articles.