April, 2005

  • The Old New Thing

    There's an awful lot of overclocking out there

    • 76 Comments

    A bunch of us were going through some Windows crashes that people sent in by clicking the "Send Error Report" button in the crash dialog. And there were huge numbers of them that made no sense whatsoever. For example, there would be code sequences like this:

       mov ecx, dword ptr [someValue]
       mov eax, dword ptr [otherValue]
       cmp ecx, eax
       jnz generateErrorReport
    

    Yet when we looked at the error report, the ecx and eax registers were equal! There were other crashes of a similar nature, where the CPU simply lots its marbles and did something "impossible".

    We had to mark these crashes as "possibly hardware failure". Since the crash reports are sent anonymously, we have no way of contacting the submitter to ask them follow-up questions. (The ones that the group I was in was investigating were failures that were hit only once or twice, but were of the type that were deemed worthy of close investigation because the types of errors they uncovered—if valid—were serious.)

    One of my colleagues had a large collection of failures where the program crashed at the instruction

      xor eax, eax
    

    How can you crash on an instruction that simply sets a register to zero? And yet there were hundreds of people crashing in precisely this way.

    He went through all the published errata to see whether any of them would affect an "xor eax, eax" instruction. Nothing.

    He sent email to some Intel people he knew to see if they could think of anything. [Aside from overclocking, of course. - Added because people apparently take my stories hyperliterally and require me to spell out the tiniest detail, even the stuff that is so obvious that it should go without saying. I didn't want to give away the story's punch line too soon!] They said that the only [other] thing they could think of was that perhaps somebody had mis-paired RAM on their motherboard, but their description of what sorts of things go wrong when you mis-pair didn't match this scenario.

    Since the failure rate for this particular error was comparatively high (certainly higher than the one or two I was getting for the failures I was looking at), he requested that the next ten people to encounter this error be given the opportunity to leave their email address and telephone number so that he could call them and ask follow-up questions. Some time later, he got word that ten people took him up on this offer, and he sent each of them e-mail asking them various questions about their hardware configurations, including whether they were overclocking. [- Continuing from above aside: See? Obviously overclocking was considered as a possibility.]

    Five people responded saying, "Oh, yes, I'm overclocking. Is that a problem?"

    The other half said, "What's overclocking?" He called them and walked them through some configuration information and was able to conclude that they were indeed all overclocked. But these people were not overclocking on purpose. The computer was already overclocked when they bought it. These "stealth overclocked" computers came from small, independent "Bob's Computer Store"-type shops, not from one of the major computer manufacturers or retailers.

    For both groups, he suggested that they stop overclocking or at least not overclock as aggressively. And in all cases, the people reported that their computer that used to crash regularly now runs smoothly.

    Moral of the story: There's a lot of overclocking out there, and it makes Windows look bad.

    I wonder if it'd be possible to detect overclocking from software and put up a warning in the crash dialog, "It appears that your computer is overclocked. This may cause random crashes. Try running the CPU at its rated speed to improve stability." But it takes only one false positive to get people saying, "Oh, there goes Microsoft blaming other people for its buggy software again."

  • The Old New Thing

    What is the HINSTANCE passed to CreateWindow and RegisterClass used for?

    • 10 Comments

    One of the less-understood parameters to the CreateWindow function and the RegisterClass function is the HINSTANCE (either passed as a parameter or as part of the WNDCLASS structure).

    The window class name is not sufficient to identify the class uniquely. Each process has its own window class list, and each entry in the window class list consists of an instance handle and a class name. For example, here's what the window class list might look like if a program has two DLLs, both of which register a class name "MyClass", passing the DLL's handle as the HINSTANCE.

    HINSTANCEClass name
    1.USER32.DLLStatic
    2.USER32.DLLButton
    3.USER32.DLLListbox
    4.USER32.DLLCombobox
    5.USER32.DLLEdit
    6.A.DLLMyClass
    7.B.DLLMyClass

    When it comes time to create a window, each module then passes its own HINSTANCE when creating the window, and the window manager uses the combination of the instance handle and the class name to look up the class.

    CreateWindow("MyClass", ..., hinstA, ...); // creates class 6
    CreateWindow("MyClass", ..., hinstB, ...); // creates class 7
    CreateWindow("MyClass", ..., hinstC, ...); // fails
    

    This is why it is okay if multiple DLLs all register a class called "MyClass"; the instance handle is used to tell them apart.

    There is an exception to the above rule, however. If you pass the CS_GLOBALCLASS flag when registering the class, then the window manager will ignore the instance handle when looking for your class. All of the USER32 classes are registered as global. Consequently, all of the following calls create the USER32 edit control:

    CreateWindow("edit", ..., hinstA, ...);
    CreateWindow("edit", ..., hinstB, ...);
    CreateWindow("edit", ..., hinstC, ...);
    

    If you are registering a class for other modules to use in dialog boxes, you need to register as CS_GLOBALCLASS, because as we saw earlier the internal CreateWindow call performed during dialog box creation to create the controls passes the dialog's HINSTANCE as the HINSTANCE parameter. Since the dialog instance handle is typically the DLL that is creating the dialog (since that same HINSTANCE is used to look up the template), failing to register with the CS_GLOBALCLASS flag means that the window class lookup will not find the class since it's registered under the instance handle of the DLL that provided the class, not the one that is using it.

    In 16-bit Windows, the instance handle did other things, too, but they are no longer relevant to Win32.

    A common mistake is to pass the HINSTANCE of some other module (typically, the primary executable) when registering a window class. Now that you understand what the HINSTANCE is used for, you should be able to explain the consequences of registering a class with the wrong HINSTANCE.

  • The Old New Thing

    The end of one of the oldest computers at Microsoft still doing useful work

    • 45 Comments

    My building was scheduled for a carpet replacement—in all my years at Microsoft, I think this is the first time this has ever happened to a building I was in—so we all had to pack up our things so the carpeters could get clear access to the floor. You go through all the pain of an office move (packing all your things) but don't get the actual reward of a new office.

    One of the machines in my office probably ranked high on the "oldest computer at Microsoft still doing useful work" charts. It was a 50MHz 486 with 12MB of memory and 500 whole megabytes of disk space. (Mind you, it wasn't born this awesome. It started out with only 8MB of memory and 200MB of disk space, but I upgraded it after a few years.) This machine started out its life as a high-end Windows 95 test machine, then when its services were no longer needed, I rescued it from the scrap heap and turned it into my little web server where among other things, Microsoft employees could read my blog article queue months before publication. It also served as my "little computer for doing little things". For example, the Internet Explorer test team used it for FTP testing since I installed a custom FTP server onto it. (Therefore, I could make it act like any type of server, or like a completely bizarro server if a security scenario required it.) It also housed various "total wastes of time" such as the "What's Raymond doing right now?" program, and the "Days without a pony" web page.

    I added a CD-ROM drive, which cost me $200. This was back in the days when getting a CD-ROM drive meant plugging in a custom ISA card and installing a MS-DOS driver into the CONFIG.SYS file. Like an MS-DOS driver gets you anywhere any more. I had to write my own driver for it.

    I took it as a challenge to see how high I could get the machine's uptime. Once the hardware stabilized (which went a lot quicker once I gave up trying to get the old network card to stop wedging and just bought a new one), I put it on a UPS that had been gifted to me in exchange for debugging why the company's monitoring software wasn't working on Windows 95. Whenever I had to move offices, I found somebody who wasn't moving and relocated the computer there for a few days. The UPS kept the machine running while I carted it down the hall or into the next building. I think I got the uptime as high as three years before the building suffered a half-day power outage that drained the UPS.

    A few years later, the machine started rebooting for no apparent reason. Turns out the UPS battery itself was dying and generating its own mini-power outages. Ironic that a UPS ended up creating power outages instead of masking them. But on the other hand, it was free, so I can't complain. Without a UPS, the machine became victim of building-wide power outages and office moves.

    Over the years, more and more parts of the machine started to wear out and had to be worked around. The CMOS battery eventually died, so restarting the computer after an outage involved lots of typing. (It always thought the date was January 1983.) The clock also drifted, so I wrote a program to re-synchronize it automatically every few days.

    When I packed up the computer for the recarpeting, I assumed that afterwards, it would fire back up like the trooper it was. But alas, it just sat there. After much fiddling and removal of non-critical hardware, I got it to power on. Now it complains "no boot device".

    The hard drive (or perhaps the hard drive controller) had finally died. The shock of being shut off and restarted proved to be its downfall. Since it's nearly impossible to find replacement parts for a computer this old, I'm going to have to return it to the scrap heap.

    Good-bye, old friend. But you won't be forgotten. I'm going to transfer your name and IP address to another computer I rescued from the scrap heap many years ago for just this eventuality. But still no mouse.

    (Alas, this was the first of a series of computers to reach retirement age within days of each other. Perhaps I'll eulogize those other machines someday.)

  • The Old New Thing

    The new scratch program

    • 49 Comments

    I think it's time to update the scratch program we've been using for the past year. I hear there's this new language called C++ that's going to become really popular any day now, so let's hop on the bandwagon!

    #define STRICT
    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    #include <windowsx.h>
    #include <ole2.h>
    #include <commctrl.h>
    #include <shlwapi.h>
    #include <shlobj.h>
    #include <shellapi.h>
    
    HINSTANCE g_hinst;
    
    class Window
    {
    public:
     HWND GetHWND() { return m_hwnd; }
    protected:
     virtual LRESULT HandleMessage(
                             UINT uMsg, WPARAM wParam, LPARAM lParam);
     virtual void PaintContent(PAINTSTRUCT *pps) { }
     virtual LPCTSTR ClassName() = 0;
     virtual BOOL WinRegisterClass(WNDCLASS *pwc)
         { return RegisterClass(pwc); }
     virtual ~Window() { }
    
     HWND WinCreateWindow(DWORD dwExStyle, LPCTSTR pszName,
           DWORD dwStyle, int x, int y, int cx, int cy,
           HWND hwndParent, HMENU hmenu)
     {
      Register();
      return CreateWindowEx(dwExStyle, ClassName(), pszName, dwStyle,
                      x, y, cx, cy, hwndParent, hmenu, g_hinst, this);
     }
    private:
     void Register();
     void OnPaint();
     void OnPrintClient(HDC hdc);
     static LRESULT CALLBACK s_WndProc(HWND hwnd,
         UINT uMsg, WPARAM wParam, LPARAM lParam);
    protected:
     HWND m_hwnd;
    };
    
    void Window::Register()
    {
        WNDCLASS wc;
        wc.style         = 0;
        wc.lpfnWndProc   = Window::s_WndProc;
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = 0;
        wc.hInstance     = g_hinst;
        wc.hIcon         = NULL;
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wc.lpszMenuName  = NULL;
        wc.lpszClassName = ClassName();
    
        WinRegisterClass(&wc);
    }
    
    LRESULT CALLBACK Window::s_WndProc(
                   HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     Window *self;
     if (uMsg == WM_NCCREATE) {
      LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
      self = reinterpret_cast<Window *>(lpcs->lpCreateParams);
      self->m_hwnd = hwnd;
      SetWindowLongPtr(hwnd, GWLP_USERDATA,
                reinterpret_cast<LPARAM>(self));
     } else {
      self = reinterpret_cast<Window *>
                (GetWindowLongPtr(hwnd, GWLP_USERDATA));
     }
     if (self) {
      return self->HandleMessage(uMsg, wParam, lParam);
     } else {
      return DefWindowProc(hwnd, uMsg, wParam, lParam);
     }
    }
    
    LRESULT Window::HandleMessage(
                              UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     LRESULT lres;
    
     switch (uMsg) {
     case WM_NCDESTROY:
      lres = DefWindowProc(m_hwnd, uMsg, wParam, lParam);
      SetWindowLongPtr(m_hwnd, GWLP_USERDATA, 0);
      delete this;
      return lres;
    
     case WM_PAINT:
      OnPaint();
      return 0;
    
     case WM_PRINTCLIENT:
      OnPrintClient(reinterpret_cast<HDC>(wParam));
      return 0;
     }
    
     return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
    }
    
    void Window::OnPaint()
    {
     PAINTSTRUCT ps;
     BeginPaint(m_hwnd, &ps);
     PaintContent(&ps);
     EndPaint(m_hwnd, &ps);
    }
    
    void Window::OnPrintClient(HDC hdc)
    {
     PAINTSTRUCT ps;
     ps.hdc = hdc;
     GetClientRect(m_hwnd, &ps.rcPaint);
     PaintContent(&ps);
    }
    
    class RootWindow : public Window
    {
    public:
     virtual LPCTSTR ClassName() { return TEXT("Scratch"); }
     static RootWindow *Create();
    protected:
     LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
     LRESULT OnCreate();
    private:
     HWND m_hwndChild;
    };
    
    LRESULT RootWindow::OnCreate()
    {
     return 0;
    }
    
    LRESULT RootWindow::HandleMessage(
                              UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
      case WM_CREATE:
       return OnCreate();  
    
      case WM_NCDESTROY:
       // Death of the root window ends the thread
       PostQuitMessage(0);
       break;
    
      case WM_SIZE:
       if (m_hwndChild) {
        SetWindowPos(m_hwndChild, NULL, 0, 0,
                     GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam),
                     SWP_NOZORDER | SWP_NOACTIVATE);
       }
       return 0;
    
      case WM_SETFOCUS:
       if (m_hwndChild) {
        SetFocus(m_hwndChild);
       }
       return 0;
     }
    
     return __super::HandleMessage(uMsg, wParam, lParam);
    }
    
    RootWindow *RootWindow::Create()
    {
     RootWindow *self = new RootWindow();
     if (self && self->WinCreateWindow(0,
           TEXT("Scratch"), WS_OVERLAPPEDWINDOW,
           CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
           NULL, NULL)) {
          return self;
      }
     delete self;
     return NULL;
    }
    
    int PASCAL
    WinMain(HINSTANCE hinst, HINSTANCE, LPSTR, int nShowCmd)
    {
     g_hinst = hinst;
    
     if (SUCCEEDED(CoInitialize(NULL))) {
      InitCommonControls();
    
      RootWindow *prw = RootWindow::Create();
      if (prw) {
       ShowWindow(prw->GetHWND(), nShowCmd);
       MSG msg;
       while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
       }
      }
      CoUninitialize();
     }
     return 0;
    }
    

    The basic idea of this program is the same as our old scratch program, but now it has that fresh lemony C++ scent. Instead of keeping our state in globals, we declare a C++ class and hook it up to the window. For simplicity, the object's lifetime is tied to the window itself.

    First, there is a bare-bones Window class which we will use as our base class for any future "class associated with a window" work. The only derived class for now is the RootWindow, the top-level frame window that for now is the only window that the program uses. As you may suspect, we may have other derived classes later as the need arises.

    The reason why the WinRegisterClass method is virtual (and doesn't do anything interesting) is so that a derived class can modify the WNDCLASS that is used when the class is registered. I don't have any immediate need for it, but it'll be there if I need it.

    We use the GWLP_USERDATA window long to store the pointer to the associated class, thereby allowing us to recover the object from the window handle.

    Observe that in the RootWindow::HandleMessage method, I used the Visual C++ __super extension. If you don't want to rely on a nonstandard extension, you can instead write

    class RootWindow : public Window
    {
    public:
     typedef Window super;
     ...
    

    and use super instead of __super.

    This program doesn't do anything interesting; it's just going to be a framework for future samples.

  • The Old New Thing

    What is the HINSTANCE passed to SetWindowsHookEx used for?

    • 20 Comments

    The SetWindowsHookEx function accepts a HINSTANCE parameter. The documentation explains that it is a handle to the DLL containing the hook procedure. Why does the window manager need to have this handle?

    It needs the handle so it knows which DLL to load into each process when the hook fires. It injects the DLL you pass, then calls the function you pass. Clearly the function needs to reside in the DLL you pass in order for its code to be there when the window manager calls it.

    This is also why hook functions must reside in DLLs. You can't load an EXE into another EXE.

    The WH_KEYBOARD_LL and WH_MOUSE_LL hooks are exceptions to this rule. These two are non-injecting hooks, as explained in their respective documentation pages. Rather, the hook function is called in its original thread context.

    Okay, armed with this information, perhaps you can solve this person's problem with global hooks.

  • The Old New Thing

    Building a dialog template at run-time

    • 21 Comments

    We've spent quite a bit of time over the past year learning about dialog templates and the dialog manager. Now we're going to put the pieces together to do something interesting: Building a dialog template on the fly.

    What we're going to write is an extremely lame version of the MessageBox function. Why bother writing a bad version of something that Windows already does? Because you can use it as a starting point for further enhancements. For example, once you learn how to generate a template dynamically, you can dynamically add buttons beyond the boring "OK" button, or you can add additional controls like a "Repeat this answer for all future occurrences of this dialog" checkbox or maybe insert an animation control.

    I'm going to start with a highly inefficient dialog template class. This is not production-quality, but it's good enough for didactic purposes.

    #include <vector>
    class DialogTemplate {
    public:
     LPCDLGTEMPLATE Template() { return (LPCDLGTEMPLATE)&v[0]; }
     void AlignToDword()
      { if (v.size() % 4) Write(NULL, 4 - (v.size() % 4)); }
     void Write(LPCVOID pvWrite, DWORD cbWrite) {
      v.insert(v.end(), cbWrite, 0);
      if (pvWrite) CopyMemory(&v[v.size() - cbWrite], pvWrite, cbWrite);
     }
     template<typename T> void Write(T t) { Write(&t, sizeof(T)); }
     void WriteString(LPCWSTR psz)
      { Write(psz, (lstrlenW(psz) + 1) * sizeof(WCHAR)); }
    
    private:
     vector<BYTE> v;
    };
    

    I didn't spend much time making this class look pretty because it's not the focus of this article. The DialogTemplate class babysits a vector of bytes to which you can Write data. There is also a little AlignToDword method that pads the buffer to the next DWORD boundary. This'll come in handy, too.

    Our message box will need a dialog procedure which ends the dialog when the IDCANCEL button is pressed. If we had made any enhancements to the dialog template, we would handle them here as well.

    INT_PTR CALLBACK DlgProc(HWND hwnd, UINT wm, WPARAM wParam, LPARAM lParam)
    {
     switch (wm) {
     case WM_INITDIALOG: return TRUE;
     case WM_COMMAND:
      if (GET_WM_COMMAND_ID(wParam, lParam) == IDCANCEL) EndDialog(hwnd, 0);
      break;
     }
     return FALSE;
    }
    

    Finally, we build the template. This is not hard, just tedious. Out of sheer laziness, we make the message box a fixed size. If this were for a real program, we would have measured the text (using ncm.lfCaptionFont and ncm.lfMessageFont) to determine the best size for the message box.

    BOOL FakeMessageBox(HWND hwnd, LPCWSTR pszMessage, LPCWSTR pszTitle)
    {
     BOOL fSuccess = FALSE;
     HDC hdc = GetDC(NULL);
     if (hdc) {
      NONCLIENTMETRICSW ncm = { sizeof(ncm) };
      if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, 0, &ncm, 0)) {
       DialogTemplate tmp;
    
       // Write out the extended dialog template header
       tmp.Write<WORD>(1); // dialog version
       tmp.Write<WORD>(0xFFFF); // extended dialog template
       tmp.Write<DWORD>(0); // help ID
       tmp.Write<DWORD>(0); // extended style
       tmp.Write<DWORD>(WS_CAPTION | WS_SYSMENU | DS_SETFONT | DS_MODALFRAME);
       tmp.Write<WORD>(2); // number of controls
       tmp.Write<WORD>(32); // X
       tmp.Write<WORD>(32); // Y
       tmp.Write<WORD>(200); // width
       tmp.Write<WORD>(80); // height
       tmp.WriteString(L""); // no menu
       tmp.WriteString(L""); // default dialog class
       tmp.WriteString(pszTitle); // title
    
       // Next comes the font description.
       // See text for discussion of fancy formula.
       if (ncm.lfMessageFont.lfHeight < 0) {
         ncm.lfMessageFont.lfHeight = -MulDiv(ncm.lfMessageFont.lfHeight,
                  72, GetDeviceCaps(hdc, LOGPIXELSY));
       }
       tmp.Write<WORD>((WORD)ncm.lfMessageFont.lfHeight); // point
       tmp.Write<WORD>((WORD)ncm.lfMessageFont.lfWeight); // weight
       tmp.Write<BYTE>(ncm.lfMessageFont.lfItalic); // Italic
       tmp.Write<BYTE>(ncm.lfMessageFont.lfCharSet); // CharSet
       tmp.WriteString(ncm.lfMessageFont.lfFaceName);
    
       // Then come the two controls.  First is the static text.
       tmp.AlignToDword();
       tmp.Write<DWORD>(0); // help id
       tmp.Write<DWORD>(0); // window extended style
       tmp.Write<DWORD>(WS_CHILD | WS_VISIBLE); // style
       tmp.Write<WORD>(7); // x
       tmp.Write<WORD>(7); // y
       tmp.Write<WORD>(200-14); // width
       tmp.Write<WORD>(80-7-14-7); // height
       tmp.Write<DWORD>(-1); // control ID
       tmp.Write<DWORD>(0x0082FFFF); // static
       tmp.WriteString(pszMessage); // text
       tmp.Write<WORD>(0); // no extra data
    
       // Second control is the OK button.
       tmp.AlignToDword();
       tmp.Write<DWORD>(0); // help id
       tmp.Write<DWORD>(0); // window extended style
       tmp.Write<DWORD>(WS_CHILD | WS_VISIBLE |
                        WS_GROUP | WS_TABSTOP | BS_DEFPUSHBUTTON); // style
       tmp.Write<WORD>(75); // x
       tmp.Write<WORD>(80-7-14); // y
       tmp.Write<WORD>(50); // width
       tmp.Write<WORD>(14); // height
       tmp.Write<DWORD>(IDCANCEL); // control ID
       tmp.Write<DWORD>(0x0080FFFF); // static
       tmp.WriteString(L"OK"); // text
       tmp.Write<WORD>(0); // no extra data
    
       // Template is ready - go display it.
       fSuccess = DialogBoxIndirect(g_hinst, tmp.Template(),
                                    hwnd, DlgProc) >= 0;
      }
      ReleaseDC(NULL, hdc); // fixed 11 May
     }
     return fSuccess;
    }
    

    The fancy formula for determining the font point size is not that fancy after all. The dialog manager converts the font height from point to pixels via the standard formula:

    fontHeight = -MulDiv(pointSize, GetDeviceCaps(hdc, LOGPIXELSY), 72);
    Therefore, to get the original pixel value back, we need to solve this formula for pointSize so that when it is sent through the formula again, we get the original value back.

    The template itself follows the format we discussed earlier, no surprises.

    One subtlety is that the control identifier for our OK button is IDCANCEL instead of the IDOK you might have expected. That's because this message box has only one button, so we want to let the user hit the ESC key to dismiss it.

    Now all that's left to do is take this function for a little spin.

    void OnChar(HWND hwnd, TCHAR ch, int cRepeat)
    {
     if (ch == TEXT(' ')) {
      FakeMessageBox(hwnd,
       L"This is the text of a dynamically-generated dialog template. "
       L"If Raymond had more time, this dialog would have looked prettier.",
       L"Title of message box");
     }
    }
    
        // add to window procedure
        HANDLE_MSG(hwnd, WM_CHAR, OnChar);
    

    Fire it up, hit the space bar, and observe the faux message box.

    Okay, so it's not very exciting visually, but that wasn't the point. The point is that you now know how to build a dialog template at run-time.

  • The Old New Thing

    Computing the interval between two moments in time

    • 20 Comments

    Computing the interval between two moments in time is easy: It's just subtraction, but subtraction may not be what you want.

    If you are displaying time units on the order of months and years, then you run into the problem that a month is of variable length. some people just take the value relative to a base date of January 1 and extract the year and month counts.

    Unfortunately, this results in somewhat non-intuitive results. Let's illustrate with some examples. I'm going to write this in C# because it lets me focus on the algorithm instead of getting distracted by "oh dear how do I convert between SYSTEMTIME and FILETIME?" issues, and because it hightlights some new issues.

    // Remember, code in italics is wrong
    using System;
    using SC = System.Console;
    
    class Program {
     static void PrintAge(DateTime bday, DateTime asof)
     {
      TimeSpan span = asof - bday;
      SC.WriteLine(span);
     }
     public static void Main(string[] args) {
      DateTime bday = DateTime.Parse(args[0]);
      DateTime asof = DateTime.Parse(args[1]);
      if (bday > asof) { SC.WriteLine("not born yet"); return; }
      PrintAge(bday, asof);
     }
    }
    

    The two parameters to the program are the victim's birthday and the date as of which you want to compute the victim's age.

    Here's a sample run:

    > howold 1/1/2001 1/1/2002
    365.00:00:00
    

    Observe that the TimeSpan structure does not attempt to produce results in any unit larger than a day, since the authors of TimeSpan realized that months and years are variable-length.

    A naive implementation might go like this:

    static void PrintAge(DateTime bday, DateTime asof)
    {
     TimeSpan span = asof - bday;
     DateTime dt = (new DateTime(1900, 1, 1)).Add(span);
     SC.WriteLine("{0} years, {1} months, {2} days",
                  dt.Year - 1900, dt.Month - 1, dt.Day - 1);
    }
    

    Try it with some command lines and see what happens:

    > howold 1/1/2001 1/1/2002
    1 years, 0 months, 0 days // good
    > howold 1/1/2001 3/1/2001
    0 years, 2 months, 0 days // good
    > howold 1/1/2000 1/1/2001
    1 years, 0 months, 1 days // wrong
    > howold 9/1/2000 11/1/2000
    0 years, 2 months, 2 days // wrong
    

    Why does it say that a person born on January 1, 2000 is one year and one day old on January 1, 2001? The person is clearly exactly one year old on that day. Similarly, it thinks that November first is two months and two days after September first, when it is clearly two months exactly.

    The reason is that months and years are variable-length, but our algorithm assumes that they are constant. Specifically, months and years are context-sensitive but the algorithm assumes that they are translation-invariant. The lengths of months and years depend which month and year you're talking about. Leap years are longer than non-leap years. Months have all different lengths.

    How do you fix this? Well, first you have to figure out how human beings compute the difference between dates when variable-length units are involved. The most common algorithm is to declare that one year has elapsed when the same month and day have arrived in the year following the starting point. Similarly, a month has elapsed when the same numerical date has arrived in the month following the starting point.

    Mentally, you add years until you can't add years any more without overshooting. Then you add as many months as fit, and then finish off with days. (Some people subtract, but the result is the same.)

    Now you get to mimic this algorithm in code.

    static void PrintAge(DateTime bday, DateTime asof)
    {
     // Accumulate years without going over.
     int years = asof.Year - bday.Year;
     DateTime t = bday.AddYears(years);
     if (t > asof) { years--; t = bday.AddYears(years); }
    
     // Accumulate months without going over.
     int months = asof.Month - bday.Month; // fixed 10pm
     if (asof.Day < bday.Day) months--;
     months = (months + 12) % 12;
     t = t.AddMonths(months);
    
     // Days are constant-length, woo-hoo!
     int days = (asof - t).Days;
    
     SC.WriteLine("{0} years, {1} months, {2} days",
                  years, months, days);
    }
    

    Notice that this algorithm agrees with the common belief that people born on February 29th have birthdays only once every four years.

    Exercise: Explain what goes wrong if you change the line

     if (t > asof) { years--; t = bday.AddYears(years); }
    

    to

     if (t > asof) { years--; t = t.AddYears(-1); }
    
  • The Old New Thing

    The dialog manager, part 4: The dialog loop

    • 24 Comments

    The dialog loop is actually quite simple. At its core, it's just

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (!IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    If you want something fancier in your dialog loop, you can take the loop above and tinker with it.

    But let's start from the beginning. The work happens in DialogBoxIndirectParam. (You should already know by now how to convert all the other DialogBoxXxx functions into DialogBoxIndirectParam.)

    INT_PTR WINAPI DialogBoxIndirectParam(
        HINSTANCE hinst,
        LPCDLGTEMPLATE lpTemplate, HWND hwndParent,
        DLGPROC lpDlgProc, LPARAM lParam)
    {
     /*
      * App hack!  Some people pass GetDesktopWindow()
      * as the owner instead of NULL.  Fix them so the
      * desktop doesn't get disabled!
      */
     if (hwndParent == GetDesktopWindow())
      hwndParent = NULL;
    

    That's right, we start with an app hack. The problem of passing GetDesktopWindow() instead of NULL was discussed in an earlier entry. So many people make this mistake that we had to put this app hack into the core OS. It would be pointless to make a shim for it since that would mean that thousands of apps would need to be shimmed.

    Since only top-level windows can be owners, we have to take the putative hwndParent (which might be a child window) and walk up the window hierarchy until we find a top-level window.

     if (hwndParent)
      hwndParent = GetAncestor(hwndParent, GA_ROOT);
    

    With that second app hack out of the way, we create the dialog.

     HWND hdlg = CreateDialogIndirectParam(hinst,
                   lpTemplate, hwndParent, lpDlgProc,
                   lParam);
    

    Note: As before, I am going to ignore error checking and various dialog box esoterica because it would just be distracting from the main point of this entry.

    Modal windows disable their parent, so do it here.

     BOOL fWasEnabled = EnableWindow(hwndParent, FALSE);
    

    We then fall into the dialog modal loop:

     MSG msg;
     while (<dialog still active> &&
            GetMessage(&msg, NULL, 0, 0)) {
      if (!IsDialogMessage(hdlg, &msg)) {
       TranslateMessage(&msg);
       DispatchMessage(&msg);
      }
     }
    

    Per the convention on quit messages, we re-post any quit message we may have received so the next outer modal loop can see it.

     if (msg.message == WM_QUIT) {
      PostQuitMessage((int)msg.wParam);
     }
    

    (Astute readers may have noticed an uninitialized variable bug: If EndDialog was called during WM_INITDIALOG handling, then msg.message is never set. I decided to ignore this fringe case for expository purposes.)

    Now that the dialog is complete, we clean up. Remember to enable the owner before destroying the owned dialog.

    if (fWasEnabled)
     EnableWindow(hwndParent, TRUE);
    DestroyWindow(hdlg);
    

    And that's all. Return the result.

     return <value passed to EndDialog>;
    }
    

    Congratulations, you are now an expert on dialog boxes. Tomorrow we'll look at how you can put this new expertise to good use.

    Exercise: Find a way to sneak through the two layers of hwndParent parameter "repair" and end up with a dialog box whose owner is the desktop window. Explain the dire consequences of this scenario.

  • The Old New Thing

    When people ask for security holes as features: Hiding files from Explorer

    • 40 Comments

    By default, Explorer does not show files that have the FILE_ATTRIBUTE_HIDDEN flag, since somebody went out of their way to hide those files from view.

    You can, of course, ask that such files be shown anyway by going to Folder Options and selecting "Show hidden files and folders". This shows files and folders even if they are marked as FILE_ATTRIBUTE_HIDDEN.

    On the other hand, files that are marked as both FILE_ATTRIBUTE_HIDDEN and FILE_ATTRIBUTE_SYSTEM remain hidden from view. These are typically files that involved in the plumbing of the operating system, messing with which can cause various types of "excitement". Files like the page file, folder configuration files, and the System Volume Information folder.

    If you want to see those files, too, then you can uncheck "Hide protected operating system files".

    Let's look at how far this game of hide/show ping-pong has gone:

    ShowHide
    1.Normal file
    2.Hidden file
    3."Show hidden files"
    4.Hidden + System
    5."Show protected
    operating system files"

    You'd think this would be the end of the hide/show arms race, but apparently some people want to add a sixth level and make something invisible to Explorer, overriding the five existing levels.

    At some point this back-and-forth has to stop, and for now, it has stopped at level five. Adding just a sixth level would create a security hole, because it would allow a file to hide from the user. As a matter of security, a sufficiently-privileged user must always have a way of seeing what is there or at least know that there is something there that can't be seen. Nothing can be undetectably invisible.

    If you add a sixth level that lets a file hide from level five, then there must be a level seven that reveals it.

  • The Old New Thing

    Thread messages are eaten by modal loops

    • 3 Comments

    Thread messages (as generated by the PostThreadMessage function) do not go anywhere when passed to the DispatchMessage function. This is obvious if you think about it, because there is no window handle associated with a thread message. DispatchMessage has no idea what to do with a message with no associated window. It has no choice but to throw the message away.

    This has dire consequences for threads which enter modal loops, which any thread with a window almost certainly will do at one time or another. Recall that the traditional modal loop looks like this:

    while (GetMessage(&msg, NULL, 0, 0)) {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
    }
    

    If a thread message is returned by the GetMessage function, it will just fall through the TranslateMessage and DispatchMessage without any action being taken. Lost forever.

    Thread messages are generally to be avoided on threads that create windows, for this very reason. Of course, if you're going to create a window, why not use PostMessage instead, passing that window as the target of the posted message? Since there is now a window handle, the DispatchMessage function knows to give the message to your window procedure. Result: Message not lost.

Page 1 of 3 (26 items) 123