Using Handle Pointers
Handle pointers are a special data type that support virtual memory management in 16-bit compilations. They let a data structure use as much as 16KB of memory and your program use as much as 16MB. Handle pointers are an alternative to DOS extenders that you can use in code for all members of the Intel 8088 family of processors.Handle pointers are a Digital Mars C++ extension to the normal far pointer type. __handle pointers provide access to memory that can be accessed only through indirection (a handle). Currently, handle pointers support the use of expanded (EMS or LIMS) memory. A handle pointer differs from a standard far pointer in that it is assumed to contain handle information in its segment address as well as a normal pointer offset. When memory is accessed through a handle pointer, the handle is automatically dereferenced to ensure that the information is obtained from the correct place. In the case of expanded memory, any page mapping that may be required is carried out automatically.
For information about other pointer types, see Mixing Languages and Compiling Code.
What's in This Chapter
- How handle pointers work.
- The handle format.
- Declaring handle pointers.
- Creating handles to dynamically allocated memory.
- Dereferencing handles.
- Tips on porting and debugging code that contains handles.
About Handle Pointers
Handle pointers are instances of a type called a handle. You use a handle instead of an ordinary pointer to refer to a data structure. Handle pointers point to data in some external memory area, such as expanded memory, extended memory, or disk.When your program needs the data, the handle is dereferenced (that is, converted into an ordinary pointer), and the data it points to is read into conventional memory.
Features of the handle pointer include:
- It is built into the compilers, so you don't need to call functions to access handles, lock or unlock pages, or initialize the virtual memory system.
- It uses expanded memory if available and is compatible with LIMS EMS versions 3.2 and 4.0.
- It lets you easily port code to operating systems with virtual memory (such as UNIX and OS/2). The program runs as efficiently as if it used conventional memory.
- It works with both C and C++ and is a compatible extension to ANSI C.
- It uses the same code in the DOSX and Phar Lap memory models. The compiler treats handle pointers as far pointers in these models.
How handle pointers work
Handle pointers work by storing data outside the DOS memory area and reading it into conventional memory only when it is needed. When you declare a handle, the compiler stores the object it refers to in a page. In order of preference pages are stored in expanded memory, extended memory, or disk. There is a maximum of four pages, and each can hold 16KB. A single page can hold several different variables.When you access a handle's data, your program reads the page that contains the data into conventional memory. For example, suppose a, b, c, i, j, and k are handles, and n1 and n2 are ordinary pointers. If you refer to i, the program swaps its page into conventional memory (see Figure 21-1) and converts i to a far pointer. If the page is stored on disk, your program automatically reads it in. The page that contains i is now called a physical page since it is in conventional memory. The other page is called a logical page since it is not currently loaded into conventional memory. With our compilers, the conversion is performed automatically; you don't need to call a function.
Figure 21-1 Accessing the handle i, Conventional Memory Handle Space (on disk or expanded memory) Physical page 2 Logical page 1 *i: 89.32 *a: 100 *j: 78.29 *b: 3.5E12 *k: 102.14 *c: "Hello\n" Data Segment n1: "John" n2: "Maria"
When you refer to a handle in another page, your program swaps that page into memory, swapping the other page out if necessary. In the above example, when your code references c, the program swaps i's page to disk and swaps c's page into memory. The result is illustrated in Figure 21-2.
Figure 21-2 Accessing the handle c Conventionl Memory Handle Space (on disk or expanded memory) Physical page 1 Logical page 2 *a: 100 *i: 89.32 *b: 3.5E12 *j: 78.29 *c: "Hello\n" *k: 102.14 Data Segment n1: "John" n2: "Maria"
The handle format
The handle type is a 32-bit type in 16-bit compilations. The high 16 bits point to the page and the low 16 bits point to an offset in the page. To convert a handle to a far pointer, the offset is added to the address of the physical page. Handles are unique; no two handles can refer to the same location in handle space.To distinguish between a handle that holds an actual handle and one that holds a converted far pointer, the compiler reserves the values 0xFE00 to 0xFFFF as page addresses. Since those are the segment addresses for the ROM BIOS, it is unlikely any program would store data there. The compiler treats a far pointer with a segment address less than 0xFE00 as an ordinary far pointer. It treats a far pointer whose segment address is greater than 0xFE00 as a handle pointing to logical page (segment -0xFE00). However, a variable explicitly declared as a far pointer that has a segment address greater than 0xFE00 is still treated as a far pointer and can access the ROM BIOS area.
Using Handles
Declaring a handle pointer is like declaring a far pointer. To declare a handle pointer, use the __handle keyword. For example, this statement declares h to be a handle to an integer:int __handle *h;Use a handle as any other pointer. For example:
*h = 3; printf(" h=% d\ n", *h);Your program doesn't need to perform any special initialization to use handles. The compiler adds initialization code (in c.asm) automatically. You must be sure your program frees memory when it exits; otherwise, it will be unavailable to other programs until the machine is re-booted. To free up the memory your program uses, be sure to call exit() at all the places where your program could end. Handles use extended memory, which DOS does not automatically free up. You might want to use special C++ error handling to make sure that exit() is called even when your program terminates abnormally (due to a system error or pressing Control-C, for example).
In comparisons and arithmetic operations, handles are treated like far pointers. In all arithmetic and the comparisons <, <=, >=, and >, the compiler uses only the 16-bit offset into the page. When testing for equality or inequality (== or !=), the compilers use the full 32-bit value.
Dynamically allocating memory
If you want to create a handle that refers to dynamically allocated memory, use the functions in handle.h, such as handle_malloc(), handle_realloc(), and handle_free(). When you try to allocate memory, these functions check a table stored in physical memory to see if there is space available in an existing page. If there is not enough expanded memory available, or if you try to allocate a block larger than 16KB, the handle functions attempt to allocate the memory from conventional memory.Dereferencing handles
Handles are converted to far pointers whenever you dereference a handle or cast a handle to a far pointer. The compiler performs the conversion with a library routine, which swaps the logical page into memory and returns a far pointer into the page. For example, the following operations all convert a handle to a far pointer:int __handle *h; struct A __handle *h2; int far *f; int i; extern void func(int far *pi); /* * These operations convert * handles to far pointers. */ f = h; *h = i; h[3] = *f; i = *(h + 6); h2->b = i; func(h); h = (int far *) h; /* * This operation performs no conversion. */ h = f;The compiler avoids converting a handle when it can use a previous conversion. In the following code, for example, h needs to be converted only once:
struct { int a, b; } __handle *h; h->a = 1; h->b = 2;The compiler converts the code to:
struct { int a, b; } __handle *h, far *p; p = h; p->a = 1; p->b = 2;The compiler can't use the result of a previous conversion if:
- The handle's value might have changed.
- Your code dereferenced another handle in the interim. Referencing a handle may cause another handle's page to be swapped out.
- You called a function. Since a function might dereference another handle, the converted handle's page might be swapped out.
Optimizing handle code
If you dereference a handle and then call a function that you know does not dereference handles, you can make sure the handle isn't converted unnecessarily by optimizing your code yourself. This code, for example, converts h twice:int __handle *h; *h = 1; /* Convert h once */ func(); /* A function call */ *h = 2; /* Convert h twice */This optimized code dereferences h only once:
int __handle *h, far *f; f = h; /* Convert h once */ *f = 1; func(); /* A function call */ *f = 2;Make sure you don't use more than four dereferenced handles at once. The handle implementation use a maximum of four pages. For more information, see the section "Debugging programs that use handles" later in this chapter.
Tips for Using Handle Pointers Efficiently
Although using handles gives you access to a large amount of memory, it can also make your program slower and larger. Your program may frequently swap pages in and out of memory and it will contain additional code to dereference handles. Here are some suggestions for making your program more efficient:- Use handles for data structures that your program accesses infrequently. Your program will read and write memory less often and will contain less code for dereferencing.
- Choose handle pointers for data structures that emphasize fast access over those that emphasize compactness.
- Keep related data structures on the same page by allocating or declaring them together.
- Use expanded memory conventionally (for example, via the EMM library routines) in the same program where you use handles. But if you don't keep the two uses independent, the results could be unpredictable.
- Perform a search over a large database by trying to put the access structures in conventional memory and the data in handle space. Look-up is fast and nothing is read from disk until the search data is found.
Porting code with handles
To port code that uses handles to a compiler that doesn't implement handles, define __handle to be nothing, like this:#define __handleAll handles become regular pointers.
If you use the dynamic allocation functions in handle. h, define NO_HANDLE to be 1 before you #include handle.h, like this:
#define NO_HANDLE 1 #include <handle.h>The functions in handle.h will call their equivalents in the standard library, such as malloc(), realloc(), and free().
Debugging programs that use handles
Here is a list of points to watch out for:- Beware of having a stray pointer into a page that has been swapped out of memory. This problem rarely exhibits symptoms.
- Don't use more than four dereferenced handles at a time. This problem shows up only if they fall on different logical pages.
- Beware of function calls that could dereference handles.
- In converting code from using malloc() to handle_malloc, be sure to convert (char *) malloc(x) to (char __handle *) handle_malloc(x). Using (char *) handle_malloc(x) dereferences the handle and stores a far pointer in h.
- If your program uses the dynamic allocation functions in handle. h, define NO_HANDLE to be 1 so the functions don't use handles. If you no longer have problems after redefining NO_HANDLE, you can be sure that handles caused your bugs.
- Encapsulate data structures that use handles in C++ classes. This confines the code that dereferences handles to a few places and isolates handle problems to a class definition.
- Look carefully for simultaneous accesses to logical pages. Make sure you don't dereference more than four handles at a time.
- Program defensively. Assume that all function calls invalidate previously dereferenced handles.