Coverage for /usr/lib/python3/dist-packages/numpy/ctypeslib.py: 32%
217 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2============================
3``ctypes`` Utility Functions
4============================
6See Also
7--------
8load_library : Load a C library.
9ndpointer : Array restype/argtype with verification.
10as_ctypes : Create a ctypes array from an ndarray.
11as_array : Create an ndarray from a ctypes array.
13References
14----------
15.. [1] "SciPy Cookbook: ctypes", https://scipy-cookbook.readthedocs.io/items/Ctypes.html
17Examples
18--------
19Load the C library:
21>>> _lib = np.ctypeslib.load_library('libmystuff', '.') #doctest: +SKIP
23Our result type, an ndarray that must be of type double, be 1-dimensional
24and is C-contiguous in memory:
26>>> array_1d_double = np.ctypeslib.ndpointer(
27... dtype=np.double,
28... ndim=1, flags='CONTIGUOUS') #doctest: +SKIP
30Our C-function typically takes an array and updates its values
31in-place. For example::
33 void foo_func(double* x, int length)
34 {
35 int i;
36 for (i = 0; i < length; i++) {
37 x[i] = i*i;
38 }
39 }
41We wrap it using:
43>>> _lib.foo_func.restype = None #doctest: +SKIP
44>>> _lib.foo_func.argtypes = [array_1d_double, c_int] #doctest: +SKIP
46Then, we're ready to call ``foo_func``:
48>>> out = np.empty(15, dtype=np.double)
49>>> _lib.foo_func(out, len(out)) #doctest: +SKIP
51"""
52__all__ = ['load_library', 'ndpointer', 'c_intp', 'as_ctypes', 'as_array',
53 'as_ctypes_type']
55import os
56from numpy import (
57 integer, ndarray, dtype as _dtype, asarray, frombuffer
58)
59from numpy.core.multiarray import _flagdict, flagsobj
61try:
62 import ctypes
63except ImportError:
64 ctypes = None
66if ctypes is None:
67 def _dummy(*args, **kwds):
68 """
69 Dummy object that raises an ImportError if ctypes is not available.
71 Raises
72 ------
73 ImportError
74 If ctypes is not available.
76 """
77 raise ImportError("ctypes is not available.")
78 load_library = _dummy
79 as_ctypes = _dummy
80 as_array = _dummy
81 from numpy import intp as c_intp
82 _ndptr_base = object
83else:
84 import numpy.core._internal as nic
85 c_intp = nic._getintp_ctype()
86 del nic
87 _ndptr_base = ctypes.c_void_p
89 # Adapted from Albert Strasheim
90 def load_library(libname, loader_path):
91 """
92 It is possible to load a library using
94 >>> lib = ctypes.cdll[<full_path_name>] # doctest: +SKIP
96 But there are cross-platform considerations, such as library file extensions,
97 plus the fact Windows will just load the first library it finds with that name.
98 NumPy supplies the load_library function as a convenience.
100 .. versionchanged:: 1.20.0
101 Allow libname and loader_path to take any
102 :term:`python:path-like object`.
104 Parameters
105 ----------
106 libname : path-like
107 Name of the library, which can have 'lib' as a prefix,
108 but without an extension.
109 loader_path : path-like
110 Where the library can be found.
112 Returns
113 -------
114 ctypes.cdll[libpath] : library object
115 A ctypes library object
117 Raises
118 ------
119 OSError
120 If there is no library with the expected extension, or the
121 library is defective and cannot be loaded.
122 """
123 # Convert path-like objects into strings
124 libname = os.fsdecode(libname)
125 loader_path = os.fsdecode(loader_path)
127 ext = os.path.splitext(libname)[1]
128 if not ext:
129 import sys
130 import sysconfig
131 # Try to load library with platform-specific name, otherwise
132 # default to libname.[so|dll|dylib]. Sometimes, these files are
133 # built erroneously on non-linux platforms.
134 base_ext = ".so"
135 if sys.platform.startswith("darwin"):
136 base_ext = ".dylib"
137 elif sys.platform.startswith("win"):
138 base_ext = ".dll"
139 libname_ext = [libname + base_ext]
140 so_ext = sysconfig.get_config_var("EXT_SUFFIX")
141 if not so_ext == base_ext:
142 libname_ext.insert(0, libname + so_ext)
143 else:
144 libname_ext = [libname]
146 loader_path = os.path.abspath(loader_path)
147 if not os.path.isdir(loader_path):
148 libdir = os.path.dirname(loader_path)
149 else:
150 libdir = loader_path
152 for ln in libname_ext:
153 libpath = os.path.join(libdir, ln)
154 if os.path.exists(libpath):
155 try:
156 return ctypes.cdll[libpath]
157 except OSError:
158 ## defective lib file
159 raise
160 ## if no successful return in the libname_ext loop:
161 raise OSError("no file with expected extension")
164def _num_fromflags(flaglist):
165 num = 0
166 for val in flaglist:
167 num += _flagdict[val]
168 return num
170_flagnames = ['C_CONTIGUOUS', 'F_CONTIGUOUS', 'ALIGNED', 'WRITEABLE',
171 'OWNDATA', 'WRITEBACKIFCOPY']
172def _flags_fromnum(num):
173 res = []
174 for key in _flagnames:
175 value = _flagdict[key]
176 if (num & value):
177 res.append(key)
178 return res
181class _ndptr(_ndptr_base):
182 @classmethod
183 def from_param(cls, obj):
184 if not isinstance(obj, ndarray):
185 raise TypeError("argument must be an ndarray")
186 if cls._dtype_ is not None \
187 and obj.dtype != cls._dtype_:
188 raise TypeError("array must have data type %s" % cls._dtype_)
189 if cls._ndim_ is not None \
190 and obj.ndim != cls._ndim_:
191 raise TypeError("array must have %d dimension(s)" % cls._ndim_)
192 if cls._shape_ is not None \
193 and obj.shape != cls._shape_:
194 raise TypeError("array must have shape %s" % str(cls._shape_))
195 if cls._flags_ is not None \
196 and ((obj.flags.num & cls._flags_) != cls._flags_):
197 raise TypeError("array must have flags %s" %
198 _flags_fromnum(cls._flags_))
199 return obj.ctypes
202class _concrete_ndptr(_ndptr):
203 """
204 Like _ndptr, but with `_shape_` and `_dtype_` specified.
206 Notably, this means the pointer has enough information to reconstruct
207 the array, which is not generally true.
208 """
209 def _check_retval_(self):
210 """
211 This method is called when this class is used as the .restype
212 attribute for a shared-library function, to automatically wrap the
213 pointer into an array.
214 """
215 return self.contents
217 @property
218 def contents(self):
219 """
220 Get an ndarray viewing the data pointed to by this pointer.
222 This mirrors the `contents` attribute of a normal ctypes pointer
223 """
224 full_dtype = _dtype((self._dtype_, self._shape_))
225 full_ctype = ctypes.c_char * full_dtype.itemsize
226 buffer = ctypes.cast(self, ctypes.POINTER(full_ctype)).contents
227 return frombuffer(buffer, dtype=full_dtype).squeeze(axis=0)
230# Factory for an array-checking class with from_param defined for
231# use with ctypes argtypes mechanism
232_pointer_type_cache = {}
233def ndpointer(dtype=None, ndim=None, shape=None, flags=None):
234 """
235 Array-checking restype/argtypes.
237 An ndpointer instance is used to describe an ndarray in restypes
238 and argtypes specifications. This approach is more flexible than
239 using, for example, ``POINTER(c_double)``, since several restrictions
240 can be specified, which are verified upon calling the ctypes function.
241 These include data type, number of dimensions, shape and flags. If a
242 given array does not satisfy the specified restrictions,
243 a ``TypeError`` is raised.
245 Parameters
246 ----------
247 dtype : data-type, optional
248 Array data-type.
249 ndim : int, optional
250 Number of array dimensions.
251 shape : tuple of ints, optional
252 Array shape.
253 flags : str or tuple of str
254 Array flags; may be one or more of:
256 - C_CONTIGUOUS / C / CONTIGUOUS
257 - F_CONTIGUOUS / F / FORTRAN
258 - OWNDATA / O
259 - WRITEABLE / W
260 - ALIGNED / A
261 - WRITEBACKIFCOPY / X
263 Returns
264 -------
265 klass : ndpointer type object
266 A type object, which is an ``_ndtpr`` instance containing
267 dtype, ndim, shape and flags information.
269 Raises
270 ------
271 TypeError
272 If a given array does not satisfy the specified restrictions.
274 Examples
275 --------
276 >>> clib.somefunc.argtypes = [np.ctypeslib.ndpointer(dtype=np.float64,
277 ... ndim=1,
278 ... flags='C_CONTIGUOUS')]
279 ... #doctest: +SKIP
280 >>> clib.somefunc(np.array([1, 2, 3], dtype=np.float64))
281 ... #doctest: +SKIP
283 """
285 # normalize dtype to an Optional[dtype]
286 if dtype is not None:
287 dtype = _dtype(dtype)
289 # normalize flags to an Optional[int]
290 num = None
291 if flags is not None:
292 if isinstance(flags, str):
293 flags = flags.split(',')
294 elif isinstance(flags, (int, integer)):
295 num = flags
296 flags = _flags_fromnum(num)
297 elif isinstance(flags, flagsobj):
298 num = flags.num
299 flags = _flags_fromnum(num)
300 if num is None:
301 try:
302 flags = [x.strip().upper() for x in flags]
303 except Exception as e:
304 raise TypeError("invalid flags specification") from e
305 num = _num_fromflags(flags)
307 # normalize shape to an Optional[tuple]
308 if shape is not None:
309 try:
310 shape = tuple(shape)
311 except TypeError:
312 # single integer -> 1-tuple
313 shape = (shape,)
315 cache_key = (dtype, ndim, shape, num)
317 try:
318 return _pointer_type_cache[cache_key]
319 except KeyError:
320 pass
322 # produce a name for the new type
323 if dtype is None:
324 name = 'any'
325 elif dtype.names is not None:
326 name = str(id(dtype))
327 else:
328 name = dtype.str
329 if ndim is not None:
330 name += "_%dd" % ndim
331 if shape is not None:
332 name += "_"+"x".join(str(x) for x in shape)
333 if flags is not None:
334 name += "_"+"_".join(flags)
336 if dtype is not None and shape is not None:
337 base = _concrete_ndptr
338 else:
339 base = _ndptr
341 klass = type("ndpointer_%s"%name, (base,),
342 {"_dtype_": dtype,
343 "_shape_" : shape,
344 "_ndim_" : ndim,
345 "_flags_" : num})
346 _pointer_type_cache[cache_key] = klass
347 return klass
350if ctypes is not None:
351 def _ctype_ndarray(element_type, shape):
352 """ Create an ndarray of the given element type and shape """
353 for dim in shape[::-1]:
354 element_type = dim * element_type
355 # prevent the type name include np.ctypeslib
356 element_type.__module__ = None
357 return element_type
360 def _get_scalar_type_map():
361 """
362 Return a dictionary mapping native endian scalar dtype to ctypes types
363 """
364 ct = ctypes
365 simple_types = [
366 ct.c_byte, ct.c_short, ct.c_int, ct.c_long, ct.c_longlong,
367 ct.c_ubyte, ct.c_ushort, ct.c_uint, ct.c_ulong, ct.c_ulonglong,
368 ct.c_float, ct.c_double,
369 ct.c_bool,
370 ]
371 return {_dtype(ctype): ctype for ctype in simple_types}
374 _scalar_type_map = _get_scalar_type_map()
377 def _ctype_from_dtype_scalar(dtype):
378 # swapping twice ensure that `=` is promoted to <, >, or |
379 dtype_with_endian = dtype.newbyteorder('S').newbyteorder('S')
380 dtype_native = dtype.newbyteorder('=')
381 try:
382 ctype = _scalar_type_map[dtype_native]
383 except KeyError as e:
384 raise NotImplementedError(
385 "Converting {!r} to a ctypes type".format(dtype)
386 ) from None
388 if dtype_with_endian.byteorder == '>':
389 ctype = ctype.__ctype_be__
390 elif dtype_with_endian.byteorder == '<':
391 ctype = ctype.__ctype_le__
393 return ctype
396 def _ctype_from_dtype_subarray(dtype):
397 element_dtype, shape = dtype.subdtype
398 ctype = _ctype_from_dtype(element_dtype)
399 return _ctype_ndarray(ctype, shape)
402 def _ctype_from_dtype_structured(dtype):
403 # extract offsets of each field
404 field_data = []
405 for name in dtype.names:
406 field_dtype, offset = dtype.fields[name][:2]
407 field_data.append((offset, name, _ctype_from_dtype(field_dtype)))
409 # ctypes doesn't care about field order
410 field_data = sorted(field_data, key=lambda f: f[0])
412 if len(field_data) > 1 and all(offset == 0 for offset, name, ctype in field_data):
413 # union, if multiple fields all at address 0
414 size = 0
415 _fields_ = []
416 for offset, name, ctype in field_data:
417 _fields_.append((name, ctype))
418 size = max(size, ctypes.sizeof(ctype))
420 # pad to the right size
421 if dtype.itemsize != size:
422 _fields_.append(('', ctypes.c_char * dtype.itemsize))
424 # we inserted manual padding, so always `_pack_`
425 return type('union', (ctypes.Union,), dict(
426 _fields_=_fields_,
427 _pack_=1,
428 __module__=None,
429 ))
430 else:
431 last_offset = 0
432 _fields_ = []
433 for offset, name, ctype in field_data:
434 padding = offset - last_offset
435 if padding < 0:
436 raise NotImplementedError("Overlapping fields")
437 if padding > 0:
438 _fields_.append(('', ctypes.c_char * padding))
440 _fields_.append((name, ctype))
441 last_offset = offset + ctypes.sizeof(ctype)
444 padding = dtype.itemsize - last_offset
445 if padding > 0:
446 _fields_.append(('', ctypes.c_char * padding))
448 # we inserted manual padding, so always `_pack_`
449 return type('struct', (ctypes.Structure,), dict(
450 _fields_=_fields_,
451 _pack_=1,
452 __module__=None,
453 ))
456 def _ctype_from_dtype(dtype):
457 if dtype.fields is not None:
458 return _ctype_from_dtype_structured(dtype)
459 elif dtype.subdtype is not None:
460 return _ctype_from_dtype_subarray(dtype)
461 else:
462 return _ctype_from_dtype_scalar(dtype)
465 def as_ctypes_type(dtype):
466 r"""
467 Convert a dtype into a ctypes type.
469 Parameters
470 ----------
471 dtype : dtype
472 The dtype to convert
474 Returns
475 -------
476 ctype
477 A ctype scalar, union, array, or struct
479 Raises
480 ------
481 NotImplementedError
482 If the conversion is not possible
484 Notes
485 -----
486 This function does not losslessly round-trip in either direction.
488 ``np.dtype(as_ctypes_type(dt))`` will:
490 - insert padding fields
491 - reorder fields to be sorted by offset
492 - discard field titles
494 ``as_ctypes_type(np.dtype(ctype))`` will:
496 - discard the class names of `ctypes.Structure`\ s and
497 `ctypes.Union`\ s
498 - convert single-element `ctypes.Union`\ s into single-element
499 `ctypes.Structure`\ s
500 - insert padding fields
502 """
503 return _ctype_from_dtype(_dtype(dtype))
506 def as_array(obj, shape=None):
507 """
508 Create a numpy array from a ctypes array or POINTER.
510 The numpy array shares the memory with the ctypes object.
512 The shape parameter must be given if converting from a ctypes POINTER.
513 The shape parameter is ignored if converting from a ctypes array
514 """
515 if isinstance(obj, ctypes._Pointer):
516 # convert pointers to an array of the desired shape
517 if shape is None:
518 raise TypeError(
519 'as_array() requires a shape argument when called on a '
520 'pointer')
521 p_arr_type = ctypes.POINTER(_ctype_ndarray(obj._type_, shape))
522 obj = ctypes.cast(obj, p_arr_type).contents
524 return asarray(obj)
527 def as_ctypes(obj):
528 """Create and return a ctypes object from a numpy array. Actually
529 anything that exposes the __array_interface__ is accepted."""
530 ai = obj.__array_interface__
531 if ai["strides"]:
532 raise TypeError("strided arrays not supported")
533 if ai["version"] != 3:
534 raise TypeError("only __array_interface__ version 3 supported")
535 addr, readonly = ai["data"]
536 if readonly:
537 raise TypeError("readonly arrays unsupported")
539 # can't use `_dtype((ai["typestr"], ai["shape"]))` here, as it overflows
540 # dtype.itemsize (gh-14214)
541 ctype_scalar = as_ctypes_type(ai["typestr"])
542 result_type = _ctype_ndarray(ctype_scalar, ai["shape"])
543 result = result_type.from_address(addr)
544 result.__keep = obj
545 return result