Fehler 'CallbackOnCollectedDelegate wurde erkannt.'

  • VB.NET

Es gibt 13 Antworten in diesem Thema. Der letzte Beitrag () ist von Hacklschorsch.

    Fehler 'CallbackOnCollectedDelegate wurde erkannt.'

    Hi Leute,

    ich habe ein Programm zur Messdatenerfassung mit einem USB-Sensor erstellt. Den Code zum ansprechen des Sensors habe ich vom Hersteller verwendet und erweitert. Funktioniert so weit gut. Jetzt habe ich das Problem, daß bei Aus- und wieder Einstecken des Sensors manchmal dieser Fehler kommt:

    CallbackOnCollectedDelegate wurde erkannt.
    Message: Für den von der Garbage Collection gesammelten Delegaten vom Typ "GapController!GapController.Oak+tNotifyFct::Invoke" wurde ein Rückruf durchgeführt. Dies kann Anwendungsabstürze, Datenbeschädigung und -verlust zur Folge haben. Beim Übergeben von Delegaten an nicht verwalteten Code müssen die Delegaten von der verwalteten Anwendung beibehalten werden, bis sichergestellt ist, dass sie nie aufgerufen werden.

    Der Code beinhaltet einen Connect- und Disconnect-Event, ich habe den m.E. wichtigen Teil mal angehängt.

    Ähnliche Probleme wurden im Inet mit z.B. GC.keepalive() gelöst, ich bekomme das ganze aber nicht auf meinem Fall angepasst.

    Hat jemand eine Idee ?

    Gruß Georg


    VB.NET-Quellcode

    1. <StructLayoutAttribute(LayoutKind.Sequential, CharSet:=CharSet.Ansi)> _
    2. Public Structure tChannel
    3. <MarshalAsAttribute(UnmanagedType.I1)> Public IsSigned As Boolean
    4. Public BitSize As Byte
    5. Public UnitExponent As SByte
    6. Public Unit As UInteger
    7. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public UnitStr As String
    8. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public ChannelName As String
    9. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public UserChannelName As String
    10. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public UserChannelName_NV As String
    11. <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=64, ArraySubType:=UnmanagedType.I1)> Public RFU() As Byte
    12. End Structure
    13. '
    14. ' Declaration of the tOak sensor structure that is compatible with the C structure defined in the C header file Oak.h
    15. '
    16. <StructLayoutAttribute(LayoutKind.Sequential, CharSet:=CharSet.Ansi)> _
    17. Public Structure tOakSensor
    18. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=256)> Public DevicePath As String
    19. Public VID As UShort
    20. Public PID As UShort
    21. Public REV As UShort
    22. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public SN As String
    23. <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=64, ArraySubType:=UnmanagedType.U1)> Public RFU() As Byte
    24. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public DeviceName As String
    25. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public UserDeviceName As String
    26. <MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst:=24)> Public UserDeviceName_NV As String
    27. Public NumChannels As UShort
    28. <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=11)> Public Channel() As tChannel
    29. End Structure
    30. Public Delegate Sub tNotifyFct(ByRef Sensor As tOakSensor, ByVal lParam As Integer) ' Declaration of the delegate function that will be used as the 'function pointer' for registereing callback
    31. '
    32. ' Step 2: Declare of the function that are present in the OakLib, With their parameter 'translated' to VB data types
    33. '
    34. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Sub Oak_Initialize_a()
    35. End Sub
    36. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Sub Oak_Hotplug_Enable_a()
    37. End Sub
    38. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_Register_Callback_a(ByVal OnConnect As tNotifyFct, ByVal OnDisconnect As tNotifyFct, ByVal PID As UShort, ByVal REV As UShort, ByVal SN As String, ByVal DeviceName As String, ByVal ChannelName As String, ByVal UserDeviceName As String, ByVal UserChannelName As String, ByVal UserParam As Integer) As Boolean
    39. End Function
    40. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Sub Oak_DestroyNotifications_a()
    41. End Sub
    42. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Sub Oak_NotifyConnectedDevices_a()
    43. End Sub
    44. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_FindSensor_a(ByVal PID As UShort, ByVal REV As UShort, ByVal SN As String, ByVal DeviceName As String, ByVal ChannelName As String, ByVal UserDeviceName As String, ByVal UserChannelName As String, ByRef SensorFound As tOakSensor) As Boolean
    45. End Function
    46. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_Feature_a(ByVal DevicePath As String, ByVal RptBuffer() As Byte, ByVal ExpectResult As Boolean) As Boolean
    47. End Function
    48. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_GetInReport_a(ByVal DevicePath As String, ByVal RptBuffer() As Byte, ByVal InReportLength As Byte, ByVal Timeout_ms As UShort) As Boolean
    49. End Function
    50. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_GetCurrentInReport_a(ByVal DevicePath As String, ByVal RptBuffer() As Byte, ByVal InReportLength As Byte) As Boolean
    51. End Function
    52. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_SendOutReport_a(ByVal DevicePath As String, ByVal RptBuffer() As Byte, ByVal InReportLength As Byte, ByVal Timeout_ms As UShort) As Boolean
    53. End Function
    54. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Sub Oak_Cleanup_a()
    55. End Sub
    56. End Module
    57. Class Form1
    58. Dim MeinSensor As tOakSensor
    59. Public Sub ConnectCallback(ByRef Sensor As tOakSensor, ByVal lParam As Integer)
    60. SetSamplingParams(Sensor, 5)
    61. End Sub
    62. Public Sub DisconnectCallback(ByRef Sensor As tOakSensor, ByVal lParam As Integer)
    63. Console.WriteLine(vbCrLf & vbCrLf & "--> Disconnected: <--\n")
    64. End Sub
    Also beim Aufruf von

    VB.NET-Quellcode

    1. <DllImport("oaka.dll", CallingConvention:=CallingConvention.Cdecl)> Public Function Oak_Register_Callback_a(ByVal OnConnect As tNotifyFct, ByVal OnDisconnect As tNotifyFct, ByVal PID As UShort, ByVal REV As UShort, ByVal SN As String, ByVal DeviceName As String, ByVal ChannelName As String, ByVal UserDeviceName As String, ByVal UserChannelName As String, ByVal UserParam As Integer) As Boolean
    2. End Function

    wird ja eine Instanz des tNotifyFct-Delegaten übergeben. Bis der Delegat innerhalb der oaka.dll nicht mehr verwendet wird, solltest du den Delegaten in einer Variable ablegen:

    VB.NET-Quellcode

    1. Private _delegateInstance As tNotifyFct = AddressOf ...

    (ggf. auch als Shared)
    Die Variable enthält somit eine verwaltete Referenz auf den Delegaten. Die GC verwaltet Speicher selbständig und gibt ungenutzte Objekte mit der Zeit wieder frei. Das funktioniert aber nur innerhalb der verwalteten Grenzen. Die GC hat keine wirkliche Möglichkeit, Objekte über die Grenzen hinaus zu überprüfen, daher wird der Delegat irgendwann einfach freigegeben und verliert somit eben seine Gültigkeit.

    Gruß
    ~blaze~
    Die Delegateninstanz gehört eigentlich in ein GCHandle. Diese Datenstruktur verhindert, dass der Delegat vom GC eingesammelt wird, während noch ein p/invoke aktiv ist. Beispiel:

    VB.NET-Quellcode

    1. Dim gch As GCHandle = GCHandle.Alloc(New tNotifyFct(AddressOf ...))
    2. Oak_Register_Callback_a(DirectCast(gch.Target, tNotifyFct), ...)
    3. gch.Free()


    Edit: 3. Codezeile angefügt.
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „hal2000“ ()

    @Hal2000:

    hey super, das klappt ! Interssanterweise wird der Disconnectcallback nicht aufgerufen, aber der ist erstmal nicht so wichtig.
    Hautpsache das Programm stürzt nicht ab...

    Vielen Dank ! (Auch Dir ~blaze~, Deinen Vorschlag habe ich leider nicht zum laufen bekommen (kann aber an mir liegen ;)

    Gruß Georg

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Hacklschorsch“ ()

    Ist das wirklich nötig? Damit muss man ja die Instanz im GC quasi direkt verwalten, wenn man's per Variable löst, hat man das Problem nicht. Freigeben muss man das Handle ja dann trotzdem quasi, sobald man es nicht mehr benötigt. Hier wäre es eben, sobald kein Verweis mehr auf die deklarierende Instanz existiert.

    Gruß
    ~blaze~
    @Hacklschorsch:
    Achtung: In meinem Code fehlte das gch.Free() - danke an blaze.
    Ich sehe auch grade, dass du eine Rückrufmethode verwendest. Wenn jetzt das gch.Free() drinsteht, tritt das Problem wieder auf, wenn das Callback asynchron zurückkommt (also der Code schon über das .Free() drübergelaufen ist). Es kommt auch darauf an, wie häufig das Callback auftritt: Bei mehr als 1x muss das GCHandle global deklariert werden, womit wir bei blaze's Lösung wären. Im MSDN-Beispiel wird eine andere Ressource mit dem GCHandle umschlossen (und der Delegat wird automatisch vor dem GC beschützt): msdn.microsoft.com/de-de/libra…chandle%28v=vs.80%29.aspx

    Wichtig ist also noch folgendes:
    - Kommt das Callback synchron oder asynchron?
    - Wie oft kommt das Callback nach der Registrierung?

    @~blaze~:
    Microsoft empfiehlt diese Vorgehensweise (irgendwo im Interop-Guide im MSDN). Das GCHandle muss so lange aufrechterhalten werden, wie der p/invoke dauert.
    Gruß
    hal2000
    Hi Hal2000,

    puh, ich muß zugeben, daß ich nicht viel von dem verstehe was hier passiert ;) (Bin ziemlich neu bei VB.net, vorher fast nur VBA gemacht).

    Hintergrund: Ich ziehe mit dem USB-Sensor Messwerte und speichere diese in einem Datagridview. Die Messung selbst besteht aus mehreren Einzelmessungen, sicherheitshalber schreibe ich nach jeder Einzelmessung den Inhalt des Datagridview in eine Excel-Datei weg (dort wird nachher auch weitergearbeitet).

    Bei der Messung könnte es nun vorkommen, daß die USB-Verbindung getrennt wird. Bisher ist mein Programm dann einfach abgestürzt. Wesentliche Daten gingen dabei Dank Sicherung nicht verloren, allerdings mußte man nachher die Daten wieder 'zusammensuchen'.

    Daher wollte ich das soweit bringen, das eine USB -Unterbrechung das Programm nicht aus dem Tritt bringt - Dank Eurer HIlfe scheint es zu klappen !

    Der Sensor war übrigens der Grund für meinen Einstieg in VB.Net - es gab nur Beispiel-Code für die .Net- Sprachen :)

    Erst fand ich das ärgerlich (neu lernen...), nun bin ich begeistert von VB.Net, das Projekt ist super gut gelaufen, bis auch Kleinigkeiten wie dieses...

    Gruß Georg
    Ich formuliere die Frage mal um - du rufst Oak_Register_Callback_a() auf und übergibst eine Delegateninstanz. Was passiert danach? Wird das Ziel (in AddressOf angegeben) nur einmal aufgerufen oder kann das mehrfach auftreten? Pseudocode:

    Quellcode

    1. Sub TuWas()
    2. ...code1...
    3. Oak_Register_Callback_a()
    4. ...code2...
    5. End Sub
    6. Sub Callback()
    7. ...code3...
    8. End Sub

    Wie ist der Ablauf? So (synchron):
    code1 - register - code3 (ein- oder mehrfach) - code2

    oder so (asynchron):
    code1 - register - code2 - (parallel dazu) code3
    ?

    Den Ablauf kannst du verfolgen, indem du Haltepunkte setzt (das solltest du aus VBA schon wissen). Wenn nicht: Auf den linken Rand des Editors klicken und einen "braunen Punkt" vor und nach "register" und in das Callback setzen. Poste bitte auch die Thread-IDs (die stehen oben beim Debugging), wenn der Debugger im Callback und VOR "register" anhält.
    Gruß
    hal2000
    Hi Hal2000,

    die Struktur ist bei mir anders: Den Register-Teil habe ich im Form_Load event, der ja nur einmal aufgerufen wird. Wie Du sehen kannst, habe ich ein 2. GCHandle (gch2) für 'Disconnect' erstellt (war das richtig/falsch/unnötig?).

    VB.NET-Quellcode

    1. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    2. ....
    3. Oak_Register_Callback_a(DirectCast(gch.Target, tNotifyFct), DirectCast(gch2.Target, tNotifyFct), 0, 0, "", "", "", "", "", 0)
    4. ' Dann das connection event einmal anstoßen, um den bereits angeschlossenen Sensor zu initialisieren
    5. Oak_NotifyConnectedDevices_a() ' Ask the already connected devices to generate a connection event
    6. ...
    7. End Sub


    Die GCHandle's habe ich global deklariert:

    VB.NET-Quellcode

    1. Dim gch As GCHandle = GCHandle.Alloc(New tNotifyFct(AddressOf ConnectCallback))
    2. Dim gch2 As GCHandle = GCHandle.Alloc(New tNotifyFct(AddressOf DisconnectCallback))


    Ging ja soweit, aber nachdem das gch.free() und gch2.free() reinsetze, kommt wieder der Fehler. Muß ich das gch.free() verwenden ?

    Ich will nur, daß das Programm weiterlebt, nachdem die USB-Verbindung getrennt und wieder angeschlossen wird.

    Danke, Gruß Georg

    Hacklschorsch schrieb:

    ein 2. GCHandle (gch2) für 'Disconnect'
    Das ist korrekt. Jedes Callback (jede Delegateninstanz) braucht ein eigenes GCHandle.

    Hacklschorsch schrieb:

    Muß ich das gch.free() verwenden ?
    Ja, unbedingt. Ansonsten hast du zwei Referenzen, die vom GC nie wieder eingesammelt werden können, d.h. ein Memory Leak. Du darfst Free() erst aufrufen, wenn du sicher sein kannst, dass der Inhalt nicht mehr benötigt wird. In deinem Fall sollte das so aussehen:

    VB.NET-Quellcode

    1. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    2. Oak_Register_Callback_a(DirectCast(gch.Target, tNotifyFct), DirectCast(gch2.Target, tNotifyFct), 0, 0, "", "", "", "", "", 0)
    3. ' Dann das connection event einmal anstoßen, um den bereits angeschlossenen Sensor zu initialisieren
    4. Oak_NotifyConnectedDevices_a() ' Ask the already connected devices to generate a connection event
    5. End Sub
    6. Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles Me.FormClosing
    7. Oak_DestroyNotifications()
    8. gch.Free()
    9. gch2.Free()
    10. End Sub
    11. 'Connect-Handler
    12. 'Disconnect-Handler

    Jetzt werden die GCHandles korrekt freigegeben. Beim Programmende wird zwar eh alles automatisch freigegeben, aber es kann ja auch sein, dass "Form1" nicht das einzige Fenster der Anwendung ist, und dann hast du ein Problem. Ich habe mal die Funktion gegoogelt und ein Oak_DestroyNotifications() in Oak.h gefunden - nachdem du diese Funktion aufgerufen hast, darfst und musst du auch die GCHandles freigeben.
    Gruß
    hal2000