Skip to content

Allow module-local classes to be loaded by external modules #1007

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

Merged
merged 1 commit into from
Aug 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 38 additions & 30 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -775,27 +775,29 @@ Module-local class bindings

When creating a binding for a class, pybind by default makes that binding
"global" across modules. What this means is that a type defined in one module
can be passed to functions of other modules that expect the same C++ type. For
can be returned from any module resulting in the same Python type. For
example, this allows the following:

.. code-block:: cpp

// In the module1.cpp binding code for module1:
py::class_<Pet>(m, "Pet")
.def(py::init<std::string>());
.def(py::init<std::string>())
.def_readonly("name", &Pet::name);

.. code-block:: cpp

// In the module2.cpp binding code for module2:
m.def("pet_name", [](Pet &p) { return p.name(); });
m.def("create_pet", [](std::string name) { return new Pet(name); });

.. code-block:: pycon

>>> from module1 import Pet
>>> from module2 import pet_name
>>> mypet = Pet("Kitty")
>>> pet_name(mypet)
'Kitty'
>>> from module2 import create_pet
>>> pet1 = Pet("Kitty")
>>> pet2 = create_pet("Doggy")
>>> pet2.name()
'Doggy'

When writing binding code for a library, this is usually desirable: this
allows, for example, splitting up a complex library into multiple Python
Expand Down Expand Up @@ -855,39 +857,45 @@ the ``py::class_`` constructor:
py::class<pets::Pet>(m, "Pet", py::module_local())
.def("get_name", &pets::Pet::name);

This makes the Python-side ``dogs.Pet`` and ``cats.Pet`` into distinct classes
that can only be accepted as ``Pet`` arguments within those classes. This
avoids the conflict and allows both modules to be loaded.
This makes the Python-side ``dogs.Pet`` and ``cats.Pet`` into distinct classes,
avoiding the conflict and allowing both modules to be loaded. C++ code in the
``dogs`` module that casts or returns a ``Pet`` instance will result in a
``dogs.Pet`` Python instance, while C++ code in the ``cats`` module will result
in a ``cats.Pet`` Python instance.

One limitation of this approach is that because ``py::module_local`` types are
distinct on the Python side, it is not possible to pass such a module-local
type as a C++ ``Pet``-taking function outside that module. For example, if the
above ``cats`` and ``dogs`` module are each extended with a function:
This does come with two caveats, however: First, external modules cannot return
or cast a ``Pet`` instance to Python (unless they also provide their own local
bindings). Second, from the Python point of view they are two distinct classes.

Note that the locality only applies in the C++ -> Python direction. When
passing such a ``py::module_local`` type into a C++ function, the module-local
classes are still considered. This means that if the following function is
added to any module (including but not limited to the ``cats`` and ``dogs``
modules above) it will be callable with either a ``dogs.Pet`` or ``cats.Pet``
argument:

.. code-block:: cpp

m.def("petname", [](pets::Pet &p) { return p.name(); });
m.def("pet_name", [](const pets::Pet &pet) { return pet.name(); });

you will only be able to call the function with the local module's class:
For example, suppose the above function is added to each of ``cats.cpp``,
``dogs.cpp`` and ``frogs.cpp`` (where ``frogs.cpp`` is some other module that
does *not* bind ``Pets`` at all).

.. code-block:: pycon

>>> import cats, dogs # No error because of the added py::module_local()
>>> import cats, dogs, frogs # No error because of the added py::module_local()
>>> mycat, mydog = cats.Cat("Fluffy"), dogs.Dog("Rover")
>>> (cats.petname(mycat), dogs.petname(mydog))
>>> (cats.pet_name(mycat), dogs.pet_name(mydog))
('Fluffy', 'Rover')
>>> cats.petname(mydog)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: petname(): incompatible function arguments. The following argument types are supported:
1. (arg0: cats.Pet) -> str

Invoked with: <dogs.Dog object at 0x123>

It is possible to use ``py::module_local()`` registrations in one module even if another module
registers the same type globally: within the module with the module-local definition, all C++
instances will be cast to the associated bound Python type. Outside the module, any such values
are converted to the global Python type created elsewhere.
>>> (cats.pet_name(mydog), dogs.pet_name(mycat), frogs.pet_name(mycat))
('Rover', 'Fluffy', 'Fluffy')

It is possible to use ``py::module_local()`` registrations in one module even
if another module registers the same type globally: within the module with the
module-local definition, all C++ instances will be cast to the associated bound
Python type. In other modules any such values are converted to the global
Python type created elsewhere.

.. note::

Expand Down
86 changes: 66 additions & 20 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ struct type_info {
std::vector<bool (*)(PyObject *, void *&)> *direct_conversions;
buffer_info *(*get_buffer)(PyObject *, void *) = nullptr;
void *get_buffer_data = nullptr;
void *(*module_local_load)(PyObject *, const type_info *) = nullptr;
/* A simple type never occurs as a (direct or indirect) parent
* of a class that makes use of multiple inheritance */
bool simple_type : 1;
Expand Down Expand Up @@ -265,23 +266,30 @@ PYBIND11_NOINLINE inline detail::type_info* get_type_info(PyTypeObject *type) {
return bases.front();
}

/// Return the type info for a given C++ type; on lookup failure can either throw or return nullptr.
/// `check_global_types` can be specified as `false` to only check types registered locally to the
/// current module.
PYBIND11_NOINLINE inline detail::type_info *get_type_info(const std::type_index &tp,
bool throw_if_missing = false,
bool check_global_types = true) {
std::type_index type_idx(tp);
inline detail::type_info *get_local_type_info(const std::type_index &tp) {
auto &locals = registered_local_types_cpp();
auto it = locals.find(type_idx);
auto it = locals.find(tp);
if (it != locals.end())
return (detail::type_info *) it->second;
if (check_global_types) {
auto &types = get_internals().registered_types_cpp;
it = types.find(type_idx);
if (it != types.end())
return (detail::type_info *) it->second;
}
return nullptr;
}

inline detail::type_info *get_global_type_info(const std::type_index &tp) {
auto &types = get_internals().registered_types_cpp;
auto it = types.find(tp);
if (it != types.end())
return (detail::type_info *) it->second;
return nullptr;
}

/// Return the type info for a given C++ type; on lookup failure can either throw or return nullptr.
PYBIND11_NOINLINE inline detail::type_info *get_type_info(const std::type_index &tp,
bool throw_if_missing = false) {
if (auto ltype = get_local_type_info(tp))
return ltype;
if (auto gtype = get_global_type_info(tp))
return gtype;

if (throw_if_missing) {
std::string tname = tp.name();
detail::clean_type_id(tname);
Expand Down Expand Up @@ -578,6 +586,8 @@ class type_caster_generic {
PYBIND11_NOINLINE type_caster_generic(const std::type_info &type_info)
: typeinfo(get_type_info(type_info)) { }

type_caster_generic(const type_info *typeinfo) : typeinfo(typeinfo) { }

bool load(handle src, bool convert) {
return load_impl<type_caster_generic>(src, convert);
}
Expand All @@ -597,7 +607,7 @@ class type_caster_generic {
auto it_instances = get_internals().registered_instances.equal_range(src);
for (auto it_i = it_instances.first; it_i != it_instances.second; ++it_i) {
for (auto instance_type : detail::all_type_info(Py_TYPE(it_i->second))) {
if (instance_type && instance_type == tinfo)
if (instance_type && same_type(*instance_type->cpptype, *tinfo->cpptype))
return handle((PyObject *) it_i->second).inc_ref();
}
}
Expand Down Expand Up @@ -655,8 +665,6 @@ class type_caster_generic {
return inst.release();
}

protected:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this removal intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Direct access to value is needed in the module_local_load lambda. And seeing as detail::type_caster_generic is already an implementation detail, there isn't really much of a need to protect access.


// Base methods for generic caster; there are overridden in copyable_holder_caster
void load_value(value_and_holder &&v_h) {
auto *&vptr = v_h.value_ptr();
Expand Down Expand Up @@ -686,13 +694,41 @@ class type_caster_generic {
}
void check_holder_compat() {}

PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) {
auto caster = type_caster_generic(ti);
if (caster.load(src, false))
return caster.value;
return nullptr;
}

/// Try to load with foreign typeinfo, if available. Used when there is no
/// native typeinfo, or when the native one wasn't able to produce a value.
PYBIND11_NOINLINE bool try_load_foreign_module_local(handle src) {
constexpr auto *local_key = "_pybind11_module_local_typeinfo";
const auto pytype = src.get_type();
if (!hasattr(pytype, local_key))
return false;

type_info *foreign_typeinfo = reinterpret_borrow<capsule>(getattr(pytype, local_key));
// Only consider this foreign loader if actually foreign and is a loader of the correct cpp type
if (foreign_typeinfo->module_local_load == &local_load
|| !same_type(*typeinfo->cpptype, *foreign_typeinfo->cpptype))
return false;

if (auto result = foreign_typeinfo->module_local_load(src.ptr(), foreign_typeinfo)) {
value = result;
return true;
}
return false;
}

// Implementation of `load`; this takes the type of `this` so that it can dispatch the relevant
// bits of code between here and copyable_holder_caster where the two classes need different
// logic (without having to resort to virtual inheritance).
template <typename ThisT>
PYBIND11_NOINLINE bool load_impl(handle src, bool convert) {
if (!src || !typeinfo)
return false;
if (!src) return false;
if (!typeinfo) return try_load_foreign_module_local(src);
if (src.is_none()) {
// Defer accepting None to other overloads (if we aren't in convert mode):
if (!convert) return false;
Expand Down Expand Up @@ -757,7 +793,17 @@ class type_caster_generic {
if (this_.try_direct_conversions(src))
return true;
}
return false;

// Failed to match local typeinfo. Try again with global.
if (typeinfo->module_local) {
if (auto gtype = get_global_type_info(*typeinfo->cpptype)) {
typeinfo = gtype;
return load(src, false);
}
}

// Global typeinfo has precedence over foreign module_local
return try_load_foreign_module_local(src);
}


Expand Down
8 changes: 7 additions & 1 deletion include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ class generic_type : public object {
pybind11_fail("generic_type: cannot initialize type \"" + std::string(rec.name) +
"\": an object with that name is already defined");

if (get_type_info(*rec.type, false /* don't throw */, !rec.module_local))
if (rec.module_local ? get_local_type_info(*rec.type) : get_global_type_info(*rec.type))
pybind11_fail("generic_type: type \"" + std::string(rec.name) +
"\" is already registered!");

Expand Down Expand Up @@ -866,6 +866,12 @@ class generic_type : public object {
auto parent_tinfo = get_type_info((PyTypeObject *) rec.bases[0].ptr());
tinfo->simple_ancestors = parent_tinfo->simple_ancestors;
}

if (rec.module_local) {
// Stash the local typeinfo and loader so that external modules can access it.
tinfo->module_local_load = &type_caster_generic::local_load;
setattr(m_ptr, "_pybind11_module_local_typeinfo", capsule(tinfo));
}
}

/// Helper function which tags all parents of a type using mult. inheritance
Expand Down
24 changes: 23 additions & 1 deletion tests/local_bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ using NonLocal2 = LocalBase<2>;
using LocalExternal = LocalBase<3>;
/// Mixed: registered local first, then global
using MixedLocalGlobal = LocalBase<4>;
/// Mixed: global first, then local (which fails)
/// Mixed: global first, then local
using MixedGlobalLocal = LocalBase<5>;

using LocalVec = std::vector<LocalType>;
Expand All @@ -29,10 +29,32 @@ using NonLocalVec2 = std::vector<NonLocal2>;
using NonLocalMap = std::unordered_map<std::string, NonLocalType>;
using NonLocalMap2 = std::unordered_map<std::string, uint8_t>;

PYBIND11_MAKE_OPAQUE(LocalVec);
PYBIND11_MAKE_OPAQUE(LocalVec2);
PYBIND11_MAKE_OPAQUE(LocalMap);
PYBIND11_MAKE_OPAQUE(NonLocalVec);
//PYBIND11_MAKE_OPAQUE(NonLocalVec2); // same type as LocalVec2
PYBIND11_MAKE_OPAQUE(NonLocalMap);
PYBIND11_MAKE_OPAQUE(NonLocalMap2);


// Simple bindings (used with the above):
template <typename T, int Adjust, typename... Args>
py::class_<T> bind_local(Args && ...args) {
return py::class_<T>(std::forward<Args>(args)...)
.def(py::init<int>())
.def("get", [](T &i) { return i.i + Adjust; });
};

// Simulate a foreign library base class (to match the example in the docs):
namespace pets {
class Pet {
public:
Pet(std::string name) : name_(name) {}
std::string name_;
const std::string &name() { return name_; }
};
}

struct MixGL { int i; MixGL(int i) : i{i} {} };
struct MixGL2 { int i; MixGL2(int i) : i{i} {} };
17 changes: 17 additions & 0 deletions tests/pybind11_cross_module_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,21 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
m.def("load_vector_via_binding", [](std::vector<int> &v) {
return std::accumulate(v.begin(), v.end(), 0);
});

// test_cross_module_calls
m.def("return_self", [](LocalVec *v) { return v; });
m.def("return_copy", [](const LocalVec &v) { return LocalVec(v); });

class Dog : public pets::Pet { public: Dog(std::string name) : Pet(name) {}; };
py::class_<pets::Pet>(m, "Pet", py::module_local())
.def("name", &pets::Pet::name);
// Binding for local extending class:
py::class_<Dog, pets::Pet>(m, "Dog")
.def(py::init<std::string>());
m.def("pet_name", [](pets::Pet &p) { return p.name(); });

py::class_<MixGL>(m, "MixGL", py::module_local()).def(py::init<int>());
m.def("get_gl_value", [](MixGL &o) { return o.i + 100; });

py::class_<MixGL2>(m, "MixGL2", py::module_local()).def(py::init<int>());
}
24 changes: 17 additions & 7 deletions tests/test_local_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@
#include <pybind11/stl_bind.h>
#include <numeric>

PYBIND11_MAKE_OPAQUE(LocalVec);
PYBIND11_MAKE_OPAQUE(LocalVec2);
PYBIND11_MAKE_OPAQUE(LocalMap);
PYBIND11_MAKE_OPAQUE(NonLocalVec);
PYBIND11_MAKE_OPAQUE(NonLocalMap);
PYBIND11_MAKE_OPAQUE(NonLocalMap2);

TEST_SUBMODULE(local_bindings, m) {
// test_local_bindings
// Register a class with py::module_local:
Expand Down Expand Up @@ -84,4 +77,21 @@ TEST_SUBMODULE(local_bindings, m) {
m.def("load_vector_via_caster", [](std::vector<int> v) {
return std::accumulate(v.begin(), v.end(), 0);
});

// test_cross_module_calls
m.def("return_self", [](LocalVec *v) { return v; });
m.def("return_copy", [](const LocalVec &v) { return LocalVec(v); });

class Cat : public pets::Pet { public: Cat(std::string name) : Pet(name) {}; };
py::class_<pets::Pet>(m, "Pet", py::module_local())
.def("get_name", &pets::Pet::name);
// Binding for local extending class:
py::class_<Cat, pets::Pet>(m, "Cat")
.def(py::init<std::string>());
m.def("pet_name", [](pets::Pet &p) { return p.name(); });

py::class_<MixGL>(m, "MixGL").def(py::init<int>());
m.def("get_gl_value", [](MixGL &o) { return o.i + 10; });

py::class_<MixGL2>(m, "MixGL2").def(py::init<int>());
}
Loading