在 C# 中释放 OLE IStorage 文件句柄

发布于 2024-08-29 23:40:52 字数 2629 浏览 8 评论 0原文

我正在尝试使用此处描述的 OLE 技术将 PDF 文件嵌入到 Word 文档中: https://learn.microsoft.com/en-us/archive/blogs/brian_jones/embedding-any-file-type-like-pdf-in-an-open-xml-file

我我已经尝试实现 C# 中提供的 C++ 代码,以便整个项目都集中在一个地方,并且除了一个障碍之外几乎就在那里。当我尝试将生成的 OLE 对象二进制数据输入 Word 文档时,出现 IOException。

IOException:进程无法访问文件“C:\Wherever\Whatever.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 对我来说是一门外语。)

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

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.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

柳若烟 2024-09-05 23:40:52

我在您的代码中发现至少有四个潜在的引用计数泄漏:

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 对象不会被 GC 收集,除非保存引用的 .Net 类型指向 RCW 包装器,并且会在其终结器中正确释放其引用计数。

IntPtr 不是这种类型,您的 var 也是 IntPtr (来自 Marshal.GetObjectForIUnknown 调用的返回类型),这样就得到了三个。

您应该调用 所有 IntPtr 变量上的 Marshal.Release

我不确定OLE32.IStorage。这可能需要 Marshal.ReleaseMarshal.ReleaseComPointer

更新:我刚刚注意到我至少错过了一次参考计数。 var 不是 IntPtr,它是一个 IDataObjectas 转换将执行隐式 QueryInterface 并添加另一个引用计数。虽然 GetObjectForIUnknown 返回一个 RCW,但这个 RCW 会被延迟,直到 GC 启动为止。您可能需要在 using 块中执行此操作以激活 IDisposable在它上面。

同时,STGMEDIUM 结构体还有一个您未释放的 IUnknown 指针。您应该调用 ReleaseStgMedium 正确处理整个结构,包括该指针。

我现在太累了,无法继续查看代码。我明天会回来尝试找到其他可能的引用计数泄漏。同时,您检查 MSDN 文档以了解您正在调用的所有接口、结构和 API,并尝试找出您可能错过的任何其他引用计数。

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.

作死小能手 2024-09-05 23:40:52

我找到了答案,而且非常简单。 (可能太简单了——感觉像是黑客攻击,但由于我对 COM 编程知之甚少,所以我就继续使用它。)

存储对象有多个引用,所以继续下去,直到它们全部消失:

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

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);
彩扇题诗 2024-09-05 23:40:52

我知道这个问题很老了,但是由于这给我带来了一些麻烦,我觉得我需要分享对我有用的东西。

首先,我尝试使用伯纳德·达恩顿自己的答案:

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

然而,尽管该解决方案一开始有效,但最终还是导致了一些附带问题。

因此,根据 Franci Penov 的回答,我在代码中添加了以下内容:

            OLE32.ReleaseStgMedium(ref stgm);
            Marshal.Release(unknownForDataObj);
            Marshal.Release(unknownFromOle);
            Marshal.ReleaseComObject(storage);

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);
〆凄凉。 2024-09-05 23:40:52

我写了这个来释放 com 对象:

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 语句中,它“通过将引用计数设置为 0 来释放对运行时可调用包装器 (RCW) 的所有引用”。

如果您想释放最后创建的引用但保留之前创建的引用,那么它不适合。

它对我有用,没有内存泄漏。

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.

自我难过 2024-09-05 23:40:52

我遵循了您的代码,因为我不需要 emf 文件,所以我只是添加了 bin 创建,但我使用了一个新线程使其在服务器 IIS 中工作,如果您找到更好的解决方案,请分享。

public class OLEHelper
{
    private void DoIt(string fin, string fout)
    {
        var result = OLE32.CoInitialize(IntPtr.Zero);
        OLE32.OleInitialize(IntPtr.Zero);

        OLE32.IStorage storage;
        result = OLE32.StgCreateStorageEx(
            fout,
            (int)(OLE32.STGM.STGM_READWRITE |
             OLE32.STGM.STGM_SHARE_EXCLUSIVE | 
             OLE32.STGM.STGM_CREATE | 
             OLE32.STGM.STGM_TRANSACTED),
            (int)OLE32.STGFMT.STGFMT_DOCFILE,
            0,
            IntPtr.Zero,
            IntPtr.Zero,
            ref OLE32.IID_IStorage,
            out storage
        );
        if (result != 0)
        {
            throw new Exception($"OLE32.StgCreateStorageEx {result}");
        }

        var CLSID_NULL = Guid.Empty;

        OLE32.IOleObject pOle;            
        result = OLE32.OleCreateFromFile(
            ref CLSID_NULL,
            fin,
            ref OLE32.IID_IOleObject,
            (int)OLE32.OLERENDER.OLERENDER_NONE,
            IntPtr.Zero,
            null,
            storage,
            out pOle
        );
        if (result != 0)
        {
            throw new Exception($"OLE32.OleCreateFromFile {result}");
        }
        
        result = OLE32.OleRun(pOle);
        if (result != 0)
        {
            throw new Exception($"OLE32.OleRun {result}");
        }
        storage.Commit((int)OLE32.STGC.DEFAULT);
        pOle.Close(0);

        int cnt = Marshal.ReleaseComObject(storage);
        cnt = Marshal.ReleaseComObject(pOle);
        
        OLE32.CoUninitialize();
    }

    public void ExportOleFile(string inputFileName, string oleOutputFileName)
    {
        Thread thread = new Thread(() => {
            DoIt(inputFileName, oleOutputFileName);
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
    }
}

I followed your code and since I do not need the emf files I just added the bin creation but I used a new thread to make it work in the server IIS, if you find a better solution please share.

public class OLEHelper
{
    private void DoIt(string fin, string fout)
    {
        var result = OLE32.CoInitialize(IntPtr.Zero);
        OLE32.OleInitialize(IntPtr.Zero);

        OLE32.IStorage storage;
        result = OLE32.StgCreateStorageEx(
            fout,
            (int)(OLE32.STGM.STGM_READWRITE |
             OLE32.STGM.STGM_SHARE_EXCLUSIVE | 
             OLE32.STGM.STGM_CREATE | 
             OLE32.STGM.STGM_TRANSACTED),
            (int)OLE32.STGFMT.STGFMT_DOCFILE,
            0,
            IntPtr.Zero,
            IntPtr.Zero,
            ref OLE32.IID_IStorage,
            out storage
        );
        if (result != 0)
        {
            throw new Exception(
quot;OLE32.StgCreateStorageEx {result}");
        }

        var CLSID_NULL = Guid.Empty;

        OLE32.IOleObject pOle;            
        result = OLE32.OleCreateFromFile(
            ref CLSID_NULL,
            fin,
            ref OLE32.IID_IOleObject,
            (int)OLE32.OLERENDER.OLERENDER_NONE,
            IntPtr.Zero,
            null,
            storage,
            out pOle
        );
        if (result != 0)
        {
            throw new Exception(
quot;OLE32.OleCreateFromFile {result}");
        }
        
        result = OLE32.OleRun(pOle);
        if (result != 0)
        {
            throw new Exception(
quot;OLE32.OleRun {result}");
        }
        storage.Commit((int)OLE32.STGC.DEFAULT);
        pOle.Close(0);

        int cnt = Marshal.ReleaseComObject(storage);
        cnt = Marshal.ReleaseComObject(pOle);
        
        OLE32.CoUninitialize();
    }

    public void ExportOleFile(string inputFileName, string oleOutputFileName)
    {
        Thread thread = new Thread(() => {
            DoIt(inputFileName, oleOutputFileName);
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文