The C/C++ Compiler Learns New Tricks

Dale Rogerson
Microsoft Developer Network Technology Group

Created: August 28, 1992
Revised: January 27, 1993
The section on simplified building was removed. (This method links all programs with the /NOI option enabled, which causes problems.)

Click to open or copy the files in the Back sample application for this technical article.

Abstract

WinMain, GlobalAlloc, and mixed-model programming—these are just some of the conventions C programmers had to accept when they started programming for the Microsoft® Windows™ operating system. Microsoft C/C++ version 7.0 can now hide these conventions so that programmers can use standard C practices; applications thus become much easier to develop and port. This article provides an overview of programming conventions that C/C++ programmers no longer need and a discussion of the new programming practices in C/C++ version 7.0. A bibliography of suggested reading material is included at the end of this article.

A sample application called Back (BACK.EXE) and its accompanying dynamic-link library (DLL) called Trace (TRACE.DLL) demonstrate many of the ideas in this article. See the "Notes on the Sample Application" section for more information about Back and Trace.

Note: The information in this article is valid only for Microsoft Windows version 3.x standard and enhanced modes.

Introduction

The Microsoft® C/C++ version 7.0 compiler and run-time libraries were designed for the Microsoft Windows™ operating system. For this reason, programmers no longer have to follow many of the conventions that differentiated Windows-based programs from MS-DOS®–based programs. For example, C/C++ programmers can now use:

The following sections discuss each of these topics in detail.

Large Model vs. Mixed Model

One of the first weird conventions that programmers moving to Windows face is mixed-model programming. Mixed-model programming brings out the worst in segmented processor architectures. Some pointers are near while others are far. Some variables default to near while others default to far. Source code becomes a confused mass with near and far casts strewn throughout. In Windows protected modes, large model is now the model of choice.

Single Instances

The behavior of Microsoft C version 6.0 was one reason why programmers were reluctant to use the large model. C version 6.0 built large-model applications with multiple read/write data segments. Windows forces an application that uses multiple read/write data segments to be single instance; therefore, applications built by C version 6.0 would run only single instance.

If you want to build a single-instance application, the Microsoft C/C++ compiler's large model gives it to you for free. There is no need to check hPrevInstance—Windows does all the work for you, including putting up an informative dialog box that tells the user that only one instance can run.

Note   If you are not using the Microsoft C/C++ compiler, you should check the documentation for your C compiler to see which options will generate multiple read/write data segments.

Multiple Instances

It is possible to get multiple instances with a large-model application. If you use Borland® C++ or Microsoft C/C++ version 7.0, it is easy to get a single 64K read/write data segment. For the Microsoft C/C++ compiler, the /Gx and /Gtnnn options will do the trick; for the Borland C++ compiler, a single data segment is the default.

For more information, see the "Programming at Large" and "Allocating Memory the Newfangled Way: The new Operator" technical articles on the Microsoft Developer Network CD (Technical Articles, C/C++ Articles).

Performance

Many programmers are concerned about the amount of overhead in a large-model application compared with a small-model or mixed-model application. Performance is never free. Just using the mixed model instead of the large model never makes an application significantly faster. The best method is to use a profiler to determine which code gets executed the most, and optimize that code.

It is preferable to optimize code using portable techniques. If you spend a week making functions near and using other optimizations specific to a segmented architecture, the optimizations (and your week of work) will be lost when you port the code to Windows NT™. Instead, you could spend the week reworking the algorithms used in the code that is executed the most. These improvements will impact performance more significantly than which language, compiler, or compiler options you use.

However, if your marketing department changes specifications faster than an 80486 can prefetch an instruction, algorithms often change overnight. In this situation, a programmer must often use the compiler (sometimes blindly) to try to speed up code instead of optimizing the code itself.

main vs. WinMain

The Microsoft C/C++ startup code first checks for a function labeled main in a Windows-based program. If it cannot find main, it tries to locate a function called WinMain. The gist of this wonderful information is that a Windows-based application can use main instead of WinMain as its entry point, just like an MS-DOS C program. One of the standard ways to declare main is:

void main(int argc, char *argv[], char **envp)
{
}

Why would a program want to use main? Possibly for portability or to use a common source between Windows and MS-DOS or UNIX®. Using main also allows programmers to build upon their MS-DOS knowledge for handling the command line and the environment.

Not to be outdone by any old application, DLLs can also use main instead of LibMain as an entry point. However, in C/C++ version 7.0, the Windows libraries include a default LibMain, so most DLLs will not need a main or LibMain function. This is covered later in the "Using DLLs" section.

The above information was found in plain and public display in the DETAILS.TXT file, which is provided with the Microsoft C/C++ compiler. Those interested in reading code should check out the SOURCE\STARTUP\WIN directory for the STUBMAIN.ASM and CRT0.ASM files.

Getting to hInstance

The careful reader will be wondering where the program is going to get its instance handle. Why, from _hInstance, of course! _hInstance is an undocumented feature of the C/C++ startup code.

When Windows calls the startup code, the instance handle is passed to the startup code in the DI register, as documented in the Microsoft Windows version 3.1 Software Development Kit (SDK) Programmer's Reference, Volume 1: Overview, in Chapter 22.

The instance handle is then placed in a global variable called _hInstance. To access this variable, you must declare it first:

extern const HINSTANCE _hInstance;

The startup code also includes the following global variables for the other parameters normally passed to WinMain:

You can access these variables by using the following declarations:

extern const HINSTANCE _hPrevInstance;
extern const LPSTR _lpszCmdLine;
extern const int _cmdShow;

The parameters passed to a DLL are different from parameters passed to an application. The following global variables are defined in the startup code for a DLL:

The following declarations will give you access to these variables:

extern const HINSTANCE _hModule ;
extern const LPSTR _lpszCmdLine ;
extern const WORD _wDataSeg ;
extern const WORD _wHeapSize ;

A quick look into the startup code uncovered the above information. The startup code is included with Microsoft C/C++ version 7.0; look in the SOURCE\STARTUP directory. For more information on the startup code and what it does, see "A Comprehensive Examination of the Microsoft C Version 6.0 Startup Code" in the Microsoft Systems Journal, Vol. 7, No. 1, on the Microsoft Developer Network CD. The article examines C version 6.0 startup code for MS-DOS, but most of the information is also valid for version 7.0. This article explains the work the startup code must perform and provides background information for reading the source code.

Note   The startup source code is subject to change between compiler releases. The inclusion of specific startup variables or functions is not guaranteed in future releases.

_fmalloc vs. GlobalAlloc

The big problem with GlobalAlloc is that it consumes a selector for each call. A selector is a limited resource in Windows version 3.x, so GlobalAlloc is inappropriate for allocating small blocks of memory such as nodes in a linked list. The solution is to implement a subsegment allocation scheme in which one segment is allocated with GlobalAlloc and divided up into small blocks.

Fortunately, Microsoft C/C++ version 7.0 includes a subsegment allocation scheme called _fmalloc. _fmalloc is the large-model or model-independent version of malloc. When you compile with the large model, malloc is mapped to _fmalloc. In other memory models, malloc must explicitly be called _fmalloc.

_fmalloc manages its own heap on top of the Windows global heap. When _fmalloc is called, it first checks whether it can satisfy the memory request by simply returning a pointer to an unused block inside its heap. If it can't, _fmalloc takes one of the following actions:

You use _ffree to free the memory blocks allocated by _fmalloc. However, _ffree does not call GlobalFree. Instead, _ffree marks a block as unused, and _fmalloc tries to satisfy future requests for memory with these unused blocks by reusing them. The _heapmin function releases unused blocks back to Windows.

For more information on using malloc in a Windows-based program, see the "Allocating Memory the Old-Fashioned Way: _fmalloc and Applications for Windows" technical article on the Microsoft Developer Network CD (Technical Articles, C/C++ Articles).

GlobalAllocPtr vs. GlobalAlloc

If you don't want to use _fmalloc, at least use GlobalAllocPtr instead of GlobalAlloc. GlobalAllocPtr is a macro defined in WINDOWSX.H that allocates the memory, locks the handle, and returns a pointer to the allocated memory. To free the memory, use GlobalFreePtr. There is no need to retain memory handles or lock and unlock memory blocks.

What makes this possible is the GlobalHandle function, which takes a pointer and returns the handle to it. GlobalHandle removes the need for saving and tracking handles, resulting in incredible savings in time, memory, and complexity.

Other convenient memory macros in WINDOWSX.H are:

If these macros were C functions, they would be prototyped as follows:

void FAR * GlobalAllocPtr(UINT flags, DWORD size) ;
      // Allocates and locks a block of size bytes with the
      // flags set.

BOOL GlobalFreePtr(void FAR* lp) ;
      // Unlocks and frees the block pointer by lp;
      // returns a non-zero on success.

void FAR * GlobalReAllocPtr(void FAR* lp, DWORD size, UINT flags) ;
      // Reallocates the block pointed to by lp to size bytes with
      // the flags set.
      // The return value is the pointer to the reallocated block.

HGLOBAL GlobalPtrHandle(void FAR* lp) ;
      // Gets global handle pointed to by lp from FAR pointer.

BOOL GlobalLockPtr(void FAR* lp) ;
      // Locks the block lp points to.
      // If successful, returns a non-zero value.

BOOL GlobalUnlockPtr(void FAR* lp) ;
      // Unlocks the block lp points to.
      // If successful, returns a non-zero value.

For the curious, here are the definitions of GlobalAllocPtr and GlobalFreePtr:

#define GlobalAllocPtr(flags, cb)  \
         (GlobalLock(GlobalAlloc((flags), (cb))))

#define GlobalFreePtr(lp)    \
         (GlobalUnlockPtr(lp),(BOOL)GlobalFree(GlobalPtrHandle(lp)))

Using DLLs

Microsoft C/C++ version 7.0 run-time libraries provide better support for building DLLs. Two changes that simplify building DLLs are:

Most of the information for this section can be found in the DETAILS.TXT file, which is included with the Microsoft C/C++ compiler.

Note   The library files that do not include the C run-time functions (for example, xNOCRTDW.LIB, where x is the memory model) do not have a default LibMain or WEP function. You must provide your own LibMain and WEP functions if you use these libraries.

LibMain

Many DLLs are collections of functions that do not need to perform initialization and therefore do nothing in the LibMain function. If a function does not do anything, it would be nice if the developer did not have to worry about it. The C run-time libraries now include a version of LIBENTRY.OBJ and a default LibMain function. So, if the DLL links to the C run-time functions, it does not have to link to LIBENTRY.OBJ or provide its own LibMain function.

The default LibMain function is very clever. It does nothing.

WEP

It is no longer necessary to include a dummy WEP function in your DLL code. The C run-time libraries now include a default version of the WEP function. The default WEP performs the following functions:

  1. Calls the optional user termination function _WEP (see next section).

  2. Performs C exit processing (calls _cexit).

  3. Returns to Windows the value returned by _WEP.

Placing WEP in a fixed segment ensures that it will exist in memory in case of error. For proper placement of WEP, include the following lines in the .DEF file:

SEGMENTS 'WEP_TEXT' FIXED PRELOAD

EXPORTS
   WEP @1 RESIDENTNAME

The source code for the default WEP function is included with the Microsoft C/C++ version 7.0 compiler. Look in the SOURCE\STARTUP\WIN directory for a file called WEP.ASM.

_WEP

To add your own processing to the default WEP function, add a _WEP function to your DLL. (Note the leading underscore character.) Here is an example:

int FAR PASCAL _WEP(int nExitType);

// Put _WEP code into same fixed segment as the WEP function.
#pragma alloc_text(WEP_TEXT, _WEP)

int FAR PASCAL _WEP(int nExitType)
{
   //
   // Exit cleanup code goes here. 
   //
   return nExitType ;
}

The _WEP function is optional; use this function for cleanup tasks that you want done when the DLL is unloaded. If you do not provide a _WEP function, the default WEP function calls the default _WEP function, which simply returns a one (1). To verify for yourself, check the source in the STUBWEP.ASM file included with the Microsoft C/C++ compiler in the SOURCE\STARTUP\WIN directory.

Avoid the following in a _WEP function:

Building DLLs becomes much easier with the default WEP and LibMain functions. It is almost possible to cut functions from an application and simply recompile them to get a DLL. Using the large model for both the DLL and the application simplifies this process.

Notes on the Sample Application

The sample application, Back, demonstrates some of the concepts presented in this article. Back lists command-line options and environment variables. It can be built for MS-DOS or for Windows. The MKWIN.BAT batch file builds the Windows version, while the MKDOS.BAT batch file builds the MS-DOS version.The code for the sample application is simple and straightforward.

To display the output, the MS-DOS version of Back uses printf and the Windows version uses trace, which is a function exported from a DLL called TRACE.DLL. trace performs printf-like printing to the debug monitor. It demonstrates how to export a CDECL variable argument function from a DLL and shows how simple a DLL can be.

To view the BACK.C and TRACE.C files, click the sample application button at the beginning of this article.

Conclusions

Microsoft C/C++ version 7.0 introduces new programming practices that facilitate the development of applications for Windows version 3.1 in protected mode. Programmers can now:

Bibliography

Technical Articles

All of the articles below are available on the Microsoft Developer Network CD (Technical Articles, C/C++ Articles):

Product Documentation

On the Microsoft Developer Network CD, you can find the following books under C/C++ 7.0 in the Product Documentation section of the Source index:

See the following book under Windows 3.1 SDK in the Product Documentation section of the Source index:

Other