1"""

2============================

3``ctypes`` Utility Functions

4============================

5

6 See Also

7--------

8 load_library : Load a C library.

9 ndpointer : Array restype/argtype with verification.

10 as_ctypes : Create a ctypes array from an ndarray.

11 as_array : Create an ndarray from a ctypes array.

12

13 References

14----------

15.. [1] "SciPy Cookbook: ctypes", https://scipy-cookbook.readthedocs.io/items/Ctypes.html

16

17 Examples

18--------

19 Load the C library:

20

21>>> _lib = np.ctypeslib.load_library('libmystuff', '.') #doctest: +SKIP

22

23 Our result type, an ndarray that must be of type double, be 1-dimensional

24 and is C-contiguous in memory:

25

26>>> array_1d_double = np.ctypeslib.ndpointer(

27... dtype=np.double,

28... ndim=1, flags='CONTIGUOUS') #doctest: +SKIP

29

30 Our C-function typically takes an array and updates its values

31 in-place. For example::

32

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 }

40

41 We wrap it using:

42

43>>> _lib.foo_func.restype = None #doctest: +SKIP

44>>> _lib.foo_func.argtypes = [array_1d_double, c_int] #doctest: +SKIP

45

46 Then, we're ready to call ``foo_func``:

47

48>>> out = np.empty(15, dtype=np.double)

49>>> _lib.foo_func(out, len(out)) #doctest: +SKIP

50

51"""

52__all__ = ['load_library', 'ndpointer', 'c_intp', 'as_ctypes', 'as_array',

53 'as_ctypes_type']

54

55 import os

56 from numpy import (

57 integer, ndarray, dtype as _dtype, asarray, frombuffer

58)

59 from numpy.core.multiarray import _flagdict, flagsobj

60

61 try:

62 import ctypes

63 except ImportError:

64 ctypes = None

65

66 if ctypes is None:

67 def _dummy(*args, **kwds):

68 """

69 Dummy object that raises an ImportError if ctypes is not available.

70

71 Raises

72 ------

73 ImportError

74 If ctypes is not available.

75

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

83 else:

84 import numpy.core._internal as nic

85 c_intp = nic._getintp_ctype()

86 del nic

87 _ndptr_base = ctypes.c_void_p

88

89 # Adapted from Albert Strasheim

90 def load_library(libname, loader_path):

91 """

92 It is possible to load a library using

93

94 >>> lib = ctypes.cdll[<full_path_name>] # doctest: +SKIP

95

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.

99

100 .. versionchanged:: 1.20.0

101 Allow libname and loader_path to take any

102 :term:`python:path-like object`.

103

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.

111

112 Returns

113 -------

114 ctypes.cdll[libpath] : library object

115 A ctypes library object

116

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 if ctypes.__version__ < '1.0.1':

124 import warnings

125 warnings.warn("All features of ctypes interface may not work "

126 "with ctypes < 1.0.1", stacklevel=2)

127

128 # Convert path-like objects into strings

129 libname = os.fsdecode(libname)

130 loader_path = os.fsdecode(loader_path)

131

132 ext = os.path.splitext(libname)[1]

133 if not ext:

134 # Try to load library with platform-specific name, otherwise

135 # default to libname.[so|pyd]. Sometimes, these files are built

136 # erroneously on non-linux platforms.

137 from numpy.distutils.misc_util import get_shared_lib_extension

138 so_ext = get_shared_lib_extension()

139 libname_ext = [libname + so_ext]

140 # mac, windows and linux >= py3.2 shared library and loadable

141 # module have different extensions so try both

142 so_ext2 = get_shared_lib_extension(is_python_ext=True)

143 if not so_ext2 == so_ext:

144 libname_ext.insert(0, libname + so_ext2)

145 else:

146 libname_ext = [libname]

147

148 loader_path = os.path.abspath(loader_path)

149 if not os.path.isdir(loader_path):

150 libdir = os.path.dirname(loader_path)

151 else:

152 libdir = loader_path

153

154 for ln in libname_ext:

155 libpath = os.path.join(libdir, ln)

156 if os.path.exists(libpath):

157 try:

158 return ctypes.cdll[libpath]

159 except OSError:

160 ## defective lib file

161 raise

162 ## if no successful return in the libname_ext loop:

163 raise OSError("no file with expected extension")

164

165

166 def _num_fromflags(flaglist):

167 num = 0

168 for val in flaglist:

169 num += _flagdict[val]

170 return num

171

172_flagnames = ['C_CONTIGUOUS', 'F_CONTIGUOUS', 'ALIGNED', 'WRITEABLE',

173 'OWNDATA', 'WRITEBACKIFCOPY']

174 def _flags_fromnum(num):

175 res = []

176 for key in _flagnames:

177 value = _flagdict[key]

178 if (num & value):

179 res.append(key)

180 return res

181

182

183 class _ndptr(_ndptr_base):

184 @classmethod

185 def from_param(cls, obj):

186 if not isinstance(obj, ndarray):

187 raise TypeError("argument must be an ndarray")

188 if cls._dtype_ is not None \

189 and obj.dtype != cls._dtype_:

190 raise TypeError("array must have data type %s" % cls._dtype_)

191 if cls._ndim_ is not None \

192 and obj.ndim != cls._ndim_:

193 raise TypeError("array must have %d dimension(s)" % cls._ndim_)

194 if cls._shape_ is not None \

195 and obj.shape != cls._shape_:

196 raise TypeError("array must have shape %s" % str(cls._shape_))

197 if cls._flags_ is not None \

198 and ((obj.flags.num & cls._flags_) != cls._flags_):

199 raise TypeError("array must have flags %s" %

200 _flags_fromnum(cls._flags_))

201 return obj.ctypes

202

203

204 class _concrete_ndptr(_ndptr):

205 """

206 Like _ndptr, but with `_shape_` and `_dtype_` specified.

207

208 Notably, this means the pointer has enough information to reconstruct

209 the array, which is not generally true.

210 """

211 def _check_retval_(self):

212 """

213 This method is called when this class is used as the .restype

214 attribute for a shared-library function, to automatically wrap the

215 pointer into an array.

216 """

217 return self.contents

218

219 @property

220 def contents(self):

221 """

222 Get an ndarray viewing the data pointed to by this pointer.

223

224 This mirrors the `contents` attribute of a normal ctypes pointer

225 """

226 full_dtype = _dtype((self._dtype_, self._shape_))

227 full_ctype = ctypes.c_char * full_dtype.itemsize

228 buffer = ctypes.cast(self, ctypes.POINTER(full_ctype)).contents

229 return frombuffer(buffer, dtype=full_dtype).squeeze(axis=0)

230

231

232# Factory for an array-checking class with from_param defined for

233# use with ctypes argtypes mechanism

234_pointer_type_cache = {}

235 def ndpointer(dtype=None, ndim=None, shape=None, flags=None):

236 """

237 Array-checking restype/argtypes.

238

239 An ndpointer instance is used to describe an ndarray in restypes

240 and argtypes specifications. This approach is more flexible than

241 using, for example, ``POINTER(c_double)``, since several restrictions

242 can be specified, which are verified upon calling the ctypes function.

243 These include data type, number of dimensions, shape and flags. If a

244 given array does not satisfy the specified restrictions,

245 a ``TypeError`` is raised.

246

247 Parameters

248 ----------

249 dtype : data-type, optional

250 Array data-type.

251 ndim : int, optional

252 Number of array dimensions.

253 shape : tuple of ints, optional

254 Array shape.

255 flags : str or tuple of str

256 Array flags; may be one or more of:

257

258 - C_CONTIGUOUS / C / CONTIGUOUS

259 - F_CONTIGUOUS / F / FORTRAN

260 - OWNDATA / O

261 - WRITEABLE / W

262 - ALIGNED / A

263 - WRITEBACKIFCOPY / X

264

265 Returns

266 -------

267 klass : ndpointer type object

268 A type object, which is an ``_ndtpr`` instance containing

269 dtype, ndim, shape and flags information.

270

271 Raises

272 ------

273 TypeError

274 If a given array does not satisfy the specified restrictions.

275

276 Examples

277 --------

278 >>> clib.somefunc.argtypes = [np.ctypeslib.ndpointer(dtype=np.float64,

279 ... ndim=1,

280 ... flags='C_CONTIGUOUS')]

281 ... #doctest: +SKIP

282 >>> clib.somefunc(np.array([1, 2, 3], dtype=np.float64))

283 ... #doctest: +SKIP

284

285 """

286

287 # normalize dtype to an Optional[dtype]

288 if dtype is not None:

289 dtype = _dtype(dtype)

290

291 # normalize flags to an Optional[int]

292 num = None

293 if flags is not None:

294 if isinstance(flags, str):

295 flags = flags.split(',')

296 elif isinstance(flags, (int, integer)):

297 num = flags

298 flags = _flags_fromnum(num)

299 elif isinstance(flags, flagsobj):

300 num = flags.num

301 flags = _flags_fromnum(num)

302 if num is None:

303 try:

304 flags = [x.strip().upper() for x in flags]

305 except Exception as e:

306 raise TypeError("invalid flags specification") from e

307 num = _num_fromflags(flags)

308

309 # normalize shape to an Optional[tuple]

310 if shape is not None:

311 try:

312 shape = tuple(shape)

313 except TypeError:

314 # single integer -> 1-tuple

315 shape = (shape,)

316

317 cache_key = (dtype, ndim, shape, num)

318

319 try:

320 return _pointer_type_cache[cache_key]

321 except KeyError:

322 pass

323

324 # produce a name for the new type

325 if dtype is None:

326 name = 'any'

327 elif dtype.names is not None:

328 name = str(id(dtype))

329 else:

330 name = dtype.str

331 if ndim is not None:

332 name += "_%dd" % ndim

333 if shape is not None:

334 name += "_"+"x".join(str(x) for x in shape)

335 if flags is not None:

336 name += "_"+"_".join(flags)

337

338 if dtype is not None and shape is not None:

339 base = _concrete_ndptr

340 else:

341 base = _ndptr

342

343 klass = type("ndpointer_%s"%name, (base,),

344 {"_dtype_": dtype,

345 "_shape_" : shape,

346 "_ndim_" : ndim,

347 "_flags_" : num})

348 _pointer_type_cache[cache_key] = klass

349 return klass

350

351

352 if ctypes is not None:

353 def _ctype_ndarray(element_type, shape):

354 """ Create an ndarray of the given element type and shape """

355 for dim in shape[::-1]:

356 element_type = dim * element_type

357 # prevent the type name include np.ctypeslib

358 element_type.__module__ = None

359 return element_type

360

361

362 def _get_scalar_type_map():

363 """

364 Return a dictionary mapping native endian scalar dtype to ctypes types

365 """

366 ct = ctypes

367 simple_types = [

368 ct.c_byte, ct.c_short, ct.c_int, ct.c_long, ct.c_longlong,

369 ct.c_ubyte, ct.c_ushort, ct.c_uint, ct.c_ulong, ct.c_ulonglong,

370 ct.c_float, ct.c_double,

371 ct.c_bool,

372 ]

373 return {_dtype(ctype): ctype for ctype in simple_types}

374

375

376 _scalar_type_map = _get_scalar_type_map()

377

378

379 def _ctype_from_dtype_scalar(dtype):

380 # swapping twice ensure that `=` is promoted to <, >, or |

381 dtype_with_endian = dtype.newbyteorder('S').newbyteorder('S')

382 dtype_native = dtype.newbyteorder('=')

383 try:

384 ctype = _scalar_type_map[dtype_native]

385 except KeyError as e:

386 raise NotImplementedError(

387 "Converting {!r} to a ctypes type".format(dtype)

388 ) from None

389

390 if dtype_with_endian.byteorder == '>':

391 ctype = ctype.__ctype_be__

392 elif dtype_with_endian.byteorder == '<':

393 ctype = ctype.__ctype_le__

394

395 return ctype

396

397

398 def _ctype_from_dtype_subarray(dtype):

399 element_dtype, shape = dtype.subdtype

400 ctype = _ctype_from_dtype(element_dtype)

401 return _ctype_ndarray(ctype, shape)

402

403

404 def _ctype_from_dtype_structured(dtype):

405 # extract offsets of each field

406 field_data = []

407 for name in dtype.names:

408 field_dtype, offset = dtype.fields[name][:2]

409 field_data.append((offset, name, _ctype_from_dtype(field_dtype)))

410

411 # ctypes doesn't care about field order

412 field_data = sorted(field_data, key=lambda f: f[0])

413

414 if len(field_data) > 1 and all(offset == 0 for offset, name, ctype in field_data):

415 # union, if multiple fields all at address 0

416 size = 0

417 _fields_ = []

418 for offset, name, ctype in field_data:

419 _fields_.append((name, ctype))

420 size = max(size, ctypes.sizeof(ctype))

421

422 # pad to the right size

423 if dtype.itemsize != size:

424 _fields_.append(('', ctypes.c_char * dtype.itemsize))

425

426 # we inserted manual padding, so always `_pack_`

427 return type('union', (ctypes.Union,), dict(

428 _fields_=_fields_,

429 _pack_=1,

430 __module__=None,

431 ))

432 else:

433 last_offset = 0

434 _fields_ = []

435 for offset, name, ctype in field_data:

436 padding = offset - last_offset

437 if padding < 0:

438 raise NotImplementedError("Overlapping fields")

439 if padding > 0:

440 _fields_.append(('', ctypes.c_char * padding))

441

442 _fields_.append((name, ctype))

443 last_offset = offset + ctypes.sizeof(ctype)

444

445

446 padding = dtype.itemsize - last_offset

447 if padding > 0:

448 _fields_.append(('', ctypes.c_char * padding))

449

450 # we inserted manual padding, so always `_pack_`

451 return type('struct', (ctypes.Structure,), dict(

452 _fields_=_fields_,

453 _pack_=1,

454 __module__=None,

455 ))

456

457

458 def _ctype_from_dtype(dtype):

459 if dtype.fields is not None:

460 return _ctype_from_dtype_structured(dtype)

461 elif dtype.subdtype is not None:

462 return _ctype_from_dtype_subarray(dtype)

463 else:

464 return _ctype_from_dtype_scalar(dtype)

465

466

467 def as_ctypes_type(dtype):

468 r"""

469 Convert a dtype into a ctypes type.

470

471 Parameters

472 ----------

473 dtype : dtype

474 The dtype to convert

475

476 Returns

477 -------

478 ctype

479 A ctype scalar, union, array, or struct

480

481 Raises

482 ------

483 NotImplementedError

484 If the conversion is not possible

485

486 Notes

487 -----

488 This function does not losslessly round-trip in either direction.

489

490 ``np.dtype(as_ctypes_type(dt))`` will:

491

492 - insert padding fields

493 - reorder fields to be sorted by offset

494 - discard field titles

495

496 ``as_ctypes_type(np.dtype(ctype))`` will:

497

498 - discard the class names of `ctypes.Structure`\ s and

499 `ctypes.Union`\ s

500 - convert single-element `ctypes.Union`\ s into single-element

501 `ctypes.Structure`\ s

502 - insert padding fields

503

504 """

505 return _ctype_from_dtype(_dtype(dtype))

506

507

508 def as_array(obj, shape=None):

509 """

510 Create a numpy array from a ctypes array or POINTER.

511

512 The numpy array shares the memory with the ctypes object.

513

514 The shape parameter must be given if converting from a ctypes POINTER.

515 The shape parameter is ignored if converting from a ctypes array

516 """

517 if isinstance(obj, ctypes._Pointer):

518 # convert pointers to an array of the desired shape

519 if shape is None:

520 raise TypeError(

521 'as_array() requires a shape argument when called on a '

522 'pointer')

523 p_arr_type = ctypes.POINTER(_ctype_ndarray(obj._type_, shape))

524 obj = ctypes.cast(obj, p_arr_type).contents

525

526 return asarray(obj)

527

528

529 def as_ctypes(obj):

530 """Create and return a ctypes object from a numpy array. Actually

531 anything that exposes the __array_interface__ is accepted."""

532 ai = obj.__array_interface__

533 if ai["strides"]:

534 raise TypeError("strided arrays not supported")

535 if ai["version"] != 3:

536 raise TypeError("only __array_interface__ version 3 supported")

537 addr, readonly = ai["data"]

538 if readonly:

539 raise TypeError("readonly arrays unsupported")

540

541 # can't use `_dtype((ai["typestr"], ai["shape"]))` here, as it overflows

542 # dtype.itemsize (gh-14214)

543 ctype_scalar = as_ctypes_type(ai["typestr"])

544 result_type = _ctype_ndarray(ctype_scalar, ai["shape"])

545 result = result_type.from_address(addr)

546 result.__keep = obj

547 return result