Tuesday, 19 November 2019

pybind11 crash on constructor call with reference

I have the following code:

#include <pybind11/pybind11.h>

namespace py = pybind11;

class Foo
{
private:
    int value;
public:
    Foo(int val) : value(val) {};
    int getValue() { return value; }
};

class Bar
{
private:
    Foo& mFoo;
public:
    Bar(Foo& foo) : mFoo(foo) {}
    int getValue() { return mFoo.getValue(); }
};

PYBIND11_MODULE(pyinterface2, m)
{
    py::class_<Foo>(m, "Foo")
        .def(py::init<int>());

    py::class_<Bar>(m, "Bar")
        .def(py::init<Foo&>(), py::keep_alive<1, 2>())
        .def("getValue", &Bar::getValue);
}

I specifically use the py::keep_alive<1, 2>() in the constructor to make sure that the Foo object in python isn't cleaned up before the Bar object is. When I call this using:

import pyinterface2

foo = pyinterface2.Foo(1)
bar = pyinterface2.Bar(foo)

print(bar.getValue())

The program crashes, when creating the bar object. When I remove the py::keep_alive it works as expected. However I am afraid that the garbage collector might destroy the foo object before it destroys the bar object, what would mean my program will crash when trying to access the bar object. What is the correct way to do this?

Update:

After some debugging it seems that the ob_type of the Nurse object, in this case the Bar object, is NULL. This in causes a call to the creation of a weakref with a NULL handle argument obj, causing obj.ptr() to crash the program.

Update2:

The code does work when I use a setup.py file to install the module. Before I was just including the .pyd file in the same directory as the python test code and it did not work, but using the following setup file, the code works as expected (code taken from https://github.com/pybind/python_example). Not sure why though...

from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
import sys
import setuptools

__version__ = '0.0.1'


class get_pybind_include(object):
    """Helper class to determine the pybind11 include path
    The purpose of this class is to postpone importing pybind11
    until it is actually installed, so that the ``get_include()``
    method can be invoked. """

    def __init__(self, user=False):
        self.user = user

    def __str__(self):
        import pybind11
        return pybind11.get_include(self.user)


ext_modules = [
    Extension(
        'pyinterface2',
        ['pyinterface2.cxx'],
        include_dirs=[
            # Path to pybind11 headers
            get_pybind_include(),
            get_pybind_include(user=True)
        ],
        language='c++'
    ),
]


# As of Python 3.6, CCompiler has a `has_flag` method.
# cf http://bugs.python.org/issue26689
def has_flag(compiler, flagname):
    """Return a boolean indicating whether a flag name is supported on
    the specified compiler.
    """
    import tempfile
    with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f:
        f.write('int main (int argc, char **argv) { return 0; }')
        try:
            compiler.compile([f.name], extra_postargs=[flagname])
        except setuptools.distutils.errors.CompileError:
            return False
    return True


def cpp_flag(compiler):
    """Return the -std=c++[11/14/17] compiler flag.
    The newer version is prefered over c++11 (when it is available).
    """
    flags = ['-std=c++17', '-std=c++14', '-std=c++11']

    for flag in flags:
        if has_flag(compiler, flag): return flag

    raise RuntimeError('Unsupported compiler -- at least C++11 support '
                       'is needed!')


class BuildExt(build_ext):
    """A custom build extension for adding compiler-specific options."""
    c_opts = {
        'msvc': ['/EHsc'],
        'unix': [],
    }
    l_opts = {
        'msvc': [],
        'unix': [],
    }

    if sys.platform == 'darwin':
        darwin_opts = ['-stdlib=libc++', '-mmacosx-version-min=10.7']
        c_opts['unix'] += darwin_opts
        l_opts['unix'] += darwin_opts

    def build_extensions(self):
        ct = self.compiler.compiler_type
        opts = self.c_opts.get(ct, [])
        link_opts = self.l_opts.get(ct, [])
        if ct == 'unix':
            opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version())
            opts.append(cpp_flag(self.compiler))
            if has_flag(self.compiler, '-fvisibility=hidden'):
                opts.append('-fvisibility=hidden')
        elif ct == 'msvc':
            opts.append('/DVERSION_INFO=\\"%s\\"' % self.distribution.get_version())
        for ext in self.extensions:
            ext.extra_compile_args = opts
            ext.extra_link_args = link_opts
        build_ext.build_extensions(self)

setup(
    name='pyinterface2',
    version=__version__,
    author='Sylvain Corlay',
    author_email='sylvain.corlay@gmail.com',
    url='https://github.com/pybind/python_example',
    description='A test project using pybind11',
    long_description='',
    ext_modules=ext_modules,
    install_requires=['pybind11>=2.4'],
    setup_requires=['pybind11>=2.4'],
    cmdclass={'build_ext': BuildExt},
    zip_safe=False,
)


from pybind11 crash on constructor call with reference

No comments:

Post a Comment