MLIR 24.0.0git
NanobindUtils.h
Go to the documentation of this file.
1//===- NanobindUtils.h - Utilities for interop with nanobind ------*- C++
2//-*-===//
3//
4// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5// See https://llvm.org/LICENSE.txt for license information.
6// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7//
8//===----------------------------------------------------------------------===//
9
10#ifndef MLIR_BINDINGS_PYTHON_PYBINDUTILS_H
11#define MLIR_BINDINGS_PYTHON_PYBINDUTILS_H
12
13#include "mlir-c/Support.h"
15
16#include <array>
17#include <atomic>
18#include <fstream>
19#include <memory>
20#include <sstream>
21#include <string>
22#include <string_view>
23#include <type_traits>
24#include <typeinfo>
25#include <variant>
26
27#if NB_VERSION_MAJOR >= 3
28template <bool IsTuple>
29struct std::iterator_traits<nanobind::detail::seq_iterator<IsTuple>> {
30 using value_type = nanobind::handle;
31 using reference = const value_type;
32 using pointer = void;
33 using difference_type = std::ptrdiff_t;
34 using iterator_category = std::forward_iterator_tag;
35};
36#else
37template <>
38struct std::iterator_traits<nanobind::detail::fast_iterator> {
39 using value_type = nanobind::handle;
40 using reference = const value_type;
41 using pointer = void;
42 using difference_type = std::ptrdiff_t;
43 using iterator_category = std::forward_iterator_tag;
44};
45#endif
46
47namespace mlir {
48namespace python {
49
50/// Safely calls Python initialization code on first use, avoiding deadlocks.
51template <typename T>
52class SafeInit {
53public:
54 typedef std::unique_ptr<T> (*F)();
55
56 explicit SafeInit(F init_fn) : initFn(init_fn) {}
57
58 T &get() {
59 if (T *result = output.load()) {
60 return *result;
61 }
62
63 // Note: init_fn() may be called multiple times if, for example, the GIL is
64 // released during its execution. The intended use case is for module
65 // imports which are safe to perform multiple times. We are careful not to
66 // hold a lock across init_fn() to avoid lock ordering problems.
67 std::unique_ptr<T> m = initFn();
68 {
69 nanobind::ft_lock_guard lock(mu);
70 if (T *result = output.load()) {
71 return *result;
72 }
73 T *p = m.release();
74 output.store(p);
75 return *p;
76 }
77 }
78
79private:
80 nanobind::ft_mutex mu;
81 std::atomic<T *> output{nullptr};
82 F initFn;
83};
84
86 size_t operator()(MlirTypeID typeID) const {
87 return mlirTypeIDHashValue(typeID);
88 }
89};
90
92 bool operator()(MlirTypeID lhs, MlirTypeID rhs) const {
93 return mlirTypeIDEqual(lhs, rhs);
94 }
95};
96
97/// CRTP template for special wrapper types that are allowed to be passed in as
98/// 'None' function arguments and can be resolved by some global mechanic if
99/// so. Such types will raise an error if this global resolution fails, and
100/// it is actually illegal for them to ever be unresolved. From a user
101/// perspective, they behave like a smart ptr to the underlying type (i.e.
102/// 'get' method and operator-> overloaded).
103///
104/// Derived types must provide a method, which is called when an environmental
105/// resolution is required. It must raise an exception if resolution fails:
106/// static ReferrentTy &resolve()
107///
108/// They must also provide a parameter description that will be used in
109/// error messages about mismatched types:
110/// static constexpr const char kTypeDescription[] = "<Description>";
111
112template <typename DerivedTy, typename T>
114public:
115 using ReferrentTy = T;
116 /// Type casters require the type to be default constructible, but using
117 /// such an instance is illegal.
118 Defaulting() = default;
119 Defaulting(ReferrentTy &referrent) : referrent(&referrent) {}
120
121 ReferrentTy *get() const { return referrent; }
122 ReferrentTy *operator->() { return referrent; }
123
124private:
125 ReferrentTy *referrent = nullptr;
126};
127
128} // namespace python
129} // namespace mlir
130
131namespace nanobind {
132namespace detail {
133
134/// Helper function to concatenate arguments into a `std::string`.
135template <typename... Ts>
136inline std::string join(const Ts &...args) {
137 std::ostringstream oss;
138 (oss << ... << args);
139 return oss.str();
140}
141
142template <typename DefaultingTy>
144 NB_TYPE_CASTER(DefaultingTy, const_name(DefaultingTy::kTypeDescription))
145
146 bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) {
147 if (src.is_none()) {
148 // Note that we do want an exception to propagate from here as it will be
149 // the most informative.
150 value = DefaultingTy{DefaultingTy::resolve()};
151 return true;
152 }
153
154 // Unlike many casters that chain, these casters are expected to always
155 // succeed, so instead of doing an isinstance check followed by a cast,
156 // just cast in one step and handle the exception. Returning false (vs
157 // letting the exception propagate) causes higher level signature parsing
158 // code to produce nice error messages (other than "Cannot cast...").
159 try {
160 value = DefaultingTy{
161 nanobind::cast<typename DefaultingTy::ReferrentTy &>(src)};
162 return true;
163 } catch (std::exception &) {
164 return false;
165 }
166 }
167
168 static handle from_cpp(DefaultingTy src, rv_policy policy,
169 cleanup_list *cleanup) noexcept {
170 return nanobind::cast(src, policy);
171 }
172};
173} // namespace detail
174} // namespace nanobind
175
176//------------------------------------------------------------------------------
177// Conversion utilities.
178//------------------------------------------------------------------------------
179
180namespace mlir {
181
182/// Accumulates into a python string from a method that accepts an
183/// MlirStringCallback.
185 nanobind::list parts;
186
187 void *getUserData() { return this; }
188
190 return [](MlirStringRef part, void *userData) {
191 PyPrintAccumulator *printAccum =
192 static_cast<PyPrintAccumulator *>(userData);
193 nanobind::str pyPart(part.data,
194 part.length); // Decodes as UTF-8 by default.
195 printAccum->parts.append(std::move(pyPart));
196 };
197 }
198
199 nanobind::str join() {
200 nanobind::str delim("", 0);
201 return nanobind::cast<nanobind::str>(delim.attr("join")(parts));
202 }
203};
204
205/// RAII wrapper for MlirLlvmRawFdOStream that ensures destruction on scope
206/// exit.
215
216/// Accumulates into a file, either writing text (default)
217/// or binary. The file may be a Python file-like object or a path to a file.
219public:
220 PyFileAccumulator(const nanobind::object &fileOrStringObject, bool binary)
221 : binary(binary) {
222 std::string filePath;
223 if (nanobind::try_cast<std::string>(fileOrStringObject, filePath)) {
224 std::string errorMessage;
225 auto errorCallback = +[](MlirStringRef message, void *userData) {
226 auto *storage = static_cast<std::string *>(userData);
227 storage->assign(message.data, message.length);
228 };
230 filePath.c_str(), binary, errorCallback, &errorMessage);
231 if (mlirLlvmRawFdOStreamIsNull(stream)) {
232 throw nanobind::value_error(
233 (std::string("Unable to open file for writing: ") + errorMessage)
234 .c_str());
235 }
236 writeTarget.emplace<RAIIMlirLlvmRawFdOStream>(stream);
237 } else {
238 writeTarget.emplace<nanobind::object>(fileOrStringObject.attr("write"));
239 }
240 }
241
243 return writeTarget.index() == 0 ? getPyWriteCallback()
244 : getOStreamCallback();
245 }
246
247 void *getUserData() { return this; }
248
249private:
250 MlirStringCallback getPyWriteCallback() {
251 return [](MlirStringRef part, void *userData) {
252 nanobind::gil_scoped_acquire acquire;
253 PyFileAccumulator *accum = static_cast<PyFileAccumulator *>(userData);
254 if (accum->binary) {
255 // Note: Still has to copy and not avoidable with this API.
256 nanobind::bytes pyBytes(part.data, part.length);
257 std::get<nanobind::object>(accum->writeTarget)(pyBytes);
258 } else {
259 nanobind::str pyStr(part.data,
260 part.length); // Decodes as UTF-8 by default.
261 std::get<nanobind::object>(accum->writeTarget)(pyStr);
262 }
263 };
264 }
265
266 MlirStringCallback getOStreamCallback() {
267 return [](MlirStringRef part, void *userData) {
268 PyFileAccumulator *accum = static_cast<PyFileAccumulator *>(userData);
270 std::get<RAIIMlirLlvmRawFdOStream>(accum->writeTarget), part);
271 };
272 }
273
274 std::variant<nanobind::object, RAIIMlirLlvmRawFdOStream> writeTarget;
275 bool binary;
276};
277
278/// Accumulates into a python string from a method that is expected to make
279/// one (no more, no less) call to the callback (asserts internally on
280/// violation).
282 void *getUserData() { return this; }
283
285 return [](MlirStringRef part, void *userData) {
287 static_cast<PySinglePartStringAccumulator *>(userData);
288 assert(!accum->invoked &&
289 "PySinglePartStringAccumulator called back multiple times");
290 accum->invoked = true;
291 accum->value = nanobind::str(part.data, part.length);
292 };
293 }
294
295 nanobind::str takeValue() {
296 assert(invoked && "PySinglePartStringAccumulator not called back");
297 return std::move(value);
298 }
299
300private:
301 nanobind::str value;
302 bool invoked = false;
303};
304
305/// A CRTP base class for pseudo-containers willing to support Python-type
306/// slicing access on top of indexed access. Calling ::bind on this class
307/// will define `__len__` as well as `__getitem__` with integer and slice
308/// arguments.
309///
310/// This is intended for pseudo-containers that can refer to arbitrary slices of
311/// underlying storage indexed by a single integer. Indexing those with an
312/// integer produces an instance of ElementTy. Indexing those with a slice
313/// produces a new instance of Derived, which can be sliced further.
314///
315/// A derived class must provide the following:
316/// - a `static const char *pyClassName ` field containing the name of the
317/// Python class to bind;
318/// - an instance method `intptr_t getRawNumElements()` that returns the
319/// number
320/// of elements in the backing container (NOT that of the slice);
321/// - an instance method `ElementTy getRawElement(intptr_t)` that returns a
322/// single element at the given linear index (NOT slice index);
323/// - an instance method `Derived slice(intptr_t, intptr_t, intptr_t)` that
324/// constructs a new instance of the derived pseudo-container with the
325/// given slice parameters (to be forwarded to the Sliceable constructor).
326///
327/// The getRawNumElements() and getRawElement(intptr_t) callbacks must not
328/// throw.
329///
330/// A derived class may additionally define:
331/// - a `static void bindDerived(ClassTy &)` method to bind additional methods
332/// the python class.
333/// - a `static constexpr std::array<const char *, N> typeParams` to make the
334/// Python class generic, parameterizable with the given type parameters.
335template <typename Derived, typename ElementTy>
337protected:
338 using ClassTy = nanobind::class_<Derived>;
339
340 /// Type parameter names for generic classes. When non-empty, the Python
341 /// class will be made generic with `typing.Generic[...]`.
342 static constexpr std::array<const char *, 0> typeParams = {};
343
344 /// Transforms `index` into a legal value to access the underlying sequence.
345 /// Returns <0 on failure.
347 if (index < 0)
348 index = length + index;
349 if (index < 0 || index >= length)
350 return -1;
351 return index;
352 }
353
354 /// Computes the linear index given the current slice properties.
356 intptr_t linearIndex = index * step + startIndex;
357 assert(linearIndex >= 0 &&
358 linearIndex < static_cast<Derived *>(this)->getRawNumElements() &&
359 "linear index out of bounds, the slice is ill-formed");
360 return linearIndex;
361 }
362
363 /// Trait to check if T provides a `maybeDownCast` method.
364 /// Note, you need the & to detect inherited members.
365 template <typename T, typename = void>
366 struct has_maybe_downcast : std::false_type {};
367
368 template <typename T>
369 struct has_maybe_downcast<T, std::void_t<decltype(&T::maybeDownCast)>>
370 : std::true_type {};
371
372 /// Returns the element at the given slice index. Supports negative indices
373 /// by taking elements in inverse order. Returns a nullptr object if out
374 /// of bounds.
375 nanobind::typed<nanobind::object, ElementTy> getItem(intptr_t index) {
376 // Negative indices mean we count from the end.
378 if (index < 0) {
379 PyErr_SetString(PyExc_IndexError, "index out of range");
380 return {};
381 }
382
383 if constexpr (has_maybe_downcast<ElementTy>::value)
384 return static_cast<Derived *>(this)
385 ->getRawElement(linearizeIndex(index))
386 .maybeDownCast();
387 else
388 return nanobind::cast(
389 static_cast<Derived *>(this)->getRawElement(linearizeIndex(index)));
390 }
391
392 /// Returns a new instance of the pseudo-container restricted to the given
393 /// slice. Returns a nullptr object on failure.
394 nanobind::object getItemSlice(PyObject *slice) {
395 Py_ssize_t start, stop, extraStep, sliceLength;
396 if (PySlice_GetIndicesEx(slice, length, &start, &stop, &extraStep,
397 &sliceLength) != 0) {
398 PyErr_SetString(PyExc_IndexError, "index out of range");
399 return {};
400 }
401 return nanobind::cast(static_cast<Derived *>(this)->slice(
402 startIndex + start * step, sliceLength, step * extraStep));
403 }
404
405public:
408 assert(length >= 0 && "expected non-negative slice length");
409 }
410
411 /// Returns the `index`-th element in the slice, supports negative indices.
412 /// Throws if the index is out of bounds.
414 // Negative indices mean we count from the end.
416 if (index < 0) {
417 throw nanobind::index_error("index out of range");
418 }
419
420 return static_cast<Derived *>(this)->getRawElement(linearizeIndex(index));
421 }
422
423 /// Returns the size of slice.
424 intptr_t size() { return length; }
425
426 /// Returns a new vector (mapped to Python list) containing elements from two
427 /// slices. The new vector is necessary because slices may not be contiguous
428 /// or even come from the same original sequence.
429 std::vector<ElementTy> dunderAdd(Derived &other) {
430 std::vector<ElementTy> elements;
431 elements.reserve(length + other.length);
432 for (intptr_t i = 0; i < length; ++i) {
433 elements.push_back(static_cast<Derived *>(this)->getElement(i));
434 }
435 for (intptr_t i = 0; i < other.length; ++i) {
436 elements.push_back(static_cast<Derived *>(&other)->getElement(i));
437 }
438 return elements;
439 }
440
441 // Manually implement the sequence protocol via the C API. We do this
442 // because it is approx 4x faster than via nanobind, largely because that
443 // formulation requires a C++ exception to be thrown to detect end of
444 // sequence.
445 // Since we are in a C-context, any C++ exception that happens here
446 // will terminate the program. There is nothing in this implementation
447 // that should throw in a non-terminal way, so we forgo further
448 // exception marshalling.
449 // See: https://github.com/pybind/pybind11/issues/2842
450 //
451 /// Binds the indexing and length methods in the Python class.
452 static void bind(nanobind::module_ &m) {
453 // These slots are passed via nanobind::type_slots() at class creation
454 // time, which is compatible with both the full and limited (stable ABI)
455 // Python APIs.
456 static PyType_Slot sequenceSlots[] = {
457 {Py_sq_length, (void *)(+[](PyObject *rawSelf) -> Py_ssize_t {
458 auto self = nanobind::cast<Derived *>(nanobind::handle(rawSelf));
459 return self->length;
460 })},
461 // sq_item is called as part of the sequence protocol for iteration,
462 // list construction, etc.
463 {Py_sq_item,
464 (void *)(+[](PyObject *rawSelf, Py_ssize_t index) -> PyObject * {
465 auto self = nanobind::cast<Derived *>(nanobind::handle(rawSelf));
466 return self->getItem(index).release().ptr();
467 })},
468 // mp_subscript is used for both slices and integer lookups.
469 {Py_mp_subscript,
470 (void *)(+[](PyObject *rawSelf, PyObject *rawSubscript) -> PyObject * {
471 auto self = nanobind::cast<Derived *>(nanobind::handle(rawSelf));
472 Py_ssize_t index =
473 PyNumber_AsSsize_t(rawSubscript, PyExc_IndexError);
474 if (!PyErr_Occurred()) {
475 // Integer indexing.
476 return self->getItem(index).release().ptr();
477 }
478 PyErr_Clear();
479
480 // Assume slice-based indexing.
481 if (PySlice_Check(rawSubscript)) {
482 return self->getItemSlice(rawSubscript).release().ptr();
483 }
484
485 PyErr_SetString(PyExc_ValueError, "expected integer or slice");
486 return nullptr;
487 })},
488 {0, nullptr}};
489 nanobind::handle elemTyInfo = nanobind::type<ElementTy>();
490 assert(elemTyInfo.is_valid() &&
491 "expected nanobind::type to succeed for Sliceable elemTy");
492 nanobind::str elemTyName = nanobind::type_name(elemTyInfo);
493 std::string sig = std::string("class ") + Derived::pyClassName +
494 "(collections.abc.Sequence[" + elemTyName.c_str() + "]";
495 if constexpr (!Derived::typeParams.empty()) {
496 sig += ", typing.Generic[";
497 for (size_t i = 0; i < Derived::typeParams.size(); ++i) {
498 if (i > 0)
499 sig += ", ";
500 const char *tp = Derived::typeParams[i];
501 sig += tp;
502 if (!nanobind::hasattr(m, tp))
503 m.attr(tp) = nanobind::type_var(tp);
504 }
505 sig += "]";
506 }
507 sig += ")";
508 ClassTy clazz;
509 if constexpr (!Derived::typeParams.empty()) {
510 clazz =
511 ClassTy(m, Derived::pyClassName, nanobind::type_slots(sequenceSlots),
512 nanobind::is_generic(), nanobind::sig(sig.c_str()));
513 } else {
514 clazz =
515 ClassTy(m, Derived::pyClassName, nanobind::type_slots(sequenceSlots),
516 nanobind::sig(sig.c_str()));
517 }
518 clazz.def("__add__", &Sliceable::dunderAdd);
519 Derived::bindDerived(clazz);
520 }
521
522 /// Hook for derived classes willing to bind more methods.
523 static void bindDerived(ClassTy &) {}
524
528};
529
530} // namespace mlir
531
532#endif // MLIR_BINDINGS_PYTHON_PYBINDUTILS_H
Accumulates into a file, either writing text (default) or binary.
PyFileAccumulator(const nanobind::object &fileOrStringObject, bool binary)
MlirStringCallback getCallback()
nanobind::typed< nanobind::object, ElementTy > getItem(intptr_t index)
Returns the element at the given slice index.
intptr_t linearizeIndex(intptr_t index)
Computes the linear index given the current slice properties.
static void bind(nanobind::module_ &m)
Binds the indexing and length methods in the Python class.
std::vector< ElementTy > dunderAdd(Derived &other)
Returns a new vector (mapped to Python list) containing elements from two slices.
ElementTy getElement(intptr_t index)
Returns the index-th element in the slice, supports negative indices.
nanobind::object getItemSlice(PyObject *slice)
Returns a new instance of the pseudo-container restricted to the given slice.
nanobind::class_< PyOpResultList > ClassTy
static void bindDerived(ClassTy &)
Hook for derived classes willing to bind more methods.
Sliceable(intptr_t startIndex, intptr_t length, intptr_t step)
intptr_t wrapIndex(intptr_t index)
Transforms index into a legal value to access the underlying sequence.
static constexpr std::array< const char *, 0 > typeParams
intptr_t size()
Returns the size of slice.
ReferrentTy * operator->()
Defaulting(ReferrentTy &referrent)
ReferrentTy * get() const
Defaulting()=default
Type casters require the type to be default constructible, but using such an instance is illegal.
std::unique_ptr< T >(* F)()
MLIR_CAPI_EXPORTED bool mlirLlvmRawFdOStreamIsNull(MlirLlvmRawFdOStream stream)
Checks if a raw_fd_ostream is null.
Definition Support.cpp:69
MLIR_CAPI_EXPORTED void mlirLlvmRawFdOStreamWrite(MlirLlvmRawFdOStream stream, MlirStringRef string)
Write a string to a raw_fd_ostream created with mlirLlvmRawFdOStreamCreate.
Definition Support.cpp:64
MLIR_CAPI_EXPORTED size_t mlirTypeIDHashValue(MlirTypeID typeID)
Returns the hash value of the type id.
Definition Support.cpp:93
MLIR_CAPI_EXPORTED void mlirLlvmRawFdOStreamDestroy(MlirLlvmRawFdOStream stream)
Destroy a raw_fd_ostream created with mlirLlvmRawFdOStreamCreate.
Definition Support.cpp:73
struct MlirStringRef MlirStringRef
Definition Support.h:82
MLIR_CAPI_EXPORTED bool mlirTypeIDEqual(MlirTypeID typeID1, MlirTypeID typeID2)
Checks if two type ids are equal.
Definition Support.cpp:89
void(* MlirStringCallback)(MlirStringRef, void *)
A callback for returning string references.
Definition Support.h:110
MLIR_CAPI_EXPORTED MlirLlvmRawFdOStream mlirLlvmRawFdOStreamCreate(const char *path, bool binary, MlirStringCallback errorCallback, void *userData)
Create a raw_fd_ostream for the given path.
Definition Support.cpp:47
Include the generated interface declarations.
std::string join(const Ts &...args)
Helper function to concatenate arguments into a std::string.
A pointer to a sized fragment of a string, not necessarily null-terminated.
Definition Support.h:78
const char * data
Pointer to the first symbol.
Definition Support.h:79
size_t length
Length of the fragment.
Definition Support.h:80
Accumulates into a python string from a method that accepts an MlirStringCallback.
MlirStringCallback getCallback()
Accumulates into a python string from a method that is expected to make one (no more,...
RAII wrapper for MlirLlvmRawFdOStream that ensures destruction on scope exit.
RAIIMlirLlvmRawFdOStream & operator=(const RAIIMlirLlvmRawFdOStream &)=delete
RAIIMlirLlvmRawFdOStream(MlirLlvmRawFdOStream stream)
RAIIMlirLlvmRawFdOStream(const RAIIMlirLlvmRawFdOStream &)=delete
Trait to check if T provides a maybeDownCast method.
bool operator()(MlirTypeID lhs, MlirTypeID rhs) const
size_t operator()(MlirTypeID typeID) const
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup)
static handle from_cpp(DefaultingTy src, rv_policy policy, cleanup_list *cleanup) noexcept