开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜