I just got bitten by the .NET Framework COM interrop. We had a problem with Xceed Zip ActiveX used in a .NET application. If the application was handling the ZipPreprocessingFile event and changed the sFilename parameter (BSTR*, or ByRef String if you wish), sometimes the library did not change the filename in the resulting zip file.
That "sometimes" was the mysterious part, though I had a good idea where the problem was. The method which fires the ZipPreprocessingFile event makes a dangerous, but up until now valid assumption. The kind of assumption that would make Raymond Chen or Don Box real mad. It took for granted that the BSTR address would change if the callee was to change the BSTR. I made this assumption based on two facts:
- A BSTR is an immutable entity. If you need to modify one, you should create a copy with the new content.
If the implementation of a function that takes a BSTR reference parameter assigns a new BSTR to the parameter, it must free the previously referenced BSTR. (written "as is" in MSDN)
The .NET code that reproduces the problem does a very simple thing:
sFilename = sFilename & "new"
Normally, languages will work with the provided BSTR* as is. And if a modification occurs, they will allocate the required new BSTR, copy chars from the old BSTR, then free it. The new string cannot have the same address as the old one.
In .NET, the COM interrop is actually making a copy of the BSTR to create a System.String, work with that System.String throughout the function, then checks if the string changed before returning control to the COM caller, making either a call to SysReAllocString on the old BSTR, using the String as the "psz" parameter, or simply freeing the old BSTR, then allocate the new one based on the String.
Bam! Turns out SysReAllocString or SysAllocString sometimes reallocate the new BSTR at the same address as the old one. Can't argue against that. My bad.
Three things to conclude with that:
You can never use what you experiment as a proof of concept. Experiments and tests are always a subset of the big picture.
Don't try to make assumptions larger than the initial statement. Assuming that a pointer wouldn't change just because a BSTR* parameter must be freed if changed was stretching the actual fact.
Optimizations may sound good, but can always introduce more problems. Simplicity is bliss.
By the way, I realized I could try a very simple VB6 test. If once the old BSTR is freed any new BSTR can end-up at the same address, does it mean that a VB6 application modifying the sFilename parameter twice can reproduce the same bug? Absolutely! My VB6 sample application did this in ZipPreprocessingFile:
sFilename = sFilename & ".foo"
sFilename = sFilename & ".bar"
Turns out files are sometimes renamed, sometimes not... Sometimes, I feel like an...