在 C# 中释放 OLE IStorage 文件句柄
我正在尝试使用此处描述的 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我在您的代码中发现至少有四个潜在的引用计数泄漏:
请注意,所有这些都是指向 COM 对象的指针。 COM 对象不会被 GC 收集,除非保存引用的 .Net 类型指向 RCW 包装器,并且会在其终结器中正确释放其引用计数。
IntPtr
不是这种类型,您的var
也是IntPtr
(来自Marshal.GetObjectForIUnknown
调用的返回类型),这样就得到了三个。您应该调用 所有
IntPtr
变量上的Marshal.Release
。我不确定
OLE32.IStorage
。这可能需要Marshal.Release
或Marshal.ReleaseComPointer
。更新:我刚刚注意到我至少错过了一次参考计数。
var
不是IntPtr
,它是一个IDataObject
。as
转换将执行隐式QueryInterface
并添加另一个引用计数。虽然GetObjectForIUnknown
返回一个 RCW,但这个 RCW 会被延迟,直到 GC 启动为止。您可能需要在using
块中执行此操作以激活IDisposable
在它上面。同时,
STGMEDIUM
结构体还有一个您未释放的IUnknown
指针。您应该调用ReleaseStgMedium
正确处理整个结构,包括该指针。我现在太累了,无法继续查看代码。我明天会回来尝试找到其他可能的引用计数泄漏。同时,您检查 MSDN 文档以了解您正在调用的所有接口、结构和 API,并尝试找出您可能错过的任何其他引用计数。
I see at least four potential refcount leaks in your code:
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 yourvar
also isIntPtr
(from the return type of theMarshal.GetObjectForIUnknown
call), so that makes three.You should call
Marshal.Release
on all yourIntPtr
variables.I am not sure about
OLE32.IStorage
. This one might need eitherMarshal.Release
orMarshal.ReleaseComPointer
.Update: I just noticed that I missed at least one ref count. The
var
is not anIntPtr
, it's anIDataObject
. Theas
cast will do an implicitQueryInterface
and add another ref count. AlthoughGetObjectForIUnknown
returns an RCW, this one is delayed until the GC kicks in. You might want to do this inusing
block to activate theIDisposable
on it.Meanwhile, the
STGMEDIUM
struct also has oneIUnknown
pointer you are not releasing. You should callReleaseStgMedium
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.
我找到了答案,而且非常简单。 (可能太简单了——感觉像是黑客攻击,但由于我对 COM 编程知之甚少,所以我就继续使用它。)
存储对象有多个引用,所以继续下去,直到它们全部消失:
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:
我知道这个问题很老了,但是由于这给我带来了一些麻烦,我觉得我需要分享对我有用的东西。
首先,我尝试使用伯纳德·达恩顿自己的答案:
然而,尽管该解决方案一开始有效,但最终还是导致了一些附带问题。
因此,根据 Franci Penov 的回答,我在代码中添加了以下内容:
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:
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:
我写了这个来释放 com 对象:
您传递要释放的对象,例如在 finally 语句中,它“通过将引用计数设置为 0 来释放对运行时可调用包装器 (RCW) 的所有引用”。
如果您想释放最后创建的引用但保留之前创建的引用,那么它不适合。
它对我有用,没有内存泄漏。
I have written this for releasing com objects:
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.
我遵循了您的代码,因为我不需要 emf 文件,所以我只是添加了 bin 创建,但我使用了一个新线程使其在服务器 IIS 中工作,如果您找到更好的解决方案,请分享。
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.