Search code examples
c#timerwinmm

How can I avoid crashing when creating a precision timer with WinMM.dll?


I'm trying to create a precision timer. I found an example created with WinMM.dll. The sample works really fine. But it crashes with the first garbage collector.

How can I prevent the garbage collector from blocking the timer?

public class WinMMWrapper : IDisposable
{
    [DllImport("WinMM.dll", SetLastError = true)]
    public static extern uint timeSetEvent(int msDelay, int msResolution,
        TimerEventHandler handler, ref int userCtx, int eventType);

    [DllImport("Winmm.dll", CharSet = CharSet.Auto)]  // <=== ADD THIS
    static extern uint timeKillEvent(uint uTimerID);  // <=== ADD THIS

    public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx,
        int rsv1, int rsv2);

    public enum TimerEventType
    {
        OneTime = 0,
        Repeating = 1,
    }

    private readonly Action _elapsedAction;
    private readonly int _elapsedMs;
    private readonly int _resolutionMs;
    private readonly TimerEventType _timerEventType;
    private uint _timerId;   // <=== ADD THIS
    private bool _disposed;   // <=== ADD THIS

    public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
    {
        _elapsedMs = elapsedMs;
        _resolutionMs = resolutionMs;
        _timerEventType = timerEventType;
        _elapsedAction = elapsedAction;
    }

    public bool StartElapsedTimer()   // <=== RETURN bool
    {
        StopTimer(); // Stop any started timer

        int myData = 1;

        // === SET _timerId
        _timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, new TimerEventHandler(TickHandler), ref myData, (int)_timerEventType);
        return _timerId != 0;
    }

    public void StopTimer()  // <=== ADD THIS
    {
        if (_timerId != 0)
        {
            timeKillEvent(_timerId);
            _timerId = 0;
        }
    }

    private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
    {
        _elapsedAction();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
            StopTimer();

        _disposed = true;
    }

    ~WinMMWrapper()
    {
        Dispose(false);
    }
}    

My Static Class

public static class Global
{
    public static WinMMWrapper timer;
}

Create WinMMWrapper

 private void TimerStart_Click(object sender, RoutedEventArgs e)
    {

        Global.timer = new WinMMWrapper(1, 1, WinMMWrapper.TimerEventType.Repeating, Tick);

        Global.timer.StartElapsedTimer();
    }

Tick Function

private static void Tick()
    {
        Console.WriteLine("Time : " + DateTime.Now.ToString("hh:mm:ss:ffff"));
    }

Error Message

Managed Debugging Assistant 'CallbackOnCollectedDelegate' : A callback was made on the garbage-collected delegate of type 'CanBusRandomDataGenerator!CanBusRandomDataGenerator.WinMMWrapper+TimerEventHandler::Invoke'. This can cause app crashes, corruption, and data loss. When delegating to unmanaged code, it must be kept alive by the managed application until it is guaranteed that the delegates will never be called.'

The code is now exactly the same. It works for about 2 3 seconds, then it crashes to the following error. Error occurs within WinMMWrapper function without falling into Dispose.


Solution

    1. You must keep the timer variable alive as long as you are using the timer. If it is a local variable, it will be reclaimed by the GC when you leave the method. Do so by converting this local variable to a class field (possibly static). In a Console application you can still use a local variable, but you must add a Console.ReadKey(); to prevent the application to exit prematurely.

      Also, stop the timer before this variable becomes eligible for garbage collection. To do so, let WinMMWrapper implement IDisposable.

    2. Make sure that the object where the callback Action lives stays alive and is not disposed! Probably this is the object where you call new WinMMWrapper(..., theAction).

    public class WinMMWrapper : IDisposable
    {
        [DllImport("WinMM.dll", SetLastError = true)]
        public static extern uint timeSetEvent(int msDelay, int msResolution,
            TimerEventHandler handler, ref int userCtx, int eventType);
    
        [DllImport("Winmm.dll", CharSet = CharSet.Auto)]  // <=== ADD THIS
        static extern uint timeKillEvent(uint uTimerID);  // <=== ADD THIS
    
        public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx,
            int rsv1, int rsv2);
    
        public enum TimerEventType
        {
            OneTime = 0,
            Repeating = 1,
        }
    
        private readonly Action _elapsedAction;
        private readonly int _elapsedMs;
        private readonly int _resolutionMs;
        private readonly TimerEventType _timerEventType;
        private iuint _timerId;   // <=== ADD THIS
        private bool _disposed;   // <=== ADD THIS
    
        public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
        {
            _elapsedMs = elapsedMs;
            _resolutionMs = resolutionMs;
            _timerEventType = timerEventType;
            _elapsedAction = elapsedAction;
        }
    
        public bool StartElapsedTimer()   // <=== RETURN bool
        {
            Stop(); // Stop any started timer
    
            int myData = 1;
    
            // === SET _timerId
            _timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, new TimerEventHandler(TickHandler), ref myData, (int)_timerEventType);
            return _timerId != 0;
        }
    
        public void StopTimer()  // <=== ADD THIS
        {
            if (_timerId != 0)
            {
                timeKillEvent(_timerId);
                _timerId = 0;
            }
        }
    
        private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
        {
            _elapsedAction();
        }
    
        // === ADD Dispose and finalizer ===
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        private void Dispose(bool disposing)
        {
            if (!_disposed && disposing)
                StopTimer();
            }
            _disposed = true;
        }
    
        ~MMTimer()
        {
            Dispose(false);
        }
    }
    

    Then you can do this in a Console application:

    using (var timer = new WinMMWrapper(1, 1, WinMMWrapper.TimerEventType.Repeating,
        () => Console.WriteLine("Time : " + DateTime.Now.ToString("hh:mm:ss:fff"))) {
    
        Console.Writeline("Hit a key to stop the timer and quit the application!");
        Console.ReadKey();
    } // <= Here timer.Dispose() gets automatically called by using.
    

    If you cannot use a using statement because your timer will be stopped at another place in your code, you can also call timer.Dispose(); explicitly.

    To make this code thread-safe, enclose your start and stop timer code in a lock(this { ... } statement.