mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
889572472f
Convert `DataValue` from a mutable reference type (`class`) to a `readonly struct` to eliminate per-allocation GC pressure on every attribute read, subscription publish, and history sample. ### Key API changes (user-visible) | Before | After | Pattern | |---|---|---| | `class DataValue` | `readonly struct DataValue : INullable` | Foundation | | `Equals(DataValue?)` / `operator ==(DataValue?, DataValue?)` | `Equals(DataValue)` / `operator ==(DataValue, DataValue)` | Value semantics | | `IEncoder.WriteDataValue(string?, DataValue?)` | `IEncoder.WriteDataValue(string?, DataValue)` | Non-nullable wire API | | `IDecoder.ReadDataValue(...) -> DataValue?` | `IDecoder.ReadDataValue(...) -> DataValue` | Same | | `IDataChangeMonitoredItemQueue.PeekLast/Oldest()` | `TryPeekLastValue/Oldest(out DataValue)` | TryGet pattern | | `IAggregateCalculator.GetProcessedValue(bool)` | `TryGetProcessedValue(bool, out DataValue)` | Same | | `IUaPubSubDataStore.ReadPublishedDataItem(...)` | `TryReadPublishedDataItem(NodeId, uint, out DataValue)` | Same | | `Dictionary<uint, DataValue?>` (NodeCacheContext) | `Dictionary<uint, DataValue>` | Default + IsNull | | `IHistoryDataSource.FirstRaw/NextRaw` | `TryFirstRaw/TryNextRaw(out DataValue)` | TryGet pattern | | `NodeState.ReadAttribute(... DataValue)` | `NodeState.ReadAttribute(... ref DataValue)` | Sync mutation via ref | | `NodeState.ReadAttributeAsync(... DataValue)` | `NodeState.ReadAttributeAsync(...) -> ValueTask<(ServiceResult, DataValue)>` | Async return tuple | | `obj as DataValue` (heterogeneous Dictionary<string, object>) | `obj is DataValue dv ? dv : default(DataValue)` | Safe struct cast | | `Assert.That(dv, Is.Not.Null)` (tests) | `Assert.That(dv.IsNull, Is.False)` | Struct test pattern | | `[Obsolete] Value` setter, all property setters | Removed; use ctor or `With*()` | Immutability | | `[DataContract]` + `[DataMember]` | Removed; surrogate via `SerializableDataValue` + `DataValueSurrogateProvider` | Serialization | ### Wire format Wire format is **byte-identical** to the previous class-based implementation across binary / JSON / XML encoders. Default DataValue (`.IsNull == true`) is treated as the equivalent of the old null reference (writes JSON `null`, binary `byte 0`, XML omitted element). ### Why TryGet pattern (instead of leaving `DataValue?`) For methods that previously returned `DataValue?` to signal "no value available" (queue peek, aggregate result, data store lookup, history navigation), the conversion to `bool TryGet(out DataValue)` matches BCL conventions (`Dictionary.TryGetValue`, `Queue.TryPeek`, `Int32.TryParse`) and gives the caller an explicit "may be absent" affordance without `Nullable<DataValue>` overhead. ### Breaking changes summary - `DataValue` is now a `readonly struct` - boxing/unboxing semantics change for callers using `object`-typed APIs. - `IEncoder.WriteDataValue` / `IDecoder.ReadDataValue` / `ReadDataValueArray` interface signatures lose the `Nullable` wrapper. - `IDataChangeMonitoredItemQueue.PeekLastValue` / `PeekOldestValue` -> `TryPeekLastValue` / `TryPeekOldestValue` (out parameter). - `IAggregateCalculator.GetProcessedValue` -> `TryGetProcessedValue` (out parameter). - `IUaPubSubDataStore.ReadPublishedDataItem` -> `TryReadPublishedDataItem` (out parameter). - `NodeState.ReadAttribute` takes `ref DataValue`; `ReadAttributeAsync` returns `ValueTask<(ServiceResult, DataValue)>`. - `DataValue` property setters removed (use constructor + `With*` mutators).
536 lines
19 KiB
C#
536 lines
19 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Sample;
|
|
using Opc.Ua.Server;
|
|
|
|
namespace MemoryBuffer
|
|
{
|
|
/// <summary>
|
|
/// The factory to create the node manager for memory buffers.
|
|
/// </summary>
|
|
public class MemoryBufferNodeManagerFactory : INodeManagerFactory
|
|
{
|
|
/// <inheritdoc/>
|
|
public INodeManager Create(IServerInternal server, ApplicationConfiguration configuration)
|
|
{
|
|
return new MemoryBufferNodeManager(server, configuration, NamespacesUris.ToArray()!);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ArrayOf<string> NamespacesUris
|
|
=> [Namespaces.MemoryBuffer, Namespaces.MemoryBuffer + "/Instance"];
|
|
}
|
|
|
|
/// <summary>
|
|
/// A node manager for a variety of memory buffers.
|
|
/// </summary>
|
|
public class MemoryBufferNodeManager : SampleNodeManager
|
|
{
|
|
/// <summary>
|
|
/// Initializes the node manager.
|
|
/// </summary>
|
|
public MemoryBufferNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
string[] namespaceUris)
|
|
: base(server)
|
|
{
|
|
NamespaceUris = namespaceUris;
|
|
|
|
// get the configuration for the node manager.
|
|
m_configuration =
|
|
configuration.ParseExtension<MemoryBufferConfiguration>() ??
|
|
new MemoryBufferConfiguration();
|
|
|
|
// use suitable defaults if no configuration exists.
|
|
|
|
m_buffers = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does any initialization required before the address space can be used.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The externalReferences is an out parameter that allows the node manager to link to nodes
|
|
/// in other node managers. For example, the 'Objects' node is managed by the CoreNodeManager and
|
|
/// should have a reference to the root folder node(s) exposed by this node manager.
|
|
/// </remarks>
|
|
public override void CreateAddressSpace(
|
|
IDictionary<NodeId, IList<IReference>> externalReferences)
|
|
{
|
|
lock (Lock)
|
|
{
|
|
base.CreateAddressSpace(externalReferences);
|
|
|
|
// create the nodes from configuration.
|
|
ushort namespaceIndex = Server.NamespaceUris
|
|
.GetIndexOrAppend(Namespaces.MemoryBuffer);
|
|
|
|
BaseInstanceState root = FindPredefinedNode<BaseInstanceState>(
|
|
new NodeId(Objects.MemoryBuffers, namespaceIndex))!;
|
|
|
|
// create the nodes from configuration.
|
|
namespaceIndex = Server.NamespaceUris
|
|
.GetIndexOrAppend(Namespaces.MemoryBuffer + "/Instance");
|
|
|
|
if (m_configuration != null && !m_configuration.Buffers.IsNull)
|
|
{
|
|
for (int ii = 0; ii < m_configuration.Buffers.Count; ii++)
|
|
{
|
|
MemoryBufferInstance instance = m_configuration.Buffers[ii]!;
|
|
|
|
// create a new buffer.
|
|
var bufferNode = new MemoryBufferState(SystemContext, instance);
|
|
|
|
// assign node ids.
|
|
bufferNode.Create(
|
|
SystemContext,
|
|
new NodeId(bufferNode.SymbolicName, namespaceIndex),
|
|
new QualifiedName(bufferNode.SymbolicName, namespaceIndex),
|
|
default,
|
|
true);
|
|
|
|
bufferNode.CreateBuffer(instance.DataType, instance.TagCount);
|
|
bufferNode.InitializeMonitoring(Server, this);
|
|
|
|
// link to root.
|
|
root.AddChild(bufferNode);
|
|
|
|
// save the buffers for easy look up later.
|
|
m_buffers[bufferNode.SymbolicName] = bufferNode;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
|
|
/// </summary>
|
|
protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
|
|
{
|
|
return new NodeStateCollection().AddMemoryBuffer(context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees any resources allocated for the address space.
|
|
/// </summary>
|
|
public override void DeleteAddressSpace()
|
|
{
|
|
lock (Lock)
|
|
{
|
|
base.DeleteAddressSpace();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a unique handle for the node.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This must efficiently determine whether the node belongs to the node manager. If it does belong to
|
|
/// NodeManager it should return a handle that does not require the NodeId to be validated again when
|
|
/// the handle is passed into other methods such as 'Read' or 'Write'.
|
|
/// </remarks>
|
|
protected override object? GetManagerHandle(
|
|
ISystemContext context,
|
|
NodeId nodeId,
|
|
IDictionary<NodeId, NodeState>? cache)
|
|
{
|
|
lock (Lock)
|
|
{
|
|
if (!IsNodeIdInNamespace(nodeId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (nodeId.TryGetValue(out string? id))
|
|
{
|
|
// check for a reference to the buffer.
|
|
|
|
if (m_buffers.TryGetValue(id!, out MemoryBufferState? buffer))
|
|
{
|
|
return buffer;
|
|
}
|
|
|
|
// tag ids have the syntax <bufferName>[<address>]
|
|
if (id![^1] != ']')
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int index = id.IndexOf('[', StringComparison.Ordinal);
|
|
|
|
if (index == -1)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string bufferName = id[..index];
|
|
|
|
// verify the buffer.
|
|
if (!m_buffers.TryGetValue(bufferName, out buffer))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// validate the address.
|
|
string offsetText = id.Substring(index + 1, id.Length - index - 2);
|
|
|
|
for (int ii = 0; ii < offsetText.Length; ii++)
|
|
{
|
|
if (!char.IsDigit(offsetText[ii]))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// check range on offset.
|
|
uint offset = Convert.ToUInt32(offsetText, CultureInfo.InvariantCulture);
|
|
|
|
if (offset >= buffer.SizeInBytes!.Value)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// the tags contain all of the metadata required to support the UA
|
|
// operations and pointers to functions in the buffer object that
|
|
// allow the value to be accessed. These tags are ephemeral and are
|
|
// discarded after the operation completes. This design pattern allows
|
|
// the server to expose potentially millions of UA nodes without
|
|
// creating millions of objects that reside in memory.
|
|
return new MemoryTagState(buffer, offset);
|
|
}
|
|
|
|
return base.GetManagerHandle(context, nodeId, cache);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new set of monitored items for a set of variables.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method only handles data change subscriptions. Event subscriptions are created by the SDK.
|
|
/// </remarks>
|
|
protected override ServiceResult CreateMonitoredItem(
|
|
ISystemContext context,
|
|
NodeState source,
|
|
uint subscriptionId,
|
|
double publishingInterval,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
MonitoredItemCreateRequest itemToCreate,
|
|
bool createDurable,
|
|
MonitoredItemIdFactory monitoredItemIdFactory,
|
|
out MonitoringFilterResult filterError,
|
|
out IMonitoredItem monitoredItem)
|
|
{
|
|
filterError = null!;
|
|
monitoredItem = null!;
|
|
|
|
// use default behavior for non-tag sources.
|
|
if (source is not MemoryTagState tag)
|
|
{
|
|
return base.CreateMonitoredItem(
|
|
context,
|
|
source,
|
|
subscriptionId,
|
|
publishingInterval,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
itemToCreate,
|
|
createDurable,
|
|
monitoredItemIdFactory,
|
|
out filterError,
|
|
out monitoredItem);
|
|
}
|
|
|
|
// validate parameters.
|
|
MonitoringParameters parameters = itemToCreate.RequestedParameters;
|
|
|
|
// no filters supported at this time.
|
|
var filter = (MonitoringFilter?)ExtensionObject.ToEncodeable(parameters.Filter);
|
|
|
|
if (filter != null)
|
|
{
|
|
return StatusCodes.BadFilterNotAllowed;
|
|
}
|
|
|
|
// index range not supported.
|
|
if (!itemToCreate.ItemToMonitor.ParsedIndexRange.IsNull)
|
|
{
|
|
return StatusCodes.BadIndexRangeInvalid;
|
|
}
|
|
|
|
// data encoding not supported.
|
|
if (!itemToCreate.ItemToMonitor.DataEncoding.IsNull)
|
|
{
|
|
return StatusCodes.BadDataEncodingUnsupported;
|
|
}
|
|
|
|
// read initial value.
|
|
var initialValue = new DataValue(
|
|
Variant.Null,
|
|
StatusCodes.Good,
|
|
DateTime.MinValue,
|
|
DateTime.UtcNow);
|
|
|
|
ServiceResult error = source.ReadAttribute(
|
|
context,
|
|
itemToCreate.ItemToMonitor.AttributeId,
|
|
itemToCreate.ItemToMonitor.ParsedIndexRange,
|
|
itemToCreate.ItemToMonitor.DataEncoding,
|
|
ref initialValue);
|
|
|
|
if (ServiceResult.IsBad(error))
|
|
{
|
|
return error;
|
|
}
|
|
|
|
// get the monitored node for the containing buffer.
|
|
if (tag.Parent is not MemoryBufferState buffer)
|
|
{
|
|
return StatusCodes.BadInternalError;
|
|
}
|
|
|
|
// determine the sampling interval.
|
|
double samplingInterval = itemToCreate.RequestedParameters.SamplingInterval;
|
|
|
|
if (samplingInterval < 0)
|
|
{
|
|
samplingInterval = publishingInterval;
|
|
}
|
|
|
|
// create the item.
|
|
MemoryBufferMonitoredItem datachangeItem = buffer.CreateDataChangeItem(
|
|
(ServerSystemContext)context,
|
|
tag,
|
|
subscriptionId,
|
|
monitoredItemIdFactory.GetNextId(),
|
|
itemToCreate.ItemToMonitor,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
itemToCreate.MonitoringMode,
|
|
itemToCreate.RequestedParameters.ClientHandle,
|
|
samplingInterval,
|
|
createDurable);
|
|
|
|
// report the initial value.
|
|
datachangeItem.QueueValue(initialValue, null);
|
|
|
|
// update monitored item list.
|
|
monitoredItem = datachangeItem;
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restore a single monitored Item after a restart
|
|
/// </summary>
|
|
/// <returns>true if sucesfully restored</returns>
|
|
protected override bool RestoreMonitoredItem(
|
|
ServerSystemContext context,
|
|
NodeState source,
|
|
IStoredMonitoredItem storedMonitoredItem,
|
|
out IMonitoredItem monitoredItem)
|
|
{
|
|
monitoredItem = null!;
|
|
|
|
// use default behavior for non-tag sources.
|
|
if (source is not MemoryTagState tag)
|
|
{
|
|
return base.RestoreMonitoredItem(
|
|
context,
|
|
source,
|
|
storedMonitoredItem,
|
|
out monitoredItem);
|
|
}
|
|
|
|
// get the monitored node for the containing buffer.
|
|
if (tag.Parent is not MemoryBufferState buffer)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// create the item.
|
|
|
|
// update monitored item list.
|
|
monitoredItem = buffer.RestoreDataChangeItem(context, tag, storedMonitoredItem);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifies the parameters for a monitored item.
|
|
/// </summary>
|
|
protected override ServiceResult ModifyMonitoredItem(
|
|
ISystemContext context,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
IMonitoredItem monitoredItem,
|
|
MonitoredItemModifyRequest itemToModify,
|
|
out MonitoringFilterResult filterError)
|
|
{
|
|
filterError = null!;
|
|
|
|
// check for valid handle.
|
|
if (monitoredItem.ManagerHandle is not MemoryBufferState)
|
|
{
|
|
return base.ModifyMonitoredItem(
|
|
context,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
monitoredItem,
|
|
itemToModify,
|
|
out filterError);
|
|
}
|
|
|
|
// owned by this node manager.
|
|
itemToModify.Processed = true;
|
|
|
|
// get the monitored item.
|
|
if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem)
|
|
{
|
|
return StatusCodes.BadMonitoredItemIdInvalid;
|
|
}
|
|
|
|
// validate parameters.
|
|
MonitoringParameters parameters = itemToModify.RequestedParameters;
|
|
|
|
// no filters supported at this time.
|
|
var filter = (MonitoringFilter?)ExtensionObject.ToEncodeable(parameters.Filter);
|
|
|
|
if (filter != null)
|
|
{
|
|
return StatusCodes.BadFilterNotAllowed;
|
|
}
|
|
|
|
// modify the monitored item parameters.
|
|
_ = datachangeItem.Modify(
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
itemToModify.RequestedParameters.ClientHandle,
|
|
itemToModify.RequestedParameters.SamplingInterval);
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a monitored item.
|
|
/// </summary>
|
|
protected override ServiceResult DeleteMonitoredItem(
|
|
ISystemContext context,
|
|
IMonitoredItem monitoredItem,
|
|
out bool processed)
|
|
{
|
|
// check for valid handle.
|
|
if (monitoredItem.ManagerHandle is not MemoryBufferState buffer)
|
|
{
|
|
return base.DeleteMonitoredItem(context, monitoredItem, out processed);
|
|
}
|
|
|
|
// owned by this node manager.
|
|
processed = true;
|
|
|
|
// get the monitored item.
|
|
if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem)
|
|
{
|
|
return StatusCodes.BadMonitoredItemIdInvalid;
|
|
}
|
|
|
|
// delete the item.
|
|
buffer.DeleteItem(datachangeItem);
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changes the monitoring mode for an item.
|
|
/// </summary>
|
|
protected override ServiceResult SetMonitoringMode(
|
|
ISystemContext context,
|
|
IMonitoredItem monitoredItem,
|
|
MonitoringMode monitoringMode,
|
|
out bool processed)
|
|
{
|
|
// check for valid handle.
|
|
if (monitoredItem.ManagerHandle is not MemoryBufferState buffer)
|
|
{
|
|
return base.SetMonitoringMode(
|
|
context,
|
|
monitoredItem,
|
|
monitoringMode,
|
|
out processed);
|
|
}
|
|
|
|
// owned by this node manager.
|
|
processed = true;
|
|
|
|
// get the monitored item.
|
|
if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem)
|
|
{
|
|
return StatusCodes.BadMonitoredItemIdInvalid;
|
|
}
|
|
|
|
// delete the item.
|
|
MonitoringMode previousMode = datachangeItem.SetMonitoringMode(monitoringMode);
|
|
|
|
// need to provide an immediate update after enabling.
|
|
if (previousMode == MonitoringMode.Disabled &&
|
|
monitoringMode != MonitoringMode.Disabled)
|
|
{
|
|
var initialValue = new DataValue(
|
|
Variant.Null,
|
|
StatusCodes.Good,
|
|
DateTime.MinValue,
|
|
DateTime.UtcNow);
|
|
|
|
var tag = new MemoryTagState(buffer, datachangeItem.Offset);
|
|
|
|
ServiceResult error = tag.ReadAttribute(
|
|
context,
|
|
datachangeItem.AttributeId,
|
|
default,
|
|
default,
|
|
ref initialValue);
|
|
|
|
datachangeItem.QueueValue(initialValue, error);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
private readonly MemoryBufferConfiguration m_configuration;
|
|
private readonly Dictionary<string, MemoryBufferState> m_buffers;
|
|
}
|
|
}
|