Sam Trenholme's webpage
This article was posted to the Usenet group alt.hackers in 1995; any technical information is probably outdated.

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|



Back to index