@@ -0,0 +1,498 @@
using System.Buffers.Binary ;
using System.Text ;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport ;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip ;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix ;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip ;
/// <summary>
/// Native <see cref="IAbCipTagRuntime"/> over the managed EtherNet/IP + CIP stack. Leases the
/// device's shared connection from <see cref="DeviceSessionPool.Shared"/> and reads/writes through a
/// per-runtime <see cref="LogixTagClient"/> over the lease's <b>shared</b> scheduler, so concurrent
/// tags on one device coalesce into Multiple Service Packets (review B1). Decoding mirrors
/// <see cref="LibplctagTagRuntime"/> byte for byte; unlike libplctag it also checks the CIP type
/// code the controller returned against the authored type.
/// </summary>
/// <remarks>
/// Not thread-safe: the driver serialises Read → GetStatus → Decode (and Encode → Write → GetStatus)
/// per runtime under its per-runtime lock (05/STAB-4), exactly as it does for libplctag.
/// </remarks>
internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
{
/// <summary>Upper bound on waiting for the lease release (and its Forward Close) in <see cref="Dispose"/>.</summary>
private static readonly TimeSpan DisposeWait = TimeSpan . FromSeconds ( 2 ) ;
/// <summary>Bytes of a Logix STRING's length word.</summary>
private const int StringLengthBytes = 4 ;
private readonly AbCipTagCreateParams _p ;
private readonly DeviceSessionPool ? _pool ;
private readonly ushort _elements ;
// Encoded in InitializeAsync, never in the constructor: a malformed name must surface from
// Initialize (inside the driver's / probe's try), not from IAbCipTagFactory.Create — as libplctag.
private int? _implicitBit ;
private byte [ ] ? _path ;
private ConnectionLease ? _lease ;
private RequestScheduler ? _privateScheduler ; // ForTest only — production always uses the lease's
private LogixTagClient ? _client ;
private byte [ ] _data = [ ] ;
private ushort? _typeCode ;
private ushort? _structHandle ;
private AbStatus _status = AbStatus . Pending ;
private byte [ ] ? _writeBuffer ;
private string? _pendingString ;
private bool _disposed ;
/// <summary>Production constructor; <see cref="InitializeAsync"/> leases the connection from <paramref name="pool"/>.</summary>
/// <param name="p">The tag creation parameters.</param>
/// <param name="pool">The connection pool (the driver passes <see cref="DeviceSessionPool.Shared"/>).</param>
internal NativeAbCipTagRuntime ( AbCipTagCreateParams p , DeviceSessionPool pool )
: this ( p )
{
_pool = pool ? ? throw new ArgumentNullException ( nameof ( pool ) ) ;
}
private NativeAbCipTagRuntime ( AbCipTagCreateParams p )
{
_p = p ? ? throw new ArgumentNullException ( nameof ( p ) ) ;
_elements = ( ushort ) Math . Clamp ( p . ElementCount , 1 , ushort . MaxValue ) ;
}
/// <summary>
/// Test seam: a runtime over <paramref name="channel"/> with a private scheduler and no pool
/// lease. Production code always goes through <see cref="NativeAbCipTagFactory"/>.
/// </summary>
/// <param name="channel">The channel to send on.</param>
/// <param name="p">The tag creation parameters.</param>
/// <returns>The runtime.</returns>
internal static NativeAbCipTagRuntime ForTest ( ICipChannel channel , AbCipTagCreateParams p )
{
ArgumentNullException . ThrowIfNull ( channel ) ;
var rt = new NativeAbCipTagRuntime ( p ) ;
rt . _privateScheduler = new RequestScheduler ( channel , p . AllowPacking ) ;
rt . _client = new LogixTagClient ( rt . _privateScheduler , channel ) ;
return rt ;
}
/// <summary>The CIP type code of the last successful read, if any.</summary>
internal ushort? TypeCode = > _typeCode ;
/// <summary>The structure handle of the last successful read, if the controller returned one.</summary>
internal ushort? StructHandle = > _structHandle ;
/// <summary>The trailing bit selector split off the tag name (<c>Tag.5</c>), if any.</summary>
internal int? ImplicitBit = > _implicitBit ;
/// <inheritdoc />
/// <remarks>
/// Performs one read to prove the tag exists: libplctag's <c>InitializeAsync</c> errors for an
/// unknown tag, and the driver's 05/STAB-8 backoff classification depends on create throwing
/// <see cref="AbStatus.NotFound"/>.
/// </remarks>
public async Task InitializeAsync ( CancellationToken cancellationToken )
{
ObjectDisposedException . ThrowIf ( _disposed , this ) ;
if ( _path is null )
{
// Before any lease: a malformed name costs no connection.
var ( name , bit ) = CipPath . SplitBitSelector ( _p . TagName ) ;
var path = CipPath . EncodeSymbolic ( name ) ;
_implicitBit = bit ;
_path = path ;
}
if ( _client is null )
{
var lease = _pool ! . Acquire ( NativeAbCipDevice . Key ( _p ) , NativeAbCipDevice . Options ( _p ) ) ;
_lease = lease ;
// The lease's shared scheduler — never a private one (B1: MSP coalescing across tags).
_client = new LogixTagClient ( lease . Scheduler , lease . Channel ) ;
}
await ReadAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
}
/// <inheritdoc />
public async Task ReadAsync ( CancellationToken cancellationToken )
{
var client = Client ( ) ;
var path = Path ( ) ;
try
{
var ( type , handle , data ) = await client . ReadAsync ( path , _elements , cancellationToken ) . ConfigureAwait ( false ) ;
_data = data ;
_typeCode = type ;
_structHandle = handle ;
_status = IsTypeMismatch ( type ) ? AbStatus . TypeMismatch : AbStatus . Ok ;
}
catch ( AbTransportException ex )
{
// libplctag reports a failed operation by exception AND leaves the status readable.
_status = ex . Status ;
throw ;
}
}
/// <inheritdoc />
public async Task WriteAsync ( CancellationToken cancellationToken )
{
var client = Client ( ) ;
var path = Path ( ) ;
// A bit tag ("Flags.3") names one bit of its host word. Writing any encoded value to it would
// write the WHOLE host and wipe its other bits on the controller; BOOL bit writes are an RMW the
// driver performs on a parent-DINT runtime (WriteBitInDIntAsync), so anything reaching here is refused.
if ( _implicitBit is int implicitBit )
throw new NotSupportedException (
$"Write to bit tag '{_p.TagName}' (bit {implicitBit}) is not supported here — bit writes go through AbCipDriver.WriteBitInDIntAsync." ) ;
try
{
if ( _pendingString is { } text )
{
// A STRING writes as the type the controller reported — a Logix STRING is a structure
// (0x02A0 + handle); ab_server reports 0x00D0 with no handle. Learn it by reading first
// when it is not yet known (we run under the driver's per-runtime lock).
if ( _typeCode is null | | ( _typeCode = = LogixTypeCode . Struct & & _structHandle is null ) )
await ReadAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( _typeCode = = LogixTypeCode . Struct & & _structHandle is null )
throw new AbTransportException ( AbStatus . Unsupported ,
$"STRING write to '{_p.TagName}': the controller returned no structure handle." ) ;
var payload = EncodeStringPayload ( text ) ;
await client . WriteAsync ( path , _typeCode ! . Value , _structHandle , 1 , payload , cancellationToken )
. ConfigureAwait ( false ) ;
}
else if ( _writeBuffer is { } buffer )
{
var type = _typeCode ? ? AtomicCode ( _p . AuthoredType )
? ? throw new AbTransportException ( AbStatus . Unsupported ,
$"Write to '{_p.TagName}': type unknown — no prior read and no authored atomic type." ) ;
// The read rule decides first: a same-width mismatch (REAL under a DINT-authored tag)
// would otherwise send the int bit pattern as a REAL and report Good.
if ( _typeCode is { } returned & & IsTypeMismatch ( returned ) )
throw new AbTransportException ( AbStatus . TypeMismatch ,
$"Write to '{_p.TagName}': the controller's type 0x{returned:X4} does not match the authored {_p.AuthoredType}." ) ;
// Width is checked only for codes with a known width: DATE / DATE_AND_TIME (0xCD/0xCF) and
// Micro800 BYTE/WORD/LWORD (0xD1/0xD2/0xD4) are outside LogixTypeCode's table and write as today.
var width = LogixTypeCode . ByteWidth ( type ) ;
if ( width > 0 & & width ! = buffer . Length )
{
// An authored BOOL over a packed BOOL-array DWORD: writing the DWORD would clobber the
// 31 sibling bools (libplctag does exactly that), so refuse it as a config error.
if ( type = = LogixTypeCode . BoolArray )
throw new NotSupportedException (
$"Write to '{_p.TagName}': a BOOL-array element (type 0xD3 DWORD) cannot be written as a single BOOL." ) ;
throw new AbTransportException ( AbStatus . TypeMismatch ,
$"Write to '{_p.TagName}': encoded {buffer.Length} byte(s) but the controller's type 0x{type:X4} is {width} byte(s) wide." ) ;
}
await client . WriteAsync ( path , type , null , 1 , buffer , cancellationToken ) . ConfigureAwait ( false ) ;
}
else
{
throw new InvalidOperationException ( "WriteAsync called before EncodeValue." ) ;
}
_status = AbStatus . Ok ;
}
catch ( AbTransportException ex )
{
_status = ex . Status ;
throw ;
}
}
/// <inheritdoc />
public int GetStatus ( ) = > ( int ) _status ;
/// <inheritdoc />
public int GetSize ( ) = > _data . Length ;
/// <inheritdoc />
public object? DecodeValue ( AbCipDataType type , int? bitIndex ) = > DecodeValueAt ( type , 0 , bitIndex ) ;
/// <inheritdoc />
public object? DecodeValueAt ( AbCipDataType type , int offset , int? bitIndex ) = >
_status = = AbStatus . TypeMismatch ? null : DecodeAt ( type , offset , bitIndex ) ;
private object? DecodeAt ( AbCipDataType type , int offset , int? bitIndex )
{
// libplctag on a bit tag ("Tag.5"): every integer getter returns that bit, and GetBit ignores
// the index it is given in favour of the tag's own bit.
if ( _implicitBit is int implicitBit & & IsIntegerShaped ( type ) )
return BoxBit ( type , GetBit ( implicitBit ) ) ;
return type switch
{
AbCipDataType . Bool = > bitIndex is int bit ? GetBit ( bit ) : Span ( offset , 1 ) [ 0 ] ! = 0 ,
AbCipDataType . SInt = > ( int ) ( sbyte ) Span ( offset , 1 ) [ 0 ] ,
AbCipDataType . USInt = > ( int ) Span ( offset , 1 ) [ 0 ] ,
AbCipDataType . Int = > ( int ) BinaryPrimitives . ReadInt16LittleEndian ( Span ( offset , 2 ) ) ,
AbCipDataType . UInt = > ( int ) BinaryPrimitives . ReadUInt16LittleEndian ( Span ( offset , 2 ) ) ,
AbCipDataType . DInt or AbCipDataType . Dt = > BinaryPrimitives . ReadInt32LittleEndian ( Span ( offset , 4 ) ) ,
AbCipDataType . UDInt = > BinaryPrimitives . ReadUInt32LittleEndian ( Span ( offset , 4 ) ) ,
AbCipDataType . LInt = > BinaryPrimitives . ReadInt64LittleEndian ( Span ( offset , 8 ) ) ,
AbCipDataType . ULInt = > BinaryPrimitives . ReadUInt64LittleEndian ( Span ( offset , 8 ) ) ,
AbCipDataType . Real = > BinaryPrimitives . ReadSingleLittleEndian ( Span ( offset , 4 ) ) ,
AbCipDataType . LReal = > BinaryPrimitives . ReadDoubleLittleEndian ( Span ( offset , 8 ) ) ,
AbCipDataType . String = > DecodeString ( offset ) ,
_ = > null ,
} ;
}
/// <inheritdoc />
public object? DecodeArray ( AbCipDataType type , int count )
{
if ( _status = = AbStatus . TypeMismatch ) return null ;
if ( count < 1 ) count = 1 ;
// Same stride rule as libplctag's runtime (which never has ElementSize after a read).
var stride = LibplctagTagRuntime . ResolveArrayStride ( null , _data . Length , count , type ) ;
return type switch
{
// A Logix BOOL array is bit-packed: element i is bit i of the buffer.
AbCipDataType . Bool = > BuildBoolArray ( count ) ,
AbCipDataType . SInt or AbCipDataType . USInt or AbCipDataType . Int or AbCipDataType . UInt
or AbCipDataType . DInt or AbCipDataType . Dt = > BuildArray < int > ( count , stride , type ) ,
AbCipDataType . UDInt = > BuildArray < uint > ( count , stride , type ) ,
AbCipDataType . LInt = > BuildArray < long > ( count , stride , type ) ,
AbCipDataType . ULInt = > BuildArray < ulong > ( count , stride , type ) ,
AbCipDataType . Real = > BuildArray < float > ( count , stride , type ) ,
AbCipDataType . LReal = > BuildArray < double > ( count , stride , type ) ,
AbCipDataType . String = > BuildArray < string > ( count , stride , type ) ,
_ = > null ,
} ;
}
/// <inheritdoc />
public void EncodeValue ( AbCipDataType type , int? bitIndex , object? value )
{
_writeBuffer = null ;
_pendingString = null ;
switch ( type )
{
case AbCipDataType . Bool :
if ( bitIndex is int )
{
// Same contract as LibplctagTagRuntime: BOOL-within-DINT writes are an RMW the driver
// performs on a parent-DINT runtime (AbCipDriver.WriteBitInDIntAsync).
throw new NotSupportedException (
"BOOL-with-bitIndex writes must go through AbCipDriver.WriteBitInDIntAsync, not NativeAbCipTagRuntime." ) ;
}
_writeBuffer = [ Convert . ToBoolean ( value ) ? ( byte ) 1 : ( byte ) 0 ] ;
break ;
case AbCipDataType . SInt :
_writeBuffer = [ ( byte ) Convert . ToSByte ( value ) ] ;
break ;
case AbCipDataType . USInt :
_writeBuffer = [ Convert . ToByte ( value ) ] ;
break ;
case AbCipDataType . Int :
_writeBuffer = new byte [ 2 ] ;
BinaryPrimitives . WriteInt16LittleEndian ( _writeBuffer , Convert . ToInt16 ( value ) ) ;
break ;
case AbCipDataType . UInt :
_writeBuffer = new byte [ 2 ] ;
BinaryPrimitives . WriteUInt16LittleEndian ( _writeBuffer , Convert . ToUInt16 ( value ) ) ;
break ;
case AbCipDataType . DInt :
case AbCipDataType . Dt :
_writeBuffer = new byte [ 4 ] ;
BinaryPrimitives . WriteInt32LittleEndian ( _writeBuffer , Convert . ToInt32 ( value ) ) ;
break ;
case AbCipDataType . UDInt :
_writeBuffer = new byte [ 4 ] ;
BinaryPrimitives . WriteUInt32LittleEndian ( _writeBuffer , Convert . ToUInt32 ( value ) ) ;
break ;
case AbCipDataType . LInt :
_writeBuffer = new byte [ 8 ] ;
BinaryPrimitives . WriteInt64LittleEndian ( _writeBuffer , Convert . ToInt64 ( value ) ) ;
break ;
case AbCipDataType . ULInt :
_writeBuffer = new byte [ 8 ] ;
BinaryPrimitives . WriteUInt64LittleEndian ( _writeBuffer , Convert . ToUInt64 ( value ) ) ;
break ;
case AbCipDataType . Real :
_writeBuffer = new byte [ 4 ] ;
BinaryPrimitives . WriteSingleLittleEndian ( _writeBuffer , Convert . ToSingle ( value ) ) ;
break ;
case AbCipDataType . LReal :
_writeBuffer = new byte [ 8 ] ;
BinaryPrimitives . WriteDoubleLittleEndian ( _writeBuffer , Convert . ToDouble ( value ) ) ;
break ;
case AbCipDataType . String :
// The payload needs the capacity learned from a read, which WriteAsync may have to do.
_pendingString = Convert . ToString ( value ) ? ? string . Empty ;
break ;
case AbCipDataType . Structure :
throw new NotSupportedException (
"Whole-UDT writes are not supported — address individual member paths instead." ) ;
default :
throw new NotSupportedException ( $"AbCipDataType {type} not writable." ) ;
}
}
/// <summary>
/// Releases the connection lease, waiting at most 2 s so an immediate re-init does not open a
/// second session to the device while this one's Forward Close is still in flight (review
/// finding 22). Never disposes the shared scheduler; the pool does that on the last release.
/// </summary>
public void Dispose ( )
{
if ( _disposed ) return ;
_disposed = true ;
try
{
if ( _lease is { } lease )
lease . DisposeAsync ( ) . AsTask ( ) . Wait ( DisposeWait ) ;
else if ( _privateScheduler is { } scheduler )
scheduler . DisposeAsync ( ) . AsTask ( ) . Wait ( DisposeWait ) ;
}
catch ( AggregateException )
{
// A faulted close is the pool's problem to log; Dispose must not throw.
}
_lease = null ;
_privateScheduler = null ;
}
private byte [ ] Path ( ) = >
_path ? ? throw new InvalidOperationException ( "InitializeAsync has not been called." ) ;
private LogixTagClient Client ( )
{
ObjectDisposedException . ThrowIf ( _disposed , this ) ;
return _client ? ? throw new InvalidOperationException ( "InitializeAsync has not been called." ) ;
}
/// <summary>
/// True when the controller returned an atomic type other than the authored one. Exempt:
/// no authored type, String/Structure, a non-atomic return, a BOOL bit selector against an integer
/// host (<c>Flags.3</c> on a DINT), and an authored BOOL answered as 0xD3 (a packed BOOL-array
/// DWORD, which is how Logix returns a BOOL-array element), and same-width integers that differ only
/// in signedness or are the Micro800 bit-string type of that width (SINT/USINT/BYTE, INT/UINT/WORD,
/// DINT/UDINT/DWORD, LINT/ULINT/LWORD): libplctag reads those Good today and a transport cutover
/// must not turn them Bad. Different widths and integer↔real still mismatch.
/// </summary>
private bool IsTypeMismatch ( ushort returned )
{
if ( AtomicCode ( _p . AuthoredType ) is not { } expected ) return false ;
if ( LogixTypeCode . ByteWidth ( returned ) < 0 | | returned = = expected ) return false ;
if ( _p . AuthoredType = = AbCipDataType . Bool )
{
if ( returned = = LogixTypeCode . BoolArray ) return false ;
if ( _implicitBit is not null & & IsIntegerCode ( returned ) ) return false ;
}
if ( IntegerWidth ( expected ) is int width & & IntegerWidth ( returned ) = = width ) return false ;
return true ;
}
/// <summary>Micro800 bit-string type codes (DWORD is <see cref="LogixTypeCode.BoolArray"/>, 0xD3).</summary>
private const ushort ByteCode = 0xD1 , WordCode = 0xD2 , LWordCode = 0xD4 ;
/// <summary>Width of an integer or bit-string code; null for anything else (BOOL, REAL, ...).</summary>
private static int? IntegerWidth ( ushort code ) = > code switch
{
LogixTypeCode . SInt or LogixTypeCode . USInt or ByteCode = > 1 ,
LogixTypeCode . Int or LogixTypeCode . UInt or WordCode = > 2 ,
LogixTypeCode . DInt or LogixTypeCode . UDInt or LogixTypeCode . BoolArray = > 4 ,
LogixTypeCode . LInt or LogixTypeCode . ULInt or LWordCode = > 8 ,
_ = > null ,
} ;
/// <summary>The Logix type code an authored type expects, or null when it is exempt from checking.</summary>
private static ushort? AtomicCode ( AbCipDataType ? type ) = > type switch
{
AbCipDataType . Bool = > LogixTypeCode . Bool ,
AbCipDataType . SInt = > LogixTypeCode . SInt ,
AbCipDataType . USInt = > LogixTypeCode . USInt ,
AbCipDataType . Int = > LogixTypeCode . Int ,
AbCipDataType . UInt = > LogixTypeCode . UInt ,
AbCipDataType . DInt or AbCipDataType . Dt = > LogixTypeCode . DInt ,
AbCipDataType . UDInt = > LogixTypeCode . UDInt ,
AbCipDataType . LInt = > LogixTypeCode . LInt ,
AbCipDataType . ULInt = > LogixTypeCode . ULInt ,
AbCipDataType . Real = > LogixTypeCode . Real ,
AbCipDataType . LReal = > LogixTypeCode . LReal ,
_ = > null , // String, Structure
} ;
private static bool IsIntegerCode ( ushort code ) = > code is LogixTypeCode . SInt or LogixTypeCode . Int
or LogixTypeCode . DInt or LogixTypeCode . LInt or LogixTypeCode . USInt or LogixTypeCode . UInt
or LogixTypeCode . UDInt or LogixTypeCode . ULInt ;
private static bool IsIntegerShaped ( AbCipDataType type ) = > type is AbCipDataType . Bool or AbCipDataType . SInt
or AbCipDataType . USInt or AbCipDataType . Int or AbCipDataType . UInt or AbCipDataType . DInt
or AbCipDataType . UDInt or AbCipDataType . LInt or AbCipDataType . ULInt or AbCipDataType . Dt ;
private static object BoxBit ( AbCipDataType type , bool bit ) = > type switch
{
AbCipDataType . Bool = > bit ,
AbCipDataType . UDInt = > bit ? 1 u : 0 u ,
AbCipDataType . LInt = > bit ? 1L : 0L ,
AbCipDataType . ULInt = > bit ? 1 UL : 0 UL ,
_ = > bit ? 1 : 0 ,
} ;
/// <summary>Bit <paramref name="bit"/> of the little-endian buffer.</summary>
private bool GetBit ( int bit )
{
if ( bit < 0 | | bit / 8 > = _data . Length )
throw new AbTransportException ( AbStatus . OutOfBounds ,
$"Bit {bit} is outside the {_data.Length}-byte buffer of '{_p.TagName}'." ) ;
return ( _data [ bit / 8 ] & ( 1 < < ( bit % 8 ) ) ) ! = 0 ;
}
/// <summary>The <paramref name="length"/> bytes at <paramref name="offset"/>; OutOfBounds like libplctag.</summary>
private ReadOnlySpan < byte > Span ( int offset , int length )
{
if ( offset < 0 | | length > _data . Length - offset )
throw new AbTransportException ( AbStatus . OutOfBounds ,
$"Offset {offset} + {length} byte(s) is outside the {_data.Length}-byte buffer of '{_p.TagName}'." ) ;
return _data . AsSpan ( offset , length ) ;
}
/// <summary>Logix STRING: <c>u32 LEN</c> then ASCII DATA, LEN clamped to what the buffer holds.</summary>
private string DecodeString ( int offset )
{
var len = BinaryPrimitives . ReadUInt32LittleEndian ( Span ( offset , StringLengthBytes ) ) ;
var available = _data . Length - offset - StringLengthBytes ;
var count = ( int ) Math . Min ( len , ( uint ) available ) ;
return Encoding . ASCII . GetString ( _data , offset + StringLengthBytes , count ) ;
}
/// <summary>
/// <c>u32 len</c> + DATA padded to the capacity learned from the last read (data length − 4), so the
/// write carries the whole structure as the controller sized it.
/// </summary>
private byte [ ] EncodeStringPayload ( string text )
{
var capacity = _data . Length - StringLengthBytes ;
var bytes = Encoding . ASCII . GetBytes ( text ) ;
if ( capacity < 0 | | bytes . Length > capacity )
throw new AbTransportException ( AbStatus . TooLarge ,
$"STRING write to '{_p.TagName}': {bytes.Length} character(s) exceeds the {Math.Max(capacity, 0)}-byte capacity." ) ;
var payload = new byte [ StringLengthBytes + capacity ] ;
BinaryPrimitives . WriteUInt32LittleEndian ( payload , ( uint ) bytes . Length ) ;
bytes . CopyTo ( payload , StringLengthBytes ) ;
return payload ;
}
private bool [ ] BuildBoolArray ( int count )
{
var arr = new bool [ count ] ;
for ( var i = 0 ; i < count ; i + + ) arr [ i ] = GetBit ( i ) ;
return arr ;
}
private T [ ] BuildArray < T > ( int count , int stride , AbCipDataType type )
{
var arr = new T [ count ] ;
for ( var i = 0 ; i < count ; i + + )
{
var decoded = DecodeAt ( type , i * stride , null ) ;
arr [ i ] = decoded is null ? default ! : ( T ) decoded ;
}
return arr ;
}
}