如何正确释放暴露给COM的.NET对象?

时间:2017-05-22 23:00:06

标签: c# .net com vb6 com-interop

所以我试图将一个.NET对象暴露给COM,以便在我们的一个旧版VB6应用程序中使用。我相信我已经正确设置了.NET类,因为我现在可以在VB6中使用它,但是当我的VB6应用程序关闭时,我不认为我发布了对COM对象的所有引用/崩溃。这是一个非常事件驱动的.NET对象,我们一直在看到奇怪的问题,我们的.NET对象的先前实例化会收到一个事件,用于更新实例化所述对象。

为了测试这个,我在我的.NET对象中添加了跟踪编写器。这些跟踪打印出本地秒表的Stopwatch.ElapsedMilliseconds属性,用于跟踪.NET对象的生命周期,以及在.NET对象的每个实例化时新创建的GUID。 / p>

此对象的第一个实例化(通过VB6)如下所示:

  

0>>>实例化电话<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

90>>>已输入的AgentLogin<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

90>>>创建CTISoftphone<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

103>>>订阅PhoneEvent<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

106>>>订阅OnCreateCtiConf<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

106>>>等待任务结果。 <<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

1206>>>收到的事件:OnCreateCTIConf:没有消息<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

1206>>> OnCreateCtiConf不是null,触发事件。 <<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

1211>>>调用LoginAgent<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

1219>>>取消订阅OnCreateCtiConf<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5

然后,任何后续尝试都可以正常工作,或无限期挂起。如果我们对后续无限期挂起的尝试遵循相同的跟踪,则输出如下:

  

0>>>已输入的AgentLogin<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc

     

0>>>创建CTISoftphone<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc

     

4>>>订阅PhoneEvent<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc

     

4>>>订阅OnCreateCtiConf<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc

     

5>>>等待任务结果。 <<< 8c820520-06d4-49e3-ba41-13d0638bfbfc

     

70855>>>收到的事件:OnCreateCTIConf:没有消息<<<的 7afc50d3-c33d-4c39-a84c-132feeefefa5

     

70855>>> OnCreateCtiConf为null,无法触发事件。 <<<的 7afc50d3-c33d-4c39-a84c-132feeefefa5

如果您在第二个输出中注意到,我们希望接收的事件正在被我们的.NET对象的前一个实例捕获!

我确保在关机时尽快将我们的.NET对象设置为VB6中的Nothing,但它似乎无法解决问题。我会说它确实似乎有点缓解,因为我们从50/50的成功/失败变成了更像75/25的东西,但这可能是我的确认偏见,所以我无法肯定地说

在阅读有关.NET / COM互操作(特别是this文章)时,我看到每种类型的对象只有一个CCW,无论有多少实例可能存在,并且参考计数器得到维护在CCW上。所以在这一点上我想知道GC是否缺少以前的实例作为准备收集,因为我们很快就会通过CCW得到我们的.NET对象的另一个实例(例如应用程序崩溃,然后立即重新打开)因此ref计数器不是0,并且由于GC只看到CCW,它不知道它背后有多个实例?我不确定这是否有意义。

我不可能是唯一一个需要确定性地发布暴露给COM的.NET对象的人,那么其他人如何做呢?

如果需要更多示例/澄清,请告诉我,我很乐意提供!

谢谢,

修改

找到一些有趣的东西。我不认为对象本身是未被收集的,但不知何故,这个问题的事件的事件处理程序是。使用WinDbg,在有此问题的测试中,我为此对象转储了堆,并找到以下内容:

0:003> !dumpheap -stat -type MyApp.ClickToDial.Phone
Statistics:
      MT    Count    TotalSize Class Name
038492c4        1           24 MyApp.ClickToDial.Phone+<>c__DisplayClass77_0
03848b8c        1           32 MyApp.ClickToDial.Phone+OnCallReleasedHandler
03848aa8        1           32 MyApp.ClickToDial.Phone+OnCallTransferredHandler
038488e0        1           32 MyApp.ClickToDial.Phone+OnLoginAgentConfHandler
038487fc        1           32 MyApp.ClickToDial.Phone+OnLogoutAgentConfHandler
03848718        1           32 MyApp.ClickToDial.Phone+OnSendDtmfConfHandler
03848634        1           32 MyApp.ClickToDial.Phone+OnGetCtiDataConfHandler
03848550        1           32 MyApp.ClickToDial.Phone+OnInitiateConferenceConfHandler
0384846c        1           32 MyApp.ClickToDial.Phone+OnCancelConferenceConfHandler
03848388        1           32 MyApp.ClickToDial.Phone+OnMakeCallConfHandler
038482a4        1           32 MyApp.ClickToDial.Phone+OnCompleteConferenceConfHandler
038481c0        1           32 MyApp.ClickToDial.Phone+OnCallConferencedHandler
038480dc        1           32 MyApp.ClickToDial.Phone+OnErrorHandler
03847ff8        1           32 MyApp.ClickToDial.Phone+OnCallDroppedHandler
03847f14        1           32 MyApp.ClickToDial.Phone+OnCallEstablishedHandler
038489c4        3           96 MyApp.ClickToDial.Phone+OnCreateCtiConfHandler
03844ac4        1          116 MyApp.ClickToDial.Phone
Total 19 objects

如果你查看MyApp.ClickToDial.Phone+OnCreateCtiConfHandler的项目数(此问题的中心位置),这恰好是我遇到此问题之前运行的测试次数。

我真的不确定这是怎么回事。以下是我对这一切的实施​​,也许更有经验的人可以看到这个问题?

这是我们实施的入口。你先打电话之前再打电话给别人。我们需要先收到OnCreateCtiConf事件才能致电LoginAgent。我们必须在此方法中实例化基础CTISoftphone,而不是我们类的构造函数,因为CTISoftphone构造函数需要代理扩展,我们在没有这些扩展时没有实例化我们的类,因此需要等待这个事件。

public bool AgentLogin(string extension, string agentId, string password)
{
    writeTrace($"Entered {nameof(AgentLogin)}");
    _extension = extension;
    _agentId = agentId;
    try
    {
        writeTrace($"Creating {nameof(CTISoftphone)}");
        _phone = new CTISoftphone($"ext={extension},logFile={Settings.LogLocation}");
    }
    catch (Exception ex)
    {
        writeTrace($"Caught exception: {ex.GetType()} | {ex.Message}");
        //this one might get lost since we haven't subscribed to CTISoftphone.PhoneEvent yet.
        OnError?.Invoke($"{_wrapperLogPrefix} Unable to create CTISoftPhone. The error was: {ex.Message}");
        return false;
    }

    writeTrace($"Subscribing to {nameof(CTISoftphone.PhoneEvent)}");
    _phoneEventHandler = new CTISoftphone.PhoneEventHandler(cti_phone_event);
    _phone.PhoneEvent += _phoneEventHandler;

    var are = new AutoResetEvent(false);
    var task = new Task(() =>
    {
        try
        {
            writeTrace("Calling LoginAgent");
            _phone.LoginAgent(agentId, password);
        }
        catch (Exception)
        {
        }
    });
    task.ContinueWith((t) => are.Set());

    var task_handler = new OnCreateCtiConfHandler(task.Start);

    writeTrace("Subscribing to OnCreateCtiConf");
    OnCreateCtiConf += task_handler;
    writeTrace("Waiting for task result");
    are.WaitOne();
    writeTrace("Unsubscribing from OnCreateCtiConf");
    OnCreateCtiConf -= task_handler;

    return true;
}

您会注意到,实际上只有一个来自CTISoftphone的活动,我们在上面订阅了这个活动。此事件PhoneEvent传递一个枚举,其中包含正在发送的事件类型,我们在cti_phone_event方法中本地处理它,该方法位于下方(OnCreateCtiConf事件具有不同的调试语法目的)。

private void cti_phone_event(CTISoftphone.CTIEvent type, string info)
{
    writeTrace($"Received event: {type} : {(string.IsNullOrEmpty(info) ? "No message" : info)}");
    switch (type)
    {
        case CTISoftphone.CTIEvent.OnMakeCallConf:
            OnMakeCallConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCompleteConferenceConf:
            OnCompleteConferenceConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCancelConferenceConf:
            OnCancelConferenceConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCallEstablished:
            OnCallEstablished?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCallDropped:
            OnCallDropped?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCallConferenced:
            OnCallConferenced?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnError:
            OnError?.Invoke(info);
            break;
        case CTISoftphone.CTIEvent.OnInitiateConferenceConf:
            OnInitiateConferenceConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnGetCTIDataConf:
            OnGetCtiDataConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnSendDTMFConf:
            OnSendDtmfConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnLogoutAgentConf:
            OnLogoutAgentConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnLoginAgentConf:
            OnLoginAgentConf?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCreateCTIConf:
            if (OnCreateCtiConf != null)
            {
                writeTrace("OnCreateCtiConf was not null, firing event");
                OnCreateCtiConf();
            }
            else
            {
                writeTrace("OnCreateCtiConf was null, CANNOT fire event");
            }
            break;
        case CTISoftphone.CTIEvent.OnReleaseCallConf:
            OnCallReleased?.Invoke();
            break;
        case CTISoftphone.CTIEvent.OnCallTransferred:
            OnCallTransferred?.Invoke();
            break;
        default:
            OnError?.Invoke($"{_wrapperLogPrefix} Received unknown CTI event type: {type}");
            break;
    }
}

最后,我确实实施了IDisposable来按照建议手动取消订阅事件(尽管这似乎没有什么区别)。以下是DisposePhone析构函数实现。

public void Dispose()
{
    writeTrace("Dispose called");
    if (_phone != null)
        _phone.PhoneEvent -= _phoneEventHandler;
    _phone = null;
    _ctiData = null;

    OnCallEstablished = null;
    OnCallDropped = null;
    OnError = null;
    OnCallConferenced = null;
    OnCompleteConferenceConf = null;
    OnMakeCallConf = null;
    OnCancelConferenceConf = null;
    OnInitiateConferenceConf = null;
    OnGetCtiDataConf = null;
    OnSendDtmfConf = null;
    OnLogoutAgentConf = null;
    OnLoginAgentConf = null;
    OnCreateCtiConf = null;
    OnCallTransferred = null;
    OnCallReleased = null;
}

~Phone()
{
    writeTrace("Destructor called");
    Dispose();
}

所以不知怎的,似乎我的活动并没有像我期待的那样被取消订阅。怎么会这样?这里还有别的东西我不在吗?

另外,作为一个额外的检查,我转储了底层CTISoftphone类型的堆,以确保事件/对象计数在那里有意义,并且它们看起来(来自与上面的WinDbg输出相同的会话) :

0:003> !dumpheap -stat -type Avaya.APS.CTD.
Statistics:
      MT    Count    TotalSize Class Name
03846d64        1           32 Avaya.APS.CTD.CTISoftphone+PhoneEventHandler
03846bf0        1          108 Avaya.APS.CTD.CTISoftphone
Total 2 objects

思考?再次,在此先感谢!

0 个答案:

没有答案