Skip to content

Commit 67f7551

Browse files
committed
Switch to pybind11 implementation
1 parent ce069df commit 67f7551

File tree

15 files changed

+606
-541
lines changed

15 files changed

+606
-541
lines changed

meson.build

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ incdir_numpy = run_command(py,
1313
],
1414
check: true
1515
).stdout().strip()
16+
incdir_pybind11 = run_command(py,
17+
[
18+
'-c',
19+
'import pybind11; print(pybind11.get_include())'
20+
],
21+
check: true
22+
).stdout().strip()
1623

1724
includes = include_directories(
1825
[
1926
incdir_numpy,
27+
incdir_pybind11,
2028
'src'
2129
]
2230
)
@@ -27,13 +35,17 @@ c_args = [
2735
'-DNPY_NO_DEPRECATED_API=NPY_2_0_API_VERSION',
2836
'-DNPY_TARGET_VERSION=NPY_2_0_API_VERSION',
2937
]
38+
if get_option('buildtype') == 'debug'
39+
# Avoid '_DEBUG' as it links to 'python*_d.lib'
40+
c_args += ['-D__DEBUG']
41+
endif
3042

3143
srcs = [
3244
'src/casts.cpp',
3345
'src/casts.h',
34-
'src/dtype.c',
46+
'src/dtype.cpp',
3547
'src/dtype.h',
36-
'src/main.c',
48+
'src/main.cpp',
3749
]
3850

3951
py.install_sources(
@@ -46,7 +58,7 @@ py.install_sources(
4658
)
4759

4860
py.extension_module(
49-
'_sample_dtypes_main',
61+
'_dtypes_ext',
5062
srcs,
5163
install: true,
5264
subdir: 'sample_dtypes',

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dev = [
3030
requires = [
3131
"meson",
3232
"meson-python",
33+
"pybind11",
3334
"wheel",
3435
"numpy",
3536
]

sample_dtypes/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""NumPy sample Dtypes"""
22

3-
from .scalar import SampleScalar
4-
from ._sample_dtypes_main import SampleDType
3+
from . import _dtypes_ext
4+
from .scalar import ScalarType
55

66
__version__ = "0.0.0" # Keep in sync with pyproject.toml:version
77
__all__ = [
88
"__version__",
99
"SampleDType",
10-
"SampleScalar",
10+
"ScalarType",
1111
]
12+
13+
# At startup create NumPy DType, based on this scalar
14+
SampleDType = _dtypes_ext.init_dtype(ScalarType)
15+
del _dtypes_ext

sample_dtypes/scalar.py

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,85 +4,61 @@
44
import numpy as np
55

66

7-
DEFAULT_SHAPE = (2,) * 2
8-
BYTES_BUF_ORDER = 'A'
7+
DEFAULT_SHAPE = tuple()
98

109

11-
def custom_as_ctypes_type(dtype: np.dtype):
12-
"""Wrapper around `numpy.ctypeslib.as_ctypes_type` that maps 'O' to py_object"""
13-
if not dtype.hasobject:
14-
return np.ctypeslib.as_ctypes_type(dtype)
15-
16-
if dtype.fields is None:
17-
# Simple dtype or array
18-
if dtype.subdtype:
19-
# Handle sub-array types like ('O', 3)
20-
base, shape = dtype.subdtype
21-
new_base = custom_as_ctypes_type(base)
22-
for dim in reversed(shape):
23-
new_base = new_base * dim
24-
return new_base
25-
else:
26-
# Simple object (non-objects are already handled)
27-
assert dtype == np.dtype(object)
28-
return ctypes.py_object
29-
30-
# Structured dtype
31-
new_fields = []
32-
for name, (field_dtype, *_) in dtype.fields.items():
33-
new_field_dtype = custom_as_ctypes_type(field_dtype)
34-
new_fields.append((name, new_field_dtype))
35-
36-
class CustomStruct(ctypes.Structure):
37-
_pack_ = dtype.alignment
38-
_fields_ = new_fields
39-
40-
return CustomStruct
41-
42-
43-
class SampleScalar:
44-
_ndarr: np.ndarray
10+
class ScalarType:
11+
shape: tuple[int, ...]
12+
dtype: np.dtype
4513

4614
def __init__(self, *, shape: tuple[int, ...] = DEFAULT_SHAPE, dtype=None):
47-
print(f'TODO: scalar.py: SampleScalar.__init__({shape=}, {dtype=})')
48-
self._ndarr = np.empty(shape, dtype=dtype)
15+
self.shape = shape
16+
self.dtype = np.dtype(dtype)
4917

50-
def copy(self) -> 'SampleScalar':
51-
return type(self)(shape=self._ndarr.shape, dtype=self._ndarr.dtype)
18+
def __repr__(self) -> str:
19+
return f'{type(self).__name__}(shape={self.shape}, dtype={self.dtype})'
5220

5321
@property
5422
def elsize(self) -> int:
55-
return self._ndarr.nbytes
23+
"""Overall size of DType item (to initialize `PyArray_Descr::elsize`)"""
24+
# Consider @cashed_property
25+
return np.empty(self.shape, self.dtype).nbytes
5626

5727
@property
5828
def alignment(self) -> int:
59-
return self._ndarr.dtype.alignment
29+
"""Alignment of DType item (to initialize `PyArray_Descr::alignment`)"""
30+
return self.dtype.alignment
6031

6132
@property
6233
def hasobject(self) -> bool:
63-
return self._ndarr.dtype.hasobject
34+
"""Whether DType contain pointers to Python objects
6435
65-
def is_compatible(self, other: 'SampleScalar') -> bool:
66-
print(f'scalar.py: SampleScalar.is_compatible({other})')
67-
return self._ndarr.shape == other._ndarr.shape
36+
(adds `PyArray_Descr::flags` like `NPY_ITEM_HASOBJECT` and `NPY_NEEDS_INIT`)
37+
"""
38+
return self.dtype.hasobject
6839

69-
def __repr__(self) -> str:
70-
return f'{type(self).__name__}(id={hex(id(self))}, shape={self._ndarr.shape}, dtype={self._ndarr.dtype})'
40+
def is_compatible(self, other: 'ScalarType') -> bool:
41+
"""Check if both objects are compatible for set/cast operations"""
42+
return self.shape == other.shape
43+
44+
def _create(self) -> 'Scalar':
45+
"""Instantiate Scalar object, that includes actual data storage"""
46+
return Scalar(shape=self.shape, dtype=self.dtype)
7147

7248
def _get_np_view(self, dataptr: int) -> np.ndarray:
7349
"""Create numpy array that uses `dataptr` memory block"""
7450
ct_array = ctypes.cast(
75-
dataptr, ctypes.POINTER(custom_as_ctypes_type(self._ndarr.dtype))
51+
dataptr, ctypes.POINTER(custom_as_ctypes_type(self.dtype))
7652
)
77-
return np.ctypeslib.as_array(ct_array, shape=self._ndarr.shape)
53+
return np.ctypeslib.as_array(ct_array, shape=self.shape)
7854

7955
def _get_strided_np_view(
8056
self, dataptr: int, size: int, stride: int
8157
) -> np.ndarray:
8258
"""Create numpy array that uses strided `dataptr` memory block"""
8359
# Start with ctypes object to cover single SampleScalar
84-
ct_base = custom_as_ctypes_type(self._ndarr.dtype)
85-
for dim in reversed(self._ndarr.shape):
60+
ct_base = custom_as_ctypes_type(self.dtype)
61+
for dim in reversed(self.shape):
8662
ct_base = ct_base * dim
8763

8864
# Single ctypes object to cover whole strided data-block
@@ -101,23 +77,27 @@ class PaddedStruct(ctypes.Structure):
10177
ct_array = ctypes.cast(dataptr, ctypes.POINTER(ct_base))
10278
return np.ctypeslib.as_array(ct_array, shape=())
10379

104-
def setitem(self, src: 'SampleScalar', dataptr: int) -> None:
80+
def setitem(self, src: 'Scalar', dataptr: int) -> int:
10581
"""Python `NPY_DT_setitem` implementation
10682
10783
NOTE:
10884
`dataptr` is a buffer address, valid during execution of the function only
10985
"""
110-
if not self.is_compatible(src):
86+
if not src.is_compatible(self):
11187
raise ValueError('Incompatible item value')
11288

89+
# TODO: Avoid private member access
11390
self._get_np_view(dataptr)[...] = src._ndarr
91+
return 0 # -1 on failure
11492

115-
def getitem(self, dataptr: int) -> 'SampleScalar':
93+
def getitem(self, dataptr: int) -> 'Scalar':
11694
"""Python `NPY_DT_getitem` implementation
11795
118-
NOTE: See setitem()
96+
NOTE:
97+
`dataptr` is a buffer address, valid during execution of the function only
11998
"""
120-
new = self.copy()
99+
new = self._create()
100+
# TODO: Avoid private member access
121101
new._ndarr[...] = self._get_np_view(dataptr)
122102
return new
123103

@@ -126,14 +106,13 @@ def clear_loop(self, data: int, size: int, stride: int) -> int:
126106
127107
NOTE: See setitem()
128108
"""
129-
print(f'scalar.py: SampleScalar.clear_loop {data=}, {size=}, {stride=}')
130109
view = self._get_strided_np_view(data, size, stride)
131110
view[...] = 0 # Force dereference of all object-entries
132111
return 0
133112

134113
def cast_loop(
135114
self,
136-
other: 'SampleScalar',
115+
other: 'ScalarType',
137116
data: tuple[int, int],
138117
dimensions: tuple[int],
139118
strides: tuple[int, int],
@@ -142,10 +121,47 @@ def cast_loop(
142121
143122
NOTE: See setitem()
144123
"""
145-
print(
146-
f'scalar.py: SampleScalar.cast_loop {other=}, {data=}, {dimensions=}, {strides=}'
147-
)
148124
in_view = other._get_strided_np_view(data[0], dimensions[0], strides[0])
149125
out_view = self._get_strided_np_view(data[1], dimensions[0], strides[1])
150126
out_view[...] = in_view
151127
return 0
128+
129+
130+
class Scalar(ScalarType):
131+
_ndarr: np.ndarray
132+
133+
def __init__(self, *args, **kwargs):
134+
super().__init__(*args, **kwargs)
135+
self._ndarr = np.empty(self.shape, dtype=self.dtype)
136+
137+
138+
def custom_as_ctypes_type(dtype: np.dtype):
139+
"""Wrapper around `numpy.ctypeslib.as_ctypes_type` that maps 'O' to py_object"""
140+
if not dtype.hasobject:
141+
return np.ctypeslib.as_ctypes_type(dtype)
142+
143+
if dtype.fields is None:
144+
# Simple dtype or array
145+
if dtype.subdtype:
146+
# Handle sub-array types like ('O', 3)
147+
base, shape = dtype.subdtype
148+
new_base = custom_as_ctypes_type(base)
149+
for dim in reversed(shape):
150+
new_base = new_base * dim
151+
return new_base
152+
else:
153+
# Simple object (non-objects are already handled)
154+
assert dtype == np.dtype(object)
155+
return ctypes.py_object
156+
157+
# Structured dtype
158+
new_fields = []
159+
for name, (field_dtype, *_) in dtype.fields.items():
160+
new_field_dtype = custom_as_ctypes_type(field_dtype)
161+
new_fields.append((name, new_field_dtype))
162+
163+
class CustomStruct(ctypes.Structure):
164+
_pack_ = dtype.alignment
165+
_fields_ = new_fields
166+
167+
return CustomStruct

0 commit comments

Comments
 (0)