This is an automated email from the ASF dual-hosted git repository.
CurtHagenlocher 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 bb2f7ed fix: Call C Data Interface callbacks through any delegate
type on .NET Framework (#492)
bb2f7ed is described below
commit bb2f7ed9723e8d74f9c6ff1390b462754820f121
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Sat Oct 31 07:33:40 2026 -0700
fix: Call C Data Interface callbacks through any delegate type on .NET
Framework (#492)
## What's Changed
On the net462 and netstandard2.0 builds,
`Marshal.GetDelegateForFunctionPointer<T>` returns the original managed
delegate cast to T when the function pointer was created in-process from
a delegate. If another library exported the callback with its own
delegate type, the cast throws `InvalidCastException`.
Add an internal `NativeDelegate.GetDelegateForFunctionPointer<T>` helper
that uses the non-generic overload and wraps a delegate of another type
by binding to its Invoke method, and use it at all call sites.
Closes #491.
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
src/Apache.Arrow/C/CArrowArray.cs | 2 +-
src/Apache.Arrow/C/CArrowArrayImporter.cs | 2 +-
src/Apache.Arrow/C/CArrowArrayStream.cs | 2 +-
src/Apache.Arrow/C/CArrowArrayStreamImporter.cs | 8 +-
src/Apache.Arrow/C/CArrowDeviceArrayStream.cs | 2 +-
.../C/CArrowDeviceArrayStreamImporter.cs | 8 +-
src/Apache.Arrow/C/CArrowSchema.cs | 2 +-
src/Apache.Arrow/C/CArrowSchemaImporter.cs | 2 +-
src/Apache.Arrow/C/NativeDelegate.cs | 20 +
.../CDataInterfaceForeignDelegateTests.cs | 422 +++++++++++++++++++++
10 files changed, 456 insertions(+), 14 deletions(-)
diff --git a/src/Apache.Arrow/C/CArrowArray.cs
b/src/Apache.Arrow/C/CArrowArray.cs
index 882ca8c..a9ba34f 100644
--- a/src/Apache.Arrow/C/CArrowArray.cs
+++ b/src/Apache.Arrow/C/CArrowArray.cs
@@ -84,7 +84,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
array->release(array);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowArrayExporter.ReleaseArrowArray>(array->release)(array);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayExporter.ReleaseArrowArray>(array->release)(array);
#endif
Debug.Assert(array->release == default,
"Calling the CArrowArray release func should have
set it to NULL");
diff --git a/src/Apache.Arrow/C/CArrowArrayImporter.cs
b/src/Apache.Arrow/C/CArrowArrayImporter.cs
index a873595..d23ec0c 100644
--- a/src/Apache.Arrow/C/CArrowArrayImporter.cs
+++ b/src/Apache.Arrow/C/CArrowArrayImporter.cs
@@ -123,7 +123,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
cArray->release(cArray);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowArrayExporter.ReleaseArrowArray>(cArray->release)(cArray);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayExporter.ReleaseArrowArray>(cArray->release)(cArray);
#endif
}
}
diff --git a/src/Apache.Arrow/C/CArrowArrayStream.cs
b/src/Apache.Arrow/C/CArrowArrayStream.cs
index a8dbfd4..df089a5 100644
--- a/src/Apache.Arrow/C/CArrowArrayStream.cs
+++ b/src/Apache.Arrow/C/CArrowArrayStream.cs
@@ -110,7 +110,7 @@ namespace Apache.Arrow.C
arrayStream->release(arrayStream);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.ReleaseArrayStream>(arrayStream->release)(arrayStream);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.ReleaseArrayStream>(arrayStream->release)(arrayStream);
#endif
}
Marshal.FreeHGlobal((IntPtr)arrayStream);
diff --git a/src/Apache.Arrow/C/CArrowArrayStreamImporter.cs
b/src/Apache.Arrow/C/CArrowArrayStreamImporter.cs
index bbe7c67..aafb666 100644
--- a/src/Apache.Arrow/C/CArrowArrayStreamImporter.cs
+++ b/src/Apache.Arrow/C/CArrowArrayStreamImporter.cs
@@ -79,7 +79,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
byte* error = arrayStream->get_last_error(arrayStream);
#else
- byte* error =
Marshal.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetLastErrorArrayStream>(arrayStream->get_last_error)(arrayStream);
+ byte* error =
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetLastErrorArrayStream>(arrayStream->get_last_error)(arrayStream);
#endif
if (error == null)
{
@@ -103,7 +103,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
int errno = cArrayStream->get_schema(cArrayStream, &cSchema);
#else
- int errno =
Marshal.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetSchemaArrayStream>(cArrayStream->get_schema)(cArrayStream,
&cSchema);
+ int errno =
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetSchemaArrayStream>(cArrayStream->get_schema)(cArrayStream,
&cSchema);
#endif
if (errno != 0)
{
@@ -141,7 +141,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
int errno = cArrayStream->get_next(cArrayStream, &cArray);
#else
- int errno =
Marshal.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetNextArrayStream>(cArrayStream->get_next)(cArrayStream,
&cArray);
+ int errno =
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.GetNextArrayStream>(cArrayStream->get_next)(cArrayStream,
&cArray);
#endif
if (errno != 0)
{
@@ -166,7 +166,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
cArrayStream->release(cArrayStream);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.ReleaseArrayStream>(cArrayStream->release)(cArrayStream);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowArrayStreamExporter.ReleaseArrayStream>(cArrayStream->release)(cArrayStream);
#endif
}
}
diff --git a/src/Apache.Arrow/C/CArrowDeviceArrayStream.cs
b/src/Apache.Arrow/C/CArrowDeviceArrayStream.cs
index 45461aa..1c7774e 100644
--- a/src/Apache.Arrow/C/CArrowDeviceArrayStream.cs
+++ b/src/Apache.Arrow/C/CArrowDeviceArrayStream.cs
@@ -115,7 +115,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
stream->release(stream);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.ReleaseDeviceArrayStream>(stream->release)(stream);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.ReleaseDeviceArrayStream>(stream->release)(stream);
#endif
}
Marshal.FreeHGlobal((IntPtr)stream);
diff --git a/src/Apache.Arrow/C/CArrowDeviceArrayStreamImporter.cs
b/src/Apache.Arrow/C/CArrowDeviceArrayStreamImporter.cs
index 6536716..e6c4e65 100644
--- a/src/Apache.Arrow/C/CArrowDeviceArrayStreamImporter.cs
+++ b/src/Apache.Arrow/C/CArrowDeviceArrayStreamImporter.cs
@@ -84,7 +84,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
error = stream->get_last_error(stream);
#else
- error =
Marshal.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetLastErrorDeviceArrayStream>(stream->get_last_error)(stream);
+ error =
NativeDelegate.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetLastErrorDeviceArrayStream>(stream->get_last_error)(stream);
#endif
}
@@ -115,7 +115,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
int errno = cDeviceArrayStream->get_schema(cDeviceArrayStream,
&cSchema);
#else
- int errno =
Marshal.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetSchemaDeviceArrayStream>(cDeviceArrayStream->get_schema)(cDeviceArrayStream,
&cSchema);
+ int errno =
NativeDelegate.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetSchemaDeviceArrayStream>(cDeviceArrayStream->get_schema)(cDeviceArrayStream,
&cSchema);
#endif
if (errno != 0)
{
@@ -153,7 +153,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
int errno =
cDeviceArrayStream->get_next(cDeviceArrayStream, &cDeviceArray);
#else
- int errno =
Marshal.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetNextDeviceArrayStream>(cDeviceArrayStream->get_next)(cDeviceArrayStream,
&cDeviceArray);
+ int errno =
NativeDelegate.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.GetNextDeviceArrayStream>(cDeviceArrayStream->get_next)(cDeviceArrayStream,
&cDeviceArray);
#endif
if (errno != 0)
{
@@ -178,7 +178,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
cDeviceArrayStream->release(cDeviceArrayStream);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.ReleaseDeviceArrayStream>(cDeviceArrayStream->release)(cDeviceArrayStream);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowDeviceArrayStreamExporter.ReleaseDeviceArrayStream>(cDeviceArrayStream->release)(cDeviceArrayStream);
#endif
}
}
diff --git a/src/Apache.Arrow/C/CArrowSchema.cs
b/src/Apache.Arrow/C/CArrowSchema.cs
index 50c363b..3695e91 100644
--- a/src/Apache.Arrow/C/CArrowSchema.cs
+++ b/src/Apache.Arrow/C/CArrowSchema.cs
@@ -75,7 +75,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
schema->release(schema);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowSchemaExporter.ReleaseArrowSchema>(schema->release)(schema);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowSchemaExporter.ReleaseArrowSchema>(schema->release)(schema);
#endif
}
Marshal.FreeHGlobal((IntPtr)schema);
diff --git a/src/Apache.Arrow/C/CArrowSchemaImporter.cs
b/src/Apache.Arrow/C/CArrowSchemaImporter.cs
index c92c0fa..dfa5c08 100644
--- a/src/Apache.Arrow/C/CArrowSchemaImporter.cs
+++ b/src/Apache.Arrow/C/CArrowSchemaImporter.cs
@@ -156,7 +156,7 @@ namespace Apache.Arrow.C
#if NET5_0_OR_GREATER
_cSchema->release(_cSchema);
#else
-
Marshal.GetDelegateForFunctionPointer<CArrowSchemaExporter.ReleaseArrowSchema>(_cSchema->release)(_cSchema);
+
NativeDelegate.GetDelegateForFunctionPointer<CArrowSchemaExporter.ReleaseArrowSchema>(_cSchema->release)(_cSchema);
#endif
}
}
diff --git a/src/Apache.Arrow/C/NativeDelegate.cs
b/src/Apache.Arrow/C/NativeDelegate.cs
index 744979e..550a700 100644
--- a/src/Apache.Arrow/C/NativeDelegate.cs
+++ b/src/Apache.Arrow/C/NativeDelegate.cs
@@ -32,4 +32,24 @@ namespace Apache.Arrow.C
public IntPtr Pointer { get; }
}
+
+#if !NET5_0_OR_GREATER
+ internal static class NativeDelegate
+ {
+ /// <summary>
+ /// Gets a delegate of type <typeparamref name="T"/> for calling a C
Data Interface callback.
+ /// </summary>
+ /// <remarks>
+ /// If the function pointer was created in this process from a managed
delegate,
+ /// <see
cref="Marshal.GetDelegateForFunctionPointer{TDelegate}(IntPtr)"/> returns that
original
+ /// delegate cast to <typeparamref name="T"/>, which throws if another
library exported the callback
+ /// using its own delegate type with the same signature. In that case,
wrap the original delegate.
+ /// </remarks>
+ public static T GetDelegateForFunctionPointer<T>(IntPtr ptr) where T :
Delegate
+ {
+ Delegate d = Marshal.GetDelegateForFunctionPointer(ptr, typeof(T));
+ return d as T ?? (T)Delegate.CreateDelegate(typeof(T), d,
"Invoke");
+ }
+ }
+#endif
}
diff --git a/test/Apache.Arrow.Tests/CDataInterfaceForeignDelegateTests.cs
b/test/Apache.Arrow.Tests/CDataInterfaceForeignDelegateTests.cs
new file mode 100644
index 0000000..f060c6f
--- /dev/null
+++ b/test/Apache.Arrow.Tests/CDataInterfaceForeignDelegateTests.cs
@@ -0,0 +1,422 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Apache.Arrow.C;
+using Apache.Arrow.Ipc;
+using Apache.Arrow.Types;
+using Xunit;
+
+namespace Apache.Arrow.Tests
+{
+ /// <summary>
+ /// Tests importing C Data Interface structures whose callbacks were
exported in this
+ /// process by another library, using that library's own delegate types
rather than
+ /// Arrow's internal ones.
+ /// </summary>
+ [Experimental("ArrowDeviceDataApi")]
+ public class CDataInterfaceForeignDelegateTests
+ {
+ private unsafe delegate void ForeignReleaseSchema(CArrowSchema*
schema);
+ private unsafe delegate void ForeignReleaseArray(CArrowArray* array);
+ private unsafe delegate int ForeignGetSchema(CArrowArrayStream*
stream, CArrowSchema* schema);
+ private unsafe delegate int ForeignGetNext(CArrowArrayStream* stream,
CArrowArray* array);
+ private unsafe delegate byte* ForeignGetLastError(CArrowArrayStream*
stream);
+ private unsafe delegate void ForeignReleaseStream(CArrowArrayStream*
stream);
+ private unsafe delegate int
ForeignGetDeviceSchema(CArrowDeviceArrayStream* stream, CArrowSchema* schema);
+ private unsafe delegate int
ForeignGetDeviceNext(CArrowDeviceArrayStream* stream, CArrowDeviceArray* array);
+ private unsafe delegate byte*
ForeignGetDeviceLastError(CArrowDeviceArrayStream* stream);
+ private unsafe delegate void
ForeignReleaseDeviceStream(CArrowDeviceArrayStream* stream);
+
+ private const string ErrorMessage = "foreign stream failure";
+ private const int ErrorCode = 5; // EIO
+
+ // The struct fields are function pointers on .NET 5+ and IntPtr
otherwise; both are pointer-sized.
+ private static unsafe void SetCallback(void* field, IntPtr callback)
+ {
+ *(IntPtr*)field = callback;
+ }
+
+ [Fact]
+ public unsafe void ImportSchema()
+ {
+ bool released = false;
+ ForeignReleaseSchema release = schema =>
+ {
+ released = true;
+ schema->release = default;
+ };
+
+ CArrowSchema* cSchema = CArrowSchema.Create();
+ IntPtr format = Marshal.StringToHGlobalAnsi("n");
+ try
+ {
+ cSchema->format = (byte*)format;
+ SetCallback(&cSchema->release,
Marshal.GetFunctionPointerForDelegate(release));
+
+ ArrowType type = CArrowSchemaImporter.ImportType(cSchema);
+
+ Assert.IsType<NullType>(type);
+ Assert.True(released);
+ }
+ finally
+ {
+ CArrowSchema.Free(cSchema);
+ Marshal.FreeHGlobal(format);
+ GC.KeepAlive(release);
+ }
+ }
+
+ [Fact]
+ public unsafe void ImportArray()
+ {
+ bool released = false;
+ ForeignReleaseArray release = array =>
+ {
+ released = true;
+ array->release = default;
+ };
+
+ CArrowArray* cArray = CArrowArray.Create();
+ try
+ {
+ cArray->length = 3;
+ cArray->null_count = 3;
+ SetCallback(&cArray->release,
Marshal.GetFunctionPointerForDelegate(release));
+
+ using (IArrowArray array =
CArrowArrayImporter.ImportArray(cArray, NullType.Default))
+ {
+ Assert.Equal(3, array.Length);
+ }
+
+ Assert.True(released);
+ }
+ finally
+ {
+ CArrowArray.Free(cArray);
+ GC.KeepAlive(release);
+ }
+ }
+
+ [Fact]
+ public unsafe void CallReleaseFuncOnArray()
+ {
+ bool released = false;
+ ForeignReleaseArray release = array =>
+ {
+ released = true;
+ array->release = default;
+ };
+
+ CArrowArray* cArray = CArrowArray.Create();
+ SetCallback(&cArray->release,
Marshal.GetFunctionPointerForDelegate(release));
+ CArrowArray.Free(cArray);
+
+ Assert.True(released);
+ GC.KeepAlive(release);
+ }
+
+ [Fact]
+ public unsafe void FreeSchema()
+ {
+ bool released = false;
+ ForeignReleaseSchema release = schema =>
+ {
+ released = true;
+ schema->release = default;
+ };
+
+ CArrowSchema* cSchema = CArrowSchema.Create();
+ SetCallback(&cSchema->release,
Marshal.GetFunctionPointerForDelegate(release));
+ CArrowSchema.Free(cSchema);
+
+ Assert.True(released);
+ GC.KeepAlive(release);
+ }
+
+ [Fact]
+ public async Task ImportArrayStream()
+ {
+ using (var foreign = new ForeignArrayStream(failGetNext: false))
+ {
+ using (IArrowArrayStream stream = foreign.Import())
+ {
+ Assert.Equal(foreign.Batch.Schema.FieldsList[0].Name,
stream.Schema.FieldsList[0].Name);
+
+ using (RecordBatch batch = await
stream.ReadNextRecordBatchAsync())
+ {
+ Assert.Equal(foreign.Batch.Length, batch.Length);
+ }
+ Assert.Null(await stream.ReadNextRecordBatchAsync());
+ Assert.False(foreign.Released);
+ }
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ [Fact]
+ public async Task ImportArrayStreamGetsLastError()
+ {
+ using (var foreign = new ForeignArrayStream(failGetNext: true))
+ {
+ using (IArrowArrayStream stream = foreign.Import())
+ {
+ Exception ex = await Assert.ThrowsAsync<Exception>(
+ async () => await stream.ReadNextRecordBatchAsync());
+ Assert.Equal(ErrorMessage, ex.Message);
+ }
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ [Fact]
+ public void FreeArrayStream()
+ {
+ using (var foreign = new ForeignArrayStream(failGetNext: false))
+ {
+ foreign.Free();
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ [Fact]
+ public async Task ImportDeviceArrayStream()
+ {
+ using (var foreign = new ForeignDeviceArrayStream(failGetNext:
false))
+ {
+ using (IArrowArrayStream stream = foreign.Import())
+ {
+ Assert.Equal(foreign.Batch.Schema.FieldsList[0].Name,
stream.Schema.FieldsList[0].Name);
+
+ using (RecordBatch batch = await
stream.ReadNextRecordBatchAsync())
+ {
+ Assert.Equal(foreign.Batch.Length, batch.Length);
+ }
+ Assert.Null(await stream.ReadNextRecordBatchAsync());
+ Assert.False(foreign.Released);
+ }
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ [Fact]
+ public async Task ImportDeviceArrayStreamGetsLastError()
+ {
+ using (var foreign = new ForeignDeviceArrayStream(failGetNext:
true))
+ {
+ using (IArrowArrayStream stream = foreign.Import())
+ {
+ Exception ex = await Assert.ThrowsAsync<Exception>(
+ async () => await stream.ReadNextRecordBatchAsync());
+ Assert.Equal(ErrorMessage, ex.Message);
+ }
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ [Fact]
+ public void FreeDeviceArrayStream()
+ {
+ using (var foreign = new ForeignDeviceArrayStream(failGetNext:
false))
+ {
+ foreign.Free();
+
+ Assert.True(foreign.Released);
+ }
+ }
+
+ private static RecordBatch CreateBatch()
+ {
+ return new RecordBatch.Builder()
+ .Append("ints", false, col => col.Int32(builder =>
builder.AppendRange(new[] { 1, 2, 3 })))
+ .Build();
+ }
+
+ /// <summary>
+ /// A single-batch stream implemented with this test's own delegate
types.
+ /// </summary>
+ private sealed unsafe class ForeignArrayStream : IDisposable
+ {
+ private readonly bool _failGetNext;
+ private readonly IntPtr _error;
+ private readonly ForeignGetSchema _getSchema;
+ private readonly ForeignGetNext _getNext;
+ private readonly ForeignGetLastError _getLastError;
+ private readonly ForeignReleaseStream _release;
+ private CArrowArrayStream* _stream;
+ private bool _batchReturned;
+
+ public ForeignArrayStream(bool failGetNext)
+ {
+ _failGetNext = failGetNext;
+ _error = Marshal.StringToHGlobalAnsi(ErrorMessage);
+ Batch = CreateBatch();
+ _getSchema = (stream, schema) =>
+ {
+ CArrowSchemaExporter.ExportSchema(Batch.Schema, schema);
+ return 0;
+ };
+ _getNext = (stream, array) =>
+ {
+ if (_failGetNext)
+ {
+ return ErrorCode;
+ }
+ if (_batchReturned)
+ {
+ *array = default;
+ }
+ else
+ {
+ CArrowArrayExporter.ExportRecordBatch(Batch, array);
+ _batchReturned = true;
+ }
+ return 0;
+ };
+ _getLastError = stream => (byte*)_error;
+ _release = stream =>
+ {
+ Released = true;
+ stream->release = default;
+ };
+
+ _stream = CArrowArrayStream.Create();
+ Export(_stream);
+ }
+
+ public RecordBatch Batch { get; }
+
+ public bool Released { get; private set; }
+
+ public IArrowArrayStream Import() =>
CArrowArrayStreamImporter.ImportArrayStream(_stream);
+
+ public void Free()
+ {
+ CArrowArrayStream.Free(_stream);
+ _stream = null;
+ }
+
+ private void Export(CArrowArrayStream* stream)
+ {
+ SetCallback(&stream->get_schema,
Marshal.GetFunctionPointerForDelegate(_getSchema));
+ SetCallback(&stream->get_next,
Marshal.GetFunctionPointerForDelegate(_getNext));
+ SetCallback(&stream->get_last_error,
Marshal.GetFunctionPointerForDelegate(_getLastError));
+ SetCallback(&stream->release,
Marshal.GetFunctionPointerForDelegate(_release));
+ }
+
+ public void Dispose()
+ {
+ if (_stream != null)
+ {
+ Free();
+ }
+ Batch.Dispose();
+ Marshal.FreeHGlobal(_error);
+ }
+ }
+
+ /// <summary>
+ /// A single-batch device stream implemented with this test's own
delegate types.
+ /// </summary>
+ private sealed unsafe class ForeignDeviceArrayStream : IDisposable
+ {
+ private readonly bool _failGetNext;
+ private readonly IntPtr _error;
+ private readonly ForeignGetDeviceSchema _getSchema;
+ private readonly ForeignGetDeviceNext _getNext;
+ private readonly ForeignGetDeviceLastError _getLastError;
+ private readonly ForeignReleaseDeviceStream _release;
+ private CArrowDeviceArrayStream* _stream;
+ private bool _batchReturned;
+
+ public ForeignDeviceArrayStream(bool failGetNext)
+ {
+ _failGetNext = failGetNext;
+ _error = Marshal.StringToHGlobalAnsi(ErrorMessage);
+ Batch = CreateBatch();
+ _getSchema = (stream, schema) =>
+ {
+ CArrowSchemaExporter.ExportSchema(Batch.Schema, schema);
+ return 0;
+ };
+ _getNext = (stream, array) =>
+ {
+ if (_failGetNext)
+ {
+ return ErrorCode;
+ }
+ if (_batchReturned)
+ {
+ *array = default;
+ }
+ else
+ {
+ CArrowDeviceArrayExporter.ExportRecordBatch(Batch,
array);
+ _batchReturned = true;
+ }
+ return 0;
+ };
+ _getLastError = stream => (byte*)_error;
+ _release = stream =>
+ {
+ Released = true;
+ stream->release = default;
+ };
+
+ _stream = CArrowDeviceArrayStream.Create();
+ Export(_stream);
+ }
+
+ public RecordBatch Batch { get; }
+
+ public bool Released { get; private set; }
+
+ public IArrowArrayStream Import() =>
CArrowDeviceArrayStreamImporter.ImportDeviceArrayStream(_stream);
+
+ public void Free()
+ {
+ CArrowDeviceArrayStream.Free(_stream);
+ _stream = null;
+ }
+
+ private void Export(CArrowDeviceArrayStream* stream)
+ {
+ stream->device_type = ArrowDeviceType.Cpu;
+ SetCallback(&stream->get_schema,
Marshal.GetFunctionPointerForDelegate(_getSchema));
+ SetCallback(&stream->get_next,
Marshal.GetFunctionPointerForDelegate(_getNext));
+ SetCallback(&stream->get_last_error,
Marshal.GetFunctionPointerForDelegate(_getLastError));
+ SetCallback(&stream->release,
Marshal.GetFunctionPointerForDelegate(_release));
+ }
+
+ public void Dispose()
+ {
+ if (_stream != null)
+ {
+ Free();
+ }
+ Batch.Dispose();
+ Marshal.FreeHGlobal(_error);
+ }
+ }
+ }
+}