Environment variables under Windows (how to use them!)
Article: 7867 of alt.hackers From: jlowrey@skat.usc.edu (Fritz Lowrey) Newsgroups: alt.hackers,comp.os.ms-windows.programmer.misc,comp.lang.pascal Subject: Environment variables under Windows (how to use them!) Date: 24 May 1995 16:27:27 -0700 Organization: University of Southern California, Los Angeles, CA Lines: 410 Sender: jlowrey@skat.usc.edu Approved: yes Distribution: world Message-ID: 3q0fcv$3d2@skat.usc.edu NNTP-Posting-Host: skat.usc.edu Status: RO
Folks, I would rather be flamed here than in the press, so feel free to offer constructive flamage. I am including a draft of an article I have written concerning what appears to be an undocumented method of handling environment variables from a Windows program. Some source code is included at the end of the text, and I will make the Word 6.0 version of it available via FTP soon. If this really is documented somewhere, whether in some manual or on the net, please let me know If it helps you out, or you have suggestions please let me know. Otherwise, I don't care if OS/2 is niftier or the Amiga can do this six ways from Sunday, and I already know that Unix could do this with its eyes shut. Note: I have cross-posted because I posted a request for this information a while back and got helpful responses, so I am providing the answers that I have discovered. Regards, Fritz ===== included file: Environment Variables, Windows 3.1, and Me Fritz Lowrey 24 May, 1995 Most operating system have some concept of an "environment" for a particular program. DOS and Unix use a set of strings in an array (referred to as "*envp[]" in C and C++) which designate certain options for the program, such as the program search path. These strings can be manipulated by the standard library calls "char *getenv(char *search)" and "int putenv(char *putstr)". A program can use and change its variables as it sees fit, however if it runs a child program, that program gets a copy of its parents environment variables (see Figure 1). This is very useful for altering the search path (e.g. "PATH=c:\;c:\dos;c:\windows") or the temporary directory (e.g. "TEMP=c:\temp") for a given process from that of the parent (see Figure 2). I wanted to do the same thing under Windows 3.1, and I thought it would be easy. Windows programs exhibit behavior based upon the environment strings, such as using the TEMP and PATH values to locate files and components. This implies that Windows programs behave like DOS programs, and that if an environment variable is modified, and child would then inherit it, yes? [ illustrations removed ] No! Windows programs get a pointer to, rather than a copy of, an environment space. What does this mean? Well, first off it means that getenv() and putenv() are either very dangerous or don't really effect the child at all. In Borland C++ the result is the latter, though it is very easy to induce the former. Think about it: if all programs get a pointer to the same chunk of memory, and each of them is able to alter it, then all of them wind up effected in unpredictable ways. None of this is well documented, and both MS Visual C++ 1.5 and Borland C++ 4.5 include a getenv() and putenv() calls that seem to work. However if you then run a child program, it will inherit the default DOS environment. How can this be? [ illustrations removed ] Well, it appears that the Windows C/C++ runtime startup code makes a copy of the default environment into an internal array which is then accessed by envp[] and the getenv() and putenv() routines. This is all well and good, but it is pretty useless for making functional changes for child processes. What's a programmer to do? [ illustration removed ] Hack! Working my way through assorted un-helpful help documents I found: HINSTANCE LoadModule(LPCSTR lpszModuleName, LPVOID lpvParameterBlock) in the Windows 3.1 API reference. The lpvParameterBlock structure must be user defined (Why isn't it defined in the headers? Beats me.). Its fields are: struct _LOADPARMS { WORD segEnv; /* child environment */ LPSTR lpszCmdLine; /* child command tail */ UINT FAR* lpShow; /* how to show child */ UINT FAR* lpReserved; /* must be NULL */ } LOADPARMS; Note the "segEnv" field. It would appear that this will run a child with a modified environment. The challenge was to figure out how to make the changes without ruining the rest of the system. My first attempt was to pass the segment value of the "*_environ[]" array which is maintained by the Borland runtime code. This turned out to be an excellent way to induce a GPF. Undeterred, I started to think... What if I made a memory buffer the same size of the environment and filled it with my own data? Note that "ENVSIZE" is determined by hand, there seems to be no way to get the value from Windows. To find your environment size, look in CONFIG.SYS at the "SHELL" line. If you are using "command.com" the "/e:" parameter sets the environment size (mine is set to 1024 bytes). To ensure that the child process would have access to this memory I made it sharable within the "GlobalAlloc()" API call, like this: hNewEnv = GlobalAlloc(GPTR | GMEM_SHARE, ENVSIZE);/* get the memory */ pNewEnv = GlobalLock(hNewEnv); /* lock it down to get a pointer */ OK, fine. Now how to get all of the DOS environment stuff into this buffer? Well, I could use envp[] and copy all of the strings in, or I could use a nifty API call "char *GetDOSEnvironment()" which returns a pointer to the first address of the current environment. It should be noted that, though this API call is documented in the help system and includes a brief example, there is no explanation of what or where the memory being accessed is. To make my copy I did: memncpy(pNewEnv, GetDOSEnvironment, ENVSIZE); Once you have a copy of the environment you can manipulate it as you see fit. To get a print out of your environment strings, you can try something like this: char *tempstr; tempstr = pNewEnv; while (*tempstr != NULL) { printf("%s\n", tempstr); tempstr += strlen(tempstr) + 1; /* move to the next string */ } This is effectively the same as: int i; for(i=0; envp[i] != NULL; i++) printf("%s\n", envp[i]); Furthermore, you can now write into your memory block without effecting the rest of the system. Code examples accompany this article (see below). The problem is now, how do you run a program so that it gets your new environment variables rather than the Windows defaults. Well, remember the "LoadModule" function that I mentioned about. It's time to use it! Most people use: UINT WinExec(LPCSTR lpszCmdLine, UINT fuCmdShow) to run a child since it is easy to construct a command line with the program name and command line parameters. This will unfortunately not work for those who want the child to inherit the new environment because WinExec() enters the kernel module and runs the child program using the kernel defaults, including the environment pointer. [ illustration deleted ] In order to pass the new environment to the child process the LoadModule call is needed. Here is the code that I used to run my child: 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); The result is that the new program gets a pointer to the memory space filled with environment strings created by this program. [ illustration removed ] The end result of all of this is that I can now use my versions of getenv() and putenv() and run a program that will inherit a new set of environment values. The only caveat in this is that the memory buffer must exist when the child needs it. This is no problem if the child is a C or C++ program (or C/C++ DLL) since the startup code will be executed before the LoadModule call returns. Once the startup code has run, the child has the environment space buffered internally for use by envp[] . However, if the child does not use this same startup code mechanism, or the child was written in a language that does not initialize an environment arena, it is critical not to free the environment memory buffer before exiting the parent. If the parent exits, freeing the new buffer, and the child then tries to access its environment (through whatever mechanism), it will GPF due to an invalid pointer access. What does all of this mean? It means: 1. Environment handling under Windows 3.1 is documented poorly or not-at-all. 2. Using a single environment space in the Windows system area for all programs is dangerous at best. 3. The Borland and Microsoft C/C++ products should document that though the getenv() and putenv() routines work under Windows, they do not effect the behavior of child processes as might be expected. It was a merry chase, one that I enjoyed (though I lost quite a bit of hair in the process). My only regret is that there is still no reliable way to create a program that can create an altered environment buffer, run a child, and the exit without having to worry about whether the child will crash or not when trying to find its PATH. ===== included source #include <windows.h> #include <stdio.h> #include <string.h> #include <dos.h> /* static variable for environment manipulations, 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 the 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 the 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)] == '=') /* '=' 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 */ memcpy(currentloc, putstr, strlen(putstr) +1); /* be sure to get the NULL */ currentloc += strlen(putstr) + 1; } } else { /* not found, so copy the current string to the 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; } /* eWinExec will run a program using the new environment values */ int eWinExec(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")); eWinExec("c:\\bc45\\envdemo.exe", "", SW_SHOWNORMAL); i = GetTickCount(); while(GetTickCount() < (i + 2000)) Yield(); fclose(outfile); EnvClose(); exit(0); } -- "I'll gently raise and softly call, | Fritz Lowrey Goodnight my friends, and joy to all."| Internet: jlowrey@ucs.usc.edu "A Parting Glass", Irish Traditional|