Victor R. Volkman received a B.S. in computer science from Michigan Technological University in 1986. His areas of interest include database internals, compiler semantics, and graphics applications. He is currently employed as software engineer at Cimage, Inc. of Ann Arbor, Michigan. He can be reached at the HAL 9000 BBS, 313-663-4173, 1200/2400 baud.
Structured Query Language (SQL) based programs are an increasingly popular method for manipulating relational databases. Since the introduction of the ANSI SQL standard in 1986, more and more users are demanding SQL solutions for their database problems. This is because standard SQL is a platform independent solution for database applications. SQL defines a high-level English-like set of commands which perform SELECT, PROJECT, JOIN, UPDATE, INSERT, DELETE, and other operations against databases [1].
However, SQL as its name implies was originally developed to function as a query language, and the current SQL standard fails to provide the tools needed for the nuts and bolts of most applications. Functions such as handling data entry screens, report writing, and user-defined menus are notably absent. The developer has two options: he can either implement an Embedded SQL (ESQL) [2] solution, or use a fourth generation language (4GL) solution. ESQL often requires scores, if not hundreds, of lines of code just to perform a single SQL query. Additionally, debugging is complicated by the addition of the ESQL preprocessor. The alternative to ESQL is a fourth generation language (4GL). A 4GL system allows SQL statements to be incorporated directly into its programming environment. Furthermore, 4GL programs are often executed by interpreters meaning that the development cycle is shorter than for complied ESQL solutions. With the addition of a C interface, a system like Informix combines the rapid development environment of a 4GL with the power and flexibility of C.
Informix-4GL is an SQL-based fourth generation language with extensions for functions, while/do loops, for/next loops, case/switch, and many other structured elements. Figure 1 is an example Informix-4GL program which displays the entire CONTACT database to the screen. Informix-4GL offers a robust development environment across many platforms including PC-DOS and most versions of UNIX. Additionally, it has built-in report writing, window management, menuing, and form-handling features.
A customizable interface for C functions allows them to be called from within the Informix-4GL interpreter. You need this C interface to augment the native fourth generation language with complex or otherwise unsupported functions. Informix-4GL contains statements to manipulate relational tables in any conceivable fashion, but lacks a library of routines to open, close, read, and write ordinary ASCII files. This oversight is easily corrected by installing a set of functions to access the built-in C stream I/O library (fopen, fclose, fgets, etc.). Since stream I/O is familiar to C programmers, this library can serve as a gentle introduction to the Informix-4GL/C interface. But before delving headlong into the C interface, you should attain a certain level of familiarity with the Informix-4GL environment.
The Informix-4GL RDS Environment
The Rapid Development System (RDS) for Informix-4GL is a "layered" environment. Several layers of programs must be loaded before an application may actually be executed. Figure 2 shows the memory map of a custom mailing labels application. Although the example environment shown in Figure 2 is for real-mode PC-DOS, the protected-mode version is logically equivalent. The first component is the SQL Engine, a resident collection of database functions available to all system and user programs. The next layer is the 4GL Runner, a program which interprets 4GL programs and calls the SQL Engine for database functions. The final component is the compiled 4GL User Application, a p-code file produced by the 4GL Program Compiler (FGLPC). (For the remainder of this article, when you see "4GL," please understand that this means Informix-4GL only.)
4GL Data Types
Informix-4GL supports a variety of data types for use with 4GL programs (see Figure 3) . The SMALLINT, INTEGER, and CHAR correspond exactly to the shor, long, and char types in C. All base types may be grouped into arrays and records as desired, and you may create arrays of records as well. The most notable omission to 4GL is the absence of any pointer concepts. All program variables are instantiated with the DEFINE statement. Scoping rules are much the same as C since local variables override global variables of the same name. Here are some examples of legal 4GL variable declarations:
define last_name char(30) define age smallint define units_sold integer define amt_owed decimal (16,2) define employees array[10] of record define emp_name char(30), define salary float end recordInformix-4GL automatically performs as much data conversion as it can during expression evaluation so that type casting is generally unneccessary. However, a failed type conversion will cause a run-time error.The string data type in 4GL has a few differences from its C counterpart. First, strings in 4GL are not terminated by a null character; instead, they are padded with trailing blanks to fill out their required size. The blank padding, however, can be trimmed from string variables in any expression by adding the CLIPPED suffix. Second, returning a string longer than 512 characters from a function causes a run-time error. This is a limitation of the Informix parameter-passing mechanism, since strings can normally be up to 32,767 chars long.
4GL Functions, Variables, And Parameters
Function declarations in Informix-4GL are quite similar to those in C. The function average_num() defined below receives two parameters, computes the sum and average, and returns them to the caller:.
function average_num(val1, val2) define val1 smallint define val2 smallint define sum float let sum = val1+ val2 return sum, sum / 2.0 end functionBoth 4GL and C use the same basic parameter passing system: pass-by-value. This means the arguments to a function are evaluated and then copied to the called function. Since 4GL lacks pointers, parameters are essentially the same as local variables with initial values. Values must be returned to the caller directly in the RETURN statement rather than by modifying parameters. This difference in semantics requires a change in the syntax of function calls. An example calling sequence for average_num might be as follows:
call average_num(12,5) returning sum_val, avg_valAny type of variable may be a parameter or return value with the exception of arrays. 4GL ascertains that each function receives the correct number of arguments and returns the number of values that the calling function was expecting. However, Informix-4GL functions written in C may receive a variable number of parameters.
Interfacing Informix-4GL To C
The development of a custom application with C functions involves creating both a 4GL program and a custom runner. The development cycle for the mailing labels custom application appears in Figure 4. User-written C functions are linked with Informix-supplied libraries to produce a new p-code interpreter. Once built, a runner can execute any number of different 4GL applications without modification.
The userfuncs[] structure array in FGIUSR.C (see Listing 1) serves as a catalog of user-supplied C functions. The cfunc_t structure supplies everything the runner needs to know to call a function: its 4GL name string, its function pointer, and the number of arguments it expects. The userfuncs[] array is terminated by a record of null values which flags the end of the list. Functions expecting a variable number of arguments are indicated by setting the argument count equal to -max, where max is the maximum allowable count. Remember, a function must be declared before a pointer to it can be referenced.
The final component is the actual set of functions listed in your userfuncs[] list. In the mailing labels application, these functions are contained in STDIO4GL.C (see Listing 2) . Informix's calling convention [3] specifies a stack as the model by which parameters are passed. An Informix parameter stack is a Last-in First-Out (LIFO) queue analogous to a CPU stack. Specifically, "push" operations add to the stack and "pop" operations take away from the stack.
Parameters are pushed from left-to-right in a 4GL function call. Functions receiving parameters must then pop them off in a right-to-left order. C functions are allowed to access the 4GL parameter stack through a set of library functions. The complete list of parameter stack function is presented in FGIPROTO.H (see Listing 3) . The only formal argument to the C function itself is an integer containing the parameter count (nargs). 4GL callable C functions must always send the number of 4GL arguments pushed as the return value.
A Stream I/O Library For 4GL
The file STDIO4GL.C contains 4GL callable C functions that provide access to the standard I/O stream library. The functions fgl_fopen, fgl_fclose, fgl_fgets, and fgl_fputs together allow you to read and write ASCII text files. In describing fgl_fopen, we'll revisit each aspect of the Informix-4GL/C interface.
The fgl_fopen function, like every other 4GL callable C function, has nargs as its only parameter. The nargs value indicates how many 4GL parameters are waiting on the 4GL stack. Since the entry for fgl_fopen() in userfuncs[] looks like this:
"fgl_fopen", fgl_fopen, 2,the runner knows there should be exactly two parameters, i.e. nargs=2. If there are a different number of parameters, then the runner will signal a run-time error and not call the function. Thus, there is no reason for fgl_fopen() to examine nargs. The first two lines of fgl_fopen() functions to pop strings from the stack:
popquote (mode, sizeof (mode) - 1); popquote (filename, sizeof(filename) -1);The first parameter tells where to put the string and the second parameter tells how large the destination string can be (excluding the terminating null). If the value being popped contains more characters than are requested, they will be lost. The best defense against this is to allow for the longest reasonable string in your destination variable. If the value being popped contains fewer characters than are requested, the destination string will be padded with blanks. Blank padding fopen()'s filename and mode arguments yields unpredictable results, so the values are clipped before use:
clipped (mode); clipped (filename);If you examine the mailing labels application in LABELS.4GL (see Listing 4) , you can verify that the parameters were indeed popped off in the reverse order from the original call:
call FGL_fopen (filename, "w") returning filehandleBack in the STDIO4GL.C file, the next three lines of fgl_open() actually open the file and return the handle.
ascfile = fopen(filename,mode); retlong ((long) ascfile); return 1;Although casting the FILE pointer to a long type may seem suspect, it is actually quite safe for most environments. Informix-4GL for MS-DOS requires that the large program model is used (code > 64K, data > 64K). Since 4GL does not support a native pointer type, our method seems to provide the most convenient solution. The alternatives would be either to maintain a list of filehandles or to restrict the user's access to one file at a time. fgl_fopen()'s last task is to return a count of the number of parameters pushed.The remainder of the stream I/O functions in STDIO4GL.C are similar in most respects to fgl_fopen(). The only function which returns more than one value is fgl_gets(). This function returns both a string value and an error code. It's crucial to remember that parameters are popped right-to-left, but pushed left-to-right. A sample call such as:
call fgl_fgets(filehandle) returning err_code, bufferrequires return values in the following order:
retshort(err_code); retquote(buffer); return 2;Variable Number Of Parameters
As mentioned earlier, the userfuncs [] array tells the runner how many arguments a function expects. A negative value in the cf_nargs indicates the function can accept a variable number of arguments. One example of a function which needs variable arguments is a function to return the maximum value of a set of numbers. The function fgl_max() in STDIO4GL.C will take up to nine arguments. The following example shows four parameters being used.
call fgl_max(4,2,1,3) returning max_valThe only difference in coding style betwen fgl_max() and the other 4GL callable functions is that fgl_max() acts upon the value of nargs. A function must pop all of its parameters, regardless of whether it has a fixed or variable number of parameters. The stack will be corrupted if unused parameters are left on it.
A Mailing List Application
The mailing labels application in LABELS.4GL (see Listing 4) demonstrates one possible utilization of a 4GL callable stream I/O library. This application essentially uses the stream I/O library as a poor man's report writer. The LABELS.4GL program reads from the CONTACT table of the CARDFILE database and writes the result to LABELS.PRN. The application executes a SELECT on the entire CONTACT table after opening the output file. Next, exactly six lines of output are written for each CONTACT record. Notice that some of the lines contain newline escapes (\n) for extra linefeeds to ensure proper address spacing. Finally, the output file is closed and control returns to the operating sytem.
Conclusion
One of the most frequent complaints by detractors of fourth-generation languages is "Sure, I would use a 4GL, but it doesn't support ____." (fill in the blank). I have discovered, however, that with the addition of a modest set of 4GL-callable C functions, I can implement every feature a developer could want. I have constructed 4GL-callable libraries for a DESQview multi-tasking shell, NetBIOS and Sun PC-NFS based database servers, and a hardware key programmer. Informix-4GL RDS offers both the raw power of SQL with the agility of C to deliver a knockout blow.
Footnotes
[1] Lusardi, David. The Database Expert's Guide to SQL, McGraw-Hill, New York, New York, 1988.
[2] Pass, E.M. "Embedding SQL Commands in Your C Source", The C Users Journal, May 1989. pp. 105-113.
[3] Informix-4GL Rapid Development System Reference Manual, 1988, Informix Software, Menlo Park, CA, pp. 1-58ff.
Figure 1 Sample INFORMIX-4GL program
main define contact_no integer declare label_cur cursor for select * from contact order by last_name, first_name foreach label_cur into label_rec.* let contact_no = contact_no + 1 display "Name: ", label_rec.first_name, label_rec.last_name end foreach display "Found ", contact_no, "records." end mainFigure 2 INFORMIX-4GL RDS Example Run-time Environment for PC-DOS
Figure 3 Summary of INFORMIX-4GL Data Types
Data Description and Usage Type -------------------------------------------------------
CHAR(N) Character string, 1 - 32,767 chars SMALLINT 16 bit signed integer (short) INTEGER 32 bit signed integer (long) DECIMAL BCD decimal, up to 32 significant digits SMALLFLOAT Decimal number with 8 significant digits FLOAT Decimal number with 16 significant digits SERIAL Unique 32 bit signed integer (long) DATE Number of days since 12/31/1899 (long)Figure 4 INFORMIX-4GL RDS Development Cycle for PC-DOS
Listing 1
*/ typedef struct { char *cf_name; /* name of function */ int(*cf_ptr)(); /* pointer to the function */ short cf_nargs; /* number of arguments, < 0 means variable */ } cfunc_t; int fgl_fopen(int); int fgl_fclose(int); int fgl_fgets(int); int fgl_fputs(int); int fgl_max(int); cfunc_t usrcfuncs[] = { "fgl_fopen", fgl_fopen, 2, "fgl_fclose", fgl_fclose, 1, "fgl_fgets", fgl_fgets, 1, "fgl_fputs", fgl_fputs, 2, "fgl_max", fgl_max, -9, 0L, 0L, 0 };
Listing 2
#include "string.h" #include "stdio.h" #include "stdlib.h" #include "fgiproto.h" #define FGL_BUFSIZE 256 /* 4GL syntax is CALL OpenAsciiFile(filename,mode) returning filehandle */ fgl_fopen(nargs) int nargs; { char filename[80]; char mode[10]; FILE *ascfile; popquote(mode,sizeof(mode)-1); popquote(filename,sizeof(filename)-1); clipped(mode); clipped(filename); ascfile = fopen(filename,mode); retlong((long)ascfile); return 1; } /* 4GL syntax is CALL fgl_fclose(filehandle) */ fgl_fclose(nargs) int nargs; { long filehandle; poplong(&filehandle); fclose((FILE *)filehandle); return 0; } /* 4GL syntax is CALL fgl_fgets(filehandle) returning err_code, buffer */ fgl_fgets(nargs) int nargs; { long filehandle; char buffer[FGL_BUFSIZE]; short err_code=0; int len; poplong(&filehandle); memset(buffer,0,sizeof(buffer)); if (fgets(buffer, sizeof(buffer)-1, (FILE *)filehandle) != NULL) { len = strlen(buffer); buffer[len-1] = '\0'; /* trim \r */ } else err_code = -1; retshort(err_code); retquote(buffer); return 2; } /* 4GL syntax is CALL fgl_fputs(buffer,filehandle) returning err_code */ fgl_fputs(nargs) int nargs; { long filehandle; char buffer[FGL_BUFSIZE]; short err_code=0; int len; short err_code=0; int len; poplong(&filehandle); popquote(buffer,sizeof(buffer)-1); clipped(buffer,(char)' '); err_code = fputs(buffer,(FILE *)filehandle); fputc('\n',(FILE *)filehandle); retshort(err_code); return 1; } /* 4GL syntax is CALL fgl_max(arg1, arg2, ...) returning max_val */ fgl_max(nargs) int nargs; { int i; short test_val; short max_val = -32767; for (i=0; i<nargs; i++) { popshort(&test_val); if (test_val > max_val) max_val = test_val; } retshort(max_val); return 1; } clipped(str) /* trim trailing blanks */ char *str; { char *blank; blank = str + strlen(str) - 1; while (*blank == ' ') *blank-- = '\0'; }
Listing 3
*/ /* SOURCE: INFORMIX 4GL REFERENCE MANUAL, VOL I, PAGE 1-59 */ void popint(int *); /* Popping Functions */ void popshort(short *); void poplong(long *); void popflo(float *); void popdub(double *); void popquote(char *, short); void popdec(struct dec_t *); void retint(int); /* Pushing Functions */ void retshort(short); void retlong(long); void retflo(float *); void retdub(double *); void retquote(char *); void retdec(struct dec_t *);
Listing 4
# database cardfile main define label_rec record like contact.* define filehandle integer define err_code smallint define buffer char(256) define filename char(80) define max_val smallint display "Labels v1.00a -- (c) Victor R. Volkman" call fgl_max(4,2,1,3) returning max_val display "max value was ",max_val let filename = "labels.prn" call FGL_fopen(filename,"w") returning filehandle declare label_cur cursor for select * from contact order by last_name, first_name foreach label_cur into label_rec.* let buffer = "\n", label_rec.first_name clipped, " ", label_rec.last_name CALL FGL_fputs(buffer,filehandle) returning err_code let buffer = label_rec.address CALL FGL_fputs(buffer,filehandle) returning err_code let buffer = label_rec.city clipped, "",label_rec.state clipped, " ", label_rec.zip, "\n\n" CALL FGL_fputs(buffer,filehandle) returning err_code end foreach CALL FGL_fclose(filehandle) end main