Repository navigation
Use pybind11 for C/C++ extensions #23787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8381ef9
f2f597f
cdfaf20
b15fd9c
293dffc
7da3539
f8226d8
2f8cfec
846f4c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -302,6 +302,8 @@ def make_release_tree(self, base_dir, files): | |
| setup_requires=[ | ||
| "certifi>=2020.06.20", | ||
| "numpy>=1.19", | ||
| "pybind11>=2.6", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a new build dependency, but is not a runtime dependency. |
||
| "setuptools>=42", | ||
| "setuptools_scm>=7", | ||
| ], | ||
| install_requires=[ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| import textwrap | ||
| import urllib.request | ||
|
|
||
| from pybind11.setup_helpers import Pybind11Extension | ||
| from setuptools import Distribution, Extension | ||
|
|
||
| _log = logging.getLogger(__name__) | ||
|
|
@@ -432,10 +433,10 @@ def get_extensions(self): | |
| add_libagg_flags_and_sources(ext) | ||
| yield ext | ||
| # qhull | ||
| ext = Extension( | ||
| ext = Pybind11Extension( | ||
| "matplotlib._qhull", ["src/_qhull_wrapper.cpp"], | ||
| cxx_std=11, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can specify the C++ standard to follow, so here we are explicitly restricting to C++11. This would probably be a configuration param at the top of the file to make it clear what C++ standard the project is currently working to. |
||
| define_macros=[("MPL_DEVNULL", os.devnull)]) | ||
| add_numpy_flags(ext) | ||
| Qhull.add_flags(ext) | ||
| yield ext | ||
| # tkagg | ||
|
|
@@ -452,12 +453,12 @@ def get_extensions(self): | |
| add_libagg_flags(ext) | ||
| yield ext | ||
| # tri | ||
| ext = Extension( | ||
| ext = Pybind11Extension( | ||
| "matplotlib._tri", [ | ||
| "src/tri/_tri.cpp", | ||
| "src/tri/_tri_wrapper.cpp", | ||
| ]) | ||
| add_numpy_flags(ext) | ||
| ], | ||
| cxx_std=11) | ||
| yield ext | ||
| # ttconv | ||
| ext = Extension( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,9 @@ | |
| * triangulation, construct an instance of the matplotlib.tri.Triangulation | ||
| * class without specifying a triangles array. | ||
| */ | ||
| #define PY_SSIZE_T_CLEAN | ||
| #include "Python.h" | ||
| #include "numpy_cpp.h" | ||
| #include <pybind11/pybind11.h> | ||
| #include <pybind11/numpy.h> | ||
|
|
||
| #ifdef _MSC_VER | ||
| /* The Qhull header does not declare this as extern "C", but only MSVC seems to | ||
| * do name mangling on global variables. We thus need to declare this before | ||
|
|
@@ -16,18 +16,28 @@ extern "C" { | |
| extern const char qh_version[]; | ||
| } | ||
| #endif | ||
|
|
||
| #include "libqhull_r/qhull_ra.h" | ||
| #include <cstdio> | ||
| #include <sstream> | ||
| #include <vector> | ||
|
|
||
|
|
||
| #ifndef MPL_DEVNULL | ||
| #error "MPL_DEVNULL must be defined as the OS-equivalent of /dev/null" | ||
| #endif | ||
|
|
||
| #define STRINGIFY(x) STR(x) | ||
| #define STR(x) #x | ||
|
|
||
| namespace py = pybind11; | ||
|
|
||
| // Input numpy array class. | ||
| typedef py::array_t<double, py::array::c_style | py::array::forcecast> CoordArray; | ||
|
|
||
| // Output numpy array class. | ||
| typedef py::array_t<int> IndexArray; | ||
|
|
||
|
|
||
|
|
||
| static const char* qhull_error_msg[6] = { | ||
| "", /* 0 = qh_ERRnone */ | ||
|
|
@@ -64,11 +74,11 @@ get_facet_neighbours(const facetT* facet, std::vector<int>& tri_indices, | |
| /* Return true if the specified points arrays contain at least 3 unique points, | ||
| * or false otherwise. */ | ||
| static bool | ||
| at_least_3_unique_points(npy_intp npoints, const double* x, const double* y) | ||
| at_least_3_unique_points(py::ssize_t npoints, const double* x, const double* y) | ||
| { | ||
| int i; | ||
| const int unique1 = 0; /* First unique point has index 0. */ | ||
| int unique2 = 0; /* Second unique point index is 0 until set. */ | ||
| py::ssize_t i; | ||
| const py::ssize_t unique1 = 0; /* First unique point has index 0. */ | ||
| py::ssize_t unique2 = 0; /* Second unique point index is 0 until set. */ | ||
|
|
||
| if (npoints < 3) { | ||
| return false; | ||
|
|
@@ -125,8 +135,8 @@ class QhullInfo { | |
| /* Delaunay implementation method. | ||
| * If hide_qhull_errors is true then qhull error messages are discarded; | ||
| * if it is false then they are written to stderr. */ | ||
| static PyObject* | ||
| delaunay_impl(npy_intp npoints, const double* x, const double* y, | ||
| py::tuple | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These can stay static (i.e. module-private)?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. |
||
| delaunay_impl(py::ssize_t npoints, const double* x, const double* y, | ||
| bool hide_qhull_errors) | ||
| { | ||
| qhT qh_qh; /* qh variable type and name must be like */ | ||
|
|
@@ -179,11 +189,12 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y, | |
| exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False, | ||
| (char*)"qhull d Qt Qbb Qc Qz", NULL, error_file); | ||
| if (exitcode != qh_ERRnone) { | ||
| PyErr_Format(PyExc_RuntimeError, | ||
| "Error in qhull Delaunay triangulation calculation: %s (exitcode=%d)%s", | ||
| qhull_error_msg[exitcode], exitcode, | ||
| hide_qhull_errors ? "; use python verbose option (-v) to see original qhull error." : ""); | ||
| return NULL; | ||
| std::ostringstream oss; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW I have a small helper in mplcairo for string formatting:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's nice! I hadn't considered dropping back to Python to do the string formatting. |
||
| oss << "Error in qhull Delaunay triangulation calculation: " << | ||
| qhull_error_msg[exitcode] << " (exitcode=" << exitcode << ")"; | ||
| if (hide_qhull_errors) | ||
| oss << "; use python verbose option (-v) to see original qhull error."; | ||
| throw std::runtime_error(oss.str()); | ||
| } | ||
|
|
||
| /* Split facets so that they only have 3 points each. */ | ||
|
|
@@ -204,12 +215,12 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y, | |
| std::vector<int> tri_indices(max_facet_id+1); | ||
|
|
||
| /* Allocate Python arrays to return. */ | ||
| npy_intp dims[2] = {ntri, 3}; | ||
| numpy::array_view<int, ndim> triangles(dims); | ||
| int* triangles_ptr = triangles.data(); | ||
| int dims[2] = {ntri, 3}; | ||
| IndexArray triangles(dims); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't even need to allocate dims on the stack; I usually just write
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know. Remember that the idea of this PR is to demonstrate the new approach works and show how to map from the old to the new implementation. There used to be an explicit line declaring the |
||
| int* triangles_ptr = triangles.mutable_data(); | ||
|
|
||
| numpy::array_view<int, ndim> neighbors(dims); | ||
| int* neighbors_ptr = neighbors.data(); | ||
| IndexArray neighbors(dims); | ||
| int* neighbors_ptr = neighbors.mutable_data(); | ||
|
|
||
| /* Determine triangles array and set tri_indices array. */ | ||
| i = 0; | ||
|
|
@@ -238,103 +249,56 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y, | |
| } | ||
| } | ||
|
|
||
| PyObject* tuple = PyTuple_New(2); | ||
| if (tuple == 0) { | ||
| throw std::runtime_error("Failed to create Python tuple"); | ||
| } | ||
|
|
||
| PyTuple_SET_ITEM(tuple, 0, triangles.pyobj()); | ||
| PyTuple_SET_ITEM(tuple, 1, neighbors.pyobj()); | ||
| return tuple; | ||
| return py::make_tuple(triangles, neighbors); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good example of improved handling of creating a tuple to return. |
||
| } | ||
|
|
||
| /* Process Python arguments and call Delaunay implementation method. */ | ||
| static PyObject* | ||
| delaunay(PyObject *self, PyObject *args) | ||
| py::tuple | ||
| delaunay(const CoordArray& x, const CoordArray& y) | ||
| { | ||
| numpy::array_view<double, 1> xarray; | ||
| numpy::array_view<double, 1> yarray; | ||
| PyObject* ret; | ||
| npy_intp npoints; | ||
| const double* x; | ||
| const double* y; | ||
|
|
||
| if (!PyArg_ParseTuple(args, "O&O&", | ||
| &xarray.converter_contiguous, &xarray, | ||
| &yarray.converter_contiguous, &yarray)) { | ||
| return NULL; | ||
| } | ||
| if (x.ndim() != 1 || y.ndim() != 1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I usually convert my arrays to use unchecked access (https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html#direct-access) (which of course means that out of bounds access can result in anything) and implicitly perform the dimensionality check at the same time as the conversion.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've included examples of both The lifetimes of these arrays is strictly controlled by the calling code so I may as well check the dims are OK just once, in the constructor. |
||
| throw std::invalid_argument("x and y must be 1D arrays"); | ||
|
|
||
| npoints = xarray.dim(0); | ||
| if (npoints != yarray.dim(0)) { | ||
| PyErr_SetString(PyExc_ValueError, | ||
| "x and y must be 1D arrays of the same length"); | ||
| return NULL; | ||
| } | ||
| auto npoints = x.shape(0); | ||
| if (y.shape(0) != npoints) | ||
| throw std::invalid_argument("x and y must be 1D arrays of the same length"); | ||
|
|
||
| if (npoints < 3) { | ||
| PyErr_SetString(PyExc_ValueError, | ||
| "x and y arrays must have a length of at least 3"); | ||
| return NULL; | ||
| } | ||
|
|
||
| x = xarray.data(); | ||
| y = yarray.data(); | ||
|
|
||
| if (!at_least_3_unique_points(npoints, x, y)) { | ||
| PyErr_SetString(PyExc_ValueError, | ||
| "x and y arrays must consist of at least 3 unique points"); | ||
| return NULL; | ||
| } | ||
| if (npoints < 3) | ||
| throw std::invalid_argument("x and y arrays must have a length of at least 3"); | ||
|
|
||
| CALL_CPP("qhull.delaunay", | ||
| (ret = delaunay_impl(npoints, x, y, Py_VerboseFlag == 0))); | ||
| if (!at_least_3_unique_points(npoints, x.data(), y.data())) | ||
| throw std::invalid_argument("x and y arrays must consist of at least 3 unique points"); | ||
|
|
||
| return ret; | ||
| return delaunay_impl(npoints, x.data(), y.data(), Py_VerboseFlag == 0); | ||
| } | ||
|
|
||
| /* Return qhull version string for assistance in debugging. */ | ||
| static PyObject* | ||
| version(PyObject *self, PyObject *arg) | ||
| std::string | ||
| version() | ||
| { | ||
| return PyBytes_FromString(qh_version); | ||
| return std::string(qh_version); | ||
| } | ||
|
|
||
| static PyMethodDef qhull_methods[] = { | ||
| {"delaunay", delaunay, METH_VARARGS, | ||
| "delaunay(x, y, /)\n" | ||
| "--\n\n" | ||
| "Compute a Delaunay triangulation.\n" | ||
| "\n" | ||
| "Parameters\n" | ||
| "----------\n" | ||
| "x, y : 1d arrays\n" | ||
| " The coordinates of the point set, which must consist of at least\n" | ||
| " three unique points.\n" | ||
| "\n" | ||
| "Returns\n" | ||
| "-------\n" | ||
| "triangles, neighbors : int arrays, shape (ntri, 3)\n" | ||
| " Indices of triangle vertices and indices of triangle neighbors.\n" | ||
| }, | ||
| {"version", version, METH_NOARGS, | ||
| "version()\n--\n\n" | ||
| "Return the qhull version string."}, | ||
| {NULL, NULL, 0, NULL} | ||
| }; | ||
|
|
||
| static struct PyModuleDef qhull_module = { | ||
| PyModuleDef_HEAD_INIT, | ||
| "qhull", "Computing Delaunay triangulations.\n", -1, qhull_methods | ||
| }; | ||
|
|
||
| #pragma GCC visibility push(default) | ||
|
|
||
| PyMODINIT_FUNC | ||
| PyInit__qhull(void) | ||
| { | ||
| import_array(); | ||
| return PyModule_Create(&qhull_module); | ||
| PYBIND11_MODULE(_qhull, m) { | ||
| m.doc() = "Computing Delaunay triangulations.\n"; | ||
|
|
||
| m.def("delaunay", &delaunay, | ||
| "delaunay(x, y, /)\n" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use https://pybind11.readthedocs.io/en/stable/basics.html instead to specify argument names (possibly together with https://pybind11.readthedocs.io/en/stable/reference.html?highlight=kw_only#_CPPv48pos_only if you want to keep them positional-only).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, done this. It does improve the docstring. |
||
| "--\n\n" | ||
| "Compute a Delaunay triangulation.\n" | ||
| "\n" | ||
| "Parameters\n" | ||
| "----------\n" | ||
| "x, y : 1d arrays\n" | ||
| " The coordinates of the point set, which must consist of at least\n" | ||
| " three unique points.\n" | ||
| "\n" | ||
| "Returns\n" | ||
| "-------\n" | ||
| "triangles, neighbors : int arrays, shape (ntri, 3)\n" | ||
| " Indices of triangle vertices and indices of triangle neighbors.\n"); | ||
|
|
||
| m.def("version", &version, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just make this a lambda
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very happy to use a lambda! There are no lambdas yet in our C++ codebase, but if you think other devs are happy to have them... |
||
| "version()\n--\n\n" | ||
| "Return the qhull version string."); | ||
| } | ||
|
|
||
| #pragma GCC visibility pop | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if None is not accepted anymore this warrants a changelog entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the internal C++ Triangulation API (
matplotllib._tri.Triangulation) that is used by the Python Triangulation class (matplotlib.tri.Triangulation). It is all private and nobody should be using it, so it doesn't need a changelog entry.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python Triangulation class still accepts
Nonefortriangles, etc.