Skip to content
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

Module-local bindings #949

Merged
merged 4 commits into from
Aug 4, 2017
Merged

Module-local bindings #949

merged 4 commits into from
Aug 4, 2017

Conversation

jagerman
Copy link
Member

This PR adds a py::local attribute that lets one confine a type registration to the module (more technically, the shared object) in which it is defined, by registering it with:

    py::class_<C>(m, "C", py::local())

This will allow the C++ class C to be registered in different modules with independent sets of class definitions. On the Python side, two such types will be completely distinct; on the C++ side, the C++ resolves to a different Python types in each module.

This also fixes #919 / #439 by making the stl_bind.h binding code apply py::local() by default (but passing in py::local(false) can be used in cases where you want to use the stl-bound type across module boundaries).

Tests are included showing this working; I haven't yet written up the documentation as I wanted feedback/input on the idea and approach first.

@wjakob
Copy link
Member

wjakob commented Jul 19, 2017

I think this is a great idea, and your implementation looks good to me.

@jagerman
Copy link
Member Author

Note: the tests added here rely on the fix from #951; I've included the #951 fix here, but filed the separate PR to allow separate discussion of that fix.

Copy link
Member

@dean0x7d dean0x7d left a comment

Choose a reason for hiding this comment

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

I believe there is an ODR violation here. See the comments for details. It should be possible to resolve.

I'd suggest renaming py::local to py::module_local in order to be more specific and avoid possible confusion with Python's locals/globals builtins. We already have a py::globals which mimics one of those on the C++ side. If the need arises for something like py::locals, local/locals will be easy to mix up.

The extra test module is a good idea. This can definitely be used for more cross-module testing, e.g. #951 could also use it, and there have been some internals issues in the past that could be covered with a cross-module test. I'd suggest renaming this new module to something along the line of pybind11_cross_module_tests to reflect that it's not only for module-local binding tests.

@@ -111,6 +128,14 @@ PYBIND11_NOINLINE inline internals &get_internals() {
return *internals_ptr;
}

namespace {
// Works like internals.registered_types_cpp, but for module-local registered types:
inline type_map<void *> &registered_local_types_cpp() {
Copy link
Member

Choose a reason for hiding this comment

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

A function inside an anonymous namespace in a header is a likely ODR violation. Explanation and suggestion in the following comment.

if (it != types.end())
return (detail::type_info *) it->second;
auto &locals = registered_local_types_cpp();
Copy link
Member

Choose a reason for hiding this comment

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

I'm reasonably sure this is an ODR violation. registered_local_types_cpp() is located inside an anonymous namespace inside a header which means there's going to be one unique function per translation unit, i.e.:

a.cpp -> pybind11::detail::<unique_name_a>::registered_local_types_cpp()
b.cpp -> pybind11::detail::<unique_name_b>::registered_local_types_cpp()

On the other hand, get_type_info() is not in an anonymous namespace, so per ODR we are promising that there is just one of those for all transaltion units. But because of the call highlighted above, the body of get_type_info() will be defined differently in different translalation units, i.e. one will call <unique_name_a>::registered_local_types_cpp(), another <unique_name_b>::, etc.

A possible solution could be to move the definition of registered_local_types_cpp into the PYBIND11_MODULE macro (without the anon namespace) which would ensure a single definition per module. Although, I'm not sure if GCC will consider the function-local static variable as unique or shared among different modules.

@jagerman
Copy link
Member Author

jagerman commented Jul 23, 2017

I believe there is an ODR violation here.

My thinking on it was that we might have wiggled out of it by virtue of python modules being loaded with RTLD_LOCAL, so that whichever registered_local_types_cpp get used will be the "right" one. That's probably assuming too much, though, and furthermore it's not going to work if the module is loaded with RTLD_GLOBAL, so indeed a better solution is needed. Never mind, that's not enough to get around it being an ODR violation.

Putting it into the PYBIND11_MODULE macro is a nice idea; I'll try that and see how it goes.

@jagerman
Copy link
Member Author

PYBIND11_MODULE works just fine (and gcc does indeed consider the variable unique, which is nice).

There is one thorny issue though—what do do about embedding that doesn't have a macro at all?

@dean0x7d
Copy link
Member

There is one thorny issue though—what do do about embedding that doesn't have a macro at all?

Yeah, that does throw a wrench in the works. Technically, there is the PYBIND11_EMBEDDED_MODULE macro, but it doesn't help much.

This also made me realize that the bindings are also made local to a .so file not a py::module which adds some nuance: all modules created with def_submodule would share the same local bindings, as would all embedded modules.

Do you think it would be possible to place the local type map inside a capsule and attach it to a py::module? (Instead of the function-scope static type map.) This would also require get_type_info to take an extra PyObject *module argument -- the context of the type lookup. This might not be the most efficient solution, but I can't think of anything leaner right now. However, it would have the advantage of making the local bindings have py::module scope.

@jagerman
Copy link
Member Author

I can't see how storing a capsule in the module would work. Storing the capsule is fine, but I don't see an obvious way to actually get the module (to pass it in to get_type_info()) in various contexts.

How about if we simply drop the anonymous namespace, and go back to the local-to-the-current-so approach? (and perhaps name it py::local_type instead of py::local--but it's not quite module-local, as you pointed out). GCC (at least gcc 7; I haven't tested earlier versions yet) doesn't actually appear to have any problems with it, and I think it solves the ODR issue.

@dean0x7d
Copy link
Member

That definitely solves the ODR issue, but I was under the impression that GCC shared function-scope statics across modules -- the issues with get_internals_ptr().

@jagerman
Copy link
Member Author

A bit of further investigation: gcc only seems to behave this way when not compiling with -flto. Static locals then get exported with STB_GNU_UNIQUE, which makes them unique, even across the RTLD_LOCAL boundary.

With -flto this doesn't appear to happen. There's also a flag dedicated to this, -fno-gnu-unique, which I'm very tempted to force on (and perhaps require for the feature to work).

@jagerman
Copy link
Member Author

It also doesn't show up under -fvisibility=hidden, which is probably why it took so long to notice #796 in the first place. It should be sufficient to force hidden visibility on internal functions to force GCC to "unshare" them.

@jagerman
Copy link
Member Author

jagerman commented Jul 24, 2017

ac21545 should fix the gcc visibility issue (I also simplified get_internals() back to what it used to be using the same trick). There are a few other static local variable functions here and there, but they are for things like caching the numpy api object--I don't think any of them are going to be an issue if shared across modules (even for different pybind versions).

@dean0x7d
Copy link
Member

Nice investigative work! The solution with the function attribute looks ideal (as opposed to the compiler flag, which we have no way of enforcing). Right now, you've included the attribute for gcc but not clang. I'd suggest including it for any compiler which defines __GNUG__ (gcc, clang and intel). It's required anyway, so best to enable it explicitly than rely on whatever the defaults are for the different gcc-compatible compilers.

@jagerman
Copy link
Member Author

jagerman commented Jul 25, 2017

Right now, you've included the attribute for gcc but not clang. I'd suggest including it for any compiler which defines __GNUG__ (gcc, clang and intel). It's required anyway, so best to enable it explicitly than rely on whatever the defaults are for the different gcc-compatible compilers.

As far as I can tell this only affects g++. It can't hurt to turn it on for the others, though.

@jagerman jagerman changed the title RFC: Module-local bindings Module-local bindings Jul 25, 2017
@jagerman
Copy link
Member Author

jagerman commented Jul 29, 2017

I've rewritten/squashed the git history here, with just one code change (to add a test for the cross-module exception handler as suggested by @dean0x7d in #951; I'll close that one and just merge the fix via this one).

This commit adds a PYBIND11_UNSHARED_STATIC_LOCALS macro that forces a
function to have hidden visibility under gcc and gcc-compatible
compilers.  gcc, in particular, needs this to to avoid sharing static
local variables across modules (which happens even under a RTLD_LOCAL
dlopen()!).  clang doesn't appear to have this issue, but the forced
visibility on internal pybind functions certainly won't hurt it and icc.

This updates the workaround from pybind#862 to use this rather than the
version-specific template.
This adds the infrastructure for a separate test plugin for cross-module
tests.  (This commit contains no tests that actually use it, but the
following commits do; this is separated simply to provide a cleaner
commit history).
The builtin exception handler currently doesn't work across modules
under clang/libc++ for builtin pybind exceptions like
`pybind11::error_already_set` or `pybind11::stop_iteration`: under
RTLD_LOCAL module loading clang considers each module's exception
classes distinct types.  This then means that the base exception
translator fails to catch the exceptions and the fall through to the
generic `std::exception` handler, which completely breaks things like
`stop_iteration`: only the `stop_iteration` of the first module loaded
actually works properly; later modules raise a RuntimeError with no
message when trying to invoke their iterators.

For example, two modules defined like this exhibit the behaviour under
clang++/libc++:

z1.cpp:
    #include <pybind11/pybind11.h>
    #include <pybind11/stl_bind.h>
    namespace py = pybind11;
    PYBIND11_MODULE(z1, m) {
        py::bind_vector<std::vector<long>>(m, "IntVector");
    }

z2.cpp:
    #include <pybind11/pybind11.h>
    #include <pybind11/stl_bind.h>
    namespace py = pybind11;
    PYBIND11_MODULE(z2, m) {
        py::bind_vector<std::vector<double>>(m, "FloatVector");
    }

Python:
    import z1, z2
    for i in z2.FloatVector():
        pass

results in:
    Traceback (most recent call last):
      File "zs.py", line 2, in <module>
        for i in z2.FloatVector():
    RuntimeError

This commit fixes the issue by adding a new exception translator each
time the internals pointer is initialized from python builtins: this
generally means the internals data was initialized by some other
module.  (The extra translator(s) are skipped under libstdc++).
This commit adds a `py::module_local` attribute that lets you confine a
registered type to the module (more technically, the shared object) in
which it is defined, by registering it with:

    py::class_<C>(m, "C", py::module_local())

This will allow the same C++ class `C` to be registered in different
modules with independent sets of class definitions.  On the Python side,
two such types will be completely distinct; on the C++ side, the C++
type resolves to a different Python type in each module.

This applies `py::module_local` automatically to `stl_bind.h` bindings
when the container value type looks like something global: i.e. when it
is a converting type (for example, when binding a `std::vector<int>`),
or when it is a registered type itself bound with `py::module_local`.
This should help resolve potential future conflicts (e.g. if two
completely unrelated modules both try to bind a `std::vector<int>`.
Users can override the automatic selection by adding a
`py::module_local()` or `py::module_local(false)`.

Note that this does mildly break backwards compatibility: bound stl
containers of basic types like `std::vector<int>` cannot be bound in one
module and returned in a different module.  (This can be re-enabled with
`py::module_local(false)` as described above, but with the potential for
eventual load conflicts).
@jagerman jagerman merged commit 7437c69 into pybind:master Aug 4, 2017
@dean0x7d
Copy link
Member

dean0x7d commented Aug 4, 2017

The revised implementation here looks excellent. Sorry, I've been busy as of late and fallen behind on the reviews. Just one quick question: what happens if a type is registered as both local and global? I didn't find any tests for that case, but I may have missed it.

@jagerman
Copy link
Member Author

jagerman commented Aug 4, 2017

Just one quick question: what happens if a type is registered as both local and global? I didn't find any tests for that case, but I may have missed it.

If a type is already registered globally, you'll get a failure if you try to register it locally. If you go the other way, there will be no failure, but the global type will take precedence (local types are only checked if the global type isn't found).

@jagerman
Copy link
Member Author

jagerman commented Aug 4, 2017

Now that I think about it, I wonder if that's the right behaviour. Perhaps it would make more sense to make the local type take precedence.

@dean0x7d
Copy link
Member

dean0x7d commented Aug 4, 2017

Appleclang seems to be broken in debug mode. Strangely, the older appleclang 7.3.0 on Travis and the latest 8.1.0 fail with different errors:

7.3.0

E    TypeError: __init__(): incompatible constructor arguments. The following argument types are supported:
E        1. pybind11_cross_module_tests.LocalVec()
E        2. pybind11_cross_module_tests.LocalVec(arg0: pybind11_cross_module_tests.LocalVec)
E        3. pybind11_cross_module_tests.LocalVec(arg0: iterable)
E       
E    Invoked with:

8.1.0

test_exceptions.py:4: in <module>
    import pybind11_cross_module_tests as cm
E   ImportError: generic_type: type "LocalType" is already registered!

The key difference in debug mode is that -fvisibility=hidden isn't being given to the compiler, but it shouldn't really matter since the relevant functions are explicitly marked with __attribute__ ((visibility("hidden"))).

@jagerman
Copy link
Member Author

jagerman commented Aug 4, 2017

I think the issue is coming about because the functions calling the functions with the static local are not becoming hidden visibility, and so we're back into the ODR issue. It seems to me that the same issue is going to apply under appleclang for get_internals() when compiled not under -fvisibility=hidden (Edit: when loading cross-version modules, that is).

What if we just force hidden visibility for everything under the pybind11 namespace? I tested with changing the NAMESPACE_BEGIN definition to namespace name __attribute__((visibility("hidden"))) {, which seems to fix clang on OS X.

Obviously we'd need to rename the macro (e.g. PYBIND11_NAMESPACE), and condition on __GNUG__, etc. But would this cause any other problems?

@dean0x7d
Copy link
Member

dean0x7d commented Aug 8, 2017

Adding the visibility attribute to the namespace seems good to me. The only other options are visibility pragmas or function-level attributes, but neither are very convenient.

@jagerman
Copy link
Member Author

jagerman commented Aug 8, 2017

There is one potential downside to this in that it leads to a warning under gcc for code like this (from test_exceptions.cpp) if compiling without -fvisibility=hidden:

struct PythonCallInDestructor {
    // ...
    py::dict d;
};
/home/jagerman/src/pybind11/tests/test_exceptions.cpp:61:8: warning: ‘PythonCallInDestructor’ declared with greater visibility than the type of its field ‘PythonCallInDestructor::d’ [-Wattributes]
 struct PythonCallInDestructor {
        ^~~~~~~~~~~~~~~~~~~~~~

That's probably minor enough to ignore, but we can at least mention it in the release notes.

@dean0x7d
Copy link
Member

dean0x7d commented Aug 8, 2017

What about limiting the visibility attribute to the detail namespace?

@jagerman
Copy link
Member Author

jagerman commented Aug 8, 2017

I'm not confident that that's going to fix the issue under macOS: e.g. some non-hidden, non-detail function resolves to a 2.1 definition module calls something in detail that in turn calls get_internals() and gets the internals pointer for the wrong version.

I'm not particular familiar with the OS X ld, but I wonder if there is some optional flags that might address this in a nicer way? Otherwise it seems that there is all sorts of potential for conflict for mixed-version modules when -fvisibility=hidden isn't turned on.

@jagerman
Copy link
Member Author

jagerman commented Aug 8, 2017

Indeed I just verified--it's not enough to just force it on detail.

@dean0x7d
Copy link
Member

dean0x7d commented Aug 9, 2017

The same issue can be reproduced on Linux by changing Python's dlopenflags to RTLD_GLOBAL, which makes perfect sense. For pybind11's test suite, this can be done in conftest.py:

if sys.platform.startswith("linux"):
    sys.setdlopenflags(os.RTLD_NOW | os.RTLD_GLOBAL)

This reproduces the issue when compiled without -fvisibility=hidden (i.e. in debug mode when building with CMake).

On the macOS side, this can't be configured at run time, but there's a compile-time option which looks to be enabled by default (at least with the homebrew build; haven't looked into others yet).

So it seems like there are two possible solutions here:

  1. Always compile pybind11 modules with -fvisibility=hidden. This is pretty simple in CMake, but it would need to be documented for other build systems. The downside here is that the functionality of py::module_local is tied to a specific (off-by-default) compiler flag.

  2. Add the visibility attribute to pybind11's namespaces. The nice part is that it's independent of the build system, but it has the downside of the attribute warning mentioned above (which may be annoying to track down and work around on the user side).

Right now I'm leaning toward solution 1. It seems a bit cleaner and it would only require user intervention for custom build systems in conjunction with py::module_local. On the other hand, the warnings produced by the namespace attribute would be a lot more common.

@jagerman
Copy link
Member Author

jagerman commented Aug 9, 2017

I don't see why it would only affect py::module_local, though; if, say, make_new_instance() could randomly go to either the 2.1 or 2.2 version there are going to be many potential segfaults.

The warning that comes up under 2. only comes up if you're not using -fvisibility=hidden; if we're going to enforce 1. anyway, the downside of 2. disappears.

Edit: what I mean is, if we require 1., it's harmless to do 2., but doing 2. allows things to still work if the user ignores 1. anyway (this wouldn't be officially supported, but in most cases would still work).

@dean0x7d
Copy link
Member

dean0x7d commented Aug 9, 2017

I don't see why it would only affect py::module_local, though; if, say, make_new_instance() could randomly go to either the 2.1 or 2.2 version there are going to be many potential segfaults.

Good point. Although, the versioning issues have a possible alternative solution of using an inline namespace (which could similarly be added to the namespace macro), but this isn't an option for module_local.

One other thing to consider with the namespace attribute is that it would need to be applied consistently, but there are cases where the namespace macro isn't used, like user-defined casters and also a couple of places internally. That could also result in mixed visibility issues. My main worry is that it could be fragile.

what I mean is, if we require 1., it's harmless to do 2., but doing 2. allows things to still work if the user ignores 1. anyway

So you're suggesting to do both the compiler flag and namespace attribute?

@jagerman
Copy link
Member Author

jagerman commented Aug 9, 2017

So you're suggesting to do both the compiler flag and namespace attribute?

Yeah. Then you'll only actually run into warnings if you forget the -fvisibility=hidden, but the warnings might actually be helpful in that case in notifying you that you forgot the flag.

@jagerman
Copy link
Member Author

jagerman commented Aug 9, 2017

Ideally we would add a warning ourselves if not compiling under -fvisibility=hidden, but I don't know if there's a way to do that.

@dean0x7d
Copy link
Member

Yeah. Then you'll only actually run into warnings if you forget the -fvisibility=hidden, but the warnings might actually be helpful in that case in notifying you that you forgot the flag.

OK, that sounds good. As far as I've gathered, you've already been experimenting with the namespace attributes. Would you be willing to turn that into a PR along with the debug visibility flag change?

jagerman added a commit to jagerman/pybind11 that referenced this pull request Aug 10, 2017
This adds a PYBIND11_NAMESPACE macro that expands to the `pybind11`
namespace with hidden visibility under gcc-type compilers, and otherwise
to the plain `pybind11`.  This then forces hidden visibility on
everything in pybind, solving the visibility issues discussed at end
end of pybind#949.
jagerman added a commit to jagerman/pybind11 that referenced this pull request Aug 10, 2017
This adds a PYBIND11_NAMESPACE macro that expands to the `pybind11`
namespace with hidden visibility under gcc-type compilers, and otherwise
to the plain `pybind11`.  This then forces hidden visibility on
everything in pybind, solving the visibility issues discussed at end
end of pybind#949.
@jagerman
Copy link
Member Author

Would you be willing to turn that into a PR along with the debug visibility flag change?

Done in #995. We can carry on discussion there.

jagerman added a commit to jagerman/pybind11 that referenced this pull request Aug 13, 2017
This adds a PYBIND11_NAMESPACE macro that expands to the `pybind11`
namespace with hidden visibility under gcc-type compilers, and otherwise
to the plain `pybind11`.  This then forces hidden visibility on
everything in pybind, solving the visibility issues discussed at end
end of pybind#949.
@dean0x7d dean0x7d modified the milestone: v2.2 Aug 13, 2017
jagerman added a commit that referenced this pull request Aug 14, 2017
This adds a PYBIND11_NAMESPACE macro that expands to the `pybind11`
namespace with hidden visibility under gcc-type compilers, and otherwise
to the plain `pybind11`.  This then forces hidden visibility on
everything in pybind, solving the visibility issues discussed at end
end of #949.
@jagerman jagerman deleted the local-bindings branch August 14, 2017 20:26
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.

Eventual collisions between opaque STL containers
3 participants