Skip to content

Commit

Permalink
Only remove callback when a tag handle was succesfully created.
Browse files Browse the repository at this point in the history
  • Loading branch information
timyhac committed Jan 25, 2025
1 parent 426641d commit 752814b
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 60 deletions.
25 changes: 0 additions & 25 deletions src/libplctag.Tests/DisposeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,5 @@ public void Can_not_use_if_already_disposed()
Assert.Throws<ObjectDisposedException>(() => tag.GetStatus());
}

[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.")]
public void Finalizer_calls_destroy()
{

// See https://www.inversionofcontrol.co.uk/unit-testing-finalizers-in-csharp/


// Arrange
var nativeTag = new Mock<INative>();
Action dispose = () =>
{
// This will go out of scope after dispose() is executed, so the garbage collector will be able to call the finalizer
var tag = new Tag(nativeTag.Object);
tag.Initialize();
};

// Act
dispose();
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();

// Assert
nativeTag.Verify(m => m.plc_tag_destroy(It.IsAny<int>()), Times.Once);
}

}
}
79 changes: 44 additions & 35 deletions src/libplctag/Tag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ public sealed class Tag : IDisposable
private uint? _stringPadBytes;
private uint? _stringTotalLength;

public Tag()
public Tag() : this(new Native())
{
_native = new Native();
}

internal Tag(INative nativeMethods)
{
_native = nativeMethods;

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

~Tag()
Expand All @@ -79,9 +81,16 @@ internal Tag(INative nativeMethods)
}

/// <summary>
/// True if <see cref="Initialize"/> or <see cref="InitializeAsync"/> has been called.
/// True if <see cref="Initialize"/> or <see cref="InitializeAsync"/> has completed succesfully.
/// </summary>
public bool IsInitialized => _isInitialized;
public bool IsInitialized
{
get
{
ThrowIfAlreadyDisposed();
return _isInitialized;
}
}

/// <summary>
/// <list type="table">
Expand Down Expand Up @@ -604,7 +613,21 @@ public uint? StringTotalLength
set => SetField(ref _stringTotalLength, value);
}


/// <summary>
/// [OPTIONAL | MODBUS-SPECIFIC]
/// The Modbus specification allows devices to queue up to 16 requests at once.
/// </summary>
///
/// <remarks>
/// The default is 1 and the maximum is 16.
/// This allows the host to send multiple requests without waiting for the device to respond.
/// Not all devices support up to 16 requests in flight.
/// </remarks>
public uint? MaxRequestsInFlight
{
get => GetField(ref _maxRequestsInFlight);
set => SetField(ref _maxRequestsInFlight, value);
}


public void Dispose()
Expand All @@ -613,10 +636,11 @@ public void Dispose()
return;

if (_isInitialized)
RemoveEventsAndRemoveCallback();

var result = (Status)_native.plc_tag_destroy(nativeTagHandle);
ThrowIfStatusNotOk(result);
{
RemoveCallback();
_native.plc_tag_destroy(nativeTagHandle);
RemoveEvents();
}

_isDisposed = true;
}
Expand Down Expand Up @@ -654,7 +678,7 @@ public void Initialize()
var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, millisecondTimeout);
if (result < 0)
{
RemoveEventsAndRemoveCallback();
RemoveEvents();
throw new LibPlcTagException((Status)result);
}
else
Expand All @@ -666,22 +690,6 @@ public void Initialize()
_isInitialized = true;
}

/// <summary>
/// [OPTIONAL | MODBUS-SPECIFIC]
/// The Modbus specification allows devices to queue up to 16 requests at once.
/// </summary>
///
/// <remarks>
/// The default is 1 and the maximum is 16.
/// This allows the host to send multiple requests without waiting for the device to respond.
/// Not all devices support up to 16 requests in flight.
/// </remarks>
public uint? MaxRequestsInFlight
{
get => GetField(ref _maxRequestsInFlight);
set => SetField(ref _maxRequestsInFlight, value);
}

/// <summary>
/// Creates the underlying data structures and references required before tag operations.
/// </summary>
Expand All @@ -706,7 +714,8 @@ public async Task InitializeAsync(CancellationToken token = default)
if (createTasks.TryPop(out var createTask))
{
Abort();
RemoveEventsAndRemoveCallback();
RemoveCallback();
RemoveEvents();

if (token.IsCancellationRequested)
createTask.SetCanceled();
Expand All @@ -727,7 +736,7 @@ public async Task InitializeAsync(CancellationToken token = default)
// Something went wrong locally
if (result < 0)
{
RemoveEventsAndRemoveCallback();
RemoveEvents();
throw new LibPlcTagException((Status)result);
}
else
Expand All @@ -744,7 +753,8 @@ public async Task InitializeAsync(CancellationToken token = default)
// On error, tear down and throw
if(createTask.Task.Result != Status.Ok)
{
RemoveEventsAndRemoveCallback();
RemoveCallback();
RemoveEvents();
throw new LibPlcTagException(createTask.Task.Result);
}

Expand Down Expand Up @@ -1206,22 +1216,21 @@ void SetUpEvents()
WriteCompleted += WriteTaskCompleter;
Created += CreatedTaskCompleter;

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

}

void RemoveEventsAndRemoveCallback()
void RemoveEvents()
{

// Used to finalize the read/write task completion sources
ReadCompleted -= ReadTaskCompleter;
WriteCompleted -= WriteTaskCompleter;
Created -= CreatedTaskCompleter;
}

var callbackRemovalResult = (Status)_native.plc_tag_unregister_callback(nativeTagHandle);
ThrowIfStatusNotOk(callbackRemovalResult);

Status RemoveCallback()
{
return (Status)_native.plc_tag_unregister_callback(nativeTagHandle);
}

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

0 comments on commit 752814b

Please sign in to comment.