Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

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

3``ctypes`` Utility Functions 

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

5 

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. 

12 

13References 

14---------- 

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

16 

17Examples 

18-------- 

19Load the C library: 

20 

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

22 

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

24and 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 

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

31in-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 

41We 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 

46Then, 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', 'ctypes_load_library', 

53 'c_intp', 'as_ctypes', 'as_array'] 

54 

55import os 

56from numpy import ( 

57 integer, ndarray, dtype as _dtype, deprecate, array, frombuffer 

58) 

59from numpy.core.multiarray import _flagdict, flagsobj 

60 

61try: 

62 import ctypes 

63except ImportError: 

64 ctypes = None 

65 

66if 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 ctypes_load_library = _dummy 

79 load_library = _dummy 

80 as_ctypes = _dummy 

81 as_array = _dummy 

82 from numpy import intp as c_intp 

83 _ndptr_base = object 

84else: 

85 import numpy.core._internal as nic 

86 c_intp = nic._getintp_ctype() 

87 del nic 

88 _ndptr_base = ctypes.c_void_p 

89 

90 # Adapted from Albert Strasheim 

91 def load_library(libname, loader_path): 

92 """ 

93 It is possible to load a library using 

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 Parameters 

101 ---------- 

102 libname : str 

103 Name of the library, which can have 'lib' as a prefix, 

104 but without an extension. 

105 loader_path : str 

106 Where the library can be found. 

107 

108 Returns 

109 ------- 

110 ctypes.cdll[libpath] : library object 

111 A ctypes library object 

112 

113 Raises 

114 ------ 

115 OSError 

116 If there is no library with the expected extension, or the 

117 library is defective and cannot be loaded. 

118 """ 

119 if ctypes.__version__ < '1.0.1': 

120 import warnings 

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

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

123 

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

125 if not ext: 

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

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

128 # erroneously on non-linux platforms. 

129 from numpy.distutils.misc_util import get_shared_lib_extension 

130 so_ext = get_shared_lib_extension() 

131 libname_ext = [libname + so_ext] 

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

133 # module have different extensions so try both 

134 so_ext2 = get_shared_lib_extension(is_python_ext=True) 

135 if not so_ext2 == so_ext: 

136 libname_ext.insert(0, libname + so_ext2) 

137 else: 

138 libname_ext = [libname] 

139 

140 loader_path = os.path.abspath(loader_path) 

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

142 libdir = os.path.dirname(loader_path) 

143 else: 

144 libdir = loader_path 

145 

146 for ln in libname_ext: 

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

148 if os.path.exists(libpath): 

149 try: 

150 return ctypes.cdll[libpath] 

151 except OSError: 

152 ## defective lib file 

153 raise 

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

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

156 

157 ctypes_load_library = deprecate(load_library, 'ctypes_load_library', 

158 'load_library') 

159 

160def _num_fromflags(flaglist): 

161 num = 0 

162 for val in flaglist: 

163 num += _flagdict[val] 

164 return num 

165 

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

167 'OWNDATA', 'UPDATEIFCOPY', 'WRITEBACKIFCOPY'] 

168def _flags_fromnum(num): 

169 res = [] 

170 for key in _flagnames: 

171 value = _flagdict[key] 

172 if (num & value): 

173 res.append(key) 

174 return res 

175 

176 

177class _ndptr(_ndptr_base): 

178 @classmethod 

179 def from_param(cls, obj): 

180 if not isinstance(obj, ndarray): 

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

182 if cls._dtype_ is not None \ 

183 and obj.dtype != cls._dtype_: 

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

185 if cls._ndim_ is not None \ 

186 and obj.ndim != cls._ndim_: 

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

188 if cls._shape_ is not None \ 

189 and obj.shape != cls._shape_: 

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

191 if cls._flags_ is not None \ 

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

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

194 _flags_fromnum(cls._flags_)) 

195 return obj.ctypes 

196 

197 

198class _concrete_ndptr(_ndptr): 

199 """ 

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

201 

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

203 the array, which is not generally true. 

204 """ 

205 def _check_retval_(self): 

206 """ 

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

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

209 pointer into an array. 

210 """ 

211 return self.contents 

212 

213 @property 

214 def contents(self): 

215 """ 

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

217 

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

219 """ 

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

221 full_ctype = ctypes.c_char * full_dtype.itemsize 

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

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

224 

225 

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

227# use with ctypes argtypes mechanism 

228_pointer_type_cache = {} 

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

230 """ 

231 Array-checking restype/argtypes. 

232 

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

234 and argtypes specifications. This approach is more flexible than 

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

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

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

238 given array does not satisfy the specified restrictions, 

239 a ``TypeError`` is raised. 

240 

241 Parameters 

242 ---------- 

243 dtype : data-type, optional 

244 Array data-type. 

245 ndim : int, optional 

246 Number of array dimensions. 

247 shape : tuple of ints, optional 

248 Array shape. 

249 flags : str or tuple of str 

250 Array flags; may be one or more of: 

251 

252 - C_CONTIGUOUS / C / CONTIGUOUS 

253 - F_CONTIGUOUS / F / FORTRAN 

254 - OWNDATA / O 

255 - WRITEABLE / W 

256 - ALIGNED / A 

257 - WRITEBACKIFCOPY / X 

258 - UPDATEIFCOPY / U 

259 

260 Returns 

261 ------- 

262 klass : ndpointer type object 

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

264 dtype, ndim, shape and flags information. 

265 

266 Raises 

267 ------ 

268 TypeError 

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

270 

271 Examples 

272 -------- 

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

274 ... ndim=1, 

275 ... flags='C_CONTIGUOUS')] 

276 ... #doctest: +SKIP 

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

278 ... #doctest: +SKIP 

279 

280 """ 

281 

282 # normalize dtype to an Optional[dtype] 

283 if dtype is not None: 

284 dtype = _dtype(dtype) 

285 

286 # normalize flags to an Optional[int] 

287 num = None 

288 if flags is not None: 

289 if isinstance(flags, str): 

290 flags = flags.split(',') 

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

292 num = flags 

293 flags = _flags_fromnum(num) 

294 elif isinstance(flags, flagsobj): 

295 num = flags.num 

296 flags = _flags_fromnum(num) 

297 if num is None: 

298 try: 

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

300 except Exception: 

301 raise TypeError("invalid flags specification") 

302 num = _num_fromflags(flags) 

303 

304 # normalize shape to an Optional[tuple] 

305 if shape is not None: 

306 try: 

307 shape = tuple(shape) 

308 except TypeError: 

309 # single integer -> 1-tuple 

310 shape = (shape,) 

311 

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

313 

314 try: 

315 return _pointer_type_cache[cache_key] 

316 except KeyError: 

317 pass 

318 

319 # produce a name for the new type 

320 if dtype is None: 

321 name = 'any' 

322 elif dtype.names is not None: 

323 name = str(id(dtype)) 

324 else: 

325 name = dtype.str 

326 if ndim is not None: 

327 name += "_%dd" % ndim 

328 if shape is not None: 

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

330 if flags is not None: 

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

332 

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

334 base = _concrete_ndptr 

335 else: 

336 base = _ndptr 

337 

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

339 {"_dtype_": dtype, 

340 "_shape_" : shape, 

341 "_ndim_" : ndim, 

342 "_flags_" : num}) 

343 _pointer_type_cache[cache_key] = klass 

344 return klass 

345 

346 

347if ctypes is not None: 

348 def _ctype_ndarray(element_type, shape): 

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

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

351 element_type = dim * element_type 

352 # prevent the type name include np.ctypeslib 

353 element_type.__module__ = None 

354 return element_type 

355 

356 

357 def _get_scalar_type_map(): 

358 """ 

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

360 """ 

361 ct = ctypes 

362 simple_types = [ 

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

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

365 ct.c_float, ct.c_double, 

366 ct.c_bool, 

367 ] 

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

369 

370 

371 _scalar_type_map = _get_scalar_type_map() 

372 

373 

374 def _ctype_from_dtype_scalar(dtype): 

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

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

377 dtype_native = dtype.newbyteorder('=') 

378 try: 

379 ctype = _scalar_type_map[dtype_native] 

380 except KeyError: 

381 raise NotImplementedError( 

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

383 ) 

384 

385 if dtype_with_endian.byteorder == '>': 

386 ctype = ctype.__ctype_be__ 

387 elif dtype_with_endian.byteorder == '<': 

388 ctype = ctype.__ctype_le__ 

389 

390 return ctype 

391 

392 

393 def _ctype_from_dtype_subarray(dtype): 

394 element_dtype, shape = dtype.subdtype 

395 ctype = _ctype_from_dtype(element_dtype) 

396 return _ctype_ndarray(ctype, shape) 

397 

398 

399 def _ctype_from_dtype_structured(dtype): 

400 # extract offsets of each field 

401 field_data = [] 

402 for name in dtype.names: 

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

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

405 

406 # ctypes doesn't care about field order 

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

408 

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

410 # union, if multiple fields all at address 0 

411 size = 0 

412 _fields_ = [] 

413 for offset, name, ctype in field_data: 

414 _fields_.append((name, ctype)) 

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

416 

417 # pad to the right size 

418 if dtype.itemsize != size: 

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

420 

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

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

423 _fields_=_fields_, 

424 _pack_=1, 

425 __module__=None, 

426 )) 

427 else: 

428 last_offset = 0 

429 _fields_ = [] 

430 for offset, name, ctype in field_data: 

431 padding = offset - last_offset 

432 if padding < 0: 

433 raise NotImplementedError("Overlapping fields") 

434 if padding > 0: 

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

436 

437 _fields_.append((name, ctype)) 

438 last_offset = offset + ctypes.sizeof(ctype) 

439 

440 

441 padding = dtype.itemsize - last_offset 

442 if padding > 0: 

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

444 

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

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

447 _fields_=_fields_, 

448 _pack_=1, 

449 __module__=None, 

450 )) 

451 

452 

453 def _ctype_from_dtype(dtype): 

454 if dtype.fields is not None: 

455 return _ctype_from_dtype_structured(dtype) 

456 elif dtype.subdtype is not None: 

457 return _ctype_from_dtype_subarray(dtype) 

458 else: 

459 return _ctype_from_dtype_scalar(dtype) 

460 

461 

462 def as_ctypes_type(dtype): 

463 r""" 

464 Convert a dtype into a ctypes type. 

465 

466 Parameters 

467 ---------- 

468 dtype : dtype 

469 The dtype to convert 

470 

471 Returns 

472 ------- 

473 ctype 

474 A ctype scalar, union, array, or struct 

475 

476 Raises 

477 ------ 

478 NotImplementedError 

479 If the conversion is not possible 

480 

481 Notes 

482 ----- 

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

484 

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

486 

487 - insert padding fields 

488 - reorder fields to be sorted by offset 

489 - discard field titles 

490 

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

492 

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

494 `ctypes.Union`\ s 

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

496 `ctypes.Structure`\ s 

497 - insert padding fields 

498 

499 """ 

500 return _ctype_from_dtype(_dtype(dtype)) 

501 

502 

503 def as_array(obj, shape=None): 

504 """ 

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

506 

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

508 

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

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

511 """ 

512 if isinstance(obj, ctypes._Pointer): 

513 # convert pointers to an array of the desired shape 

514 if shape is None: 

515 raise TypeError( 

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

517 'pointer') 

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

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

520 

521 return array(obj, copy=False) 

522 

523 

524 def as_ctypes(obj): 

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

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

527 ai = obj.__array_interface__ 

528 if ai["strides"]: 

529 raise TypeError("strided arrays not supported") 

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

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

532 addr, readonly = ai["data"] 

533 if readonly: 

534 raise TypeError("readonly arrays unsupported") 

535 

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

537 # dtype.itemsize (gh-14214) 

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

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

540 result = result_type.from_address(addr) 

541 result.__keep = obj 

542 return result