Skip to content

Commit d3f8d64

Browse files
authored
Support pinning of IPythonBuffer (#1411)
* Support pinning of IPythonBuffer * Update from_buffer to accept an IBufferProtocol * Align error messages with CPython * Update after review
1 parent 9503e1d commit d3f8d64

15 files changed

Lines changed: 264 additions & 131 deletions

File tree

Src/IronPython.Modules/_ctypes/ArrayType.cs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,15 @@ public _Array from_address(CodeContext/*!*/ context, BigInteger ptr) {
100100
return res;
101101
}
102102

103-
public _Array from_buffer(CodeContext/*!*/ context, ArrayModule.array array, [DefaultParameterValue(0)] int offset) {
104-
ValidateArraySizes(array, offset, ((INativeType)this).Size);
105-
103+
public _Array from_buffer(CodeContext/*!*/ context, object/*?*/ data, int offset = 0) {
106104
_Array res = (_Array)CreateInstance(context);
107-
IntPtr addr = array.GetArrayAddress();
108-
res.MemHolder = new MemoryHolder(addr.Add(offset), ((INativeType)this).Size);
109-
res.MemHolder.AddObject("ffffffff", array);
105+
res.InitializeFromBuffer(data, offset, ((INativeType)this).Size);
110106
return res;
111107
}
112108

113-
public _Array from_buffer_copy(CodeContext/*!*/ context, [NotNull] IBufferProtocol data, int offset = 0) {
114-
using var buffer = data.GetBuffer();
115-
var span = buffer.AsReadOnlySpan();
116-
var size = ((INativeType)this).Size;
117-
ValidateArraySizes(span.Length, offset, size);
118-
span = span.Slice(offset, size);
119-
109+
public _Array from_buffer_copy(CodeContext/*!*/ context, object/*?*/ data, int offset = 0) {
120110
_Array res = (_Array)CreateInstance(context);
121-
res.MemHolder = new MemoryHolder(size);
122-
res.MemHolder.WriteSpan(0, span);
111+
res.InitializeFromBufferCopy(data, offset, ((INativeType)this).Size);
123112
return res;
124113
}
125114

Src/IronPython.Modules/_ctypes/CData.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
#if FEATURE_CTYPES
88

99
using System;
10+
using System.Buffers;
1011
using System.Collections.Generic;
1112
using System.Diagnostics;
1213

1314
using IronPython.Runtime;
15+
using IronPython.Runtime.Exceptions;
16+
using IronPython.Runtime.Operations;
1417
using IronPython.Runtime.Types;
1518

1619
namespace IronPython.Modules {
@@ -38,7 +41,10 @@ public static partial class CTypes {
3841
public abstract class CData : IBufferProtocol, IPythonBuffer {
3942
internal MemoryHolder MemHolder {
4043
get => _memHolder ?? throw new InvalidOperationException($"{nameof(CData)} object not fully initialized.");
41-
set => _memHolder = value;
44+
set {
45+
_memHolder?.Dispose();
46+
_memHolder = value;
47+
}
4248
}
4349
private MemoryHolder? _memHolder;
4450

@@ -55,13 +61,50 @@ protected CData() { }
5561

5662
internal INativeType NativeType => (INativeType)DynamicHelpers.GetPythonType(this);
5763

58-
public virtual object _objects => MemHolder.Objects;
64+
public virtual object? _objects => MemHolder.Objects;
5965

6066
internal void SetAddress(IntPtr address) {
61-
Debug.Assert(_memHolder == null);
67+
// TODO: Debug.Assert(_memHolder == null); // fails for structures
6268
MemHolder = new MemoryHolder(address, NativeType.Size);
6369
}
6470

71+
internal void InitializeFromBuffer(object? data, int offset, int size) {
72+
var bp = data as IBufferProtocol
73+
?? throw PythonOps.TypeErrorForBadInstance("{0} object does not have the buffer interface", data);
74+
// Python 3.5: PythonOps.TypeErrorForBytesLikeTypeMismatch(data);
75+
76+
IPythonBuffer buffer = bp.GetBuffer(BufferFlags.FullRO);
77+
if (buffer.IsReadOnly) {
78+
buffer.Dispose();
79+
throw PythonOps.TypeError("underlying buffer is not writable");
80+
}
81+
if (!buffer.IsCContiguous()) {
82+
buffer.Dispose();
83+
throw PythonOps.TypeError("underlying buffer is not C contiguous");
84+
}
85+
86+
try {
87+
ValidateArraySizes(buffer.NumBytes(), offset, size);
88+
MemHolder = new MemoryHolder(buffer, offset, size);
89+
} catch {
90+
buffer.Dispose();
91+
throw;
92+
}
93+
MemHolder.AddObject("ffffffff", buffer.Object);
94+
}
95+
96+
internal void InitializeFromBufferCopy(object? data, int offset, int size) {
97+
var bp = data as IBufferProtocol
98+
?? throw PythonOps.TypeErrorForBadInstance("{0} object does not have the buffer interface", data);
99+
// Python 3.5: PythonOps.TypeErrorForBytesLikeTypeMismatch(data);
100+
101+
using IPythonBuffer buffer = bp.GetBuffer();
102+
var span = buffer.AsReadOnlySpan();
103+
ValidateArraySizes(span.Length, offset, size);
104+
MemHolder = new MemoryHolder(size);
105+
MemHolder.WriteSpan(0, span.Slice(offset, size));
106+
}
107+
65108
internal virtual PythonTuple GetBufferInfo()
66109
=> PythonTuple.MakeTuple(NativeType.TypeFormat, 0, PythonTuple.EMPTY);
67110

@@ -70,7 +113,7 @@ internal virtual PythonTuple GetBufferInfo()
70113

71114
IPythonBuffer IBufferProtocol.GetBuffer(BufferFlags flags) => this;
72115

73-
void IDisposable.Dispose() { }
116+
void IDisposable.Dispose() { } // TODO
74117

75118
object IPythonBuffer.Object => this;
76119

@@ -80,6 +123,9 @@ unsafe ReadOnlySpan<byte> IPythonBuffer.AsReadOnlySpan()
80123
unsafe Span<byte> IPythonBuffer.AsSpan()
81124
=> new Span<byte>(MemHolder.UnsafeAddress.ToPointer(), MemHolder.Size);
82125

126+
unsafe MemoryHandle IPythonBuffer.Pin()
127+
=> new MemoryHandle(MemHolder.UnsafeAddress.ToPointer());
128+
83129
int IPythonBuffer.Offset => 0;
84130

85131
[PythonHidden]

Src/IronPython.Modules/_ctypes/MemoryHolder.cs

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
#nullable enable
6+
57
#if FEATURE_CTYPES
68
#pragma warning disable SYSLIB0004 // The Constrained Execution Region (CER) feature is not supported in .NET 5.0.
79

810
using System;
11+
using System.Buffers;
12+
using System.Diagnostics;
913
using System.IO;
1014
using System.Runtime.CompilerServices;
1115
using System.Runtime.ConstrainedExecution;
@@ -19,17 +23,22 @@ namespace IronPython.Modules {
1923
/// A wrapper around allocated memory to ensure it gets released and isn't accessed
2024
/// when it could be finalized.
2125
/// </summary>
22-
internal sealed class MemoryHolder : CriticalFinalizerObject {
26+
internal sealed class MemoryHolder : CriticalFinalizerObject, IDisposable {
2327
private readonly IntPtr _data;
2428
private readonly bool _ownsData;
2529
private readonly int _size;
26-
private PythonDictionary _objects;
27-
#pragma warning disable 414 // TODO: unused field?
28-
private readonly MemoryHolder _parent;
29-
#pragma warning restore 414
30+
private readonly IPythonBuffer? _buffer;
31+
private readonly MemoryHandle _handle;
32+
private bool _disposed;
33+
private PythonDictionary? _objects;
34+
#pragma warning disable IDE0052 // Remove unread private members
35+
// Field not accessed but keeps a reference to the parent holder preventing it from being garbage-collected.
36+
private readonly MemoryHolder? _parent;
37+
#pragma warning restore IDE0052 // Remove unread private members
3038

3139
/// <summary>
3240
/// Creates a new MemoryHolder and allocates a buffer of the specified size.
41+
/// The buffer will be released on a call to <see cref="Dispose"/> or by the finalizer.
3342
/// </summary>
3443
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
3544
public MemoryHolder(int size) {
@@ -49,6 +58,7 @@ public MemoryHolder(int size) {
4958
/// <summary>
5059
/// Creates a new MemoryHolder at the specified address which is not tracked
5160
/// by us and we will never free.
61+
/// Therefore no need to call <see cref="Dispose"/>.
5262
/// </summary>
5363
public MemoryHolder(IntPtr data, int size) {
5464
GC.SuppressFinalize(this);
@@ -59,6 +69,7 @@ public MemoryHolder(IntPtr data, int size) {
5969
/// <summary>
6070
/// Creates a new MemoryHolder at the specified address which will keep alive the
6171
/// parent memory holder.
72+
/// TODO: Dispose() usage? Reference-count the parent?
6273
/// </summary>
6374
public MemoryHolder(IntPtr data, int size, MemoryHolder parent) {
6475
GC.SuppressFinalize(this);
@@ -69,39 +80,43 @@ public MemoryHolder(IntPtr data, int size, MemoryHolder parent) {
6980
}
7081

7182
/// <summary>
72-
/// Gets the address of the held memory. The caller should ensure the MemoryHolder
73-
/// is always alive as long as the address will continue to be accessed.
83+
/// Creates a new MemoryHolder from a Python buffer
84+
/// that will be disposed on a call to <see cref="Dispose"/> or by the finalizer.
7485
/// </summary>
75-
public IntPtr UnsafeAddress {
76-
get {
77-
return _data;
78-
}
86+
public MemoryHolder(IPythonBuffer buffer, int offset, int size) {
87+
if (buffer.IsReadOnly) throw new ArgumentException("Buffer must be writable.");
88+
if (!buffer.IsCContiguous()) throw new ArgumentException("Buffer must be c-contiguous.");
89+
if (size < 0) throw new ArgumentOutOfRangeException(nameof(size), size, $"Non-negative number required.");
90+
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), offset, $"Non-negative number required.");
91+
if (size > buffer.NumBytes() - offset) throw new ArgumentException($"Requested memory block exceeds buffer boundaries.");
92+
93+
_buffer = buffer;
94+
_handle = buffer.Pin();
95+
unsafe { _data = (IntPtr)_handle.Pointer + offset; }
96+
_size = size;
7997
}
8098

81-
public int Size {
82-
get {
83-
return _size;
84-
}
85-
}
99+
/// <summary>
100+
/// Gets the address of the held memory. The caller should ensure the MemoryHolder
101+
/// is always alive and not disposed as long as the address will continue to be accessed.
102+
/// </summary>
103+
public IntPtr UnsafeAddress => !_disposed ? _data : IntPtr.Zero;
104+
105+
public int Size => _size;
86106

87107
/// <summary>
88108
/// Gets a list of objects which need to be kept alive for this MemoryHolder to be
89109
/// remain valid.
90110
/// </summary>
91-
public PythonDictionary Objects {
92-
get {
93-
return _objects;
94-
}
95-
set {
96-
_objects = value;
97-
}
111+
public PythonDictionary? Objects {
112+
get => _objects;
113+
set => _objects = value;
98114
}
99115

100116
internal PythonDictionary EnsureObjects() {
101-
if (_objects == null) {
117+
if (_objects is null) {
102118
Interlocked.CompareExchange(ref _objects, new PythonDictionary(), null);
103119
}
104-
105120
return _objects;
106121
}
107122

@@ -178,15 +193,15 @@ public MemoryHolder ReadMemoryHolder(int offset) {
178193
return new MemoryHolder(res, IntPtr.Size, this);
179194
}
180195

181-
internal Bytes ReadBytes(int offset) {
196+
internal Bytes? ReadBytes(int offset) {
182197
try {
183198
return ReadBytes(_data, offset);
184199
} finally {
185200
GC.KeepAlive(this);
186201
}
187202
}
188203

189-
internal string ReadUnicodeString(int offset) {
204+
internal string? ReadUnicodeString(int offset) {
190205
try {
191206
return Marshal.PtrToStringUni(_data.Add(offset));
192207
} finally {
@@ -214,8 +229,8 @@ internal static Bytes ReadBytes(IntPtr addr, int offset, int length) {
214229
return Bytes.Make(buffer);
215230
}
216231

217-
internal static Bytes ReadBytes(IntPtr addr, int offset) {
218-
if (addr == IntPtr.Zero && offset == 0) return null;
232+
internal static Bytes? ReadBytes(IntPtr addr, int offset) {
233+
if (addr == IntPtr.Zero) return null;
219234

220235
// instead of Marshal.PtrToStringAnsi we do this because
221236
// ptrToStringAnsi gives special treatment to values >= 128.
@@ -305,10 +320,30 @@ public void CopyTo(MemoryHolder/*!*/ destAddress, int writeOffset, int size) {
305320
GC.KeepAlive(this);
306321
}
307322

323+
public void Dispose() {
324+
if (!_disposed) {
325+
_disposed = true;
326+
327+
if (_ownsData) {
328+
Marshal.FreeHGlobal(_data);
329+
}
330+
_handle.Dispose();
331+
_buffer?.Dispose();
332+
333+
GC.SuppressFinalize(this);
334+
}
335+
}
336+
308337
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
309338
~MemoryHolder() {
310-
if (_ownsData) {
311-
Marshal.FreeHGlobal(_data);
339+
if (!_disposed) {
340+
_disposed = true;
341+
342+
if (_ownsData) {
343+
Marshal.FreeHGlobal(_data);
344+
}
345+
try { _handle.Dispose(); } catch { /*ignore*/ }
346+
try { _buffer?.Dispose(); } catch { /*ignore*/ }
312347
}
313348
}
314349
}

Src/IronPython.Modules/_ctypes/SimpleType.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Runtime.InteropServices;
1414

1515
using IronPython.Runtime;
16+
using IronPython.Runtime.Exceptions;
1617
using IronPython.Runtime.Operations;
1718
using IronPython.Runtime.Types;
1819

@@ -131,26 +132,15 @@ public SimpleCData from_address(CodeContext/*!*/ context, IntPtr ptr) {
131132
return res;
132133
}
133134

134-
public SimpleCData from_buffer(ArrayModule.array array, int offset = 0) {
135-
ValidateArraySizes(array, offset, ((INativeType)this).Size);
136-
137-
SimpleCData res = (SimpleCData)CreateInstance(Context.SharedContext);
138-
IntPtr addr = array.GetArrayAddress();
139-
res.MemHolder = new MemoryHolder(addr.Add(offset), ((INativeType)this).Size);
140-
res.MemHolder.AddObject("ffffffff", array);
135+
public SimpleCData from_buffer(CodeContext/*!*/ context, object/*?*/ data, int offset = 0) {
136+
SimpleCData res = (SimpleCData)CreateInstance(context);
137+
res.InitializeFromBuffer(data, offset, ((INativeType)this).Size);
141138
return res;
142139
}
143140

144-
public SimpleCData from_buffer_copy(CodeContext/*!*/ context, [NotNull] IBufferProtocol data, int offset = 0) {
145-
using var buffer = data.GetBuffer();
146-
var span = buffer.AsReadOnlySpan();
147-
var size = ((INativeType)this).Size;
148-
ValidateArraySizes(span.Length, offset, size);
149-
span = span.Slice(offset, size);
150-
141+
public SimpleCData from_buffer_copy(CodeContext/*!*/ context, object/*?*/ data, int offset = 0) {
151142
SimpleCData res = (SimpleCData)CreateInstance(context);
152-
res.MemHolder = new MemoryHolder(size);
153-
res.MemHolder.WriteSpan(0, span);
143+
res.InitializeFromBufferCopy(data, offset, ((INativeType)this).Size);
154144
return res;
155145
}
156146

0 commit comments

Comments
 (0)