J
jacob navia
This is a message about programming in C. It is not:
(1) about Schildt
(2) about the errors of Mr Cunningham
(3) Some homework, even if it was made at home and it was surely a lot
of work.
Building generic components
---------------------------
If you take the source code of a container like “arraylist”, for
instance, you will notice that all those “void *”are actually a single
type, i.e. the type of the objects being stored in the container. All
generic containers use “void *” as the type under which the objects are
stored so that the same code works with many different types.
Obviously another way is possible. You could actually replace the object
type within that code and build a family of functions and types that can
be specialized by its type parameter. For instance:
struct tag$(TYPE)ArrayInterface;
typedef struct _$(TYPE)Array {
struct tag$(TYPE)ArrayInterface *VTable;
size_t count;
unsigned int Flags;
$(TYPE) *contents;
size_t capacity;
size_t ElementSize;
unsigned timestamp;
CompareFunction CompareFn;
ErrorFunction RaiseError;
} $(TYPE)_Array ;
Now, if we just substitute $(TYPE) with “double” in the code above, we
obtain:
struct tagdoubleArrayInterface;
typedef struct _doubleArray {
struct tagdoubleArrayInterface *VTable;
size_t count;
unsigned int Flags;
double *contents;
size_t capacity;
size_t ElementSize;
unsigned timestamp;
CompareFunction CompareFn;
ErrorFunction RaiseError;
} double_Array ;
We use the name of the parameter to build a family of names, and we use
the name of the type parameter to declare an array of elements of that
specific type as the contents of the array. This double usage allows us
to build different name spaces for each different array type, so that we
can declare arrays of different types without problems.
Using the same pattern, we can build a family of functions for this
container that is specialized to a concrete type of element. For
instance we can write:
static int RemoveAt($(TYPE)_Array *AL,size_t idx)
{
$(TYPE) *p;
if (idx >= AL->count)
return CONTAINER_ERROR_INDEX;
if (AL->Flags & AL_READONLY)
return CONTAINER_ERROR_READONLY;
if (AL->count == 0)
return -2;
p = AL->contents+idx;
if (idx < (AL->count-1)) {
memmove(p,p+1,(AL->count-idx)*sizeof($(TYPE)));
}
AL->count--;
AL->timestamp++;
return AL->count;
}
when transformed, the function above becomes:
static int RemoveAt(double_Array *AL,size_t idx)
{
double *p;
if (idx >= AL->count)
return CONTAINER_ERROR_INDEX;
if (AL->Flags & AL_READONLY)
return CONTAINER_ERROR_READONLY;
if (AL->count == 0)
return -2;
p = AL->contents+idx;
if (idx < (AL->count-1)) {
memmove(p,p+1,(AL->count-idx)*sizeof(double));
}
AL->count--;
AL->timestamp++;
return AL->count;
}
Now we can build a simple program in C that will do the substitution
work for us. To make things easier, that program should build two files:
1.The header file, that will contain the type definitions for our array.
2.The C source file, containing all the parametrized function definitions.
We separate the commands to change the name of the file from the rest of
the text by introducing in the first positions of a line a sequence of
three or more @ signs. Normally we will have two of those “commands”:
one for the header file, another for the c file.
Besides that, our program is just a plain text substitution. No parsing,
nor anything else is required. If we write “$(TYPE)” within a comment or
a character string, it will be changed too.
#include <stdlib.h>
#include <string.h>
#define MAXLINE_LEN 2048
#define MAX_FNAME 1024
#define EXPANSION_LENGTH 256
int main(int argc,char *argv[])
{
FILE *input,*output=NULL;
char buf[MAXLINE_LEN],
tmpLine[MAXLINE_LEN+EXPANSION_LENGTH];
char tmpBuf[MAX_FNAME];
char outputFile[MAX_FNAME];
char *TypeDefinition;
unsigned lineno = 1;
if (argc < 3) {
fprintf(stderr,
"Usage: %s <template file to expand> <type name>\n",
argv[0]);
return EXIT_FAILURE;
}
input = fopen(argv[1],"r");
if (input == NULL) {
fprintf(stderr,"Unable to open file '%s'\n",argv[1]);
return EXIT_FAILURE;
}
TypeDefinition = argv[2];
delta = strlen(TypeDefinition) - strlen("$(TYPE)");
while (fgets(buf,sizeof(buf)-1,input)) {
if (buf[0]=='@' && buf[1] == '@' && buf[2] == '@') {
int i=0,j=0;
while (buf == '@')
i++;
while (buf != 0 &&
buf != '\n' &&
i < MAX_FNAME-1) {
tmpBuf[j++] = buf;
i++;
}
tmpBuf = 0;
strrepl(tmpBuf,"$(TYPE)",TypeDefinition,outputFile);
if (output != NULL)
fclose(output);
output = fopen(outputFile,"w");
if (output == NULL) {
fprintf(stderr,
"Impossible to open '%s'\n",outputFile);
return(EXIT_FAILURE);
}
}
else if (lineno == 1) {
fprintf(stderr,
"Error: First line should contain the file name\n");
exit(EXIT_FAILURE);
}
else {
/* Normal lines here */
if (strrepl(buf,"$(TYPE)",TypeDefinition,NULL)
"Line buffer overflow line %d\n",lineno);
break;
}
strrepl(buf,"$(TYPE)",TypeDefinition,tmpLine);
fwrite(tmpLine,1,strlen(tmpLine),output);
}
lineno++;
}
fclose(input);
fclose(output);
return EXIT_SUCCESS;
}
The heart of this program is the “strrepl” function that replaces a
given character string in a piece of text. If you call it with a NULL
output parameter, it will return the number of characters that the
replacement would need if any. For completeness, here is the code for
strrepl:
int strrepl(char *InputString,
char *StringToFind,
char *StringToReplace,
char *output)
{
char *offset = NULL, *CurrentPointer = NULL;
int insertlen;
int findlen = strlen(StringToFind);
int result = 0;
if (StringToReplace)
insertlen = strlen(StringToReplace);
else
insertlen = 0;
if (output) {
if (output != InputString)
memmove(output,InputString,strlen(InputString)+1);
InputString = output;
}
else
result = strlen(InputString)+1;
while (*InputString) {
offset = strstr (!offset ? InputString : CurrentPointer,
StringToFind);
if (offset == NULL)
break;
CurrentPointer = (offset + (output ? insertlen : findlen));
if (output) {
strcpy (offset, (offset + findlen));
memmove (offset + insertlen,
offset, strlen (offset) + 1);
if (insertlen)
memcpy (offset, StringToReplace, insertlen);
result++;
}
else {
result -= findlen;
result += insertlen;
}
}
return result;
}
And now we are done. The usage of this program is very simple:
expand <template file> <type name>
For instance to substitute by “double” in the template file
“arraylist.tpl” we would use:
expand arraylist.tpl double
We would obtain doublearray.h and doublearray.c
BUG: Obviously, this supposes that the type name does NOT contain any
spaces. Some type names do contain spaces: long double and long long. If
you want to use those types you should substitute the space with a “_”
for instance, and make a typedef:
typedef long double long_double;
And use that type (“long_double”) as the substitution type.
All container code of the library arrives in two versions:
A library version, that can be used in its generic form.
A "templated" version that can be used to build type specific code.
The reuslts are compatible, i.e. you can start by using the generic
functions of the library and then switch to the type specific ones
without changing your code at all, using only a different constructor
function.
jacob
(1) about Schildt
(2) about the errors of Mr Cunningham
(3) Some homework, even if it was made at home and it was surely a lot
of work.
Building generic components
---------------------------
If you take the source code of a container like “arraylist”, for
instance, you will notice that all those “void *”are actually a single
type, i.e. the type of the objects being stored in the container. All
generic containers use “void *” as the type under which the objects are
stored so that the same code works with many different types.
Obviously another way is possible. You could actually replace the object
type within that code and build a family of functions and types that can
be specialized by its type parameter. For instance:
struct tag$(TYPE)ArrayInterface;
typedef struct _$(TYPE)Array {
struct tag$(TYPE)ArrayInterface *VTable;
size_t count;
unsigned int Flags;
$(TYPE) *contents;
size_t capacity;
size_t ElementSize;
unsigned timestamp;
CompareFunction CompareFn;
ErrorFunction RaiseError;
} $(TYPE)_Array ;
Now, if we just substitute $(TYPE) with “double” in the code above, we
obtain:
struct tagdoubleArrayInterface;
typedef struct _doubleArray {
struct tagdoubleArrayInterface *VTable;
size_t count;
unsigned int Flags;
double *contents;
size_t capacity;
size_t ElementSize;
unsigned timestamp;
CompareFunction CompareFn;
ErrorFunction RaiseError;
} double_Array ;
We use the name of the parameter to build a family of names, and we use
the name of the type parameter to declare an array of elements of that
specific type as the contents of the array. This double usage allows us
to build different name spaces for each different array type, so that we
can declare arrays of different types without problems.
Using the same pattern, we can build a family of functions for this
container that is specialized to a concrete type of element. For
instance we can write:
static int RemoveAt($(TYPE)_Array *AL,size_t idx)
{
$(TYPE) *p;
if (idx >= AL->count)
return CONTAINER_ERROR_INDEX;
if (AL->Flags & AL_READONLY)
return CONTAINER_ERROR_READONLY;
if (AL->count == 0)
return -2;
p = AL->contents+idx;
if (idx < (AL->count-1)) {
memmove(p,p+1,(AL->count-idx)*sizeof($(TYPE)));
}
AL->count--;
AL->timestamp++;
return AL->count;
}
when transformed, the function above becomes:
static int RemoveAt(double_Array *AL,size_t idx)
{
double *p;
if (idx >= AL->count)
return CONTAINER_ERROR_INDEX;
if (AL->Flags & AL_READONLY)
return CONTAINER_ERROR_READONLY;
if (AL->count == 0)
return -2;
p = AL->contents+idx;
if (idx < (AL->count-1)) {
memmove(p,p+1,(AL->count-idx)*sizeof(double));
}
AL->count--;
AL->timestamp++;
return AL->count;
}
Now we can build a simple program in C that will do the substitution
work for us. To make things easier, that program should build two files:
1.The header file, that will contain the type definitions for our array.
2.The C source file, containing all the parametrized function definitions.
We separate the commands to change the name of the file from the rest of
the text by introducing in the first positions of a line a sequence of
three or more @ signs. Normally we will have two of those “commands”:
one for the header file, another for the c file.
Besides that, our program is just a plain text substitution. No parsing,
nor anything else is required. If we write “$(TYPE)” within a comment or
a character string, it will be changed too.
#include <stdlib.h>
#include <string.h>
#define MAXLINE_LEN 2048
#define MAX_FNAME 1024
#define EXPANSION_LENGTH 256
int main(int argc,char *argv[])
{
FILE *input,*output=NULL;
char buf[MAXLINE_LEN],
tmpLine[MAXLINE_LEN+EXPANSION_LENGTH];
char tmpBuf[MAX_FNAME];
char outputFile[MAX_FNAME];
char *TypeDefinition;
unsigned lineno = 1;
if (argc < 3) {
fprintf(stderr,
"Usage: %s <template file to expand> <type name>\n",
argv[0]);
return EXIT_FAILURE;
}
input = fopen(argv[1],"r");
if (input == NULL) {
fprintf(stderr,"Unable to open file '%s'\n",argv[1]);
return EXIT_FAILURE;
}
TypeDefinition = argv[2];
delta = strlen(TypeDefinition) - strlen("$(TYPE)");
while (fgets(buf,sizeof(buf)-1,input)) {
if (buf[0]=='@' && buf[1] == '@' && buf[2] == '@') {
int i=0,j=0;
while (buf == '@')
i++;
while (buf != 0 &&
buf != '\n' &&
i < MAX_FNAME-1) {
tmpBuf[j++] = buf;
i++;
}
tmpBuf = 0;
strrepl(tmpBuf,"$(TYPE)",TypeDefinition,outputFile);
if (output != NULL)
fclose(output);
output = fopen(outputFile,"w");
if (output == NULL) {
fprintf(stderr,
"Impossible to open '%s'\n",outputFile);
return(EXIT_FAILURE);
}
}
else if (lineno == 1) {
fprintf(stderr,
"Error: First line should contain the file name\n");
exit(EXIT_FAILURE);
}
else {
/* Normal lines here */
if (strrepl(buf,"$(TYPE)",TypeDefinition,NULL)
fprintf(stderr,>= sizeof(tmpLine)) {
"Line buffer overflow line %d\n",lineno);
break;
}
strrepl(buf,"$(TYPE)",TypeDefinition,tmpLine);
fwrite(tmpLine,1,strlen(tmpLine),output);
}
lineno++;
}
fclose(input);
fclose(output);
return EXIT_SUCCESS;
}
The heart of this program is the “strrepl” function that replaces a
given character string in a piece of text. If you call it with a NULL
output parameter, it will return the number of characters that the
replacement would need if any. For completeness, here is the code for
strrepl:
int strrepl(char *InputString,
char *StringToFind,
char *StringToReplace,
char *output)
{
char *offset = NULL, *CurrentPointer = NULL;
int insertlen;
int findlen = strlen(StringToFind);
int result = 0;
if (StringToReplace)
insertlen = strlen(StringToReplace);
else
insertlen = 0;
if (output) {
if (output != InputString)
memmove(output,InputString,strlen(InputString)+1);
InputString = output;
}
else
result = strlen(InputString)+1;
while (*InputString) {
offset = strstr (!offset ? InputString : CurrentPointer,
StringToFind);
if (offset == NULL)
break;
CurrentPointer = (offset + (output ? insertlen : findlen));
if (output) {
strcpy (offset, (offset + findlen));
memmove (offset + insertlen,
offset, strlen (offset) + 1);
if (insertlen)
memcpy (offset, StringToReplace, insertlen);
result++;
}
else {
result -= findlen;
result += insertlen;
}
}
return result;
}
And now we are done. The usage of this program is very simple:
expand <template file> <type name>
For instance to substitute by “double” in the template file
“arraylist.tpl” we would use:
expand arraylist.tpl double
We would obtain doublearray.h and doublearray.c
BUG: Obviously, this supposes that the type name does NOT contain any
spaces. Some type names do contain spaces: long double and long long. If
you want to use those types you should substitute the space with a “_”
for instance, and make a typedef:
typedef long double long_double;
And use that type (“long_double”) as the substitution type.
All container code of the library arrives in two versions:
A library version, that can be used in its generic form.
A "templated" version that can be used to build type specific code.
The reuslts are compatible, i.e. you can start by using the generic
functions of the library and then switch to the type specific ones
without changing your code at all, using only a different constructor
function.
jacob