博客园  :: 首页  :: 联系 :: 管理

Persisting View State Update, Using Managed Extensions in a DLL

Posted on 2006-12-15 22:24  sunrack  阅读(744)  评论(0编辑  收藏  举报

This month marks the 11th anniversary of my column and the inauguration of a new title: C++ At Work. We're also adding a new bimonthly column, "Pure C++," by my pal Stan Lippman, one of the great C++ Masters. Stan will cover more of the pure C++/CLI language stuff (he can tell you more himself), and I'll continue to write about the everyday application of C++, MFC (and now the Managed Extensions) to real-world Windows® programming, just as I've done for 11 years. Stan's new column represents the recognition by Microsoft of C++ as one of the most popular and vibrant programming languages alive today, and MSDN®Magazine's commitment to provide our readers even more great C++ coverage. Yeah!

I chose the title C++ At Work because it has a double meaning. C++ at work could mean "C++ on-the-job," as in: for people who use C++ at work. But for me the other more important meaning is putting C++ to work—that is, making C++ do work for you. That's what my column has always been about, and this won't change. The only formal change you can expect is that since the letters "Q&A" no longer appear in the moniker, I reserve the right to occasionally write about some C++ topic or technique I consider important or interesting or valuable, but doesn't necessarily come directly from a reader question. But mostly I plan to keep the Q&A format, so by all means—keep your questions coming!


Q I read your November 2004 column on how to persist the Open dialog view state across user sessions with great interest, but I think you overlooked something. Your CListViewShellWnd (m_wndListViewShell in the dialog) will be destroyed at the moment the user changes folders. So when you close the dialog, it won't save the current view mode, rather the view mode it was in when the user changed to another folder. Is there some way to fix this?

John E. Noël

A Well tie me up and put me in solitary confinement! You're absolutely right, there's a bug in my November code. For readers who missed that issue, I described how to implement a customized Open dialog (CPersistOpenDlg) that remembers the view mode across user sessions. So if the user selects Thumbnails in the Open dialog, then runs your program again tomorrow, the dialog opens in thumbnail view. I did this by subclassing a special window SHELLDLL_DefView, which the Open dialog uses to display files and folders. I used Spy++ to discover the magic command IDs to set the view mode, and I showed how to use LVM_GETITEMSPACING to distinguish between icon and thumbnail views—both of which return LV_VIEW_ICON from LVM_GETVIEW/GetView.

Whenever you want to save your window's state, whether it be size/position or something like the view mode, the natural place to do it is just before the window is destroyed, in your WM_DESTROY/OnDestroy handler. So that's what I did in CListViewShellWnd, my MFC class for SHELLDLL_DefView:

void CListViewShellWnd::OnDestroy() {
m_lastViewMode = GetViewMode(); // remember current view mode
}

 

The dialog object (CPersistOpenDlg) then writes m_lastViewMode to the user's profile as it's destroyed, in its destructor. This all makes perfect sense—it's the normal way to do this sort of thing. But as John discovered, the Open dialog doesn't destroy the shell view only when the user closes the dialog; it destroys it every time the user changes folders. As if the only way to change a list is to blow up the world and rebuild it from scratch. (Now you know why the Open dialog can be so slow changing folders.) To see the folder view destroyed, all you have to do is add a TRACE:

void CListViewShellWnd::OnDestroy() {
TRACE(_T("CListViewShellWnd::OnDestroy\n"));
...
}

 

Now run the dialog and change folders. Sure enough, the folder view is clobbered. So CListViewShellWnd faithfully saves its view mode, but when the user changes folders, the Open dialog destroys its SHELLDLL_DefView, which causes MFC to automatically unsubclass it (in CWnd::OnNcDestroy) and sets my m_hWnd to NULL. The Open dialog then creates a brand new SHELLDLL_DefView which CPersistOpenDlg never sees, so when CPersistOpenDlg writes the view mode to the user's profile, it writes the old mode before the user changed folders. Oops.

Fortunately, this is one of those bugs that points the way to its own solution. Fixing CPersistOpenDlg isn't difficult, it just requires a little thought. The obvious thing would be to trap OnFolderChange and resubclass the folder view. Resubclassing is the right idea, but why do it in OnFolderChange? Whenever you discover something unexpected in Windows, you should always ask: where else might things go awry? If the Open dialog destroys its folder view when the user changes folders, how do you know it doesn't destroy it some other time? Like when the clock strikes midnight on Halloween, or when the Red Sox win the World Series during a lunar eclipse? Rather than patch every event, it's better to find a solution closer to the problem.

CPersistOpenDlg already has a private message, MYWM_POSTINIT, to initialize the dialog. CPersistOpenDlg posts this message to itself in OnInitDialog when the dialog first starts up; the handler subclasses the folder view. All I have to do is post MYWM_POSTINIT again when the folder view is destroyed:

void CListViewShellWnd::OnDestroy() {
m_lastViewMode = GetViewMode(); // as before
m_pDialog->PostMessage(MYWM_POSTINIT); // re-post init msg
}

 

Now if the Open dialog decides to destroy its folder view for whatever reason (folder change or Red Sox), CListViewShellWnd posts the initialization message, causing my dialog to resubclass the new folder view. Clever, eh? It's important to post—not send—MYWM_POSTINIT, so the Open dialog can create the new folder view before CPersistOpenDlg::OnPostInit subclasses it. Fixing the problem in CListViewShellWnd::OnDestroy is guaranteed to catch all places the Open dialog might destroy its folder view, and has the added benefit of not requiring any new event handlers. Never write more code than you have to. If the code doesn't exist, it can't be riddled with obscure bugs.

Astute readers may have noticed I added a data member, m_pDialog, which points to the "parent" CPersistOpenDlg object. Posting MYWM_POSTINIT to the view's parent (GetParent) would be cleaner (because it doesn't require adding a data member), but doesn't work here due to the peculiar way CPersistOpenDlg is actually a child of the real dialog. So I added a back pointer, initialized at construction.

I also had to modify CPersistOpenDlg::OnPostInit slightly. The original handler not only subclassed the folder view, it also initialized the view mode from the user's profile. That's what I want when the dialog first starts—but I don't want to reset the view mode to the saved state every time the user changes folders. So I need a way to distinguish first-time initialization from subsequent reinitialization. Since I wasn't using WPARAM for anything, I used that. In the revised code, OnInitDialog posts MYWM_POSTINIT with WPARAM set to TRUE while CListViewShellWnd::OnDestroy posts it with WPARAM set to FALSE. Figure 1 shows the new OnPostInit handler in action.

Finally, some of you may be wondering—what happens when the Open dialog destroys the folder view because the dialog is really shutting down? What happens to the MYWM_POSTINIT message posted from CListViewShellWnd::OnDestroy? Nothing. CListViewShellWnd::OnDestroy posts its message into the void where, like the philosophical tree falling in the woods, it makes no sound because nobody's listening. The entire message queue is destroyed along with the dialog.

Because I'm so nice I rewrote CPersistOpenDlg to include the fixes described here. You can download this updated sample from the MSDN Magazine Web site.


Q I have a DLL that uses MFC and now I want to use the Managed Extensions to call classes in the Microsoft® .NET Framework. When I compile, I get a linker warning LNK4243: "DLL containing objects compiled with /clr is not linked with /NOENTRY; image may not run correctly." I don't really understand what this means so I ignore it—but now my app crashes when I run. Can't I use Managed Extensions in a DLL?

Jimmy Preston

A Just when programming has gotten so easy your Aunt Wendy is writing Web services in C#, you do something simple and suddenly the universe topples down on your poor little head. Fortunately, in this case the solution is described in the article "Converting Managed Extensions for C++ Projects from Pure Intermediate Language to Mixed Mode," which can be found in the MSDN Library. But since you're probably not the only programmer in readerland who's been hit by this train, I'll go over it again here with a little more emphasis on what goes on behind the scenes. You must delve into DLLs, but it's good for you—like eating your peas.

Let's start with a plain old C DLL. No C++, nothing fancy, just a bunch of exported functions that apps can call. Remember: a DLL is fundamentally just a library of subroutines that get linked at run time as opposed to compile time. (That's the "Dynamic" in DLL.) The same way every C program has an entry point called main, every DLL has an entry called DllMain. (At least, in the vanilla case. As you'll see, DllMain isn't always required.) DllMain's only role in life is to provide a place for you to initialize your DLL. Say you need to create some global state that all your functions use. DllMain is the place to do it. The system calls DllMain whenever a process attaches to, or detaches from, your DLL. Figure 2 shows the basic structure for DllMain.

So far, so good. Turn the calendar several years ahead to C++, and now your DLL can have classes. Say you have a Bobble class like the one shown here:

class Bobble {
public:
Bobble()  { /* create  */ }
~Bobble() { /* destroy */ }
};
And suppose your Bobble DLL has a static Bobble instance defined at global scope:
Bobble MyBobble;

 

MyBobble is a global object all your functions can use, like theApp in MFC. Somehow the compiler must now arrange to have your Bobble constructor called before apps can call any functions in bobble.dll. This never happened in C, where the only static initialization is something like:

int GlobalVal=0;
That is, initialization of primitive types to constant values, which the compiler can manage by itself. But now initialization requires calling functions that execute at run time, not compile time. In C++, you can even write the following line of code to initialize an int by calling a function:
UINT WM_MYFOOMSG = RegisterWindowMessage("MYFOOMSG");
Who calls these functions and when? Answer: the startup code does it just before it calls DllMain. You may think life begins with DllMain, but even before that, the C runtime DLL startup code calls your constructors. If you peer inside crtdll.c, the source code for the CRT's DLL initialization sequence (it's in \VS.NET\VC7\crt\src), you'll find the following function:
BOOL WINAPI _DllMainCRTStartup(...) {
if ( /* DLL_PROCESS_ATTACH */ ) {
_CRT_INIT(...); // initialize CRT including ctors
DllMain(...);
}
...
}

 

I've simplified to highlight what happens: _DllMainCRTStartup calls another crtdll function, _CRT_INIT, then DllMain. _CRT_INIT initializes the C runtime, then calls all your static object constructors. How does _CRT_INIT know which constructors to call? The compiler has generated a list. So when an application invokes your C++ DLL, the load sequence is as follows:

  1. The app loads your DLL (LoadLibrary)
  2. LoadLibrary calls _DllMainCRTStartup
  3. _DllMainCRTStartup calls _CRT_INIT
  4. _CRT_INIT calls static ctors
  5. _DllMainCRTStartup calls DllMain
  6. DllMain does more initialization
  7. The app calls DLL functions

 

If you're using MFC, you don't even know DllMain exists because MFC provides it for you. In MFC, life begins with CWinApp::InitInstance. But that's only because MFC has its own DllMain stub that calls it for you.

Okay, now let's spin the time dial forward once more to .NET and the Managed Extensions. Your MFC DLL is working fine, you didn't even know it had a DllMain, and now you want to call the Framework. So you #include <mscorlib.dll>, compile with /clr, and you get the enigmatic /NOENTRY warning. What's going on?

To answer that question, I have to explain one more piece of the puzzle. DllMain runs at a critical time in your DLL's life, when it's in the middle of loading. You can't do anything you want inside DllMain. In particular, you can't call other DLLs because they might not be loaded yet. Calling other DLLs would require loading them first, which could end up through a circular chain of references trying to load your DLL, which is still in the process of loading. Can you say "infinite loop, stack overflow?" This restriction applies even to system DLLs like user32, shell32, and COM objects. Many programmers are dismayed to discover that their DLL crashes when they call MessageBox from DllMain, hoping to display some information during debugging. The only DLL that's guaranteed to be loaded and safe to call from DllMain is kernel32.dll. (For some more information on what you can and can't do in DllMain, see DllMain.)

Having explained that, you might guess what goes wrong with managed DLLs. Once you flip the /clr switch, your DLL becomes managed. Initializing a managed DLL could require running unsafe code during DllMain. The compiler generates Microsoft intermediate language (MSIL) now, not native machine instructions. Only the Redmondtonians know exactly what happens and how many DLLs the system invokes when it encounters the first MSIL instruction in your DLL. Whatever happens, you can bet it involves more than calling the kernel. So by default, managed DLLs don't link with the C runtime, msvcrt.lib. They don't have _DllMainCRTStartup and they don't call DllMain—even if it exists. In other words, in order to prevent calling unsafe code during DLL initialization, the Redmondtonians have simply decreed that managed DLLs shall be /NOENTRY DLLs. A /NOENTRY DLL is one that has no entry point. If your DLL doesn't need to initialize or terminate itself, it doesn't need DllMain and so you can use the /NOENTRY linker option. Such DLLs can't have static objects that require initialization; all they can do is export functions.

But MFC has static objects that require initialization all over the place; it needs for them to work. Does that mean you can't use MFC in your mixed-mode DLL? Of course not! The Redmontonians offer a way out. The solution is described in the "Converting Managed Extensions" article mentioned earlier, and varies slightly depending on whether you're using LoadLibrary or import libs and whether you're writing a normal DLL or a COM object.

Since you asked about MFC, I'll describe what to do for C++/MFC DLLs that use import libraries, which is the more common case. The basic idea is simple: first make your DLL a NOENTRY DLL by adding /NOENTRY to the linker options. That keeps the linker happy. To initialize your objects, you have to write your own initialization/termination functions and export them. Any application that uses your DLL must call your init function before it calls any functions in your DLL, and must likewise call your term function when it's finished.

That sounds like a good plan, but how do you implement the init/term functions? Lucky for you, the Redmondtonians have provided a file with a couple of functions that make life easy; all you have to do is include the file and call them:

#include <_vcclrit.h>
BOOL __declspec(dllexport) MyDllInit() {
return __crt_dll_initialize();
}
BOOL__declspec(dllexport) MyDllTerm() {
return __crt_dll_terminate();
}

 

That wasn't hard, was it? If you peek inside _vcclrit.h and read past all the gnarly multithreading stuff, you'll see all __crt_dll_ initialize does is call _DllMainCRTStartup(DLL_ PROCESS_ ATTACH). Likewise, __crt_dll_terminate calls _DllMainCRTStartup (DLL_PROCESS_DETACH). If you're writing in C++ and you don't mind losing the Boolean return code, you can write an auto-init class whose constructor/destructor encapsulates the __crt_dll_xxx calls, so all client apps have to do is instantiate an auto-init object somewhere at global scope:

CMyLibInit initlib; // in main app

 

Figure 4 LibTest
Figure 4 LibTest

Note that the CMyLibInit approach works only when your DLL is consumed by an EXE; if your client is another DLL and you try to instantiate CMyLibInit in the DLL's global scope, __crt_dll_initialize will be called under loader lock again, potentially triggering the same DLL-deadlock problem you're trying to avoid. To initialize a mixed DLL from another native DLL, you have to find an appropriate place to call the mixed DLL's init/term functions—for example, if the native DLL is a COM object, you could do it from DllGetClassObject/DllCanUnloadNow. Otherwise you need to export yet another layer of init/term functions from your native DLL that in turn call the constructors or DllMain since these are under loader lock.

Figure 3 shows a DLL I wrote that uses the CMyLibInit approach. ManLib exports a function, FormatRunningProcesses, that calls the Process class in the .NET Framework to format a list of running processes and return it in a CString. The test app calls FormatRunningProcesses and displays the list in a message box (see Figure 4). I sprinkled ManLib with TRACE diagnostics to help you see which functions get called when. Figure 5 is a sample run that shows the order of events. You can compare this output with the TRACE statements in Figure 3 to better understand the call order. If you implement something like CManLibInit at home, remember: you can't do anything before calling __crt_dll_initialize (or after calling __crt_dll_terminate). The first time I wrote ManLib, I put my TRACE at the top without thinking, like the following:

CManLibInit::CManLibInit() {
TRACE(...); // Oops!
__crt_dll_initialize();
}
This crashed my program because MFC's trace mechanism isn't initialized yet. That's what __crt_dll_ initialize does!

 

Figure 5 TraceWin
Figure 5 TraceWin

One more thing before I go. The solution described in the MSDN Library article tells you to add msvcrt.lib to your link libraries and __DllMainCRTStartup@12 (the mangled name) under "Force Symbol References" in your project settings. If you're starting with a DLL that already uses MFC, these steps aren't necessary because MFC already links what you need. All that you have to do is add /NOENTRY to your linker options, write the init and term functions, and call them from your application (see the sidebar Mixed Code Initialization in Visual C++ 2005). Happy programming!