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

Conversation

jagerman
Copy link
Member

The main point of py::module_local is to make the C++ -> Python cast unique so that returning/casting a C++ instance is well-defined. Unfortunately it also makes loading unique, but this isn't particularly desirable or necessary: when an instance contains Type instance there's no reason it shouldn't be possible to pass that instance to a bound function taking a Type parameter, even if that function is in another module.

This commit solves the issue by storing the module-local &type_caster_generic::load in a global multimap so that we can call into local modules' load() methods for module_local types. For module-local types that override a globally-registered types a global type conversion is also attempted.

Fixes #919.

It increases .so size slightly (a little over 4KB, not counting the added tests), but as the additions are entirely in non-templated code, that number shouldn't change with the number of bound types.

@dean0x7d
Copy link
Member

Just a quick comment for now. The search for an external loader looks really complicated. Instead of the multimap in internals, have you considered attaching the external load as a capsule to the Python type object in generic_type::initialize()?

@jagerman
Copy link
Member Author

That simplifies it; I've pushed a couple of commits to stash things in an attribute of the python type. The second commit is simpler, but increases the type_info size by the size of a member function pointer.

@dean0x7d
Copy link
Member

I had a go at simplifying this just a bit more: reduced the reliance on caster state (recursion flag, typeinfo state change) because these will need to go anyway with the planned new-style maybe loaders. Also switched to a regular function pointer for portability (pointer to member function can be weird). The changes reduce binary size (at least on clang). Everything should still work as before. Let me know if you agree with these changes.

@jagerman
Copy link
Member Author

Looks great!

@jagerman jagerman added this to the v2.2 milestone Aug 19, 2017
@jagerman
Copy link
Member Author

I think we should include this in 2.2 -- I noticed you also put it in the upgrade doc, so I'm guessing you agree.

@dean0x7d
Copy link
Member

Yes, definitely. It makes module_local significantly more convenient (and easier to explain because there's one less caveat). If everything else looks good, feel free to squash and merge this.

@jagerman
Copy link
Member Author

The current build is just for the comment change, right? (i.e. safe to not wait for the ci tests to finish?)

@dean0x7d
Copy link
Member

Yeah.


type_info *foreign_typeinfo = reinterpret_borrow<capsule>(getattr(pytype, local_key));
if (foreign_typeinfo == typeinfo)
return false; // Only consider this foreign loader if actually foreign
Copy link
Member Author

Choose a reason for hiding this comment

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

I think there's a slight problem here: if the type has a masked global registration, typeinfo will have been changed to the global one, in which case the foreign_typeinfo == typeinfo isn't going to be true, so we could end recursing.

Copy link
Member

@dean0x7d dean0x7d Aug 19, 2017

Choose a reason for hiding this comment

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

Do you mean a local binding masking a global one? That should work and is covered by the tests. (Not the cpptype mismatch mentioned below.)

Edit: meant local masking global.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm thinking:

  • the local type masks the global type, is the correct type_info for the given argument, but the load fails anyway.
  • the global type gets checked, which also fails (the global type is not right).
  • foreign loaders get checked, but because typeinfo has been changed to the global one, we don't short-circuit in try_load_foreign and so end up recursing.

It's not immediately obvious to me that the first step is impossible; though it's not immediately obvious to me how I can trigger it in a test, either.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think 225caf3 ought to solve this: when using a proper function (rather than a lambda) we can just test whether that function is foreign rather than relying on typeinfo.

Copy link
Member

Choose a reason for hiding this comment

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

I see, seems reasonable.

@jagerman
Copy link
Member Author

There's another problem here that I'm looking into: We aren't checking that the foreign loader is actually a loader of the right type.

@@ -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.

@dean0x7d
Copy link
Member

There's another problem here that I'm looking into: We aren't checking that the foreign loader is actually a loader of the right type.

Good catch.

The main point of `py::module_local` is to make the C++ -> Python cast
unique so that returning/casting a C++ instance is well-defined.
Unfortunately it also makes loading unique, but this isn't particularly
desirable: when an instance contains `Type` instance there's no reason
it shouldn't be possible to pass that instance to a bound function
taking a `Type` parameter, even if that function is in another module.

This commit solves the issue by allowing foreign module (and global)
type loaders have a chance to load the value if the local module loader
fails.  The implementation here does this by storing a module-local
loading function in a capsule in the python type, which we can then call
if the local (and possibly global, if the local type is masking a global
type) version doesn't work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants