This is an automated email from the ASF dual-hosted git repository.
curth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-dotnet.git
The following commit(s) were added to refs/heads/main by this push:
new aaa07fe Add support for an alternative `ArrowBuffer` allocation
mechanism. (#304)
aaa07fe is described below
commit aaa07feb04f08cc34c60894e6e84f9b1beee3bc3
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Wed Apr 1 16:33:02 2026 -0700
Add support for an alternative `ArrowBuffer` allocation mechanism. (#304)
## What's Changed
Adds support for an alternative `ArrowBuffer` allocation mechanism to
optimize array creation.
---
src/Apache.Arrow/Apache.Arrow.csproj | 1 +
src/Apache.Arrow/Memory/AlignedNative.cs | 118 +++++++++
.../Memory/INativeAllocationTracker.cs | 30 +++
.../Memory/MemoryPressureAllocationTracker.cs | 37 +++
src/Apache.Arrow/Memory/NativeBuffer.cs | 224 +++++++++++++++++
src/Apache.Arrow/Memory/NoOpAllocationTracker.cs | 26 ++
test/Apache.Arrow.Tests/NativeBufferTests.cs | 270 +++++++++++++++++++++
7 files changed, 706 insertions(+)
diff --git a/src/Apache.Arrow/Apache.Arrow.csproj
b/src/Apache.Arrow/Apache.Arrow.csproj
index cb5a03b..62725d0 100644
--- a/src/Apache.Arrow/Apache.Arrow.csproj
+++ b/src/Apache.Arrow/Apache.Arrow.csproj
@@ -44,6 +44,7 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Remove="Extensions\StreamExtensions.netstandard.cs" />
<Compile Remove="Extensions\TupleExtensions.netstandard.cs" />
+ <Compile Remove="Memory\AlignedNative.cs" />
</ItemGroup>
<ItemGroup
Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework),
'net5.0'))">
<Compile Remove="Arrays\HalfFloatArray.cs" />
diff --git a/src/Apache.Arrow/Memory/AlignedNative.cs
b/src/Apache.Arrow/Memory/AlignedNative.cs
new file mode 100644
index 0000000..ec8aedc
--- /dev/null
+++ b/src/Apache.Arrow/Memory/AlignedNative.cs
@@ -0,0 +1,118 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace Apache.Arrow.Memory
+{
+ /// <summary>
+ /// Provides aligned memory allocation on downlevel platforms (.NET
Framework / netstandard2.0)
+ /// by P/Invoking <c>_aligned_malloc</c> / <c>_aligned_free</c> /
<c>_aligned_realloc</c> from
+ /// the Universal C Runtime (ucrtbase.dll). Falls back to <see
cref="Marshal.AllocHGlobal"/>
+ /// with manual alignment if the CRT functions are unavailable.
+ /// </summary>
+ internal static unsafe class AlignedNative
+ {
+ private static readonly bool s_hasCrt = ProbeCrt();
+
+ public static void* AlignedAlloc(int size, int alignment, out int
offset)
+ {
+ void* ptr;
+ if (s_hasCrt)
+ {
+ ptr = UcrtInterop.AlignedMalloc((IntPtr)size,
(IntPtr)alignment);
+ offset = 0;
+ }
+ else
+ {
+ int length = size + alignment;
+ IntPtr address = Marshal.AllocHGlobal(length);
+ offset = (int)(alignment - (address.ToInt64() & (alignment -
1)));
+ ptr = (void*)address;
+ }
+
+ if (ptr == null)
+ {
+ throw new OutOfMemoryException($"_aligned_malloc({size},
{alignment}) returned null.");
+ }
+
+ return ptr;
+ }
+
+ public static void AlignedFree(void* ptr)
+ {
+ if (s_hasCrt)
+ UcrtInterop.AlignedFree(ptr);
+ else
+ Marshal.FreeHGlobal((IntPtr)ptr);
+ }
+
+ public static void* AlignedRealloc(void* ptr, int newSize, int
alignment, int oldSize, ref int offset)
+ {
+ void* newPtr;
+ if (s_hasCrt)
+ {
+ newPtr = UcrtInterop.AlignedRealloc(ptr, (IntPtr)newSize,
(IntPtr)alignment);
+ if (newPtr == null)
+ throw new
OutOfMemoryException($"_aligned_realloc({newSize}, {alignment}) returned
null.");
+ }
+ else
+ {
+ int length = newSize + alignment;
+ IntPtr address = Marshal.AllocHGlobal(length);
+ if (address == IntPtr.Zero)
+ throw new
OutOfMemoryException($"_aligned_realloc({newSize}, {alignment}) returned
null.");
+ int newOffset = (int)(alignment - (address.ToInt64() &
(alignment - 1)));
+ Buffer.MemoryCopy(
+ (void*)((byte*)ptr + offset),
+ (void*)(address + newOffset),
+ newSize,
+ Math.Min(oldSize, newSize));
+ offset = newOffset;
+ newPtr = (void*)address;
+ Marshal.FreeHGlobal((IntPtr)ptr);
+ }
+ return newPtr;
+ }
+
+ private static bool ProbeCrt()
+ {
+ try
+ {
+ void* ptr = UcrtInterop.AlignedMalloc((IntPtr)64, (IntPtr)64);
+ if (ptr == null) return false;
+ UcrtInterop.AlignedFree(ptr);
+ return true;
+ }
+ catch (DllNotFoundException) { return false; }
+ catch (EntryPointNotFoundException) { return false; }
+ }
+
+ internal static unsafe class UcrtInterop
+ {
+ private const string Lib = "ucrtbase.dll";
+
+ [DllImport(Lib, CallingConvention = CallingConvention.Cdecl,
EntryPoint = "_aligned_malloc")]
+ public static extern void* AlignedMalloc(IntPtr size, IntPtr
alignment);
+
+ [DllImport(Lib, CallingConvention = CallingConvention.Cdecl,
EntryPoint = "_aligned_free")]
+ public static extern void AlignedFree(void* ptr);
+
+ [DllImport(Lib, CallingConvention = CallingConvention.Cdecl,
EntryPoint = "_aligned_realloc")]
+ public static extern void* AlignedRealloc(void* ptr, IntPtr size,
IntPtr alignment);
+ }
+ }
+}
diff --git a/src/Apache.Arrow/Memory/INativeAllocationTracker.cs
b/src/Apache.Arrow/Memory/INativeAllocationTracker.cs
new file mode 100644
index 0000000..a07a05b
--- /dev/null
+++ b/src/Apache.Arrow/Memory/INativeAllocationTracker.cs
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Apache.Arrow.Memory
+{
+ /// <summary>
+ /// Allows tracking native memory allocations.
+ /// </summary>
+ public interface INativeAllocationTracker
+ {
+ /// <summary>
+ /// Record an allocation event
+ /// </summary>
+ /// <param name="count">+1 when a new native chunk is allocated, -1
when deallocated and 0 when resized</param>
+ /// <param name="bytes">the delta in allocated bytes</param>
+ void Track(int count, long bytes);
+ }
+}
diff --git a/src/Apache.Arrow/Memory/MemoryPressureAllocationTracker.cs
b/src/Apache.Arrow/Memory/MemoryPressureAllocationTracker.cs
new file mode 100644
index 0000000..4a556bd
--- /dev/null
+++ b/src/Apache.Arrow/Memory/MemoryPressureAllocationTracker.cs
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+
+namespace Apache.Arrow.Memory
+{
+ /// <summary>
+ /// Allows control over the way native allocations interact with the GC.
+ /// </summary>
+ public struct MemoryPressureAllocationTracker : INativeAllocationTracker
+ {
+ public void Track(int count, long bytes)
+ {
+ if (bytes > 0)
+ {
+ GC.AddMemoryPressure(bytes);
+ }
+ else
+ {
+ GC.RemoveMemoryPressure(-bytes);
+ }
+ }
+ }
+}
diff --git a/src/Apache.Arrow/Memory/NativeBuffer.cs
b/src/Apache.Arrow/Memory/NativeBuffer.cs
new file mode 100644
index 0000000..a1eacd5
--- /dev/null
+++ b/src/Apache.Arrow/Memory/NativeBuffer.cs
@@ -0,0 +1,224 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace Apache.Arrow.Memory
+{
+ public sealed class NativeBuffer<TItem, TTracker> : IDisposable
+ where TItem : unmanaged
+ where TTracker : struct, INativeAllocationTracker
+ {
+ private const int Alignment = MemoryAllocator.DefaultAlignment;
+
+ private MemoryManager _owner;
+ private int _byteLength;
+
+ /// <summary>Number of <typeparamref name="TItem"/> elements that fit
in the buffer.</summary>
+ public int Length { get; private set; }
+
+ /// <summary>Creates a native buffer sized for <paramref
name="elementCount"/> elements of <typeparamref name="TItem"/>.</summary>
+ /// <param name="elementCount">Number of elements.</param>
+ /// <param name="zeroFill">If true, the buffer is zeroed. Set to false
if the caller will initialize the entire span itself.</param>
+ /// <param name="tracker">Allows native allocation sizes to be tracked
and affect the GC.</param>
+ public NativeBuffer(int elementCount, bool zeroFill = true, TTracker
tracker = default)
+ {
+ if (elementCount < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elementCount));
+ }
+
+ int elementSize = Unsafe.SizeOf<TItem>();
+ _byteLength = checked(elementCount * elementSize);
+ Length = elementCount;
+ _owner = new MemoryManager(_byteLength, tracker);
+
+ if (zeroFill)
+ {
+ Span.Clear();
+ }
+ }
+
+ /// <summary>Gets a <see cref="Span{T}"/> over the native
buffer.</summary>
+ public Span<TItem> Span
+ {
+ get
+ {
+ var byteSpan = _owner!.Memory.Span;
+ return MemoryMarshal.Cast<byte, TItem>(byteSpan);
+ }
+ }
+
+ /// <summary>Gets a <see cref="Span{T}"/> over the raw bytes of the
native buffer.</summary>
+ public Span<byte> ByteSpan => _owner!.Memory.Span.Slice(0,
_byteLength);
+
+ /// <summary>
+ /// Transfers ownership to an <see cref="ArrowBuffer"/>. This instance
becomes unusable.
+ /// </summary>
+ public ArrowBuffer Build()
+ {
+ var owner = _owner ?? throw new
ObjectDisposedException(nameof(NativeBuffer<TItem, TTracker>));
+ _owner = null;
+ return new ArrowBuffer(owner);
+ }
+
+ /// <summary>
+ /// Grows the buffer to hold at least <paramref
name="newElementCount"/> elements,
+ /// preserving existing data.
+ /// </summary>
+ public void Grow(int newElementCount, bool zeroFill = true)
+ {
+ if (_owner == null)
+ throw new ObjectDisposedException(nameof(NativeBuffer<TItem,
TTracker>));
+
+ if (newElementCount <= Length)
+ return;
+
+ // Exponential growth (2x) to amortise repeated grows
+ // TODO: There might be a size that's big enough to work for this
case but not too big to overflow.
+ // We could use that instead of blindly doubling.
+ int newCount = Math.Max(newElementCount, checked(Length * 2));
+ int elementSize = Unsafe.SizeOf<TItem>();
+ int needed = checked(newCount * elementSize);
+
+ var owner = _owner ?? throw new
ObjectDisposedException(nameof(NativeBuffer<TItem, TTracker>));
+ owner.Reallocate(needed);
+
+ if (zeroFill)
+ {
+ Span.Slice(Length, newCount - Length).Clear();
+ }
+
+ _byteLength = needed;
+ Length = newCount;
+ }
+
+ public void Dispose()
+ {
+ IDisposable disposable = _owner;
+ _owner = null;
+ disposable?.Dispose();
+ }
+
+ /// <summary>
+ /// A <see cref="MemoryManager{T}"/> backed by aligned native memory.
+ /// On .NET 6+ uses <see cref="NativeMemory.AlignedAlloc"/>; on
downlevel platforms
+ /// uses <see cref="AlignedNative"/> (P/Invoke to ucrtbase.dll with
fallback).
+ /// Disposing frees the native memory.
+ /// </summary>
+ private sealed class MemoryManager : MemoryManager<byte>
+ {
+ private unsafe void* _pointer;
+#if !NET6_0_OR_GREATER
+ private int _offset;
+#endif
+ private int _length;
+ private int _pinCount;
+ private TTracker _tracker;
+
+ public unsafe MemoryManager(int length, TTracker tracker)
+ {
+ _length = length;
+#if NET6_0_OR_GREATER
+ _pointer = NativeMemory.AlignedAlloc((nuint)length,
(nuint)Alignment);
+#else
+ _pointer = AlignedNative.AlignedAlloc(length, Alignment, out
_offset);
+#endif
+ _tracker = tracker;
+ _tracker.Track(1, length);
+ }
+
+ public override Span<byte> GetSpan()
+ {
+ unsafe
+ {
+#if NET6_0_OR_GREATER
+ return new Span<byte>(_pointer, _length);
+#else
+ return new Span<byte>((void*)((byte*)_pointer + _offset),
_length);
+#endif
+ }
+ }
+
+ public override MemoryHandle Pin(int elementIndex = 0)
+ {
+ Interlocked.Increment(ref _pinCount);
+ unsafe
+ {
+#if NET6_0_OR_GREATER
+ return new MemoryHandle((byte*)_pointer + elementIndex,
pinnable: this);
+#else
+ return new MemoryHandle((byte*)_pointer + _offset +
elementIndex, pinnable: this);
+#endif
+ }
+ }
+
+ public override void Unpin()
+ {
+ Interlocked.Decrement(ref _pinCount);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ unsafe
+ {
+ if (_pointer != null)
+ {
+ if (disposing && _pinCount > 0)
+ {
+ throw new InvalidOperationException("cannot free
native memory while it is pinned");
+ }
+
+#if NET6_0_OR_GREATER
+ NativeMemory.AlignedFree(_pointer);
+#else
+ AlignedNative.AlignedFree(_pointer);
+#endif
+ _pointer = null;
+ _tracker.Track(-1, -_length);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Reallocates the native buffer to <paramref name="newLength"/>
bytes in place,
+ /// preserving existing data. Equivalent to
<c>_aligned_realloc</c>.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the
memory is currently pinned.</exception>
+ public unsafe void Reallocate(int newLength)
+ {
+ if (Volatile.Read(ref _pinCount) > 0)
+ throw new InvalidOperationException(
+ "Cannot reallocate a native buffer that is currently
pinned.");
+
+ int oldLength = _length;
+#if NET6_0_OR_GREATER
+ void* newPtr = NativeMemory.AlignedRealloc(_pointer,
(nuint)newLength, Alignment);
+ if (newPtr == null)
+ throw new
OutOfMemoryException($"NativeMemory.AlignedRealloc({newLength}, {Alignment})
returned null.");
+ _pointer = newPtr;
+#else
+ _pointer = AlignedNative.AlignedRealloc(_pointer, newLength,
Alignment, oldLength, ref _offset);
+#endif
+ _length = newLength;
+ _tracker.Track(0, newLength - oldLength);
+ }
+ }
+ }
+}
diff --git a/src/Apache.Arrow/Memory/NoOpAllocationTracker.cs
b/src/Apache.Arrow/Memory/NoOpAllocationTracker.cs
new file mode 100644
index 0000000..905846d
--- /dev/null
+++ b/src/Apache.Arrow/Memory/NoOpAllocationTracker.cs
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Apache.Arrow.Memory
+{
+ /// <summary>
+ /// Avoid informing the GC about native allocations. Consider using for
micro-benchmarks where
+ /// the impact of GC.AddMemoryPressure and GC.RemoveMemoryPressure can
skew timing results.
+ /// </summary>
+ public struct NoOpAllocationTracker : INativeAllocationTracker
+ {
+ public void Track(int count, long bytes) { }
+ }
+}
diff --git a/test/Apache.Arrow.Tests/NativeBufferTests.cs
b/test/Apache.Arrow.Tests/NativeBufferTests.cs
new file mode 100644
index 0000000..84d050c
--- /dev/null
+++ b/test/Apache.Arrow.Tests/NativeBufferTests.cs
@@ -0,0 +1,270 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Reflection;
+using Apache.Arrow.Memory;
+using Xunit;
+
+namespace Apache.Arrow.Tests
+{
+ public class NativeBufferTests
+ {
+ [Fact]
+ public void AllocWriteReadRoundTrip()
+ {
+ using var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+
+ Assert.Equal(4, buf.Length);
+
+ buf.Span[0] = 10;
+ buf.Span[1] = 20;
+ buf.Span[2] = 30;
+ buf.Span[3] = 40;
+
+ Assert.Equal(10, buf.Span[0]);
+ Assert.Equal(20, buf.Span[1]);
+ Assert.Equal(30, buf.Span[2]);
+ Assert.Equal(40, buf.Span[3]);
+ }
+
+ [Fact]
+ public void ZeroFillInitializesToZero()
+ {
+ using var buf = new NativeBuffer<long, NoOpAllocationTracker>(8,
zeroFill: true);
+
+ for (int i = 0; i < buf.Length; i++)
+ {
+ Assert.Equal(0L, buf.Span[i]);
+ }
+ }
+
+ [Fact]
+ public void GrowPreservesExistingData()
+ {
+ using var buf = new NativeBuffer<int, NoOpAllocationTracker>(3);
+
+ buf.Span[0] = 100;
+ buf.Span[1] = 200;
+ buf.Span[2] = 300;
+
+ buf.Grow(10);
+
+ Assert.True(buf.Length >= 10);
+ Assert.Equal(100, buf.Span[0]);
+ Assert.Equal(200, buf.Span[1]);
+ Assert.Equal(300, buf.Span[2]);
+ }
+
+ [Fact]
+ public void GrowWithSmallerOrEqualCountIsNoOp()
+ {
+ using var buf = new NativeBuffer<int, NoOpAllocationTracker>(5);
+
+ buf.Span[0] = 42;
+
+ buf.Grow(5);
+ Assert.Equal(5, buf.Length);
+ Assert.Equal(42, buf.Span[0]);
+
+ buf.Grow(3);
+ Assert.Equal(5, buf.Length);
+ Assert.Equal(42, buf.Span[0]);
+ }
+
+ [Fact]
+ public void BuildTransfersOwnershipToArrowBuffer()
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ buf.Span[0] = 1;
+ buf.Span[1] = 2;
+
+ using ArrowBuffer arrow = buf.Build();
+
+ Assert.True(arrow.Length > 0);
+ var span = arrow.Span.CastTo<int>();
+ Assert.Equal(1, span[0]);
+ Assert.Equal(2, span[1]);
+ }
+
+ [Fact]
+ public void BuildMakesBufferUnusable()
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ using ArrowBuffer arrow = buf.Build();
+
+ Assert.Throws<ObjectDisposedException>(() => buf.Grow(10));
+ }
+
+ [Fact]
+ public void DoubleDisposeDoesNotThrow()
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ buf.Dispose();
+ buf.Dispose();
+ }
+
+ [Fact]
+ public void GrowAfterDisposeThrows()
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ buf.Dispose();
+
+ Assert.Throws<ObjectDisposedException>(() => buf.Grow(10));
+ }
+
+ [Fact]
+ public void BuildAfterDisposeThrows()
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ buf.Dispose();
+
+ Assert.Throws<ObjectDisposedException>(() => buf.Build());
+ }
+
+ [Fact]
+ public void ZeroElementBufferIsValid()
+ {
+ using var buf = new NativeBuffer<int, NoOpAllocationTracker>(0);
+
+ Assert.Equal(0, buf.Length);
+ Assert.Equal(0, buf.ByteSpan.Length);
+ }
+
+ [Fact]
+ public void ByteSpanReflectsTypedSize()
+ {
+ using var buf = new NativeBuffer<long, NoOpAllocationTracker>(3);
+
+ Assert.Equal(3, buf.Length);
+ Assert.Equal(3 * sizeof(long), buf.ByteSpan.Length);
+ }
+
+ [Fact]
+ public void MemoryPressureTrackerNotifiesGC()
+ {
+ // Smoke test: just verify it doesn't throw.
+ // We can't directly observe GC memory pressure, but we verify the
+ // alloc/dealloc cycle completes without error.
+ var buf = new NativeBuffer<byte,
MemoryPressureAllocationTracker>(1024);
+ buf.Span[0] = 0xFF;
+ buf.Dispose();
+ }
+
+#if !NET6_0_OR_GREATER
+ /// <summary>
+ /// Helper that forces <c>AlignedNative.s_hasCrt</c> to the given
value via reflection,
+ /// runs <paramref name="action"/>, then restores the original value.
This is only used
+ /// on net462 and net472, where we know it will work.
+ /// </summary>
+ private static void WithForcedFallback(Action action)
+ {
+ var type = typeof(Apache.Arrow.Memory.AlignedNative);
+ var field = type.GetField("s_hasCrt", BindingFlags.NonPublic |
BindingFlags.Static);
+ bool original = (bool)field.GetValue(null);
+ try
+ {
+ field.SetValue(null, false);
+ action();
+ }
+ finally
+ {
+ field.SetValue(null, original);
+ }
+ }
+
+ [Fact]
+ public void FallbackAllocWriteReadRoundTrip()
+ {
+ WithForcedFallback(() =>
+ {
+ using var buf = new NativeBuffer<int,
NoOpAllocationTracker>(4);
+
+ buf.Span[0] = 10;
+ buf.Span[1] = 20;
+ buf.Span[2] = 30;
+ buf.Span[3] = 40;
+
+ Assert.Equal(10, buf.Span[0]);
+ Assert.Equal(20, buf.Span[1]);
+ Assert.Equal(30, buf.Span[2]);
+ Assert.Equal(40, buf.Span[3]);
+ });
+ }
+
+ [Fact]
+ public void FallbackGrowPreservesExistingData()
+ {
+ WithForcedFallback(() =>
+ {
+ using var buf = new NativeBuffer<int,
NoOpAllocationTracker>(3);
+
+ buf.Span[0] = 100;
+ buf.Span[1] = 200;
+ buf.Span[2] = 300;
+
+ buf.Grow(10);
+
+ Assert.True(buf.Length >= 10);
+ Assert.Equal(100, buf.Span[0]);
+ Assert.Equal(200, buf.Span[1]);
+ Assert.Equal(300, buf.Span[2]);
+ });
+ }
+
+ [Fact]
+ public void FallbackBuildTransfersOwnership()
+ {
+ WithForcedFallback(() =>
+ {
+ var buf = new NativeBuffer<int, NoOpAllocationTracker>(4);
+ buf.Span[0] = 1;
+ buf.Span[1] = 2;
+
+ using ArrowBuffer arrow = buf.Build();
+
+ var span = arrow.Span.CastTo<int>();
+ Assert.Equal(1, span[0]);
+ Assert.Equal(2, span[1]);
+ });
+ }
+
+ [Fact]
+ public void FallbackMultipleGrowsPreserveData()
+ {
+ WithForcedFallback(() =>
+ {
+ using var buf = new NativeBuffer<long,
NoOpAllocationTracker>(2);
+
+ buf.Span[0] = 111;
+ buf.Span[1] = 222;
+
+ buf.Grow(5);
+ buf.Span[2] = 333;
+ buf.Span[3] = 444;
+ buf.Span[4] = 555;
+
+ buf.Grow(20);
+
+ Assert.Equal(111, buf.Span[0]);
+ Assert.Equal(222, buf.Span[1]);
+ Assert.Equal(333, buf.Span[2]);
+ Assert.Equal(444, buf.Span[3]);
+ Assert.Equal(555, buf.Span[4]);
+ });
+ }
+#endif
+ }
+}