簡體   English   中英

在C#中加載COM對象拋出異常“無法將類型為'System .__ ComObject'的COM對象轉換為接口類型...”,但C ++或VB沒有

[英]Loading COM object in C# throws exception “Unable to cast COM object of type 'System.__ComObject' to interface type …”, but C++ or VB not

我需要在非托管C ++中使用COM服務器,在C#中使用COM客戶端。 我在C ++中找到了COM Hello World世界( http://antonio.cz/static/com/5.html )。 Page是捷克語。 COM服務器在IHello接口調用函數Print()之后顯示帶有文本“Hello world”的MessageBox。 源代碼在這里: http//antonio.cz/static/com/Hello.zip 該存檔包含C ++中COM服務器和COM客戶端的源代碼,它可以工作。

但我的C#COM客戶端不起作用。 它是一個C#控制台應用程序,引用了“Interop.Hello.dll”。 我使用命令制作interop dll:

tlbimp Hello.tlb /out:Interop.Hello.dll

C#代碼:

static void Main(string[] args)
{
    Interop.Hello.IHello Hello = new Interop.Hello.CHello();
    Hello.Print();
}

但是C#客戶端拋出異常:

Unable to cast COM object of type 'System.__ComObject' to interface type
'Interop.Hello.CHello'. This operation failed because the QueryInterface call on the
COM component for the interface with IID '{B58DF060-EAD9-11D7-BB81-000475BB5B75}' 
failed due to the following error: No such interface supported (Exception from 
HRESULT: 0x80004002 (E_NOINTERFACE)).

我也試過從Visual Basic加載COM服務器。 它有效。 我在VB中使用“Interop.Hello.dll”進行了控制台應用程序。

VB代碼:

Module Module1
    Sub Main()

        Dim ic As Interop.Hello.CHello

        ic = CreateObject("MyCorporation.Hello")
        ic.Print()

    End Sub
End Module

我從C#客戶端加載時調試了COM服務器。 QueryInterface()方法在變量“riid”中返回S_OK是IHello接口guid。

有什么想法為什么C#代碼不起作用?

沒有這樣的界面支持

錯誤消息不明確。 每個人都會認為這是他們不受支持界面,IHello就是你的情況。 但情況並非如此,錯誤信息並不能說清楚。 它是不受支持的IMarshal接口

COM負責.NET不支持的編程細節,它不會忽略線程。 眾所周知,線程很難正確,有很多代碼不是線程安全的。 .NET允許您在工作線程中使用此類代碼,並且不會反對您弄錯,通常會產生非常難以診斷的錯誤。 COM設計者最初認為線程太難以正確,應由聰明人照顧。 並構建在基礎結構中,以便在工作線程安全中使用非線程安全的代碼。 這很好用,它可以解決95%的典型線程問題。 然而,最后5%往往會給你一個相當重要的頭痛。 像這個。

與您的COM組件一樣,COM組件可以發布從注冊表中的線程使用是否安全。 注冊表值名稱為“ThreadingModel”。 一個非常常見的值,也就是缺失時的默認值,是“公寓”。 解釋公寓有點超出了這個答案的范圍,它的確意味着“我不是線程安全的”。 COM基礎結構確保對對象的任何調用都是從創建對象的同一線程中進行的,從而確保線程安全。

然而,這需要一些魔力。 將一個線程的調用編組到一個特定的其他線程是一件非常重要的事情。 .NET使用Dispatcher.BeginInvoke和Control.BeginInvoke等方法使其看起來很簡單,但它隱藏了一個相當大的代碼冰山,99%的水下代碼。 COM很難做到這一點,它缺少.NET功能,這使得它更容易實現,它不直接支持Reflection。

例如,在目標線程上構建堆棧幀以便可以進行調用。 這需要知道該方法的參數是什么樣的。 COM需要幫助,它不知道它們的樣子,因為它不能依賴於Reflection。 需要的是兩段代碼,稱為代理和存根。 代理確實知道參數是什么樣的,並將方法的參數序列化為RPC數據包。 該代碼由COM自動調用,使用的虛擬接口看起來與原始接口完全相同 ,但每個方法都進行代理調用。 在目標線程上,存根代碼接收RPC數據包,構建堆棧幀並進行調用。

這可能在.NET術語中都很常見,這正是.NET Remoting和WCF的工作方式。 除了.NET可以通過Reflection自動創建代理和存根。 在COM中,它們需要由您創建。 完成的兩種基本方法,一般方法是使用名為IDL的語言描述COM接口,並使用midl.exe工具進行編譯。 哪個可以從IDL中的接口描述自動生成代理和存根代碼。 或者有一種簡單的方法,當您的COM服務器將自身限制為自動化子集並且可以生成類型庫時可用。 在這種情況下,您可以使用內置於Windows中的代理/存根實現,它使用類型庫來確定參數的外觀。 這真的很像反射。 有了額外的步驟,必須在注冊表中注冊HKCR \\ Interfaces鍵,以便COM可以找到它需要的代碼。

那么異常消息的真正含義是COM無法找到編組調用的方法。 它在注冊表中查找並找不到代理/存根的注冊表項。 然后它問你的COM對象“你知道如何自己組織嗎?” 通過查詢IMarshal。 答案是否定的! 這就是它的結束,給你留下一個很難解釋的異常消息。 錯誤報告是COM的致命弱點。


接下來,我需要關注為什么 COM決定它應該封送你的COM服務器的調用,這是你不期望發生的事情。 調用COM對象的線程的一個基本要求是它需要告訴COM它為編組調用提供了什么樣的支持。 除了構建堆棧框架之外,第二件事情很難做,調用需要在一個非常特定的線程上進行,即創建COM對象的線程。 實現線程的代碼需要使這成為可能,這不是一件容易的事情。 它需要解決一般的生產者/消費者問題 ,這是軟件工程中的一般問題。 其中“producer”是進行調用的線程,“consumer”是創建對象的線程。

那么線程必須告訴COM的是“我實施了生產者/消費者問題的解決方案,繼續並隨意生產”。 大多數Windows程序員都知道這個問題的通用解決方案,它是GUI線程實現的“消息循環”。

你很早就告訴COM,每個進行COM調用的線程都必須調用CoInitializeEx()。 您可以指定兩個選項之一,您可以指定COINIT_APARTMENTTHREADED(也稱為STA)以保證為非線程安全的COM對象提供安全的主頁。 還有“公寓”這個詞。 或者你也可以指定COINIT_MULTITHREADED(又名MTA),基本上說,你什么都不做,以幫助COM,並把它留給了COM架構梳理出來。

.NET程序不直接調用CoInitializeEx(),CLR會為您調用。 它還需要知道您的線程是STA還是MTA。 您可以使用Main()方法的屬性為程序的主線程指定,指定[STAThread]或[MTAThread]。 MTA是默認設置,也是線程池線程的默認選項。 或者,當您創建自己的Thread時,可以通過調用Thread.SetApartmentState()來指定它。

MTA和一個非線程安全的COM對象的組合,或者換句話說“我什么都不幫助COM”場景是這里問題的一部分。 你強迫COM給對象一個安全的家。 COM基礎結構將自動創建一個新線程 ,一個STA線程。 它必須,沒有其他方法來確保對象的調用將是線程安全的,因為你選擇了不幫助。 因此,您對該對象進行的任何調用都將被編組。 這非常低效,創建自己的STA線程可以避免編組成本。 但最重要的是,COM將需要代理和存根來進行調用。 你沒有實現它們,所以這是一個kaboom。

這適用於您的C ++客戶端代碼,因為它可能稱為CoInitialize()。 選擇STA。 它在您的VB.NET代碼中工作,因為vb.net運行時支持自動選擇STA,這是該語言的典型代表,它會自動執行大量操作以幫助程序員陷入成功之中。

但這不是C#方式,它會自動完成很少的事情。 你得到了kaboom,因為你的Main()方法沒有[STAThread]屬性所以它默認為MTA。

但請注意,這實際上並不是技術上正確的解決方案。 當你向STA承諾時,你也必須履行這一承諾。 這說明你解決了生產者/消費者的問題。 這要求您在.NET中使用消息循環Application.Run()。 你沒有。

打破這種承諾可能會產生令人不快的后果。 COM將依賴您的承諾,並會在需要時嘗試編組呼叫,期望它能夠正常工作。 它不起作用,因為你沒有調用GetMessage(),所以不會在你的線程上調度調用。 你沒有消耗。 您可以通過調試器輕松看到這一點,線程將死鎖 ,調用永遠不會完成。 公寓線程COM服務器通常也很容易假設您的STA線程泵送消息循環並將使用它來實現自己的線程間編組,通常是通過從工作線程調用PostMessage()。 WebBrowser控件就是一個很好的例子。 PostMessage()消息不會出現在任何地方的副作用通常是組件不會引發事件或者不執行任務。 對於WebBrowser,您永遠不會獲得DocumentCompleted事件。

聽起來像你的COM服務器沒有做出這些假設,否則你不會在工作線程上進行調用。 或者你會注意到你的C ++或VB.NET客戶端代碼出現故障。 這是一個危險的假設,可以隨時發送,但你可能會僥幸逃脫。

正確的C#代碼:

 [STAThread]
 static void Main(string[] args)
 {
     Interop.Hello.IHello Hello = new Interop.Hello.CHello();
     Hello.Print();
 }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM