Lately, I have had a series of cases where the lifetime of something has been a problem.
The first was actually with the filing system. A customer was doing something innovative with VB6. What they were doing was some rather clever automation of the IDE but that isn’t very important for this story as the crash was occurring before we got to their automation code. They were shelling VB6.EXE but just before they did that, they deleted some files so that VB would have a nice fresh start. This often worked just fine but sometimes VB would die. The reason proved to be interesting. When you delete a file using the conventional approach, it will get deleted. When? Soon. Soon enough? Maybe. What was happening in this case was that the instruction to delete would be issued and the file was doomed. Then VB was started and tried to open the file. The file may have been doomed but VB could still find it and load it. VB then got rather confused when the file that it was working with vanished shortly afterwards. This wasn’t something that we had coded for because the files were not really supposed to be touched except by VB and VB had no intention of the deleting the file just then. The solution was to use MoveFileEx with the MOVEFILE_WRITE_THROUGH option so that the program that shelled VB would block until the file was really dead and not just mortally wounded.
The lifetime of objects is something that most people don’t give a lot of thought to. It isn’t generally a problem except when it is. When it is a problem, you tend to get sporadic errors that don’t reproduce in test. This generally causes unhappiness, late nights and a call to Microsoft where I or one of my colleagues will try to work out what is going wrong. So, what happens when you tell something to go away. It depends what it is.
An unmanaged DLL. If you tell it that it can go (with FreeLibrary) then it goes. It says so in MSDN. To quote “The FreeLibrary function decrements the reference count of the loaded dynamic-link library (DLL). When the reference count reaches zero, the module is unmapped from the address space of the calling process and the handle is no longer valid.” This is almost entirely true but not quite. It should read “When the reference count reaches zero, the DLL becomes a candidate for unloading and will be unloaded at some point just so long as a call to DLLCanUnloadNow returns S_OK. Until then, the DLL is mapped in to your process space and the handle is valid although it is anyone’s guess what the state data of the DLL will be”. The MSDN version is snappier, I admit. So, what happens if I call in to a DLL function after the reference count is zero? Well, you might get away with it. Or it might crash because its state data is in a mess. Or it might AV because the DLL really isn’t in your process space at all. Best yet, you could call it and it could unload in the middle of the call so the instruction pointer is in the middle of nowhere much. Of course, you never call FreeLibrary and then call a method because that would be stupid. However, the reference count is done for the instance of the DLL and so is process wide. Some other component could call FreeLibrary twice and you could find that your reference was worthless. Quite often, the code that crashes is a long, long way from the bug. Oh, and as an added bonus, the unload order is not guaranteed.
COM objects are reference counted as well. Replace LoadLibrary and FreeLibrary with AddRef and Release and you have the same story.
Let’s look at another reference counting problem. DLL_A has references to objects in DLL_B. When A exits, it releases the references to B. In the normal case, the references are cleaned up before unload but imagine that didn’t happen for some reason and there are a lot of reasons why it might not. The process is exiting. It wants to unload the DLLs. Is it safe to do so? No, it is not. What are the alternatives? You can fail to unload and hang. That isn’t a good alternative. You had best unload the DLLs and hope for the best. You might get lucky. If DLL_A unloads first then the calls to clean up B will probably be OK. If B unloads first, your calls to Release will run whatever happens to be in memory there if anything is there. This is not a happy event.
Reference counting has some good points but there are some definite problems as well.
There are also some possible scenarios where lifetime can be a problem even with good reference counting. I am working on one now. A VB6 COM object is running in an environment that is not VB but a highly multithreaded environment. The VB component tells one of the containers in this environment that it should delete a control from the container. The container will do this, on another thread as it happens. Later on, the VB component asks the container if it has any controls of that type loaded. The possible answers are “yes” and “no”. The correct answer is “Kind of”. There is one loaded and it might be loaded for a few milliseconds yet. The VB code proceeds on the assumption that the control will still be there in a few instructions time. Often it is correct. Sometimes it isn’t.
Life is better in the managed world although that is not without its special areas of fun. I will talk about that another time.