Search code examples
c#.netgarbage-collectioninteropmarshalling

Function works in .NET framework 2.0 but not in any other version


On .NET 5 (and .NET core 3 and 3.1) when debugging after around 5 seconds the code throws System.ExecutionEngineException which seems like it should never pop up as it is something obsolete as far as I understood from searching.

The same code on .NET Framework >2 (e.g 4.8 or 4.7.2) similarly works for around 5 seconds then throws the following exception:

Managed Debugging Assistant 'CallbackOnCollectedDelegate' : 'A callback was made on a garbage collected delegate of type 'SoundCheck!SoundCheck.CHCNetSDK+VOICEDATACALLBACKV30::Invoke'. This may cause application crashes, corruption and data loss. When passing delegates to unmanaged code, they must be kept alive by the managed application until it is guaranteed that they will never be called.'

But on .NET Framework 2 it works magically without any issues.

As far as I understand I should somehow make it so that garbage collection stops and does not collect this method. But I am unfamiliar with this. I do not know how I should approach this.

I am using the Hikvision SDK https://www.hikvision.com/en/support/download/sdk/

The code:

Dll import:

public delegate void VOICEDATACALLBACKV30(int lVoiceComHandle, IntPtr pRecvDataBuffer, uint dwBufSize, byte byAudioFlag, System.IntPtr pUser);

[DllImport(@"..\bin\HCNetSDK.dll")]
public static extern int NET_DVR_StartVoiceCom_V30(int lUserID, uint dwVoiceChan, bool bNeedCBNoEncData, VOICEDATACALLBACKV30 fVoiceDataCallBack, IntPtr pUser);

Start button:

   private void btnVioceTalk_Click(object sender, EventArgs e)
        {
            if (m_bTalk == false)
            {
               
                CHCNetSDK.VOICEDATACALLBACKV30 VoiceData = new CHCNetSDK.VOICEDATACALLBACKV30(VoiceDataCallBack);

                lVoiceComHandle = CHCNetSDK.NET_DVR_StartVoiceCom_V30(m_lUserID, 1, true, VoiceData, IntPtr.Zero);
                

                if (lVoiceComHandle < 0)
                {
                    iLastErr = CHCNetSDK.NET_DVR_GetLastError();
                    str = "NET_DVR_StartVoiceCom_V30 failed, error code= " + iLastErr;
                    MessageBox.Show(str);
                    return;
                }
                else
                {
                    btnVioceTalk.Text = "Stop Talk";
                    m_bTalk = true;
                }
            }
            else
            {
               
                if (!CHCNetSDK.NET_DVR_StopVoiceCom(lVoiceComHandle))
                {
                    iLastErr = CHCNetSDK.NET_DVR_GetLastError();
                    str = "NET_DVR_StopVoiceCom failed, error code= " + iLastErr;
                    MessageBox.Show(str);
                    return;
                }
                else
                {
                    btnVioceTalk.Text = "Start Talk";
                    m_bTalk = false;
                }
            }
        }

Callback function:

   public void VoiceDataCallBack(int lVoiceComHandle, IntPtr pRecvDataBuffer, uint dwBufSize, byte byAudioFlag, System.IntPtr pUser)
    {
        byte[] sString = new byte[dwBufSize];
        Marshal.Copy(pRecvDataBuffer, sString, 0, (Int32)dwBufSize);

        if (byAudioFlag ==0)
        {
         
            string str = "sound1.pcm";
            FileStream fs = new FileStream(str, FileMode.Create);
            int iLen = (int)dwBufSize;
            fs.Write(sString, 0, iLen);
            fs.Close();
        }
        if (byAudioFlag == 1)
        {
            
            string str = "sound2.pcm";
            FileStream fs = new FileStream(str, FileMode.Create);
            int iLen = (int)dwBufSize;
            fs.Write(sString, 0, iLen);
            fs.Close();
        }

    }

Solution

  • From what I understood you declare a delegate type VOICEDATACALLBACKV30 and you have a method implementing that signature. This is method VoiceDataCallBack
    In the line CHCNetSDK.VOICEDATACALLBACKV30 VoiceData = new CHCNetSDK.VOICEDATACALLBACKV30(VoiceDataCallBack); you instantiate that callback inside an eventhandler, so it's managed memory allocation "lives" on the stack, for a short time.
    Then you pass it to some SDK function.
    Seems the unmanaged SDK functions keeps on working with that delegate, for an obvoiusy longer time and hold an unmanaged reference/pointer to that delegate
    . But your .NET code has already garbage collected it after a short while, because it is no longer referenced in managed code.
    So when the SDK invokes the callback the first time after its managed memory has been collected, it's crashing.

    So need to keep a reference in managed code, by simply assigning it to a field, in order to keep it alive on the heap. So it is not GC'ed.

     //declare field
     private CHCNetSDK.VOICEDATACALLBACKV30 _voiceData;
     .... 
     //inside 'btnVioceTalk_Click' event handler
     if (_voiceData == null) {
        _voiceData = new CHCNetSDK.VOICEDATACALLBACKV30(VoiceDataCallBack);
     }
    

    Likely .NET Framework 2 had another GC implementation/algorithm.