ctypes-dlpack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ from ._c_api import (
2
+ DLPACK_FLAG_BITMASK_IS_COPIED,
3
+ DLPACK_FLAG_BITMASK_IS_SUBBYTE_TYPE_PADDED,
4
+ DLPACK_FLAG_BITMASK_READ_ONLY,
5
+ DLPACK_MAJOR_VERSION,
6
+ DLPACK_MINOR_VERSION,
7
+ DLDataType,
8
+ DLDataTypeCode,
9
+ DLDevice,
10
+ DLDeviceType,
11
+ DLManagedTensor,
12
+ DLManagedTensorVersioned,
13
+ DLPackCurrentWorkStream,
14
+ DLPackDLTensorFromPyObjectNoSync,
15
+ DLPackExchangeAPI,
16
+ DLPackExchangeAPIHeader,
17
+ DLPackManagedTensorAllocator,
18
+ DLPackManagedTensorFromPyObjectNoSync,
19
+ DLPackManagedTensorToPyObjectNoSync,
20
+ DLPackVersion,
21
+ DLTensor,
22
+ )
23
+ from ._dlpack import DLPackArray, array_as_dlpack
24
+
25
+
26
+ __version__ = "0.1.0"
27
+
28
+
29
+ __all__ = [
30
+ "DLPACK_FLAG_BITMASK_IS_COPIED",
31
+ "DLPACK_FLAG_BITMASK_IS_SUBBYTE_TYPE_PADDED",
32
+ "DLPACK_FLAG_BITMASK_READ_ONLY",
33
+ "DLPACK_MAJOR_VERSION",
34
+ "DLPACK_MINOR_VERSION",
35
+ "DLPackManagedTensorAllocator",
36
+ "DLPackManagedTensorFromPyObjectNoSync",
37
+ "DLPackDLTensorFromPyObjectNoSync",
38
+ "DLPackCurrentWorkStream",
39
+ "DLPackManagedTensorToPyObjectNoSync",
40
+ "DLDeviceType",
41
+ "DLDataTypeCode",
42
+ "DLPackVersion",
43
+ "DLDevice",
44
+ "DLDataType",
45
+ "DLTensor",
46
+ "DLManagedTensor",
47
+ "DLManagedTensorVersioned",
48
+ "DLPackExchangeAPIHeader",
49
+ "DLPackExchangeAPI",
50
+ "DLPackArray",
51
+ "array_as_dlpack",
52
+ ]
53
+
54
+
55
+ for _name in __all__:
56
+ _value = globals()[_name]
57
+ if getattr(_value, "__module__", None) == "ctypes_dlpack._c_api":
58
+ _value.__module__ = __name__
59
+
60
+
61
+ del _name, _value
@@ -0,0 +1,210 @@
1
+ # ruff: noqa
2
+ import ctypes
3
+
4
+ DLPACK_FLAG_BITMASK_IS_COPIED = 1 << 1
5
+ DLPACK_FLAG_BITMASK_IS_SUBBYTE_TYPE_PADDED = 1 << 2
6
+ DLPACK_FLAG_BITMASK_READ_ONLY = 1 << 0
7
+ DLPACK_MAJOR_VERSION = 1
8
+ DLPACK_MINOR_VERSION = 3
9
+
10
+
11
+ class _EnumType(type(ctypes.c_int32)):
12
+ def __new__(metacls, name, bases, namespace):
13
+ if "_members_" not in namespace:
14
+ members = {}
15
+ for key, value in namespace.items():
16
+ if not key.startswith("_"):
17
+ members[key] = value
18
+ namespace["_members_"] = members
19
+ else:
20
+ members = namespace["_members_"]
21
+
22
+ namespace["_reverse_map_"] = {v: k for k, v in members.items()}
23
+ return type(ctypes.c_int32).__new__(metacls, name, bases, namespace)
24
+
25
+ def __repr__(self):
26
+ return f"<Enum {self.__name__}>"
27
+
28
+
29
+ class _Enum(ctypes.c_int32, metaclass=_EnumType):
30
+ _members_ = {}
31
+
32
+ def __repr__(self):
33
+ value_name = self._reverse_map_.get(self.value, str(self.value))
34
+ return f"{self.__class__.__name__}.{value_name}"
35
+
36
+ def __eq__(self, other):
37
+ if isinstance(other, int):
38
+ return self.value == other
39
+ if type(self) is type(other):
40
+ return self.value == other.value
41
+ return NotImplemented
42
+
43
+ def __hash__(self):
44
+ return hash(self.value)
45
+
46
+
47
+ class DLDeviceType(_Enum):
48
+ pass
49
+ kDLCPU = 1
50
+ kDLCUDA = 2
51
+ kDLCUDAHost = 3
52
+ kDLOpenCL = 4
53
+ kDLVulkan = 7
54
+ kDLMetal = 8
55
+ kDLVPI = 9
56
+ kDLROCM = 10
57
+ kDLROCMHost = 11
58
+ kDLExtDev = 12
59
+ kDLCUDAManaged = 13
60
+ kDLOneAPI = 14
61
+ kDLWebGPU = 15
62
+ kDLHexagon = 16
63
+ kDLMAIA = 17
64
+ kDLTrn = 18
65
+
66
+
67
+ class DLDataTypeCode(_Enum):
68
+ pass
69
+ kDLInt = 0
70
+ kDLUInt = 1
71
+ kDLFloat = 2
72
+ kDLOpaqueHandle = 3
73
+ kDLBfloat = 4
74
+ kDLComplex = 5
75
+ kDLBool = 6
76
+ kDLFloat8_e3m4 = 7
77
+ kDLFloat8_e4m3 = 8
78
+ kDLFloat8_e4m3b11fnuz = 9
79
+ kDLFloat8_e4m3fn = 10
80
+ kDLFloat8_e4m3fnuz = 11
81
+ kDLFloat8_e5m2 = 12
82
+ kDLFloat8_e5m2fnuz = 13
83
+ kDLFloat8_e8m0fnu = 14
84
+ kDLFloat6_e2m3fn = 15
85
+ kDLFloat6_e3m2fn = 16
86
+ kDLFloat4_e2m1fn = 17
87
+
88
+
89
+ class DLPackVersion(ctypes.Structure):
90
+ pass
91
+
92
+
93
+ class DLDevice(ctypes.Structure):
94
+ pass
95
+
96
+
97
+ class DLDataType(ctypes.Structure):
98
+ pass
99
+
100
+
101
+ class DLTensor(ctypes.Structure):
102
+ pass
103
+
104
+
105
+ class DLManagedTensor(ctypes.Structure):
106
+ pass
107
+
108
+
109
+ class DLManagedTensorVersioned(ctypes.Structure):
110
+ pass
111
+
112
+
113
+ class DLPackExchangeAPIHeader(ctypes.Structure):
114
+ pass
115
+
116
+
117
+ class DLPackExchangeAPI(ctypes.Structure):
118
+ pass
119
+
120
+
121
+ DLPackManagedTensorAllocator = ctypes.CFUNCTYPE(
122
+ ctypes.c_int,
123
+ ctypes.POINTER(DLTensor),
124
+ ctypes.POINTER(ctypes.POINTER(DLManagedTensorVersioned)),
125
+ ctypes.c_void_p,
126
+ ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p),
127
+ )
128
+
129
+
130
+ DLPackManagedTensorFromPyObjectNoSync = ctypes.CFUNCTYPE(
131
+ ctypes.c_int,
132
+ ctypes.c_void_p,
133
+ ctypes.POINTER(ctypes.POINTER(DLManagedTensorVersioned)),
134
+ )
135
+
136
+
137
+ DLPackDLTensorFromPyObjectNoSync = ctypes.CFUNCTYPE(
138
+ ctypes.c_int,
139
+ ctypes.c_void_p,
140
+ ctypes.POINTER(DLTensor),
141
+ )
142
+
143
+
144
+ DLPackCurrentWorkStream = ctypes.CFUNCTYPE(
145
+ ctypes.c_int,
146
+ DLDeviceType,
147
+ ctypes.c_int32,
148
+ ctypes.POINTER(ctypes.c_void_p),
149
+ )
150
+
151
+
152
+ DLPackManagedTensorToPyObjectNoSync = ctypes.CFUNCTYPE(
153
+ ctypes.c_int,
154
+ ctypes.POINTER(DLManagedTensorVersioned),
155
+ ctypes.POINTER(ctypes.c_void_p),
156
+ )
157
+
158
+ DLPackVersion._fields_ = [
159
+ ("major", ctypes.c_uint32),
160
+ ("minor", ctypes.c_uint32),
161
+ ]
162
+
163
+ DLDevice._fields_ = [
164
+ ("device_type", DLDeviceType),
165
+ ("device_id", ctypes.c_int32),
166
+ ]
167
+
168
+ DLDataType._fields_ = [
169
+ ("code", ctypes.c_uint8),
170
+ ("bits", ctypes.c_uint8),
171
+ ("lanes", ctypes.c_uint16),
172
+ ]
173
+
174
+ DLTensor._fields_ = [
175
+ ("data", ctypes.c_void_p),
176
+ ("device", DLDevice),
177
+ ("ndim", ctypes.c_int32),
178
+ ("dtype", DLDataType),
179
+ ("shape", ctypes.POINTER(ctypes.c_int64)),
180
+ ("strides", ctypes.POINTER(ctypes.c_int64)),
181
+ ("byte_offset", ctypes.c_uint64),
182
+ ]
183
+
184
+ DLManagedTensor._fields_ = [
185
+ ("dl_tensor", DLTensor),
186
+ ("manager_ctx", ctypes.c_void_p),
187
+ ("deleter", ctypes.CFUNCTYPE(None, ctypes.POINTER(DLManagedTensor))),
188
+ ]
189
+
190
+ DLManagedTensorVersioned._fields_ = [
191
+ ("version", DLPackVersion),
192
+ ("manager_ctx", ctypes.c_void_p),
193
+ ("deleter", ctypes.CFUNCTYPE(None, ctypes.POINTER(DLManagedTensorVersioned))),
194
+ ("flags", ctypes.c_uint64),
195
+ ("dl_tensor", DLTensor),
196
+ ]
197
+
198
+ DLPackExchangeAPIHeader._fields_ = [
199
+ ("version", DLPackVersion),
200
+ ("prev_api", ctypes.POINTER(DLPackExchangeAPIHeader)),
201
+ ]
202
+
203
+ DLPackExchangeAPI._fields_ = [
204
+ ("header", DLPackExchangeAPIHeader),
205
+ ("managed_tensor_allocator", DLPackManagedTensorAllocator),
206
+ ("managed_tensor_from_py_object_no_sync", DLPackManagedTensorFromPyObjectNoSync),
207
+ ("managed_tensor_to_py_object_no_sync", DLPackManagedTensorToPyObjectNoSync),
208
+ ("dltensor_from_py_object_no_sync", DLPackDLTensorFromPyObjectNoSync),
209
+ ("current_work_stream", DLPackCurrentWorkStream),
210
+ ]
@@ -0,0 +1,524 @@
1
+ import ctypes
2
+
3
+ from ._c_api import (
4
+ DLDevice,
5
+ DLManagedTensor,
6
+ DLManagedTensorVersioned,
7
+ DLPackVersion,
8
+ DLTensor,
9
+ )
10
+
11
+
12
+ PYTHON_API = ctypes.pythonapi
13
+
14
+ # Configure Python C-API entry points once, then reuse them from hot paths.
15
+ PYTHON_API.PyCapsule_GetPointer.restype = ctypes.c_void_p
16
+ PYTHON_API.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
17
+
18
+ PYTHON_API.PyCapsule_SetName.restype = ctypes.c_int
19
+ PYTHON_API.PyCapsule_SetName.argtypes = [ctypes.py_object, ctypes.c_char_p]
20
+
21
+ PYTHON_API.PyCapsule_GetName.restype = ctypes.c_char_p
22
+ PYTHON_API.PyCapsule_GetName.argtypes = [ctypes.py_object]
23
+
24
+ PYTHON_API.PyMem_RawMalloc.restype = ctypes.c_void_p
25
+ PYTHON_API.PyMem_RawMalloc.argtypes = [ctypes.c_size_t]
26
+
27
+ PYTHON_API.PyMem_RawFree.restype = None
28
+ PYTHON_API.PyMem_RawFree.argtypes = [ctypes.c_void_p]
29
+
30
+ PYTHON_API.PyCapsule_New.restype = ctypes.py_object
31
+ PYTHON_API.PyCapsule_New.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p]
32
+
33
+ PYTHON_API.PyErr_Fetch.restype = None
34
+ PYTHON_API.PyErr_Fetch.argtypes = [
35
+ ctypes.POINTER(ctypes.py_object),
36
+ ctypes.POINTER(ctypes.py_object),
37
+ ctypes.POINTER(ctypes.py_object),
38
+ ]
39
+
40
+ PYTHON_API.PyErr_Clear.restype = None
41
+ PYTHON_API.PyErr_Clear.argtypes = []
42
+
43
+
44
+ def _fetch_pyerr():
45
+ """Detach and return the current C-API exception instance, or ``None``.
46
+
47
+ ``PyErr_Fetch`` clears the thread-local exception state, so the caller is free to
48
+ raise a fresh exception without CPython discarding the original one.
49
+ """
50
+ etype = ctypes.py_object()
51
+ evalue = ctypes.py_object()
52
+ etb = ctypes.py_object()
53
+ PYTHON_API.PyErr_Fetch(ctypes.byref(etype), ctypes.byref(evalue), ctypes.byref(etb))
54
+ return evalue.value or None
55
+
56
+
57
+ # ``PyCapsule_GetName``/``PyCapsule_GetPointer`` re-bound with a raw ``c_void_p``
58
+ # argument. These are needed from inside ``_versioned_capsule_destructor``:
59
+ # CPython invokes the destructor during ``PyCapsule dealloc`` while the capsule
60
+ # refcount is already 0, so the argument cannot be passed as ``py_object``
61
+ # (which would INCREF then DECREF back to 0, recursively re-triggering
62
+ # ``dealloc``). The raw pointer is obtained from the cached symbol address.
63
+ _PyCapsule_GetName_c_void_p = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_void_p)(
64
+ ctypes.cast(PYTHON_API.PyCapsule_GetName, ctypes.c_void_p).value
65
+ )
66
+
67
+ _PyCapsule_GetPointer_c_void_p = ctypes.CFUNCTYPE(
68
+ ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p
69
+ )(ctypes.cast(PYTHON_API.PyCapsule_GetPointer, ctypes.c_void_p).value)
70
+
71
+
72
+ DLPACK_VERSIONED_NAME = b"dltensor_versioned"
73
+ USED_DLPACK_VERSIONED_NAME = b"used_dltensor_versioned"
74
+ DLPACK_NAME = b"dltensor"
75
+ USED_DLPACK_NAME = b"used_dltensor"
76
+
77
+
78
+ def _versioned_capsule_destructor(capsule_ptr):
79
+ """Release the ``DLManagedTensorVersioned`` held by a capsule.
80
+
81
+ This is invoked by CPython when the capsule is garbage collected without having
82
+ been consumed by a ``__dlpack__`` importer (i.e. its name is still
83
+ ``dltensor_versioned``). Capsules renamed to ``used_dltensor_versioned`` have
84
+ already been taken over by an importer and are left untouched.
85
+
86
+ The argument is a raw ``void*`` (the capsule pointer) rather than
87
+ ``py_object`` because CPython invokes the destructor during ``dealloc`` with
88
+ the capsule already at refcount 0: wrapping it as ``py_object`` would INCREF
89
+ then DECREF it back to 0, recursively re-triggering ``dealloc``.
90
+ """
91
+ if not capsule_ptr:
92
+ return
93
+
94
+ name = _PyCapsule_GetName_c_void_p(capsule_ptr)
95
+ if name != DLPACK_VERSIONED_NAME:
96
+ # Clear any C-API exception (e.g. set by ``GetName`` on failure)
97
+ PYTHON_API.PyErr_Clear()
98
+ return
99
+
100
+ ptr = _PyCapsule_GetPointer_c_void_p(capsule_ptr, DLPACK_VERSIONED_NAME)
101
+ if not ptr:
102
+ PYTHON_API.PyErr_Clear()
103
+ return
104
+
105
+ versioned_ptr = ctypes.cast(ptr, ctypes.POINTER(DLManagedTensorVersioned))
106
+ versioned = versioned_ptr.contents
107
+ if versioned.deleter:
108
+ versioned.deleter(versioned_ptr)
109
+
110
+
111
+ # Keep a reference so the CFUNCTYPE is not garbage-collected while CPython holds
112
+ # the destructor pointer inside the capsule.
113
+ _VERSIONED_CAPSULE_DESTRUCTOR_C = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(
114
+ _versioned_capsule_destructor
115
+ )
116
+
117
+
118
+ def _unversioned_capsule_destructor(capsule_ptr):
119
+ """Release the ``DLManagedTensor`` held by a legacy capsule.
120
+
121
+ See ``_versioned_capsule_destructor`` for why the argument is a raw
122
+ ``void*`` instead of ``py_object``.
123
+ """
124
+ if not capsule_ptr:
125
+ return
126
+
127
+ name = _PyCapsule_GetName_c_void_p(capsule_ptr)
128
+ if name != DLPACK_NAME:
129
+ # Clear any C-API exception (see ``_versioned_capsule_destructor``).
130
+ PYTHON_API.PyErr_Clear()
131
+ return
132
+
133
+ ptr = _PyCapsule_GetPointer_c_void_p(capsule_ptr, DLPACK_NAME)
134
+ if not ptr:
135
+ PYTHON_API.PyErr_Clear()
136
+ return
137
+
138
+ unversioned_ptr = ctypes.cast(ptr, ctypes.POINTER(DLManagedTensor))
139
+ unversioned = unversioned_ptr.contents
140
+ if unversioned.deleter:
141
+ unversioned.deleter(unversioned_ptr)
142
+
143
+
144
+ # Keep a reference so the CFUNCTYPE is not garbage-collected while CPython holds
145
+ # the destructor pointer inside the capsule.
146
+ _UNVERSIONED_CAPSULE_DESTRUCTOR_C = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(
147
+ _unversioned_capsule_destructor
148
+ )
149
+
150
+
151
+ @ctypes.CFUNCTYPE(None, ctypes.POINTER(DLManagedTensorVersioned))
152
+ def _versioned_wrapper_deleter(versioned_ptr):
153
+ """Forward to the legacy deleter, then free the wrapper allocation."""
154
+ if not versioned_ptr:
155
+ return
156
+
157
+ versioned = versioned_ptr.contents
158
+ original_ptr = ctypes.cast(versioned.manager_ctx, ctypes.POINTER(DLManagedTensor))
159
+
160
+ if original_ptr and original_ptr.contents.deleter:
161
+ original_ptr.contents.deleter(original_ptr)
162
+
163
+ PYTHON_API.PyMem_RawFree(ctypes.cast(versioned_ptr, ctypes.c_void_p))
164
+
165
+
166
+ @ctypes.CFUNCTYPE(None, ctypes.POINTER(DLManagedTensor))
167
+ def _unversioned_wrapper_deleter(unversioned_ptr):
168
+ """Forward to the versioned deleter, then free the wrapper allocation."""
169
+ if not unversioned_ptr:
170
+ return
171
+
172
+ unversioned = unversioned_ptr.contents
173
+ original_ptr = ctypes.cast(
174
+ unversioned.manager_ctx, ctypes.POINTER(DLManagedTensorVersioned)
175
+ )
176
+
177
+ if original_ptr and original_ptr.contents.deleter:
178
+ original_ptr.contents.deleter(original_ptr)
179
+
180
+ PYTHON_API.PyMem_RawFree(ctypes.cast(unversioned_ptr, ctypes.c_void_p))
181
+
182
+
183
+ def _wrap_unversioned_as_versioned(unversioned_ptr):
184
+ """Wrap a legacy DLManagedTensor pointer in a versioned allocation."""
185
+ versioned_size = ctypes.sizeof(DLManagedTensorVersioned)
186
+ versioned_mem = PYTHON_API.PyMem_RawMalloc(versioned_size)
187
+ if not versioned_mem:
188
+ raise MemoryError("failed to allocate DLManagedTensorVersioned")
189
+
190
+ try:
191
+ versioned_ptr = ctypes.cast(
192
+ versioned_mem, ctypes.POINTER(DLManagedTensorVersioned)
193
+ )
194
+ versioned = versioned_ptr.contents
195
+
196
+ versioned.version = DLPackVersion()
197
+ versioned.version.major = 1
198
+ versioned.version.minor = 0
199
+ versioned.manager_ctx = ctypes.cast(unversioned_ptr, ctypes.c_void_p)
200
+ versioned.deleter = _versioned_wrapper_deleter
201
+ versioned.flags = 0
202
+
203
+ # DLManagedTensor starts with a DLTensor, so copying from the struct base
204
+ # address into versioned.dl_tensor preserves the payload layout.
205
+ ctypes.memmove(
206
+ ctypes.addressof(versioned.dl_tensor),
207
+ unversioned_ptr,
208
+ ctypes.sizeof(DLTensor),
209
+ )
210
+
211
+ return versioned_ptr
212
+ except BaseException:
213
+ PYTHON_API.PyMem_RawFree(versioned_mem)
214
+ raise
215
+
216
+
217
+ def _wrap_versioned_as_unversioned(versioned_ptr):
218
+ """Wrap a ``DLManagedTensorVersioned`` pointer in a legacy allocation.
219
+
220
+ This is used to expose a versioned tensor to importers that only support the
221
+ unversioned DLPack protocol (``max_version`` is ``None`` or has a major
222
+ version of ``0``).
223
+ """
224
+ unversioned_size = ctypes.sizeof(DLManagedTensor)
225
+ unversioned_mem = PYTHON_API.PyMem_RawMalloc(unversioned_size)
226
+ if not unversioned_mem:
227
+ raise MemoryError("failed to allocate DLManagedTensor")
228
+
229
+ try:
230
+ unversioned_ptr = ctypes.cast(unversioned_mem, ctypes.POINTER(DLManagedTensor))
231
+ unversioned = unversioned_ptr.contents
232
+
233
+ unversioned.manager_ctx = ctypes.cast(versioned_ptr, ctypes.c_void_p)
234
+ unversioned.deleter = _unversioned_wrapper_deleter
235
+
236
+ # Both structs embed a ``DLTensor`` with identical layout; copy the
237
+ # payload from the versioned struct into the legacy one.
238
+ ctypes.memmove(
239
+ ctypes.addressof(unversioned.dl_tensor),
240
+ ctypes.addressof(versioned_ptr.contents.dl_tensor),
241
+ ctypes.sizeof(DLTensor),
242
+ )
243
+
244
+ return unversioned_ptr
245
+ except BaseException:
246
+ PYTHON_API.PyMem_RawFree(unversioned_mem)
247
+ raise
248
+
249
+
250
+ def _make_dlpack_versioned_capsule(dl_managed_versioned_ptr):
251
+ ptr_value = ctypes.cast(dl_managed_versioned_ptr, ctypes.c_void_p).value
252
+ if not ptr_value:
253
+ raise ValueError("DLManagedTensorVersioned pointer is null")
254
+
255
+ destructor = ctypes.cast(_VERSIONED_CAPSULE_DESTRUCTOR_C, ctypes.c_void_p)
256
+ return PYTHON_API.PyCapsule_New(ptr_value, DLPACK_VERSIONED_NAME, destructor)
257
+
258
+
259
+ def _make_dlpack_capsule(dl_managed_ptr):
260
+ ptr_value = ctypes.cast(dl_managed_ptr, ctypes.c_void_p).value
261
+ if not ptr_value:
262
+ raise ValueError("DLManagedTensor pointer is null")
263
+
264
+ destructor = ctypes.cast(_UNVERSIONED_CAPSULE_DESTRUCTOR_C, ctypes.c_void_p)
265
+ return PYTHON_API.PyCapsule_New(ptr_value, DLPACK_NAME, destructor)
266
+
267
+
268
+ def _call_dlpack_with_fallbacks(array, stream, max_version, dl_device, copy):
269
+ """Try multiple __dlpack__ signatures for broad compatibility.
270
+
271
+ This tries both the ``2023.12`` array API signature (with ``max_version``,
272
+ ``dl_device``, and ``copy`` kwargs), as well as the legacy signature (with only
273
+ ``stream``). The first signature that does not raise a ``TypeError`` (due to unknown
274
+ keyword arguments) is used, and the resulting capsule is returned.
275
+ """
276
+ last_error = None
277
+
278
+ try:
279
+ return array.__dlpack__(
280
+ stream=stream,
281
+ max_version=max_version,
282
+ dl_device=dl_device,
283
+ copy=copy,
284
+ )
285
+ except TypeError as error:
286
+ last_error = error
287
+
288
+ try:
289
+ return array.__dlpack__(stream=stream)
290
+ except TypeError as error:
291
+ last_error = error
292
+
293
+ try:
294
+ return array.__dlpack__()
295
+ except TypeError as error:
296
+ last_error = error
297
+
298
+ raise RuntimeError("__dlpack__ call failed") from last_error
299
+
300
+
301
+ class DLPackArray:
302
+ """
303
+ Take a ctypes pointer to a ``DLManagedTensorVersioned`` and wrap it in a Python
304
+ object that implements the ``__dlpack__`` protocol. This allows the pointer to be
305
+ passed to other libraries that support DLPack, while ensuring that the pointer is
306
+ only used once and that the underlying memory is freed when the object is garbage
307
+ collected.
308
+
309
+ >>> import numpy as np
310
+ >>> import torch
311
+ >>> # Get the pointer in some way
312
+ >>> pointer = ...
313
+ >>> array = DLPackArray(pointer)
314
+ >>> # the array can be used with `from_dlpack`
315
+ >>> use_numpy = True
316
+ >>> if use_numpy:
317
+ ... np_array = np.from_dlpack(array)
318
+ ... print(np_array.shape)
319
+ ... else:
320
+ ... torch_tensor = torch.from_dlpack(array)
321
+ ... print(torch_tensor.shape)
322
+ """
323
+
324
+ def __init__(self, pointer: ctypes.POINTER(DLManagedTensorVersioned)):
325
+ """
326
+ :param pointer: pointer to a ``DLManagedTensorVersioned`` to be re-exposed as
327
+ Python object following the ``__dlpack__`` protocol.
328
+ """
329
+ if not isinstance(pointer, ctypes.POINTER(DLManagedTensorVersioned)):
330
+ raise TypeError(
331
+ "`pointer` must be a ctypes.POINTER(DLManagedTensorVersioned), "
332
+ f"got {type(pointer).__name__}"
333
+ )
334
+
335
+ if not pointer:
336
+ raise ValueError("`pointer` is null")
337
+
338
+ self._pointer = pointer
339
+
340
+ def __del__(self):
341
+ pointer = getattr(self, "_pointer", None)
342
+ if pointer is None:
343
+ return
344
+
345
+ self._pointer = None
346
+ if pointer.contents.deleter:
347
+ pointer.contents.deleter(pointer)
348
+
349
+ def __dlpack__(
350
+ self,
351
+ stream: int | None = None,
352
+ max_version: tuple[int, int] | None = None,
353
+ dl_device: tuple[int, int] | None = None,
354
+ copy: bool | None = None,
355
+ ):
356
+ pointer = self._pointer
357
+ if pointer is None:
358
+ raise RuntimeError("can not call __dlpack__ twice on DLPackArray")
359
+
360
+ if stream is not None:
361
+ raise RuntimeError("only `stream=None` is currently supported")
362
+
363
+ if dl_device is not None and dl_device != self.__dlpack_device__():
364
+ raise BufferError("device conversion is not supported")
365
+
366
+ if copy is None:
367
+ copy = False
368
+
369
+ if copy:
370
+ raise BufferError("only `copy=False` is supported")
371
+
372
+ if max_version is None or max_version[0] == 0:
373
+ # The caller only supports the unversioned DLPack protocol (or did
374
+ # not request a specific version): wrap our versioned tensor into a
375
+ # legacy ``DLManagedTensor`` capsule.
376
+ unversioned_ptr = _wrap_versioned_as_unversioned(pointer)
377
+ result = _make_dlpack_capsule(unversioned_ptr)
378
+ else:
379
+ result = _make_dlpack_versioned_capsule(pointer)
380
+
381
+ self._pointer = None
382
+ return result
383
+
384
+ def __dlpack_device__(self):
385
+ if self._pointer is None:
386
+ raise RuntimeError("can not query __dlpack_device__ after __dlpack__")
387
+
388
+ device = self._pointer[0].dl_tensor.device
389
+ return (device.device_type.value, device.device_id)
390
+
391
+ def __repr__(self):
392
+ if self._pointer is None:
393
+ return "<DLPackArray (consumed)>"
394
+
395
+ return "<DLPackArray>"
396
+
397
+
398
+ def array_as_dlpack(
399
+ array,
400
+ *,
401
+ stream=None,
402
+ max_version: DLPackVersion | tuple[int, int] | None = None,
403
+ dl_device: DLDevice | tuple[int, int] | None = None,
404
+ copy: bool | None = None,
405
+ ) -> ctypes.POINTER(DLManagedTensorVersioned):
406
+ """
407
+ Get a ctypes pointer to a ``DLManagedTensorVersioned`` struct from any array
408
+ implementing the ``__dlpack__`` protocol. See
409
+ https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__dlpack__.html
410
+ for the protocol specification.
411
+
412
+ This function will try to work with older version of the ``__dlpack__`` protocol,
413
+ wrapping the unversioned ``DLManagedTensor`` returned into a
414
+ ``DLManagedTensorVersioned``. Some arguments might be ignored in this case, since
415
+ they do not have an equivalent.
416
+
417
+ :param array: any array type that implements the ``__dlpack__`` protocol
418
+ :param stream: the CUDA or ROCm synchronization stream, passed as-is to the
419
+ ``__dlpack__`` method of ``array``.
420
+ :param max_version: the maximum DLPack version that the caller of this function
421
+ supports. If this is a ``DLPackVersion``, it is converted to the 2-tuple
422
+ expected by ``__dlpack__``.
423
+ :param dl_device: the DLPack device type. Default is ``None``, meaning the exported
424
+ pointer should be on the same device as ``array``. If this is a ``DLDevice``, it
425
+ is converted to the 2-tuple expected by ``__dlpack__``.
426
+ :param copy: boolean indicating whether or not to copy the input.
427
+ """
428
+
429
+ if max_version is None:
430
+ max_version_tuple = None
431
+ elif isinstance(max_version, DLPackVersion):
432
+ max_version_tuple = (max_version.major, max_version.minor)
433
+ elif (
434
+ isinstance(max_version, tuple)
435
+ and len(max_version) == 2
436
+ and isinstance(max_version[0], int)
437
+ and isinstance(max_version[1], int)
438
+ ):
439
+ max_version_tuple = max_version
440
+ else:
441
+ raise TypeError(
442
+ "`max_version` must be a DLPackVersion, a 2-tuple of ints, or None; "
443
+ f"got {type(max_version).__name__}"
444
+ )
445
+
446
+ if dl_device is None:
447
+ dl_device_tuple = None
448
+ elif isinstance(dl_device, DLDevice):
449
+ dl_device_tuple = (int(dl_device.device_type.value), dl_device.device_id)
450
+ elif (
451
+ isinstance(dl_device, tuple)
452
+ and len(dl_device) == 2
453
+ and isinstance(dl_device[0], int)
454
+ and isinstance(dl_device[1], int)
455
+ ):
456
+ dl_device_tuple = dl_device
457
+ else:
458
+ raise TypeError(
459
+ "`dl_device` must be a DLDevice, a 2-tuple of ints, or None; "
460
+ f"got {type(dl_device).__name__}"
461
+ )
462
+
463
+ capsule = _call_dlpack_with_fallbacks(
464
+ array,
465
+ stream,
466
+ max_version_tuple,
467
+ dl_device_tuple,
468
+ copy,
469
+ )
470
+ capsule_name = PYTHON_API.PyCapsule_GetName(capsule)
471
+
472
+ if capsule_name == DLPACK_VERSIONED_NAME:
473
+ pointer = PYTHON_API.PyCapsule_GetPointer(capsule, DLPACK_VERSIONED_NAME)
474
+ if not pointer:
475
+ err = _fetch_pyerr()
476
+ raise RuntimeError("failed to get pointer from versioned capsule") from err
477
+
478
+ result = ctypes.cast(pointer, ctypes.POINTER(DLManagedTensorVersioned))
479
+
480
+ status = PYTHON_API.PyCapsule_SetName(capsule, USED_DLPACK_VERSIONED_NAME)
481
+ if status != 0:
482
+ err = _fetch_pyerr()
483
+ raise RuntimeError("failed to mark versioned capsule as used") from err
484
+
485
+ elif capsule_name == DLPACK_NAME:
486
+ pointer = PYTHON_API.PyCapsule_GetPointer(capsule, DLPACK_NAME)
487
+ if not pointer:
488
+ err = _fetch_pyerr()
489
+ raise RuntimeError("failed to get pointer from legacy capsule") from err
490
+
491
+ unversioned_ptr = ctypes.cast(pointer, ctypes.POINTER(DLManagedTensor))
492
+ result = _wrap_unversioned_as_versioned(unversioned_ptr)
493
+
494
+ # Only disown the capsule once ``result`` is fully built; if SetName ran
495
+ # earlier and any subsequent step raised, the capsule destructor would
496
+ # see the ``used_`` name and skip the deleter, leaking the source tensor.
497
+ status = PYTHON_API.PyCapsule_SetName(capsule, USED_DLPACK_NAME)
498
+ if status != 0:
499
+ err = _fetch_pyerr()
500
+ raise RuntimeError("failed to mark legacy capsule as used") from err
501
+ else:
502
+ # Unexpected capsule name. If ``GetName`` itself failed (returned NULL)
503
+ # it set a C-API exception which we clear here; otherwise there is
504
+ # nothing to clear.
505
+ PYTHON_API.PyErr_Clear()
506
+ name = capsule_name.decode() if capsule_name else "NULL"
507
+ raise RuntimeError(
508
+ f"unexpected capsule name: {name}; expected 'dltensor' "
509
+ "or 'dltensor_versioned'"
510
+ )
511
+
512
+ actual_device = result.contents.dl_tensor.device
513
+ actual_device = (actual_device.device_type.value, actual_device.device_id)
514
+ if dl_device_tuple is not None and actual_device != dl_device_tuple:
515
+ if result.contents.deleter:
516
+ result.contents.deleter(result)
517
+
518
+ raise ValueError(
519
+ "DLPack device mismatch: "
520
+ f"expected type={dl_device_tuple[0]} id={dl_device_tuple[1]}, "
521
+ f"got type={actual_device[0]} id={actual_device[1]}"
522
+ )
523
+
524
+ return result
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctypes-dlpack
3
+ Version: 0.1.0
4
+ Summary: Ctypes helpers for DLPack interop
5
+ Author-email: Guillaume Fraux <guillaume.fraux@epfl.ch>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: repository, https://github.com/metatensor/ctypes-dlpack
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # ctypes-dlpack
21
+
22
+ <h4 align="center">
23
+
24
+ [![tests status](https://github.com/metatensor/ctypes-dlpack/actions/workflows/tests.yml/badge.svg)](https://github.com/metatensor/ctypes-dlpack/actions?query=branch%3Amain)
25
+ [![documentation](https://img.shields.io/badge/📚_documentation-latest-sucess)](https://docs.metatensor.org/ctypes-dlpack/)
26
+ </h4>
27
+
28
+ The [DLPack](https://dmlc.github.io/dlpack/latest/) standard allows to share
29
+ arrays/tensors data between different frameworks. It is defined as a C API, and
30
+ the standard way to use it from Python is the `__dlpack__` protocol defined in
31
+ the [array
32
+ api](https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__dlpack__.html),
33
+ which relies on `PyCapsule` to pass the pointer around through Python code.
34
+
35
+ However, there might be times where one does not have a `PyCapsule` but just a
36
+ raw pointer to the DLPack C struct. In particular, this might be the case when
37
+ adding bindings to a C library through `ctypes` instead of going through a
38
+ CPython extension module. This library is here to bridge this gap, creating
39
+ ctypes pointers from the `__dlpack__` protocol, and making existing pointers
40
+ compatible with this protocol.
41
+
42
+ This library exposes both the C structs/enums/constants from the DLPack C header
43
+ as `ctypes` types (containing among other the version of the DLPack library
44
+ that was used to generate the ctypes declarations), and a Python API to convert
45
+ between DLPack pointers and Python objects. See the [documentation](http://docs.metatensor.org/ctypes-dlpack/) for more details.
46
+
47
+ You can install the code with `pip install ctypes-dlpack`.
48
+
49
+ ### License and contributions
50
+
51
+ This project is distributed under the terms of the [BSD license](LICENSE), and
52
+ [maintained](https://github.com/lab-cosmo/.github/blob/main/Maintainers.md) by
53
+ [@Luthaf](https://github.com/Luthaf), who will reply to issues and pull requests
54
+ opened on this repository as soon as possible. You can mention them directly if
55
+ you did not receive an answer after a couple of days.
@@ -0,0 +1,8 @@
1
+ ctypes_dlpack/__init__.py,sha256=uUb2KN_4d8_g9XT5nym8HCS4DSLahYVrW6g746WUjdw,1495
2
+ ctypes_dlpack/_c_api.py,sha256=R41osT7m9iwo8bBC-gC6nMexQrGdbhMiX7V_XvrbAr4,4944
3
+ ctypes_dlpack/_dlpack.py,sha256=_dRFVagWaDtBvQ033sPkuJVmKOqdb-K-x8Jo1deoWR0,19207
4
+ ctypes_dlpack-0.1.0.dist-info/licenses/LICENSE,sha256=ePI5b-CYfiIBh1k1S-Mx-pl8PoryJ1cbUq1E94keQgs,1518
5
+ ctypes_dlpack-0.1.0.dist-info/METADATA,sha256=pjXQeW54mtmNdFXENCYhw9YcXi0TEzOOLjxE8HzaXzM,2746
6
+ ctypes_dlpack-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
7
+ ctypes_dlpack-0.1.0.dist-info/top_level.txt,sha256=wjlIXBuJ4vMD4BrUkXvvqTsOunDr-8EB9rDiTzPaPwU,14
8
+ ctypes_dlpack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 Guillaume Fraux and contributors
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ ctypes_dlpack