1

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.

Ozgur Saklanmaz
  • 528
  • 3
  • 17

1 Answers1

3
  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.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Thank you for the answer. I couldn't find the stop() function. Is there a field I should add? – Ozgur Saklanmaz Oct 12 '21 at 13:25
  • Its a typo, I have called it `StopTimer`. Corrected it now. (I have not tested this code) – Olivier Jacot-Descombes Oct 12 '21 at 13:28
  • I defined the timer as a global static variable but the problem is not solved. – Ozgur Saklanmaz Oct 12 '21 at 13:37
  • Make sure that the object where the Action lives also stays alive! It is what the exception message is telling you. – Olivier Jacot-Descombes Oct 12 '21 at 13:54
  • The object with the "Tick" function is the "WinMMWrapper" class. I have defined this class as static. Unfortunately, I don't understand how and why the object disappears. – Ozgur Saklanmaz Oct 12 '21 at 13:58
  • I am speaking of the delegate you pass to the constructor of the wrapper (`() => Console.WriteLine("Time : " + DateTime.Now.ToString("hh:mm:ss:fff")`` in the example. I don't know how your the real code is, but the class defining this action must stay alive in addition to the wrapper itself. – Olivier Jacot-Descombes Oct 12 '21 at 14:11
  • I edited the code exactly the same as above. I think something is not left alive in the WinMMWrapper class. But I couldn't find the exact reason. Debugging with Breakpoint doesn't give a good result either. – Ozgur Saklanmaz Oct 13 '21 at 05:18
  • The interesting part is; I added a few buttons etc to the interface. (The background codes are blank.) If I click on some points in the interface, the error time of the program is about 6-7 seconds. If you do not interfere with the interface, it crashes after 15 seconds. – Ozgur Saklanmaz Oct 13 '21 at 05:25
  • I did not understand the source of the problem. This example worked for me. Thank you for taking your time. https://stackoverflow.com/a/24843946/14875740 – Ozgur Saklanmaz Oct 13 '21 at 06:39