diff --git a/docs/reference/index.md b/docs/reference/index.md index e6303eba..0b3a554f 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -131,6 +131,7 @@ NumPy-specific typing utilities (requires NumPy): - **[UFunc](numpy/ufunc.md)**: Universal functions - **[Type Aliases](numpy/aliases.md)**: Common type aliases - **[Low-level](numpy/low-level.md)**: Low-level NumPy interfaces +- **[ctypeslib](numpy/ctypeslib.md)**: Typed `ctypes` aliases ## Type Variance Notation diff --git a/docs/reference/numpy/ctypeslib.md b/docs/reference/numpy/ctypeslib.md new file mode 100644 index 00000000..13610d2d --- /dev/null +++ b/docs/reference/numpy/ctypeslib.md @@ -0,0 +1,228 @@ +# `ctypeslib` submodule + +Typed `ctypes` aliases for NumPy scalar-like C types. + +This module is named after [`numpy.ctypeslib`][np-ctypeslib], and follows the +dtype-to-`ctypes` correspondence that +[`np.ctypeslib.as_ctypes_type`][as_ctypes_type] implements. + +`optype.numpy.ctypeslib` re-exports standard-library `ctypes` constructors under +names that mirror NumPy's scalar naming — `Int32` for the `ctypes` counterpart of +`np.int32`, `LongDouble` for `np.longdouble`, and so on — and adds abstract type +aliases mirroring NumPy's scalar hierarchy (`SignedInteger`, `Inexact`, +`Generic`, …) for use in annotations. + +The concrete names are plain re-exports: they *are* the `ctypes` types, so they +can be instantiated and passed to `argtypes`/`restype` as usual. The abstract +names are typing-only. + +Throughout this page, `ctypes` is imported as `ct`: + +```python +import ctypes as ct +``` + +The module assumes a C99-compatible compiler, a 32- or 64-bit system, and an +[`ILP32`, `LLP64`, or `LP64` data model][data-models]. If that isn't the case +for your platform, please [open an issue][issues]. + +[np-ctypeslib]: https://numpy.org/doc/stable/reference/routines.ctypeslib.html +[as_ctypes_type]: https://numpy.org/doc/stable/reference/routines.ctypeslib.html#numpy.ctypeslib.as_ctypes_type +[data-models]: https://en.cppreference.com/w/c/language/arithmetic_types +[issues]: https://github.com/jorenham/optype/issues + +!!! note + + `Generic` shadows `typing.Generic`, and `Array` means something different + here than it does in the parent `optype.numpy` namespace. Both are + intentional — the names mirror `np.generic` and `np.ndarray` — and are safe + as long as the module is imported as a namespace rather than star-imported. + +## Concrete types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
aliasctypesNumPy analoguekind
Boolc_boolnp.bool_boolean
Int8c_int8np.int8fixed-width integer
UInt8c_uint8np.uint8fixed-width integer
Int16c_int16np.int16fixed-width integer
UInt16c_uint16np.uint16fixed-width integer
Int32c_int32np.int32fixed-width integer
UInt32c_uint32np.uint32fixed-width integer
Int64c_int64np.int64fixed-width integer
UInt64c_uint64np.uint64fixed-width integer
Bytec_bytenp.byteC-native integer
UBytec_ubytenp.ubyteC-native integer
Shortc_shortnp.shortC-native integer
UShortc_ushortnp.ushortC-native integer
IntCc_intnp.intcC-native integer
UIntCc_uintnp.uintcC-native integer
IntPc_ssize_tnp.intpC-native integer
UIntPc_size_tnp.uintpC-native integer
Longc_longnp.longC-native integer
ULongc_ulongnp.ulongC-native integer
LongLongc_longlongnp.longlongC-native integer
ULongLongc_ulonglongnp.ulonglongC-native integer
Float32c_floatnp.float32floating-point
Float64c_doublenp.float64floating-point
LongDoublec_longdoublenp.longdoublefloating-point
Complex64c_float_complexnp.complex64complex floating-point
Complex128c_double_complexnp.complex128complex floating-point
CLongDoublec_longdouble_complexnp.clongdoublecomplex floating-point
Bytesc_charnp.bytes_character/byte
Objectpy_objectnp.object_Python object reference
+ +### Aliased names + +Unlike NumPy, `ctypes` has no distinct fixed-width types. `c_int8` *is* +`c_byte`, `c_int16` *is* `c_short`, and the rest are resolved by `ctypes` at +import time from the platform's type sizes. A type checker cannot distinguish +names that resolve to the same class: + + + + + + + + + + + + + +
aliasILP32LP64LLP64
Int8ByteByteByte
UInt8UByteUByteUByte
Int16ShortShortShort
UInt16UShortUShortUShort
Int32IntC = LongIntCIntC = Long
UInt32UIntC = ULongUIntCUIntC = ULong
Int64LongLongLong = LongLongLongLong
UInt64ULongLongULong = ULongLongULongLong
IntPIntC = LongLong = LongLongLongLong
UIntPUIntC = ULongULong = ULongLongULongLong
+ +`ctypes` aliases `c_int` to `c_long` when the two have equal size, and +`c_longlong` to `c_long` likewise. So on `LP64`, `Long` **is** `LongLong`; on +`ILP32` and `LLP64`, `IntC` **is** `Long`. There is no data model on which all +three are distinct. + +Annotating with either name of a pair is equally correct. The pairs exist so +code can be written in whichever vocabulary — NumPy's or C's — reads better at +the call site. + +### `LongDouble` + +`ctypes` collapses `c_longdouble` into `c_double` whenever the two have the same +size, so `LongDouble` *is* `Float64` on MSVC and on arm64 macOS. + +`c_longdouble` also works only as a *type*, not as a value: its `.value` is a +Python `float`, i.e. a C `double`, so it cannot carry the extra precision of an +80- or 128-bit long double. Use it in `argtypes`/`restype` and in annotations; +don't use it to hold values. + +### Complex types + +`c_float_complex`, `c_double_complex`, and `c_longdouble_complex` were added in +Python 3.14 and are not available on Windows. + +Where they are unavailable — on Windows, or on any Python before 3.14 — +`Complex64`, `Complex128`, and `CLongDouble` are still importable but are +aliases of `Never`. They are therefore uninhabited: any value assigned to them +is a type error, and unions containing them silently drop those members. + +### NumPy version notes + +`UIntP` is `c_size_t`. On `numpy < 2`, `np.uintp` was `c_void_p` rather than +`c_size_t`. The two have the same width on every supported data model and are +almost always equivalent, but they remain distinct classes with different +`.value` semantics: `c_void_p(0).value` is `None`, where `c_size_t(0).value` +is `0`. + +`np.long` and `np.ulong` are the NumPy 2.0 names for the C `long` types, and do +not exist under that spelling on `numpy < 2`. + +### Types with no `ctypes` equivalent + +`np.float16`, `np.str_`, `np.datetime64`, and `np.timedelta64` have no `ctypes` +counterpart and are deliberately absent. `c_wchar` is likewise not exposed, +since NumPy maps no dtype onto it. + +`Bytes` maps `np.bytes_` onto `c_char`, following +[`np.ctypeslib.as_ctypes_type`][as_ctypes_type]. Note that `c_char` is a single +byte whereas `np.bytes_` is variable-length; the correspondence is with the +dtype's element type, not its length. + +## Abstract type aliases + +`CType` and `CScalar` correspond to the private `ctypes` base classes that every +C type derives from. They exist at runtime, but neither is importable from +`ctypes` by name, and neither is meant to be instantiated — use them in +annotations. + + + + + + +
aliasdefinition
CTypect._CData
CScalar[T]ct._SimpleCData[T]
Array[CT: CType]ct.Array[CT] | ct.Array[Array[CT]]
+ +`Array` is recursive, so it matches arbitrarily nested `ctypes` arrays — +`c_int * 3`, `c_int * 3 * 4`, and deeper. + +The remaining aliases mirror the `np.generic` hierarchy: + + + + + + + + + + + + + +
aliasC types
SignedIntegerInt8 | Int16 | Int32 | Int64 | Short | IntC | IntP | Long | LongLong
UnsignedIntegerUInt8 | UInt16 | UInt32 | UInt64 | UShort | UIntC | UIntP | ULong | ULongLong
IntegerSignedInteger | UnsignedInteger
FloatingFloat32 | Float64 | LongDouble
ComplexFloatingComplex64 | Complex128 | CLongDouble
InexactFloating | ComplexFloating
NumberInteger | Inexact
Voidct.Structure | ct.Union
FlexibleBytes | Void
GenericBool | Number | Flexible | Object
+ +`Byte` and `UByte` are absent from the two integer unions because they are +aliases of `Int8` and `UInt8`; including them would be redundant. + +### How the numeric aliases are defined + +`Integer`, `Floating`, and `ComplexFloating` are not literally spelled as the +unions above. They are defined as `CScalar[int]`, `CScalar[float]`, and +`CScalar[complex]`, and `Inexact`/`Number` are built from those. + +`ct._SimpleCData` is **invariant** in its type parameter, so `CScalar[int]` +resolves to exactly the C integer types listed above — and, unlike a literal +union, also admits any third-party `_SimpleCData[int]` subclass. The unions in +the table describe what these aliases match among the types in this module. + +Invariance is also why `Bool` is listed separately in `Generic`: `c_bool` is a +`_SimpleCData[bool]`, and `bool` is not `int` under invariance, so `Bool` is not +covered by `Number`. + +## Example + +```python +import ctypes as ct + +import numpy as np +import optype.numpy.ctypeslib as opct + +# the concrete names are the `ctypes` types themselves +assert opct.Int32 is ct.c_int32 + +buf = (opct.Int32 * 4)(1, 2, 3, 4) +arr = np.ctypeslib.as_array(buf) +assert arr.dtype == np.int32 +``` + +`ctypes` converts simple return types to Python objects on call, so these names +belong on `argtypes`/`restype`, not on the Python-side return annotation: + +```python +lib = ct.CDLL("libsum.so") + +# int64_t sum_i32(const int32_t *values, int n) +lib.sum_i32.argtypes = [ct.POINTER(opct.Int32), opct.IntC] +lib.sum_i32.restype = opct.Int64 + + +def zeroed[T: opct.Number](ctype: type[T], n: int) -> opct.Array[T]: + return (ctype * n)() +``` diff --git a/zensical.toml b/zensical.toml index a3dd3a0c..4d3cb52b 100644 --- a/zensical.toml +++ b/zensical.toml @@ -27,7 +27,7 @@ extra_javascript = ["scripts/posthog.js"] "reference/index.md", { "Core Types" = ["reference/core/just.md", "reference/core/conversion.md", "reference/core/relations.md", "reference/core/binary.md", "reference/core/reflected.md", "reference/core/inplace.md", "reference/core/unary.md", "reference/core/rounding.md", "reference/core/callables.md", "reference/core/iteration.md", "reference/core/awaitables.md", "reference/core/async-iteration.md", "reference/core/containers.md", "reference/core/attributes.md", "reference/core/context.md", "reference/core/descriptors.md", "reference/core/metaclasses.md", "reference/core/buffer.md"] }, { "Standard Library" = ["reference/stdlib/copy.md", "reference/stdlib/dataclasses.md", "reference/stdlib/inspect.md", "reference/stdlib/io.md", "reference/stdlib/json.md", "reference/stdlib/pickle.md", "reference/stdlib/string.md", "reference/stdlib/typing.md", "reference/stdlib/dlpack.md"] }, - { "NumPy" = ["reference/numpy/index.md", "reference/numpy/shape.md", "reference/numpy/array-likes.md", "reference/numpy/literals.md", "reference/numpy/compat.md", "reference/numpy/random.md", "reference/numpy/dtype.md", "reference/numpy/scalar.md", "reference/numpy/ufunc.md", "reference/numpy/aliases.md", "reference/numpy/low-level.md"] }, + { "NumPy" = ["reference/numpy/index.md", "reference/numpy/shape.md", "reference/numpy/array-likes.md", "reference/numpy/literals.md", "reference/numpy/compat.md", "reference/numpy/random.md", "reference/numpy/dtype.md", "reference/numpy/scalar.md", "reference/numpy/ufunc.md", "reference/numpy/aliases.md", "reference/numpy/low-level.md", "reference/numpy/ctypeslib.md"] }, { "Experimental" = ["reference/experimental/infer.md", "reference/experimental/test.md"] }, ]