Releasing an OLE IStorage file handle in C#
I'm trying to embed a PDF file into a Word document using the OLE technique described here: https://learn.microsoft.com/en-us/archive/blogs/brian_jones/embedding-any-file-type-like-pdf-in-an-open-xml-file
I've tried to implement the C++ code provided in C# so that the whole project's in one place and am almost there except for one roadblock. When I try to feed the generated OLE object binary data into the Word document I get an IOException.
IOException: The process cannot access the file 'C:\Wherever\Whatever.pdf.bin' because it is being used by another process.
There is a file handle open the .bin file ("oleOutputFileName" below) and I don't know how to get rid of it. I don't know a huge amount about COM - I'm winging it here - and I don't know where the file handle is or how to release it.
Here's what my C#-ised code looks like. What am I missing?
public void ExportOleFile(string oleOutputFileName, string emfOutputFileName)
{
OLE32.IStorage storage;
var result = OLE32.StgCreateStorageEx(
oleOutputFileName,
OLE32.STGM.STGM_READWRITE | OLE32.STGM.STGM_SHARE_EXCLUSIVE | OLE32.STGM.STGM_CREATE | OLE32.STGM.STGM_TRANSACTED,
OLE32.STGFMT.STGFMT_DOCFILE,
0,
IntPtr.Zero,
IntPtr.Zero,
ref OLE32.IID_IStorage,
out storage
);
var CLSID_NULL = Guid.Empty;
OLE32.IOleObject pOle;
result = OLE32.OleCreateFromFile(
ref CLSID_NULL,
_inputFileName,
ref OLE32.IID_IOleObject,
OLE32.OLERENDER.OLERENDER_NONE,
IntPtr.Zero,
null,
storage,
out pOle
);
result = OLE32.OleRun(pOle);
IntPtr unknownFromOle = Marshal.GetIUnknownForO开发者_如何学Pythonbject(pOle);
IntPtr unknownForDataObj;
Marshal.QueryInterface(unknownFromOle, ref OLE32.IID_IDataObject, out unknownForDataObj);
var pdo = Marshal.GetObjectForIUnknown(unknownForDataObj) as IDataObject;
var fetc = new FORMATETC();
fetc.cfFormat = (short)OLE32.CLIPFORMAT.CF_ENHMETAFILE;
fetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
fetc.lindex = -1;
fetc.ptd = IntPtr.Zero;
fetc.tymed = TYMED.TYMED_ENHMF;
var stgm = new STGMEDIUM();
stgm.unionmember = IntPtr.Zero;
stgm.tymed = TYMED.TYMED_ENHMF;
pdo.GetData(ref fetc, out stgm);
var hemf = GDI32.CopyEnhMetaFile(stgm.unionmember, emfOutputFileName);
storage.Commit((int)OLE32.STGC.STGC_DEFAULT);
pOle.Close(0);
GDI32.DeleteEnhMetaFile(stgm.unionmember);
GDI32.DeleteEnhMetaFile(hemf);
}
UPDATE 1: Clarified which file I meant by "the .bin file".
UPDATE 2: I'm not using "using" blocks because the things I want to get rid of aren't disposable. (And to be perfectly honest I'm not sure what I need to release to remove the file handle, COM being a foreign language to me.)I see at least four potential refcount leaks in your code:
OLE32.IStorage storage; // ref counted from OLE32.StgCreateStorageEx(
IntPtr unknownFromOle = Marshal.GetIUnknownForObject(pOle); // ref counted
IntPtr unknownForDataObj; // re counted from Marshal.QueryInterface(unknownFromOle
var pdo = Marshal.GetObjectForIUnknown(unknownForDataObj) as IDataObject; // ref counted
Note that all these are pointers to COM objects. COM objects are not collected by GC unless the .Net type that holds the reference points to an RCW wrapper and will properly release its reference count in its finalizer.
IntPtr
is not such type and your var
also is IntPtr
(from the return type of the Marshal.GetObjectForIUnknown
call), so that makes three.
You should call Marshal.Release
on all your IntPtr
variables.
I am not sure about OLE32.IStorage
. This one might need either Marshal.Release
or Marshal.ReleaseComPointer
.
Update: I just noticed that I missed at least one ref count. The var
is not an IntPtr
, it's an IDataObject
. The as
cast will do an implicit QueryInterface
and add another ref count. Although GetObjectForIUnknown
returns an RCW, this one is delayed until the GC kicks in. You might want to do this in using
block to activate the IDisposable
on it.
Meanwhile, the STGMEDIUM
struct also has one IUnknown
pointer you are not releasing. You should call ReleaseStgMedium
to properly dispose of the whole struct, including that pointer.
I am too tired to continue looking through the code right now. I'll come back tomorrow and try to find other possible ref count leaks. Meanwhile, you check the MSDN docs for all interfaces, structs and APIs you are calling and try to figure out any other ref counts you might have missed.
I found the answer and it's pretty simple. (Probably too simple - it feels like a hack but since I know so little about COM programming I'm just going to go with it.)
The storage object had multiple references on it, so just keep going until they're all gone:
var storagePointer = Marshal.GetIUnknownForObject(storage);
int refCount;
do
{
refCount = Marshal.Release(storagePointer);
} while (refCount > 0);
I know the question is old, but as this has caused me some trouble I feel I need to share what has worked for me.
At first, I've tried to use Bernard Darnton's own answer:
var storagePointer = Marshal.GetIUnknownForObject(storage); int refCount; do { refCount = Marshal.Release(storagePointer); } while (refCount > 0);
However, even though the solution worked at first, it ended up causing some collateral issues.
So, following Franci Penov answer, I added what follows to the code:
OLE32.ReleaseStgMedium(ref stgm);
Marshal.Release(unknownForDataObj);
Marshal.Release(unknownFromOle);
Marshal.ReleaseComObject(storage);
I have written this for releasing com objects:
public static void ReleaseComObjects(params object[] objects)
{
if (objects == null)
{
return;
}
foreach (var obj in objects)
{
if (obj != null)
{
try
{
Marshal.FinalReleaseComObject(obj);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
}
}
You pass the objects you want to release e.g. in a finally statement and it "releases all references to a Runtime Callable Wrapper (RCW) by setting its reference count to 0."
It is not suitable if you want to release the last created reference but keep references created before.
It has worked for me with no memory leaks.
精彩评论