Сборщик мусора собрал делегат который передавался в неуправляемый код, как этого не допустить?

80
12 октября 2021, 23:50

В общем, отлаживал библиотеку. решил поставить музыку которая идет в потоке на паузу, и отойти на минут 10. Пришел, и увидел нечто невероятное для меня... При продолжении воспроизведения, неуправляемая библиотека попыталась вызвать делегат, который я передавал ей, и при попытке продолжить воспроизведение потока, вылетело исключение о том, что неуправляемая библиотека попыталась вызвать делегат который был собран сборщиком мусора.

Как избежать сборки этого переданного делагата?

Код базового класса:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void FreeDelegate(IntPtr cObjectPtr);
internal abstract class CObject : ICObject
{
    private FreeDelegate _free;
    protected CObject(IntPtr cObjectPtr, FreeDelegate freeDelegate, Action afterObjectIsSet = null)
    {
        if (IntPtr.Zero == cObjectPtr)
        {
            throw new InvalidEnumArgumentException(nameof(cObjectPtr));
        }
        CObjectPtr = cObjectPtr;
        _free = freeDelegate ??
                throw new ArgumentNullException(nameof(freeDelegate), "freeDelegate cannot be NULL!");
        afterObjectIsSet?.Invoke();
        GC.KeepAlive(this);
    }
    public IntPtr CObjectPtr { get; protected set; }
    public bool IsDisposed { get; private set; }
    public bool IsInvalid => IntPtr.Zero == CObjectPtr;
    public static implicit operator IntPtr(CObject currentObject)
    {
        return currentObject.CObjectPtr;
    }
    #region IDisposable Support
    protected virtual void Dispose(bool disposing)
    {
        if (!IsDisposed)
        {
            _free?.Invoke(CObjectPtr);
            if (disposing)
            {
                _free = null;
                CObjectPtr = IntPtr.Zero;
            }
            IsDisposed = true;
        }
    }
    ~CObject()
    {
        Dispose(false);
#if DEBUG
        Debug.WriteLine("[{0}] -> ~CObject(), Collected by GC.", this);
#endif
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}

Как видно, в конструкторе вызывается KeepAlive.

Далее, представлю объект класса-наследника, метод которого передается в неуправляемый код:

[SuppressUnmanagedCodeSecurity]
internal sealed class Media : CObject, IMedia
{
    private readonly IntPtr _mediaEventManager;
    protected override void Dispose(bool disposing)
    {
        DetachEvents();
        NativeMethods.Media.Release(this);
        base.Dispose(disposing);
    }
    internal Media(CObject factory, string location, bool isLocal = false) :
        this(factory, location.ToCString(), isLocal)
    {
    }
    private Media(IntPtr instance, IntPtr pszMrl, bool isLocal) :
        this(NativeMethods.Media.New(instance, pszMrl, isLocal), () => pszMrl.FreeCString())
    {
    }
    internal Media(IntPtr mediaPtr, Action postAction = null) :
        base(mediaPtr, NativeMethods.Media.Release, postAction)
    {
        _mediaEventManager = NativeMethods.Media.GetEventManager(this);
        AttachEvents();
        NativeMethods.Media.Retain(this);
    }
    private unsafe void AttachEvents()
    {
        for (EventType i = EventType.MediaSubItemTreeAdded; i >= EventType.MediaMetaChanged; i--)
        {
            NativeMethods.EventManager.Attach(_mediaEventManager, i, EventHandler);
        }
    }
    private unsafe void DetachEvents()
    {
        for (EventType i = EventType.MediaSubItemTreeAdded; i >= EventType.MediaMetaChanged; i--)
        {
            NativeMethods.EventManager.Detach(_mediaEventManager, i, EventHandler);
        }
    }
    private unsafe void EventHandler(Event* e, IntPtr userData)
    {
        switch (e->Type)
        {
            case EventType.MediaMetaChanged:
                OnMetaChanged(new MediaMetaChangedEventArgs(e->Data.Media.MetaChanged.MetaType));
                break;
            case EventType.MediaSubItemAdded:
                OnSubItemAdded(new MediaSubItemAddedEventArgs(new Media(e->Data.Media.SubItemAdded.NewChild)));
                break;
            case EventType.MediaDurationChanged:
                OnDurationChanged(new MediaDurationChangedEventArgs(e->Data.Media.DurationChanged.NewDuration));
                break;
            case EventType.MediaParsedChanged:
                OnParsedChanged(new MediaParsedChangedEventArgs(e->Data.Media.ParsedChanged.NewStatus != 0));
                break;
            case EventType.MediaFreed:
                Console.WriteLine("Media {0} no more valid.", e->Data.Media.Freed.MediaPtr);
                break;
            case EventType.MediaStateChanged:
                OnStateChanged(new MediaStateChangedEventArgs(e->Data.Media.StateChanged.NewState));
                break;
            case EventType.MediaSubItemTreeAdded:
                OnSubItemTreeAdded(new MediaSubItemTreeAddedEventArgs(new Media(e->Data.Media.SubItemTreeAdded.NewItem)));
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
    public event EventHandler<MediaStateChangedEventArgs> StateChanged;
    public event EventHandler<MediaMetaChangedEventArgs> MetaChanged;
    public event EventHandler<MediaDurationChangedEventArgs> DurationChanged;
    public event EventHandler<MediaParsedChangedEventArgs> ParsedChanged;
    public event EventHandler<MediaSubItemAddedEventArgs> SubItemAdded;
    public event EventHandler<MediaSubItemTreeAddedEventArgs> SubItemTreeAdded;
    public Uri Location
    {
        get
        {
            unsafe
            {
                sbyte* mrl = NativeMethods.Media.GetMrl(this);
                if (null == mrl)
                {
                    return null;
                }
                sbyte* mrlCpy = mrl;
                int size = 0;
                while (*mrlCpy != 0)
                {
                    mrlCpy++;
                    size++;
                }
                byte[] bytes = new byte[size];
                Marshal.Copy((IntPtr) mrl, bytes, 0, size);
                return new Uri(Encoding.UTF8.GetString(bytes));
            }
        }
    }
    public State State => NativeMethods.Media.GetState(this);
    public void Parse()
    {
        NativeMethods.Media.Parse(this);
    }
    public void ParseAsync()
    {
        NativeMethods.Media.ParseAsync(this);
    }
    public bool GetStats(out IMediaStats stats)
    {
        // FIXME: here
        throw new NotImplementedException();
    }
    public TimeSpan Duration => TimeSpan.FromMilliseconds(NativeMethods.Media.GetDuration(this));
    public bool IsParsed => NativeMethods.Media.IsParsed(this) != 0;
    public ICollection<IVideoTrack> VideoTracks
    {
        get
        {
            unsafe
            {
                if (!IsParsed)
                {
                    Parse();
                }
                MediaTrack** tracks = null;
                uint count = NativeMethods.Media.GetTracks(this, &tracks);
                if (count == 0 || tracks == null)
                {
                    return Array.Empty<IVideoTrack>();
                }
                LinkedList<IVideoTrack> videoTracks = new LinkedList<IVideoTrack>();
                for (int i = 0; i < count; i++)
                {
                    //TODO: please... do not edit next line... it is not possible to get NullReferenceException when we get any objects from C...
                    IVideoTrack currentTrack = (*(tracks + i))->ToVideoTrack();
                    if (currentTrack is null)
                    {
                        continue;
                    }
                    videoTracks.AddLast(currentTrack);
                }
                NativeMethods.Media.TracksRelease(tracks, count);
                return videoTracks;
            }
        }
    }
    public ICollection<IAudioTrack> AudioTracks
    {
        get
        {
            unsafe
            {
                if (!IsParsed)
                {
                    Parse();
                }
                MediaTrack** tracks = null;
                uint count = NativeMethods.Media.GetTracks(this, &tracks);
                if (count == 0 || tracks == null)
                {
                    return Array.Empty<IAudioTrack>();
                }
                LinkedList<IAudioTrack> videoTracks = new LinkedList<IAudioTrack>();
                for (int i = 0; i < count; i++)
                {
                    //TODO: please... do not edit next line... it is not possible to get NullReferenceException when we get any objects from C...
                    IAudioTrack currentTrack = (*(tracks + i))->ToAudioTrack();
                    if (currentTrack is null)
                    {
                        continue;
                    }
                    videoTracks.AddLast(currentTrack);
                }
                NativeMethods.Media.TracksRelease(tracks, count);
                return videoTracks;
            }
        }
    }
    public ICollection<ISubtitleTrack> SubtitleTracks
    {
        get
        {
            unsafe
            {
                if (!IsParsed)
                {
                    Parse();
                }
                MediaTrack** tracks = null;
                uint count = NativeMethods.Media.GetTracks(this, &tracks);
                if (count == 0 || tracks == null)
                {
                    return Array.Empty<ISubtitleTrack>();
                }
                LinkedList<ISubtitleTrack> videoTracks = new LinkedList<ISubtitleTrack>();
                for (int i = 0; i < count; i++)
                {
                    //TODO: please... do not edit next line... it is not possible to get NullReferenceException when we get any objects from C...
                    ISubtitleTrack currentTrack = (*(tracks + i))->ToSubtitleTrack();
                    if (currentTrack is null)
                    {
                        continue;
                    }
                    videoTracks.AddLast(currentTrack);
                }
                NativeMethods.Media.TracksRelease(tracks, count);
                return videoTracks;
            }
        }
    }
    public IMedia Duplicate()
    {
        return new Media(NativeMethods.Media.Duplicate(this));
    }
    private void OnStateChanged(MediaStateChangedEventArgs e)
    {
        StateChanged?.Invoke(this, e);
    }
    private void OnMetaChanged(MediaMetaChangedEventArgs e)
    {
        MetaChanged?.Invoke(this, e);
    }
    private void OnDurationChanged(MediaDurationChangedEventArgs e)
    {
        DurationChanged?.Invoke(this, e);
    }
    private void OnParsedChanged(MediaParsedChangedEventArgs e)
    {
        ParsedChanged?.Invoke(this, e);
    }
    private void OnSubItemAdded(MediaSubItemAddedEventArgs e)
    {
        SubItemAdded?.Invoke(this, e);
    }
    private void OnSubItemTreeAdded(MediaSubItemTreeAddedEventArgs e)
    {
        SubItemTreeAdded?.Invoke(this, e);
    }
}

В классе Media, метод EventHandler передается в неуправляемый код (неявно преобразованный в делегат). При этом, сам объект этого класса, на момент исключения, собран не был, т.к. его состояние было валидным, плюс к тому же, в окне отладки не было сообщения об уничтожении какого либо из объектов посредством вызова финализатора...

Но делегат был уничтожен. Я не понимаю что происходит.

Answer 1

Нужно сохранять ссылку на сам делегат (а не только на объект, чей метод он вызывает) в течении всего времени, пока метод может быть вызван. То есть, этот делегат не должен быть простой локальной переменной того метода, в котором он передаётся в неуправляемый код. Это должна быть внешняя (по отношению к этому методу) переменная, например поле в каком-нибудь объекте, который гарантированно просуществует нужное время.

READ ALSO
Запуск службы через консоль

Запуск службы через консоль

Как средствами С# на Linux запустить консоль и выполнить в ней определённую команду (в моём случае это запуск/остановка/проверка статуса службы...

202
Неопределенный объект

Неопределенный объект

2 проекта, серверный и клиент, сервер ASPNET Core, Клиент- Angular 8

86
Вызов хранимой процедуры

Вызов хранимой процедуры

Есть код на VBТаблица выводит данные

168