Fritz is a PC consultant and developer for the University of Southern California University Computer Services department working in C, Visual Basic, and Borland Delphi. He can be contacted at jlowrey@ucs.usc.edu.
What do you do if you have several hundred networked computers (like we do at the University of Southern California student computer lab) running software that demands a customized run-time environment? If you are using DOS, the usual solution is to set aside enough space for the environment (in CONFIG.SYS), then add the program's settings (such as FOOBAR=/a /s /d /f:2) to an entry in the AUTOEXEC.BAT file. You can even create a separate batch file that sets this variable before running the program, then clears it after the program exits.
At USC, several programs installed on a network need a pointer to themselves in either the PATH statement or the Novell search path. This isn't simple, as some PATHs are already approaching the maximum allowable length (127 characters in DOS 6.2), and we are mapping several servers to satisfy the needs of the lab's users. Imagine trying to get the PATH to hold a number of directory names such as F:\PROGRAMS\CLASS\BIO\BIGAPP\ or F:\PROGRAMS\WP\WINWORD\WFW6.0\. You approach critical mass pretty fast! Consequently, I needed to write a program that would alter a program's environment values (PATH and TEMP mostly) on a "per run" basis, so that we would need to set only a few universal values in AUTOEXEC.BAT. Essentially my program would "wrap" the target program, changing the environment as needed, then exit once the target program was run. Since this sort of thing is trivial under DOS and UNIX, I figured it would also be easy under Windows. I was wrong.
Most operating systems have some concept of an "environment" for a particular program. DOS and UNIX (and Windows, for that matter) use a set of strings in an array (char *envp[] or char **environ in C/C++) that designates certain program options, such as search path and temporary directory. These strings are manipulated by the standard library calls; char *getenv(char *search) which gets environment string identified by search, and intputenv(char *putstr), which changes or adds an environment variable.
A program uses and changes its variables as it sees fit. Child programs, however, get copies of the parent's environment variables; see Figure 1. Thus, variables for a given child task can be altered from that of the parent. Examples are the search path (PATH=c:\;c:\dos;c:\windows) and the temporary directory (TEMP=c:\temp); see Figure 2.
Windows programs exhibit behavior based upon environment settings; for instance, using the TEMP and PATH values to locate files and components. This DOS-like behavior may lead you to assume that if an environment variable is modified, a child would then inherit it, but this isn't the case.
By default, Windows programs get a pointer to--rather than a copy of--an environment space; see Figure 3. To me, this meant that though getenv() should work, putenv() must be either very dangerous or totally ineffectual. The problem is that if all programs get a pointer to the same chunk of memory and each of them is able to alter it, then any alteration to the environment space will affect all tasks unpredictably. Books such as Undocumented Windows, by Andrew Schulman et al. (Addison-Wesley, 1992), describe the Windows application-startup process; however, no mention is made of environment-related behavior. To make things more confusing, both Visual C++ 1.5 and Borland C++ 4.5 include getenv() and putenv() calls that seem to work; that is, getenv("PATH") returns the value of the PATH from your AUTOEXEC.BAT file, and putenv("FOO=BAR") followed by getenv("FOO") returns BAR. But if you then run a child program, it inherits the unchanged DOS environment. How can this be?
The Borland and Microsoft Windows C/C++ run-time startup code appears to make a copy of the default environment into an internal array that is then accessed by envp[], getenv(), and putenv(); see Listing One. While useful within a single program, this is pretty useless for making functional changes to the environment used by child tasks; see Figure 4.
Working my way through assorted documents in the Windows 3.1 API reference, I found HINSTANCE LoadModule(LPCSTR lpszModuleName, LPVOID lpvParameterBlock). The lpvParameterBlock structure must be user defined; Example 1 identifies its fields.
It seemed to me that the segEnv field would run a child with a modified environment, but I had to make the changes without ruining the rest of the system. I first attempted to pass the segment value of the char **_environ array maintained by the Borland run-time code, but this induced a GPF. The next logical step was to make a memory buffer the same size as the environment and filling it with my own data. To determine how much space to allocate, I created an integer value called "ENVSIZE." This value was determined by hand, as there seems to be no straightforward way to get it from Windows. In the demonstration code the size is passed to EnvInit() as an argument. (To find your environment size, look at the "SHELL" line in CONFIG.SYS. If you are using command.com, the /e: parameter sets the environment size; mine is set to 1024 bytes.) To ensure the child task access to this memory, I made it sharable within the GlobalAlloc() API call, as shown in Example 2.
To get all of the DOS environment strings into this buffer, you can loop through envp[] and copy all of the strings (and NULLs) or use the LPSTR GetDOSEnvironment() API call, which returns a pointer to the first address of either the default Windows environment or an environment space created for (and passed to) this program. While LPSTR GetDOSEnvironment() is documented in the help system, there is no mention or warning about the dangers of altering such a globally accessible memory space. I made my copy this way: memcpy(pNewEnv, GetDOSEnvironment(), ENVSIZE);.
Once you have a copy of the environment, you can manipulate it as you see fit. For instance, Example 3(a) gives you a print-out of your environment strings in Windows. Under DOS or UNIX, the loop would look like Example 3(b).
At this point, you can write into your memory block without affecting the rest of the system. I reimplemented getenv() and putenv() to use my memory buffer, emulating the documented functionality of the original routines as closely as possible; see Listing Two (winenv.c).
Next I wanted to run a program so that it got my new environment variables rather than the Windows defaults. To run a child, most people use UINT WinExec(LPCSTR lpszCmdLine, UINT fuCmdShow) since it is easy to construct a command line with the program name and command-line parameters; for example, WinExec("notepad c:\autoexec.bat", SW_SHOWNORMAL);. However, if you want the child to inherit the new environment, this won't work because WinExec() enters the kernel module and runs the child program using the kernel defaults, including the environment pointer; see Figure 5.
To pass the new environment to the child task, call LoadModule(). Example 4 is the code I use to run my child. The resulting new program gets a pointer to the memory space filled with environment strings created by the parent program when it calls GetDOSEnvironment(); see Figure 6. The Windows 3.1 API reference alludes to this behavior but does not document it.
The result of all this is that I can now use my versions of getenv() and putenv() and run a program that inherits a new set of environment values. The only caveat is that the memory buffer must exist when the child needs it. Because the startup code is executed before the LoadModule() call returns, this is not a problem if the child is a C/C++ program (but not a DLL--these don't get run-time environment pointers and may be unloaded, then loaded by a different task). Once the startup code has run, the child has the environment space buffered internally for use by envp[]/environ. If the child does not use this startup-code mechanism (or the child was written in a language like Borland Delphi that does not initialize an environment arena), it is critical not to free the environment memory buffer before exiting the parent. If the parent frees the new buffer, then, when the child tries to access its environment, it will GPF due to an invalid pointer access.
Environment handling under Windows 3.1 is poorly documented, and using a single environment space in the Windows system area for all programs is dangerous. Although the Borland C++ and Microsoft C/C++ getenv() and putenv() routines appear to work under Windows, they do not affect the behavior of child tasks as might be expected, and this should be documented.
Solving this problem was a merry chase, and I enjoyed it (though I lost quite a bit of hair in the process). I regret only that I have not found a way to create a program that can build an altered environment buffer, run a child, and then exit without worrying about whether or not the child will crash when trying to find its PATH. Perhaps you can help out here.
Thanks to C.J. Zinngrabe, Chuck Hellier, Frank Callaham, Steve Bridges, and Mike Beatrice.
Figure 1: The child gets a copy of the parent environment.
Figure 2: If Child 1 changes the environment, Child 2 will get a copy of the changes.
Figure 3: Windows programs get a pointer to--not a copy of--the environment variables.
Figure 4: Since envp[], getenv(), and putenv() work on a buffered environment, the child program doesn't see any changes.
Figure 5: A WinExec()ed child will get the default Windows environment pointer.
Figure 6: LoadModule() lets you point the child at a new environment space.
Example 1: lpvParameterBlock fields.
Copyright © 1995, Dr. Dobb's Journal
struct lpvParameterBlock {
WORD segEnv; /* child environment */
LPSTR lpszCmdLine; /* child command tail */
UINT FAR* lpShow; /* how to show child */
UINT FAR* lpReserved; /* must be NULL */
} LOADPARMS;
Example 2: Allocating sharable memory for environment information.
hNewEnv = GlobalAlloc(GPTR | GMEM_SHARE, ENVSIZE);/* get the memory */
pNewEnv = GlobalLock(hNewEnv); /* lock it down to get a pointer */
Example 3: (a) Printing out environment strings; (b) loop for DOS or UNIX.
(a)
char *tempstr;
tempstr = pNewEnv;
while (*tempstr != NULL) {
printf("%s\n", tempstr);
tempstr += strlen(tempstr) + 1; /* move to the next string */
}
(b)
int i;
for(i=0; envp[i] != NULL; i++)
printf("%s\n", envp[i]);
Example 4: Code to run a child that can access its parent's environment.
struct LOADPARMS parms; /* needed for LoadModule */
char *progname = "envtst.exe"; /* see included source */
word show[2] = {2, SW_SHOWNORMAL];
parms.segEnv = FP_SEG(pNewEnv); /* BC++ macro to get seg address */
parms.lpszCmdLine = (LPSTR) ""; /* No command line options */
parms.lpShow = &show; /* address of show state array */
parms.lpReserved = (LPSTR) 0;
result = LoadModule(progname, &parms);Listing One
/* envtst.c : This prints out the current envp[] set and the current Windows
default environment settings. I used this to determine if a change to the
environment settings had taken place. Build this as a Borland EasyWin
(Windows command line) executable. 5/95 Fritz Lowrey
*/
#include <stdio.h>
int main(int argc, char *argv[], char *envp[]) {
int i;
char *denv; /* default environment */
printf("Program environment array:\n");
for (i=0; envp[i] != NULL; i++)
printf("%s\n", envp[i]);
printf("\nWindows default environment:\n");
denv = GetDOSEnvironment();
while (*denv != NULL) {
printf("%s\n", denv);
denv += strlen(denv) + 1; /* move to the next string */
}
exit(0);
}
Listing Two
/* winenv.c: Build using Borland C++ EasyWin environment to allow for stdio
function calls. Copyright John "Fritz" Lowrey, 24 May, 1995. This code and
research that made it possible were done in conjunction with the University of
Southern California University Computer Services Dept.
*/
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <dos.h>
#define DEMO
/* static variable for environment manipulation, not visible to other modules*/
static char *lpNewEnv; /* pointer to environment space */
static HGLOBAL hNewEnv; /* handle to environment memory */
static int ENVSIZE; /* size of environment space */
/* LOADPARMS stucture needed by LoadModule */
struct LOADPARMS{
WORD segEnv; /* child environment */
LPSTR lpszCmdLine; /* child command tail */
UINT FAR* lpShow; /* how to show child */
UINT FAR* lpReserved; /* must be NULL */
} ;
/* Initialize the environment space, ENVSIZE is the size of the environment
region (defined on the SHELL line of CONFIG.SYS */
/* returns -1 on error or 0 on sucess */
int EnvInit(int esize) {
ENVSIZE = esize;
if((hNewEnv = GlobalAlloc(GPTR | GMEM_SHARE, ENVSIZE)) == NULL)
return -1;
if ((lpNewEnv = GlobalLock(hNewEnv)) == NULL)
return -1;
/* we now have a pointer to the memory, fill it from the env space */
if (memcpy(lpNewEnv, GetDOSEnvironment(), ENVSIZE) == NULL)
return -1;
/* environment space is initialized, return 0 */
return 0;
}
/* definitions for new getenv and putenv routines */
/* Simple new getenv() routine. Seach must be a label only */
LPSTR NewGetEnv(LPSTR search) {
LPSTR tmpstr;
/* point tmpstr at the environment space */
tmpstr = lpNewEnv;
/* scan through the space */
while (tmpstr[0] != NULL) {
/* if "search" is found at begining of tmpstr, return tmpstr */
if (strstr(tmpstr, search) == tmpstr)
return tmpstr;
tmpstr += strlen(tmpstr) + 1; /* move to next string */
}
/* if we fall through to here, return NULL */
return NULL;
}
/* new putenv(): returns 0 on sucess -1 on failure */
int NewPutEnv(LPSTR putstr) {
LPSTR currentloc; /* currentlocation in the buffer */
LPSTR tmpstr; /* used to move through buffer */
char label[30]; /* the lable portion of putstr */
HGLOBAL hHoldEnv;
char *pHoldEnv; /* holding area for the environment */
int deleting = 0;
/* if there's nothing to do, return failure */
if(putstr == NULL)
return -1;
/* if the = in input is in 1st position, or there is no '=', fail */
if((strchr(putstr, '=') == 0) || (strchr(putstr, '=') == putstr))
return -1;
/* create holding area for the new environment */
if((hHoldEnv = GlobalAlloc(GPTR, ENVSIZE)) == NULL)
return -1;
if((pHoldEnv = GlobalLock(hHoldEnv)) == NULL)
return -1;
/* assume we're OK, get the label */
memset(label, '\0', 30);
memcpy(label, putstr, strchr(putstr, '=') - putstr);
/* check to see if were deleting */
if(putstr[strlen(putstr)-1] == '=')
/* '=' is last character, were deleting */
deleting = 1;
/* now move through the input, trying to find the label */
tmpstr = lpNewEnv;
currentloc = pHoldEnv;
while(tmpstr[0] != NULL) {
if(strstr(tmpstr, label) == tmpstr) {
if(!deleting) {
/* string found, copy in new string */
/* be sure to get the NULL */
memcpy(currentloc, putstr, strlen(putstr) +1);
currentloc += strlen(putstr) + 1;
}
}
else {
/* not found. copy current string to holding area */
memcpy(currentloc, tmpstr, strlen(tmpstr) + 1);
currentloc += strlen(tmpstr) + 1;
}
tmpstr += strlen(tmpstr) + 1; /* get next string */
}
currentloc[0] = NULL; /* ensure a trailing NULL */
/* now copy all of this stuff back on top of the envspace */
memcpy(lpNewEnv, pHoldEnv, ENVSIZE);
/* free up the hold environment */
GlobalUnlock(hHoldEnv);
GlobalFree(hHoldEnv);
return 0;
}
/* NewWinExec will run a program using the new environment values */
int NewWinExec(char *progname, char *cmdline, int showstate) {
UINT shows[2];
struct LOADPARMS parms;
shows[0] = 2;
shows[1] = showstate;
parms.segEnv = FP_SEG(lpNewEnv);
parms.lpszCmdLine = cmdline;
parms.lpShow = &shows[0];
parms.lpReserved = NULL;
return LoadModule(progname, &parms);
}
/* free up the environment memory space */
void EnvClose(void) {
GlobalUnlock(hNewEnv);
GlobalFree(hNewEnv);
}
#ifdef DEMO
int main(int argc, char *argv[], char *envp[]){
LPSTR tmpstr;
int i;
FILE *outfile;
outfile = fopen("c:\\temp\\envfile.txt", "w");
printf("Environment from envp:\n");
for (i =0; envp[i] != NULL; i++) {
printf("%s\n", envp[i]);
fprintf(outfile, "%s\n", envp[i]);
}
/* initialize the holding environment */
if(EnvInit(1024) == -1) {
printf("Environment failure!\n");
exit(-1);
}
printf("\nEnvironment from pNewEnv:\n");
fprintf(outfile, "\nEnvironment from pNewEnv:\n");
tmpstr = lpNewEnv;
while(tmpstr[0] != NULL) {
printf("%s\n", tmpstr);
fprintf(outfile, "%s\n", tmpstr);
tmpstr += strlen(tmpstr) + 1;
}
printf("\nPATH from NewGetEnv = %s\n", NewGetEnv("PATH"));
fprintf(outfile, "\nPATH from NewGetEnv = %s\n", NewGetEnv("PATH"));
printf("\nTEMP from NewGetEnv = %s\n", NewGetEnv("TEMP"));
fprintf(outfile, "\nTEMP from NewGetEnv = %s\n", NewGetEnv("TEMP"));
printf("\nSetting PATH and TEMP...\n");
fprintf(outfile, "\nSetting PATH and TEMP...\n");
if(NewPutEnv("TEMP=c:\\fritz") == -1) {
printf("NewPutEnv error!\n");
fprintf(outfile, "NewPutEnv error!\n");
}
if(NewPutEnv("PATH=c:\\;c:\\dos;c:\\windows") == -1) {
printf("NewPutEnv error!\n");
fprintf(outfile, "NewPutEnv error!\n");
}
printf("\nPATH from NewGetEnv = %s\n", NewGetEnv("PATH"));
fprintf(outfile, "\nPATH from NewGetEnv = %s\n", NewGetEnv("PATH"));
printf("\nTEMP from NewGetEnv = %s\n", NewGetEnv("TEMP"));
fprintf(outfile, "\nTEMP from NewGetEnv = %s\n", NewGetEnv("TEMP"));
fclose(outfile);
EnvClose();
exit(0);
}
#endif DEMO