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
+    }
+}

Reply via email to