【问题标题】:Regfree COM event fails from other threadRegfree COM 事件从其他线程失败
【发布时间】:2014-04-29 07:58:07
【问题描述】:

我有一个 COM 可见的 .NET 类,它公开事件并在 VB6 中使用。在过去的几天里,我一直试图让它与 regfree COM 一起工作,但没有成功。

  • 当事件从原始线程触发时,VB6 事件在无注册模式下运行。
  • VB6 事件在注册类型库时从另一个线程触发时运行。 (regasm /tlb /codebase 后跟 regasm /codebase /unregister,后者不会注销 tlb)

在 regfree 模式下从另一个线程触发时,它会引发异常,因此永远不会执行 VB6 事件代码。

System.Reflection.TargetException: Object does not match target type.

   at System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters)
   at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Int32[] aWrapperTypes, MessageData& msgData)
   at Example.Vb6RegFreeCom.IExampleClassEvents.TestEvent()
   at Example.Vb6RegFreeCom.ExampleClass.OnTestEvent(Action func) in ExampleClass.cs:line 78

我可以想到两种情况:1)清单缺少与 tlb 注册相关的内容,或者 2)在创建新线程时丢失了激活上下文。不幸的是,我不知道如何找出是哪种情况,或者甚至可能是由其他原因引起的。

下面是一个显示我的问题的基本示例。

清单(VB6 可执行文件)

<?xml version="1.0" encoding="utf-8"?>
<assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity name="VB6COM" version="1.0.0.0" type="win32" />
  <dependency xmlns="urn:schemas-microsoft-com:asm.v2">
    <dependentAssembly codebase="Example.Vb6RegFreeCom.tlb">
      <assemblyIdentity name="Example.Vb6RegFreeCom" version="1.0.0.0" publicKeyToken="B5630FCEE39CF455" language="neutral" processorArchitecture="x86" />
    </dependentAssembly>
  </dependency>
</assembly>

清单(C# DLL)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity name="Example.Vb6RegFreeCom" version="1.0.0.0" publicKeyToken="b5630fcee39cf455" processorArchitecture="x86"></assemblyIdentity>
  <clrClass clsid="{8D51802D-0DAE-40F2-8559-7BF63C92E261}" progid="Example.Vb6RegFreeCom.ExampleClass" threadingModel="Both" name="Example.Vb6RegFreeCom.ExampleClass" runtimeVersion="v4.0.30319"></clrClass>
  <file name="Example.Vb6RegFreeCom.dll" hashalg="SHA1"></file>
  <!--
  <file name="Example.Vb6RegFreeCom.TLB">
    <typelib tlbid="{FABD4158-AFDB-4223-BB09-AB8B45E3816E}" version="1.0" flags="" helpdir="" />
  </file>
  -->
</assembly>

C#(平台目标:x86)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

using Timer = System.Threading.Timer;
using FormsTimer = System.Windows.Forms.Timer;

namespace Example.Vb6RegFreeCom {
    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [Guid("467EB602-B7C4-4752-824A-B1BC164C7962")]
    public interface IExampleClass {
        [DispId(1)] int Test(int mode);
    }

    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [Guid("2669EBDB-16D9-45C8-B0A3-ED2CEE26862C")]
    public interface IExampleClassEvents {
        [DispId(1)] void TestEvent();
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(IExampleClassEvents))]
    [Guid("8D51802D-0DAE-40F2-8559-7BF63C92E261")]
    public class ExampleClass: IExampleClass {
        public event Action TestEvent;

        public int Test(int mode) {
            var tempEvent = TestEvent;
            if (tempEvent == null) return -1;

            switch (mode) {
                case 0:
                    tempEvent();
                    break;
                case 1:
                    var staThread = new Thread(() => OnTestEvent(tempEvent) );

                    //if (!staThread.TrySetApartmentState(ApartmentState.STA)) MessageBox.Show("Failed to set STA thread.");

                    staThread.Start();
                    break;
                case 2:
                    var invoker = new Invoker();
                    var otherThread = new Thread(() => invoker.Invoke((Action)(() => OnTestEvent(tempEvent))));
                    otherThread.Start();
                    break;
                case 3:
                    var timer = new FormsTimer();
                    timer.Tick += (_1, _2) => { timer.Dispose(); OnTestEvent(tempEvent); };
                    timer.Interval = 100;
                    timer.Start();
                    break;
                default:
                    return -2;
            }

            return 1;
        }

        internal static void OnTestEvent(Action func) {
            try { func(); } catch (Exception err) { MessageBox.Show(err.ToString()); }
        }
    }

    internal class Invoker : Control {
        internal Invoker() {
            this.CreateHandle();
        }
    }
}

VB6

Option Explicit

Dim WithEvents DotNetObject As ExampleClass

Private Sub cmdImmediate_Click()
    CallDotNet 0
End Sub

Private Sub cmdOtherThread_Click()
    CallDotNet 1
End Sub

Private Sub cmdSameThread_Click()
    CallDotNet 2
End Sub

Private Sub Form_Load()
    Set DotNetObject = New ExampleClass
End Sub

Private Sub CallDotNet(TestMode As Long)
    Dim ReturnValue As Long
    ReturnValue = DotNetObject.Test(TestMode)

    If ReturnValue <> 1 Then MsgBox "Return value is " & ReturnValue
End Sub

Private Sub DotNetObject_TestEvent()
    MsgBox "Event was raised."
End Sub

【问题讨论】:

  • 似乎在注册 tlb 时工作。期望在DotNetObject_TestEvent 中的哪个线程上执行MsgBox? (它必须在创建 VB6 对象的 STA 上——从其他单元调用将涉及编组,这就是 STA 的重点)
  • @wqw:澄清一下,我的问题是在 regfree 模式下它会引发异常,因此无法到达消息框。我更喜欢使用 winforms 的自动编组而不是我的 quickfix。我会更新问题。
  • 是否可能需要类型库只是为了在线程之间编组?我好像记得如果你通过DCOM使用组件,你需要将类型库与客户端分发,否则DCOM层不知道如何进行编组。
  • @Herman:它似乎有效。在 Form_Load 中,您在主线程 (Set DotNetObject = New ExampleClass) 上创建 C# 对象,因此不使用封送处理,并且您不能从单独的线程引发事件。您必须在单独的线程上创建 coclass(New ExampleClass),将接口编组回来,然后下沉。
  • @wqw:如果tlb注册到现在,VB6事件实际上是一致执行的。 '似乎工作'你的意思是这是幸运的情况,但它是一个不正确的方法?我将尝试在另一个线程上创建对象。

标签: c# .net com vb6 regfreecom


【解决方案1】:

对于多线程,必须对调用进行编组。这需要由comInterfaceExternalProxyStubtypelib 元素提供的额外信息。我曾尝试过这些,但直到现在才找到合适的组合。

清单更改(C# DLL)

  <file name="Example.Vb6RegFreeCom.dll" hashalg="SHA1">
    <typelib tlbid="{FABD4158-AFDB-4223-BB09-AB8B45E3816E}" version="1.0" 
             flags="hasdiskimage" helpdir="" />
  </file>

  <comInterfaceExternalProxyStub name="IExampleClassEvents"
    iid="{2669EBDB-16D9-45C8-B0A3-ED2CEE26862C}"
    tlbid="{FABD4158-AFDB-4223-BB09-AB8B45E3816E}"
    proxyStubClsid32="{00020420-0000-0000-C000-000000000046}">
  </comInterfaceExternalProxyStub>
  <comInterfaceExternalProxyStub name="IExampleClass"
    iid="{467EB602-B7C4-4752-824A-B1BC164C7962}"
    tlbid="{FABD4158-AFDB-4223-BB09-AB8B45E3816E}"
    proxyStubClsid32="{00020420-0000-0000-C000-000000000046}">
  </comInterfaceExternalProxyStub>

一旦我走上了正确的轨道,我就发现了几个指向正确方向的指针。我遇到的最好的描述如下。在我的示例中,也使用了 IDispatch。

摘自“免注册激活 COM 组件:演练” http://msdn.microsoft.com/en-us/library/ms973913.aspx

这些元素提供了原本会出现在 注册表。 comInterfaceExternalProxyStub 元素提供 发生类型库编组的足够信息,它是 适用于从 IDispatch 派生的 COM 接口(其中 包括所有自动化接口)。在这些情况下,ole32.dll 提供 使用的外部代理存根(即,在 部件)。如果你的 COM 组件只实现 dispatch 或 dual 接口,那么这是你应该使用的元素。

【讨论】:

  • IExampleClassEvents是双接口还是分接口? VB6 只能接收源调度接口。请注意,具有不同 CLSID 的双重和调度接口有不同的代理/存根:PSOAInterface = {00020424-0000-0000-C000-000000000046} 用于双重和 PSDispatch = {00020420-0000-0000-C000-000000000046} 用于调度接口。您的示例正在使用第二个示例,因此 VB6 只能使用后期绑定的 IDispatch 调用来进行编组以在您的应用程序中工作。 (IExampleClass 最好使用 PSOAInterface)
  • 接口是 IDispatch (就像我的问题的例子一样)。 Regasm 也在注册表中使用了这个 CLSID。界面是很久以前做的,不记得为什么选择IDispatch了。也许 InteropForms Toolkit 就是这样做的,我以它为例。我的界面非常笨重,所以性能不是一个真正的问题,使用双/iunknown 是否还有其他好处?
  • 不,你已经得到了编组器的足够开销 :-)) Internet 充满了用于 regfree 编组(包括 CLSID)的代理/存根示例,但没有明确说明何时使用哪个。目前还不清楚有两个内置代理,GUID 也有细微差别(只有一个数字)。这些代理不兼容也不可互换,即您不能在分配接口上使用 PSOAInterface ——您最终会遇到运行时错误。我在这里发布此信息以供后代使用。
  • 是的,很好,当我查看 CLSID 指向的接口时,我不得不再看一遍。我的建议是 regasm /tlb 然后从注册表中获取值。
  • @herman:iDispatch 是 COM 的标准双接口(意思是 iUnknown 和 iDispatch)。 VB6 COM 需要双接口,因为如果你只使用iUnknown,你不能支持后期绑定,VB6 也不想大惊小怪。所以,你可能是对的; InteropForms Toolkit 做到了。有关 iDispatch 的更多信息,请参阅this
猜你喜欢
  • 2017-06-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-11
  • 2012-04-06
  • 1970-01-01
  • 2011-01-11
  • 1970-01-01
相关资源
最近更新 更多