Освобождение дескриптора файла OLE IStorage в C#

Я пытаюсь встроить PDF-файл в документ Word, используя метод OLE, описанный здесь: http://blogs.msdn.com/brian_jones/archive/2009/07/21/embedding-any-file-type-like-pdf-in-an-open-xml-file.aspx

Я попытался реализовать код C++, предоставляемый в C#, чтобы весь проект находился в одном месте и был почти там, за исключением одного контрольно-пропускного пункта. Когда я пытаюсь передать сгенерированные двоичные данные объекта OLE в документ Word, я получаю IOException.

IOException: процесс не может получить доступ к файлу 'C:\Wherever\Whever.pdf.bin', поскольку он используется другим процессом.

Есть дескриптор файла, открывающий файл.bin ("oleOutputFileName" ниже), и я не знаю, как от него избавиться. Я не очень разбираюсь в COM - я его здесь разрабатываю - и я не знаю, где находится дескриптор файла или как его выпустить.

Вот как выглядит мой код на C#. Что мне не хватает?

    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.GetIUnknownForObject(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);
    }

ОБНОВЛЕНИЕ 1: Уточнил, какой файл я имел в виду под ".bin файл".
ОБНОВЛЕНИЕ 2: я не использую блоки "использование", потому что вещи, от которых я хочу избавиться, не одноразовые. (И, если честно, я не уверен, что мне нужно выпустить, чтобы удалить дескриптор файла, поскольку COM для меня - иностранный язык.)

4 ответа

Решение

Я нашел ответ, и это довольно просто. (Возможно, слишком просто - это похоже на хак, но так как я так мало знаю о программировании COM, я просто собираюсь пойти на это.)

Объект хранилища имел несколько ссылок, так что продолжайте, пока все они не исчезнут:

var storagePointer = Marshal.GetIUnknownForObject(storage);
int refCount;
do
{
    refCount = Marshal.Release(storagePointer);
} while (refCount > 0);

Я вижу по крайней мере четыре потенциальных утечки в вашем коде:

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

Обратите внимание, что все это указатели на COM-объекты. 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.

Вам следует позвонить Marshal.Release on all your IntPtr переменные.

Я не уверен насчет OLE32.IStorage, This one might need either Marshal.Release или же Marshal.ReleaseComPointer,

Update: I just noticed that I missed at least one ref count. var это не IntPtr, это IDataObject, as cast will do an implicit QueryInterface and add another ref count. Хотя 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 в теме.

Между тем, STGMEDIUM struct also has one IUnknown pointer you are not releasing. Вам следует позвонить ReleaseStgMediumправильно распоряжаться всей структурой, включая этот указатель.

Я слишком устал, чтобы продолжать просматривать код прямо сейчас. Я вернусь завтра и попытаюсь найти другие возможные утечки. Тем временем вы проверяете документы MSDN для всех интерфейсов, структур и API, которые вы вызываете, и пытаетесь выяснить любые другие ссылки, которые вы, возможно, пропустили.

Я написал это для выпуска ком объектов:

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);
                }
            }
        }
    }

Вы передаете объекты, которые хотите освободить, например, в операторе finally, и он "освобождает все ссылки на Runtime Callable Wrapper (RCW), устанавливая его счетчик ссылок равным 0."

Это не подходит, если вы хотите освободить последнюю созданную ссылку, но сохранить ссылки, созданные ранее.

Это сработало для меня без утечек памяти.

Я знаю, что вопрос старый, но так как это вызвало у меня некоторые проблемы, я чувствую, что мне нужно поделиться тем, что сработало для меня.

Сначала я попытался использовать собственный ответ Бернарда Дарнтона:

var storagePointer = Marshal.GetIUnknownForObject(storage);
int refCount;
do
{
    refCount = Marshal.Release(storagePointer);
} while (refCount > 0);

Однако, хотя решение сначала сработало, оно привело к возникновению некоторых проблем с обеспечением.

Итак, следуя ответу Франци Пенова, я добавил следующее к коду:

            OLE32.ReleaseStgMedium(ref stgm);
            Marshal.Release(unknownForDataObj);
            Marshal.Release(unknownFromOle);
            Marshal.ReleaseComObject(storage);
Другие вопросы по тегам