Skip to content

Commit 0fdbc61

Browse files
authored
Only remove callback when a tag handle was succesfully created. (#433)
* Only remove callback when a tag handle was succesfully created. * Add asserts to catch errors in `Dispose()` during development * Adjust phrasing and location of Dispose() comments Signed-off-by: timyhac <timyhac@gmail.com> --------- Signed-off-by: timyhac <timyhac@gmail.com>
1 parent 386a09e commit 0fdbc61

2 files changed

Lines changed: 50 additions & 59 deletions

File tree

src/libplctag.Tests/DisposeTests.cs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -88,30 +88,5 @@ public void Can_not_use_if_already_disposed()
8888
Assert.Throws<ObjectDisposedException>(() => tag.GetStatus());
8989
}
9090

91-
[Fact(Skip = "The finalizer is no longer called because the Mock is holding a reference to a callback defined on the Tag. This would not happen outside of unit tests.")]
92-
public void Finalizer_calls_destroy()
93-
{
94-
95-
// See https://www.inversionofcontrol.co.uk/unit-testing-finalizers-in-csharp/
96-
97-
98-
// Arrange
99-
var nativeTag = new Mock<INative>();
100-
Action dispose = () =>
101-
{
102-
// This will go out of scope after dispose() is executed, so the garbage collector will be able to call the finalizer
103-
var tag = new Tag(nativeTag.Object);
104-
tag.Initialize();
105-
};
106-
107-
// Act
108-
dispose();
109-
GC.Collect(0, GCCollectionMode.Forced);
110-
GC.WaitForPendingFinalizers();
111-
112-
// Assert
113-
nativeTag.Verify(m => m.plc_tag_destroy(It.IsAny<int>()), Times.Once);
114-
}
115-
11691
}
11792
}

src/libplctag/Tag.cs

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System;
99
using System.Collections.Concurrent;
1010
using System.Collections.Generic;
11+
using System.Diagnostics;
1112
using System.Linq;
1213
using System.Runtime.CompilerServices;
1314
using System.Text;
@@ -63,14 +64,16 @@ public sealed class Tag : IDisposable
6364
private uint? _stringPadBytes;
6465
private uint? _stringTotalLength;
6566

66-
public Tag()
67+
public Tag() : this(new Native())
6768
{
68-
_native = new Native();
6969
}
7070

7171
internal Tag(INative nativeMethods)
7272
{
7373
_native = nativeMethods;
74+
75+
// Need to keep a reference to the delegate in memory so it doesn't get garbage collected
76+
coreLibCallbackFuncExDelegate = new callback_func_ex(coreLibEventCallback);
7477
}
7578

7679
~Tag()
@@ -79,9 +82,16 @@ internal Tag(INative nativeMethods)
7982
}
8083

8184
/// <summary>
82-
/// True if <see cref="Initialize"/> or <see cref="InitializeAsync"/> has been called.
85+
/// True if <see cref="Initialize"/> or <see cref="InitializeAsync"/> has completed succesfully.
8386
/// </summary>
84-
public bool IsInitialized => _isInitialized;
87+
public bool IsInitialized
88+
{
89+
get
90+
{
91+
ThrowIfAlreadyDisposed();
92+
return _isInitialized;
93+
}
94+
}
8595

8696
/// <summary>
8797
/// <list type="table">
@@ -604,7 +614,21 @@ public uint? StringTotalLength
604614
set => SetField(ref _stringTotalLength, value);
605615
}
606616

607-
617+
/// <summary>
618+
/// [OPTIONAL | MODBUS-SPECIFIC]
619+
/// The Modbus specification allows devices to queue up to 16 requests at once.
620+
/// </summary>
621+
///
622+
/// <remarks>
623+
/// The default is 1 and the maximum is 16.
624+
/// This allows the host to send multiple requests without waiting for the device to respond.
625+
/// Not all devices support up to 16 requests in flight.
626+
/// </remarks>
627+
public uint? MaxRequestsInFlight
628+
{
629+
get => GetField(ref _maxRequestsInFlight);
630+
set => SetField(ref _maxRequestsInFlight, value);
631+
}
608632

609633

610634
public void Dispose()
@@ -613,10 +637,17 @@ public void Dispose()
613637
return;
614638

615639
if (_isInitialized)
616-
RemoveEventsAndRemoveCallback();
640+
{
641+
// These should always succeed unless bugs exist in this wrapper or the core library
642+
var removeCallbackResult = RemoveCallback();
643+
var destroyResult = (Status)_native.plc_tag_destroy(nativeTagHandle);
617644

618-
var result = (Status)_native.plc_tag_destroy(nativeTagHandle);
619-
ThrowIfStatusNotOk(result);
645+
// However, we cannot recover if they do fail, so ignore except during development
646+
Debug.Assert(removeCallbackResult == Status.Ok);
647+
Debug.Assert(destroyResult == Status.Ok);
648+
649+
RemoveEvents();
650+
}
620651

621652
_isDisposed = true;
622653
}
@@ -654,7 +685,7 @@ public void Initialize()
654685
var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, millisecondTimeout);
655686
if (result < 0)
656687
{
657-
RemoveEventsAndRemoveCallback();
688+
RemoveEvents();
658689
throw new LibPlcTagException((Status)result);
659690
}
660691
else
@@ -666,22 +697,6 @@ public void Initialize()
666697
_isInitialized = true;
667698
}
668699

669-
/// <summary>
670-
/// [OPTIONAL | MODBUS-SPECIFIC]
671-
/// The Modbus specification allows devices to queue up to 16 requests at once.
672-
/// </summary>
673-
///
674-
/// <remarks>
675-
/// The default is 1 and the maximum is 16.
676-
/// This allows the host to send multiple requests without waiting for the device to respond.
677-
/// Not all devices support up to 16 requests in flight.
678-
/// </remarks>
679-
public uint? MaxRequestsInFlight
680-
{
681-
get => GetField(ref _maxRequestsInFlight);
682-
set => SetField(ref _maxRequestsInFlight, value);
683-
}
684-
685700
/// <summary>
686701
/// Creates the underlying data structures and references required before tag operations.
687702
/// </summary>
@@ -706,7 +721,8 @@ public async Task InitializeAsync(CancellationToken token = default)
706721
if (createTasks.TryPop(out var createTask))
707722
{
708723
Abort();
709-
RemoveEventsAndRemoveCallback();
724+
RemoveCallback();
725+
RemoveEvents();
710726

711727
if (token.IsCancellationRequested)
712728
createTask.SetCanceled();
@@ -727,7 +743,7 @@ public async Task InitializeAsync(CancellationToken token = default)
727743
// Something went wrong locally
728744
if (result < 0)
729745
{
730-
RemoveEventsAndRemoveCallback();
746+
RemoveEvents();
731747
throw new LibPlcTagException((Status)result);
732748
}
733749
else
@@ -744,7 +760,8 @@ public async Task InitializeAsync(CancellationToken token = default)
744760
// On error, tear down and throw
745761
if(createTask.Task.Result != Status.Ok)
746762
{
747-
RemoveEventsAndRemoveCallback();
763+
RemoveCallback();
764+
RemoveEvents();
748765
throw new LibPlcTagException(createTask.Task.Result);
749766
}
750767

@@ -1206,22 +1223,21 @@ void SetUpEvents()
12061223
WriteCompleted += WriteTaskCompleter;
12071224
Created += CreatedTaskCompleter;
12081225

1209-
// Need to keep a reference to the delegate in memory so it doesn't get garbage collected
1210-
coreLibCallbackFuncExDelegate = new callback_func_ex(coreLibEventCallback);
12111226

12121227
}
12131228

1214-
void RemoveEventsAndRemoveCallback()
1229+
void RemoveEvents()
12151230
{
12161231

12171232
// Used to finalize the read/write task completion sources
12181233
ReadCompleted -= ReadTaskCompleter;
12191234
WriteCompleted -= WriteTaskCompleter;
12201235
Created -= CreatedTaskCompleter;
1236+
}
12211237

1222-
var callbackRemovalResult = (Status)_native.plc_tag_unregister_callback(nativeTagHandle);
1223-
ThrowIfStatusNotOk(callbackRemovalResult);
1224-
1238+
Status RemoveCallback()
1239+
{
1240+
return (Status)_native.plc_tag_unregister_callback(nativeTagHandle);
12251241
}
12261242

12271243
private readonly ConcurrentStack<TaskCompletionSource<Status>> createTasks = new ConcurrentStack<TaskCompletionSource<Status>>();

0 commit comments

Comments
 (0)