Regfree COM event fails from other thread
Asked Answered
D

1

12

I have a COM visible .NET class which exposes events and is used from VB6. For the last couple of days I have been trying to get this to work with regfree COM, but without success.

  • The VB6 event runs in regfree mode when the event is fired from the original thread.
  • The VB6 event runs when fired from another thread when the typelib is registered. (regasm /tlb /codebase followed by regasm /codebase /unregister, the latter does not unregister the tlb)

When firing from another thread in regfree mode it throws an exception, thus the VB6 event code is never executed.

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

There are two scenarios I can think of: 1) the manifest is missing something related to the tlb registration, or 2) the activation context is lost when creating the new thread. Unfortunately, I don't know how to find out which is the case, or maybe it is even caused by something else.

Below is a basic example showing my problem.

Manifest (VB6 executable)

<?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>

Manifest (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# (platform target: 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
Duumvirate answered 28/4, 2014 at 12:29 Comment(10)
It seems to work when the tlb is registered. Which thread do expect MsgBox in DotNetObject_TestEvent to be executed on? (It has to be on the STA the VB6 object is created on -- invocation from other apartments will involve marshaling and that's the whole point of STA in first place)Undoubted
@wqw: To clarify, my problem is that in regfree mode it throws an exception thus the messagebox is not reached. I would prefer the automatic marshalling over my quickfix using winforms. I'll update the question.Duumvirate
Is it possible that the type library is required just for the purposes of marshalling between threads? I seem to remember that if you use a component via DCOM, you need to distribute the type library with the client, otherwise the DCOM layer doesn't know how to do the marshalling.Sonnysonobuoy
@Herman: It seems to work. In Form_Load you create your C# object on main thread (Set DotNetObject = New ExampleClass) so no marshaling is used and you cannot raise events from a separate thread. You have to create the coclass (the New ExampleClass) on a separate thread, marshal the interface back, then sink it.Undoubted
@wqw: The VB6 event is actually executed consistently if the tlb is registered up to now. With 'seems to work' do you mean that this is lucky circumstance but it is an incorrect method? I will try creating the object on another thread.Duumvirate
@MarkBertenshaw: The TLB file is present in the same directory, but it fails when linked through manifest files instead of through the registry.Duumvirate
Is the TLB file registered? If not, it will not be found AFAIK.Sonnysonobuoy
@MarkBertenshaw: I am looking for a way to use manifest files for the registration (regfree COM). Generally it works, only not with events fired from another thread.Duumvirate
@wqw: I think it is actually marshalling, the VB6 event is handled in the main thread when fired from another thread. Creating the class on another thread is not working since .NET will always convert it back to a managed object, and I don't know how to convert the pointer to an object on the VB side.Duumvirate
Thanks for your help, I've fixed it and added the answer on SO. @wqw: the marshalling was one of the puzzle pieces I was missing :)Duumvirate
D
11

With multi-threading the calls have to be marshalled. This requires extra information, which is provided by the comInterfaceExternalProxyStub and typelib element. I had experimented with those, but did not find the right combination until now.

Manifest changes (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>

Once I was on the right track I found several pointers into the right direction. The best description I came across is below. In my example also IDispatch was used.

Excerpt from "Registration-Free Activation of COM Components: A Walkthrough" http://msdn.microsoft.com/en-us/library/ms973913.aspx

These elements provide information that would otherwise be present in the registry. The comInterfaceExternalProxyStub element provides enough information for type library marshalling to occur and it is appropriate for COM interfaces that derive from IDispatch (which includes all Automation interfaces). In these cases ole32.dll provides the external proxy-stub used (i.e., external to the files in the assembly). If your COM components implement only dispatch or dual interfaces then this is the element you should use.

Duumvirate answered 28/4, 2014 at 23:58 Comment(5)
Is IExampleClassEvents dual interface or dispinterface? VB6 can sink source dispinterfaces only. Notice that there are different proxy/stubs for dual and dispinterface with different CLSIDs: PSOAInterface = {00020424-0000-0000-C000-000000000046} for dual and PSDispatch = {00020420-0000-0000-C000-000000000046} for dispinterfaces. Your sample is using the second one, so VB6 can only use late-bound IDispatch calls for marshaling to work in your app. (Better use PSOAInterface for IExampleClass)Undoubted
The interface is IDispatch (like in the example of my question). Regasm also used this CLSID in the registry. The interface was made a long time ago and I don't remember why I chose IDispatch. Perhaps the InteropForms Toolkit did it that way, I used that as example. My interface is very chunky so performance is not a real issue, is there another benefit of using dual/iunknown?Duumvirate
No, you get enough overhead by the marshaller already :-)) Internet is full of proxy/stubs samples for regfree marshaling (including CLSIDs) but no clear description which one when to use. It's not immediately clear there are two built-in proxies, there is a subtle difference in the GUIDs too (only a single digit). These proxies are not compatible nor interchangable i.e. you can't use PSOAInterface on dispinterfaces -- you'll eventually get a run-time error. I post this info here for posterity.Undoubted
Yeah good point, I had to take a second look when I was looking which interface the CLSID pointed to. My advice would be to regasm /tlb and then take the value from the registry.Duumvirate
@herman: iDispatch is the standard dual interface for COM (meaning, iUnknown and iDispatch). VB6 COM requires a dual interface, because if you only use iUnknown, you can't support late binding, and VB6 doesn't want to fuss with that. So, you're probably right; the InteropForms Toolkit did it. See this for more info on iDispatch.Factitive

© 2022 - 2024 — McMap. All rights reserved.