• The Old New Thing

    A history of GlobalLock, part 2: Selectors

    • 15 Comments

    With the advent of the 80286, Windows could take advantage of that processor's "protected mode". processor. There was still no virtual memory, but you did have memory protection. Global handles turned into "descriptors", more commonly known as "selectors".

    Architectural note: The 80286 did have support for both a "local descriptor table" and a "global descriptor table", thereby making it possible to have each process run in something vaguely approximating a separate address space, but doing so would have broken Windows 1.0 compatibility, where all memory was global.

    Addresses on the 80286 in protected mode consisted of a selector and an offset rather than a segment and an offset. This may seem like a trivial change, but it actually is important because a selector acts like a handle table in hardware.

    When you created a selector, you specified a whole bunch of attributes, such as whether it was a code selector or a data selector, whether it was present or discarded, and where in memory it resided. (Still no virtual memory, so all memory is physical.)

    GlobalAlloc() now returned a selector. If you wanted to, you could just use it directly as the selector part of an address. When you loaded a selector, the CPU checked whether the selector was present, discarded, or invalid.

    • If present, then everything was fine.
    • If discarded, a "not present" exception was raised. (Wow, we have exceptions now!) The memory manager trapped this exception and did whatever was necessary to make the selector present. This meant allocating the memory (possibly compacting and discarding to make room for it), and if it was a code selector, loading the code back off the disk and fixing it up.
    • If invalid, an Unrecoverable Application Error was raised. This is the infamous "UAE".

    Since memory accesses were now automatically routed through the descriptor table by the hardware, it meant that memory could be moved around with relative impunity. All existing pointers would remain valid since the selector remains the same; all that changes is the internal bookkeeping in the descriptor table that specified which section of memory the descriptor referred to.

    For compatibility with Windows 1.0, GlobalAlloc() continued to emulate all the moveability rules as before. It's just that the numeric value of the selector never really changed any more. (And please let's just agree to disagree on whether backwards compatibility is a good thing or not.)

    Next time, transitioning to Win32.

  • The Old New Thing

    A history of GlobalLock, part 1: The early years

    • 33 Comments

    Once upon a time, there was Windows 1.0. This was truly The Before Time. 640K. Segments. Near and far pointers. No virtual memory. Co-operative multitasking.

    Since there was no virtual memory, swapping had to be done with the co-operation of the application. When there was an attempt to allocate memory (either for code or data) and insufficient contiguous memory was available, the memory manager had to perform a process called "compaction" to make the desired amount of contiguous memory available.

    • Code segments could be discarded completely, since they can be reloaded from the original EXE. (No virtual memory - there is no such thing as "paged out".) Discarding code requires extra work to make sure that the next time the code got called, it was re-fetched from memory. How this was done is not relevant here, although it was quite a complicated process in and of itself.
    • Memory containing code could be moved around, and references to the old address were patched up to refer to the new address. This was also a complicated process not relevant here.
    • Memory containing data could be moved around, but references to the old addresses were not patched up. It was the application's job to protect against its memory moving out from under it if it had a cached pointer to that memory.
    • Memory that was locked or fixed (or a third category, "wired" -- let's not get into that) would never be moved.

    When you allocated memory via GlobalAlloc(), you first had to decide whether you wanted "moveable" memory (memory which could be shuffled around by the memory manager) or "fixed" memory (memory which was immune from motion). Conceptually, a "fixed" memory block was like a moveable block that was permanently locked.

    Applications were strongly discouraged from allocating fixed memory because it gummed up the memory manager. (Think of it as the memory equivalent of an immovable disk block faced by a defragmenter.)

    The return value of GlobalAlloc() was a handle to a global memory block, or an HGLOBAL. This value was useless by itself. You had to call GlobalLock() to convert this HGLOBAL into a pointer that you could use.

    GlobalLock() did a few things:

    • It forced the memory present (if it had been discarded). Other memory blocks may need to be discarded or moved around to make room for the memory block being locked.
    • If the memory block was "moveable", then it also incremented the "lock count" on the memory block, thus preventing the memory manager from moving the memory block during compaction. (Lock counts on "fixed" memory aren't necessary because they can't be moved anyway.)

    Applications were encouraged to keep global memory blocks locked only as long as necessary in order to avoid fragmenting the heap. Pointers to unlocked moveable memory were forbidden since even the slightest breath -- like calling a function that happened to have been discarded -- would cause a compaction and invalidate the pointer.

    Okay, so how did this all interact with GlobalReAlloc()?

    It depends on how the memory was allocated and what its lock state was.

    If the memory was allocated as "moveable" and it wasn't locked, then the memory manager was allowed to find a new home for the memory elsewhere in the system and update its bookkeeping so the next time somebody called GlobalLock(), they got a pointer to the new location.

    If the memory was allocated as "moveable" but it was locked, or if the memory was allocated as "fixed", then the memory manager could only resize it in place. It couldn't move the memory either because (if moveable and locked) there were still outstanding pointers to it, as evidenced by the nonzero lock count, or (if fixed) fixed memory was allocated on the assumption that it would never move.

    If the memory was allocated as "moveable" and was locked, or if it was allocated as "fixed", then you can pass the GMEM_MOVEABLE flag to override the "may only resize in place" behavior, in which case the memory manager would attempt to move the memory if necessary. Passing the GMEM_MOVEABLE flag meant, "No, really, I know that according to the rules, you can't move the memory, but I want you to move it anyway. I promise to take the responsibility of updating all pointers to the old location to point to the new location."

    (Raymond actually remembers using Windows 1.0. Fortunately, the therapy sessions have helped tremendously.)

    Next time, the advent of selectors.

  • The Old New Thing

    Why do I sometimes see redundant casts before casting to LPARAM?

    • 25 Comments

    If you read through old code, you will often find casts that seem redundant.

    SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)(LPSTR)"string");
    

    Why was "string" cast to LPSTR? It's already an LPSTR!

    These are leftovers from 16-bit Windows. Recall that in 16-bit Windows, pointers were near by default. Consequently, "string" was a near pointer to a string. If the code had been written as

    SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)"string");
    

    then it would have taken the near pointer and cast it to a long. Since a near pointer is a 16-bit value, the pointer would have been zero-extended to the 32-bit size of a long.

    However, all pointers in window messages must be far pointers because the window procedure for the window might very well be implemented in a different module from the sender. Recall that near pointers are interpreted relative to the default selector, and the default selector for each module is different. Sending a near pointer to another module will result in the pointer being interpreted relative to the recipient's default selector, which is not the same as the sender's default selector.

    The intermediate cast to LPSTR converts the near pointer to a far pointer, LP being the Hungarian prefix for far pointers (also known as "long pointers"). Casting a near pointer to a far pointer inserts the previously-implied default selector, so that the cast to LPARAM captures the full 16:16 far pointer.

    Aren't you glad you don't have to worry about this any more?

  • The Old New Thing

    What was the point of the GMEM_SHARE flag?

    • 4 Comments

    The GlobalAlloc function has a GMEM_SHARE flag. What was it for?

    In 16-bit Windows, the GMEM_SHARE flag controlled whether the memory should outlive the process that allocated it. By default, all memory allocated by a process was automatically freed when that process exited.

    Passing the GMEM_SHARE flag suppressed this automatic cleanup. That's why you had to use this flag when allocating memory to be placed on the clipboard or when you transfer it via OLE to another process. Since the clipboard exists after your program exits, any data you put on the clipboard needs to outlive the program. If you neglected to set this flag, then once your program exited, the memory that you put on the clipboard would be cleaned up, resulting in a crash the next time someone tried to read that data from the clipboard.

    (The GMEM_SHARE flag also controlled whether the memory could be freed by a process other than the one that allocated it. This makes sense given the above semantics.)

    Note that the cleanup rule applies to global memory allocated by DLLs on behalf of a process. Authors of DLLs had to be careful to keep track of whether any particular memory allocation was specific to a process (and should be freed when the process exited) or whether it was something the DLL was planning on sharing across processes for its own internal bookkeeping (in which case it shouldn't be freed). Failure to be mindful of this distinction led to bugs like this one.

    Thank goodness this is all gone in Win32.

  • The Old New Thing

    And to think they let me get away with it for five years

    • 23 Comments

    According to the Constitution of the State of New Jersey, Article II, paragraph 6:

    No idiot or insane person shall enjoy the right of suffrage.

    Why are constitutional articles labelled with Roman numerals? Makes it sound like the Super Bowl or something.

    The state appellate court did rule a few years ago that being hospitalized for psychiatric treatment does not constitute being insane for the purpose of determining voter eligibility.

    Today is Election Day in the United States. Don't forget to vote! (Void where prohibited.)

  • The Old New Thing

    What was the difference between LocalAlloc and GlobalAlloc?

    • 26 Comments

    Back in the days of 16-bit Windows, the difference was significant.

    In 16-bit Windows, memory was accessed through values called "selectors", each of which could address up to 64K. There was a default selector called the "data selector"; operations on so-called "near pointers" were performed relative to the data selector. For example, if you had a near pointer p whose value was 0x1234 and your data selector was 0x012F, then when you wrote *p, you were accessing the memory at 012F:1234. (When you declared a pointer, it was near by default. You had to say FAR explicitly if you wanted a far pointer.)

    Important: Near pointers are always relative to a selector, usually the data selector.

    The GlobalAlloc function allocated a selector that could be used to access the amount of memory you requested. (If you asked for more than 64K, then something exciting happened, which is not important here.) You could access the memory in that selector with a "far pointer". A "far pointer" is a selector combined with a near pointer. (Remember that a near pointer is relative to a selector; when you combine the near pointer with an appropriate selector, you get a far pointer.)

    Every instance of a program and DLL got its own data selector, known as the HINSTANCE, which I described in an earlier entry. The default data selector for code in a program executable was the HINSTANCE of that instance of the program; the default data selector for code in a DLL was the HINSTANCE of that DLL. Therefore, if you had a near pointer p and accessed it via *p from a program executable, it accessed memory relative to the program instance's HINSTANCE. If you accessed it from a DLL, you got memory relative to your DLL's HINSTANCE.

    The memory referenced by the default selector could be turned into a "local heap" by calling the LocalInit function. Initialing the local heap was typically one of the first things a program or DLL did when it started up. (For DLLs, it was usually the only thing it did!) Once you have a local heap, you can call LocalAlloc to allocate memory from it. The LocalAlloc function returned a near pointer relative to the default selector, so if you called it from a program executable, it allocated memory from the executable's HINSTANCE; if you called it from a DLL, it allocated memory from the DLL's HINSTANCE.

    If you were clever, you realized that you could use LocalAlloc to allocate from memory other than HINSTANCEs. All you had to do was change your default selector to the selector for some memory you had allocated via GlobalAlloc, call the LocalAlloc function, then restore the default selector. This gave you a near pointer relative to something other than the default selector, which was a very scary thing to have, but if you were smart and kept careful track, you could keep yourself out of trouble.

    Observe, therefore, that in 16-bit Windows, the LocalAlloc and GlobalAlloc functions were completely different! LocalAlloc returned a near pointer, whereas GlobalAlloc returned a selector.

    Pointers that you intended to pass between modules had to be in the form of "far pointers" because each module has a different default selector. If you wanted to transfer ownership of memory to another module, you had to use GlobalAlloc since that permitted the recipient to call GlobalFree to free it. (The recipient can't use LocalFree since LocalFree operates on the local heap, which would be the local heap of the recipient - not the same as your local heap.)

    This historical difference between local and global memory still has vestiges in Win32. If you have a function that was inherited from 16-bit Windows and it transfers ownership of memory, it will take the form of an HGLOBAL. The clipboard functions are a classic example of this. If you put a block of memory onto the clipboard, it must have been allocated via HGLOBAL because you are transferring the memory to the clipboard, and the clipboard will call GlobalFree when it no longer needs the memory. Memory transferred via STGMEDIUM takes the form of HGLOBALs for the same reason.

    Even in Win32, you have to be careful not to confuse the local heap from the global heap. Memory allocated from one cannot be freed on the other. The functional differences have largely disappeared; the semantics are pretty much identical by this point. All the weirdness about near and far pointers disappeared with the transition to Win32. But the local heap functions and the global heap functions are nevertheless two distinct heap interfaces.

    I'm going to spend the next few entries describing some of the features of the 16-bit memory manager. Even though you don't need to know them, having some background may help you understand the reason behind the quirks of the Win32 memory manager. We saw a little of that today, where the mindset of the 16-bit memory manager established the rules for the clipboard.

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Why didn't the desktop window shrink to exclude the taskbar?

    • 36 Comments

    The taskbar created all sorts of interesting problems, since the work area was not equal to the entire screen dimensions. (Multiple monitors created similar problems.) "Why didn't the gui return the usable workspace as the root window (excluding the taskbar)?"

    That would have made things even worse.

    Lots of programs want to cover the entire screen. Games, for example, are very keen on covering the entire screen. Slideshow programs also want to cover the entire screen. (This includes both slideshows for digital pictures as well as business presentations.) Screen savers of course must cover the entire screen.

    If the desktop window didn't include the taskbar, then those programs would leave a taskbar visible while they did their thing. This is particularly dangerous for screen savers, since a user could just click on the taskbar to switch to another program without going through the screen saver's password lock!

    And if the taskbar were docked at the top or left edge of the screen, this would have resulted in the desktop window not beginning at coordinates (0,0), which would no doubt have caused widespread havoc. (Alternatively, one could have changed the coordinate system so that (0, 0) was no longer the top left corner of the screen, but that would have broken so many programs it wouldn't have been funny.)

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Where did windows minimize to before the taskbar was invented?

    • 39 Comments

    Before Explorer was introduced in Windows 95, the Windows desktop was a very different place.

    The icons on your desktop did not represent files; rather, when you minimized a program, it turned into an icon on the desktop. To open a minimized program, you had to hunt for its icon, possibly minimizing other programs to get them out of the way, and then double-click it. (You could also Alt+Tab to the program.)

    Explorer changed the desktop model so that icons on your desktop represent objects (files, folders) rather than programs. The job of managing programs fell to the new taskbar.

    But where did the windows go when you minimized them?

    Under the old model, when a window was minimized, it displayed as an icon, the icon had a particular position on the screen, and the program drew the icon in response to paint messages. (Of course, most programs deferred to DefWindowProc which just drew the icon.) In other words, the window never went away; it just changed its appearance.

    But with the taskbar, the window really does go away when you minimize it. Its only presence is in the taskbar. The subject of how to handle windows when they were minimized went through several iterations, because it seemed that no matter what we did, some program somewhere didn't like it.

    The first try was very simple: When a window was minimized, the Windows 95 window manager set it to hidden. That didn't play well with many applications, which cared about the distinction between minimized (and visible) and hidden (and not visible).

    Next, the Windows 95 window manager minimized the window just like the old days, but put the minimized window at coordinates (-32000, -32000), This didn't work because some programs freaked out if they found their coordinates were negative.

    So the Windows 95 window manager tried putting minimized windows at coordinates (32000, 32000), This still didn't work because some programs freaked out if they found their coordinates were positive and too large!

    Finally the Windows 95 window manager tried coordinates (3000, 3000), This seemed to keep everybody happy. Not negative, not too large, but large enough that it wouldn't show up on the screen (at least not at screen resolutions that were readily available in 1995).

    If you have a triple-monitor Windows 98 machine lying around, you can try this: Set the resolution of each monitor to 1024x768 and place them corner-to-corner. At the bottom right corner of the third monitor, you will see all your minimized windows parked out in the boonies.

    (Windows NT stuck with the -32000 coordinates and didn't pick up the compatibility fixes for some reason. I guess they figured that by the time Windows NT became popular, all those broken programs would have been fixed. In other words: Let Windows 95 do your dirty work!)

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Sometimes you can't read the text under the cursor

    • 13 Comments

    I had previously written on how you can retrieve the text under the cursor, and you may have noticed that it produces mixed results. It works great with some programs but not with others.

    It depends on the program in question. Some programs were written with greater attention to supporting screen readers than others. Internet Explorer, for example, has excellent support for ActiveAccessibility because browsing the web is a great way for people with disabilities to get involved in the world around them.

    Other programs don't do quite as good a job. For example, the program we developed to demonstrate various scrollbar techniques does not handle the WM_GETOBJECT message and is not accessible.

    So whether ActiveAccessibility works for any particular program depends heavily on how much the author of that program had accessibility in mind when they wrote it.

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    The strangest way of detecting Windows NT

    • 20 Comments

    A colleague of mine nominated this code for Function of the Year. (This is the same person who was the first to report that a Windows beta used a suspect URL.) I have to admit that this code is pretty impressive. Of all the ways to check the operating system, you have to agree that sniffing at an undocumented implementation detail of memory-mapped files is certainly creative!

    // following the typographical convention that code
    // in italics is wrong
    int AreWeRunningOnWindowsNT()
    {
          HANDLE hFile, hFileMapping;
          BYTE *pbFile, *pbFile2;
          char szFile[MAX_PATH];
    
          GetSystemDirectory(szFile, MAX_PATH);
          strcat(szFile, "\\MAIN.CPL");
          hFile = CreateFile(szFile, GENERIC_READ | GENERIC_WRITE, 0,
                NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    
          hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE,
                0, 0, NULL);
    
          pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE,
                0, 0, 0);
    
          pbFile2 = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE,
                0, 65536, 0);
    
          if (pbFile + 65536 != pbFile2)
                return 1;
    
          return 0;
    }
    

    Nevermind that the function also leaves a file locked and leaks two handles and two views each time you call it!

    What's more, this function may erroneously report FALSE on a Windows NT machine if by an amazing coincidence the memory manager happens to assign the second file view to the very next 64K block of memory (which it is permitted to do since address space granularity is 64K).

    It can also erroneously report TRUE on a Windows 95 machine if the MAIN.CPL file happens to be smaller than 64K, or if you don't have write permission on the file. (Notice that the program requests read-write access to the MAIN.CPL file.)

    This particular function is from a library that is used by many popular multimedia titles.

    The quickest way to detect whether you are running on a Windows 95-series system or a Windows NT-series system is to use the hopefully-obviously-named function GetVersion.

    int AreWeRunningOnWindowsNT()
    {
        return (GetVersion() & 0x80000000) == 0;
    }
    

    [Raymond is currently on vacation; this message was pre-recorded.]

Page 380 of 436 (4,356 items) «378379380381382»