Coverage for /usr/lib/python3/dist-packages/numpy/ctypeslib.py: 32%

217 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

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', 'c_intp', 'as_ctypes', 'as_array', 

53 'as_ctypes_type'] 

54 

55import os 

56from numpy import ( 

57 integer, ndarray, dtype as _dtype, asarray, 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 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 

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 # Convert path-like objects into strings 

124 libname = os.fsdecode(libname) 

125 loader_path = os.fsdecode(loader_path) 

126 

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] 

145 

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 

151 

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") 

162 

163 

164def _num_fromflags(flaglist): 

165 num = 0 

166 for val in flaglist: 

167 num += _flagdict[val] 

168 return num 

169 

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 

179 

180 

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 

200 

201 

202class _concrete_ndptr(_ndptr): 

203 """ 

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

205 

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 

216 

217 @property 

218 def contents(self): 

219 """ 

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

221 

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) 

228 

229 

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. 

236 

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. 

244 

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: 

255 

256 - C_CONTIGUOUS / C / CONTIGUOUS 

257 - F_CONTIGUOUS / F / FORTRAN 

258 - OWNDATA / O 

259 - WRITEABLE / W 

260 - ALIGNED / A 

261 - WRITEBACKIFCOPY / X 

262 

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. 

268 

269 Raises 

270 ------ 

271 TypeError 

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

273 

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 

282 

283 """ 

284 

285 # normalize dtype to an Optional[dtype] 

286 if dtype is not None: 

287 dtype = _dtype(dtype) 

288 

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) 

306 

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,) 

314 

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

316 

317 try: 

318 return _pointer_type_cache[cache_key] 

319 except KeyError: 

320 pass 

321 

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) 

335 

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

337 base = _concrete_ndptr 

338 else: 

339 base = _ndptr 

340 

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 

348 

349 

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 

358 

359 

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} 

372 

373 

374 _scalar_type_map = _get_scalar_type_map() 

375 

376 

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 

387 

388 if dtype_with_endian.byteorder == '>': 

389 ctype = ctype.__ctype_be__ 

390 elif dtype_with_endian.byteorder == '<': 

391 ctype = ctype.__ctype_le__ 

392 

393 return ctype 

394 

395 

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) 

400 

401 

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))) 

408 

409 # ctypes doesn't care about field order 

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

411 

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)) 

419 

420 # pad to the right size 

421 if dtype.itemsize != size: 

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

423 

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)) 

439 

440 _fields_.append((name, ctype)) 

441 last_offset = offset + ctypes.sizeof(ctype) 

442 

443 

444 padding = dtype.itemsize - last_offset 

445 if padding > 0: 

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

447 

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 )) 

454 

455 

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) 

463 

464 

465 def as_ctypes_type(dtype): 

466 r""" 

467 Convert a dtype into a ctypes type. 

468 

469 Parameters 

470 ---------- 

471 dtype : dtype 

472 The dtype to convert 

473 

474 Returns 

475 ------- 

476 ctype 

477 A ctype scalar, union, array, or struct 

478 

479 Raises 

480 ------ 

481 NotImplementedError 

482 If the conversion is not possible 

483 

484 Notes 

485 ----- 

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

487 

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

489 

490 - insert padding fields 

491 - reorder fields to be sorted by offset 

492 - discard field titles 

493 

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

495 

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 

501 

502 """ 

503 return _ctype_from_dtype(_dtype(dtype)) 

504 

505 

506 def as_array(obj, shape=None): 

507 """ 

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

509 

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

511 

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 

523 

524 return asarray(obj) 

525 

526 

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") 

538 

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