-
Notifications
You must be signed in to change notification settings - Fork 221
Description
Summary
The following anomaly applies to boost::python operating with NumPy version >= 1.21.0; prior to this version the degradation doesn't occur:
Symptom
If a BOOST_PYTHON_MODULE calls boost::python::numpy::initialize(), boost::python's parameter type detection feature is partially-defeated. This affects any method or function type checking and is particularly noticeable in overloads. If an incorrect parameter type (such as an object) is
passed to a method/function that expects an intrinsic int, float, or string, a segfault occurs instead of
the helpful boost error throw. In the event of an overload, the overload-dispatcher may send a legitimate parameter to the incorrect overload instance.
These cases are exercised in Python example 1 and Python example 2 below, respectively.
Relevant Software Versions Information:
For Failure:
(several were tried, this is representative)
| package | Version | Code | Source |
|---|---|---|---|
| boost | 1.72.0 | py39hb64e6f8_1 | conda-forge |
| python | 3.9.7 | h88f2d9e_1 | |
| numpy | 1.21.2 | py39h0fa1045_0 | |
| clang | Apple clang version 11.0.0 (clang-1100.0.33.17) |
For Success(expected behavior):
just change numpy:
| package | Version | Code | Source |
|---|---|---|---|
| numpy | 1.20.3 | py39h7eed0ac_1 | conda-forge |
C++ sample:
minipy.cpp
#include <boost/python.hpp>
#include <boost/python/extract.hpp>
#include <boost/python/numpy.hpp>
#include <string>
#include <sstream>
#include <vector>
using namespace std;
using namespace boost::python;
namespace np = boost::python::numpy;
class A {
public:
A():sum(0.0) {}
void add(double d) {sum += d;}
void add(const A& s) {sum += s.sum;}
void mul(double d) {sum *= d;}
double val() const { return sum;}
private:
double sum;
};
BOOST_PYTHON_MODULE(minipy)
{
Py_Initialize();
if(0) np::initialize(false);
else np::initialize();
class_<A>("A", init<>())
.def("add", static_cast<void (A::*) (const A&)>(&A::add))
.def("add", static_cast<void (A::*) (double)>(&A::add))
.def("mul", &A::mul)
.def("__call__", &A::val)
;
};Build with standard boost libraries/paths. in this case for me, conda venv = compile2:
g++ -I/Users/Alan/miniconda3/envs/compile2/include/python3.9 -I/Users/Alan/miniconda3/envs/compile2/include/python3.9 -I/Users/Alan/miniconda3/envs/compile2/include -O3 -fPIC -std=c++11 -Iinclude -MMD -c -o objs/minipy.o src/minipy.cpp
g++ -Wl,-rpath,/Users/Alan/miniconda3/envs/compile2/lib -shared -o lib/minipy.so objs/minipy.o -L/Users/Alan/miniconda3/envs/compile2/lib -Llib -ldl -framework CoreFoundation -undefined dynamic_lookup -lboost_python39 -lboost_numpy39 -larmadillo
Observations:
- Parameters: A.add() can be passed a double or another A. A.mul() expects a double.
- I added an if(0) (line 28/29 of the source) to turn register_scalar_converters on or off
Python example 1:
(This is fast to reproduce, so a command line python may be simpler)
import minipy as m
a = m.A()
a.add(a) # Segmentation fault: 11 if nump>1.21; register_scalar_converters is trueExpected output:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
A.mul(A, A)
did not match C++ signature:
mul(A {lvalue}, double)
Incorrect output:
Segmentation fault: 11
Python example 2 -- boost overloading failure:
import minipy as m
a = m.A()
a.add(a)
print(a())Expected output:
0.0
Incorrect output:
Segmentation fault: 11
Observation: It appears from this example that the numpy registration in numpy >= 1.21
is defeating boost::python's type matching for overloads. If the overloads are declared
in the opposite order, the code works regardless of the numpy version. Given the last-in-first-checked order
of boost::python dispatcher, add(double) is checked first in the code example above. Normally this is fine, but if numpy>=1.21 registration has occurred the
m.A object is incorrectly considered convertible to double, then leading to the segmentation fault.