Skip to content

[BUG] register_exception_translator have different behaviour when building with different STL. #2847

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

Closed
oraluben opened this issue Feb 6, 2021 · 12 comments · Fixed by #2999

Comments

@oraluben
Copy link
Contributor

oraluben commented Feb 6, 2021

Issue description

If I have a module a who have an exception translator and module b who throws an exception,
and import a, b in order in another python module.

With libstdc++, when b throws an error, it will be catched by the registered translator,
while with libc++, it won't.

Output1:

+ python -c 'import foo; foo.bar.thr()'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
RuntimeError

Output2:

+ python -c 'import foo; foo.bar.thr()'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
KeyError: ''

See below for detailed information.

Reproducible example code

This repro requires additional dependencies. I recommend you run it from a docker container (debian:buster has been verified to work).

Install dependencies

#!/bin/sh
set -ex

apt-get update

apt-get install -y -qq python-dev libc++1 libc++-dev pybind11-dev g++
ln -s /usr/lib/llvm-7/lib/libc++abi.so.1 /usr/lib/llvm-7/lib/libc++abi.so || true

Generate test files

#!/bin/sh
set -ex

cat > foo.h <<EOF
#include <stdexcept>

struct e : public std::runtime_error {
    explicit e(const char* what) :
        std::runtime_error(what) {
    }
};
EOF

cat > foo.cc <<EOF
#include <pybind11/pybind11.h>

#include "foo.h"

namespace py = pybind11;

PYBIND11_MODULE(_foo, m) {
    py::register_exception_translator([](std::exception_ptr p) {
        try {
            if (p) std::rethrow_exception(p);
        } catch (const e &_e) {
            PyErr_SetString(PyExc_KeyError, _e.what());
        }
    });
};
EOF

cat > bar.cc <<EOF
#include <pybind11/pybind11.h>

#include "foo.h"

void thr() {
    throw e{""};
}

namespace py = pybind11;

PYBIND11_MODULE(bar, m)
{
    m.def("thr", &thr);
}
EOF

cat > foo.py <<EOF
import _foo

import bar
EOF

Build and run

#!/bin/sh
set -ex

# https://libcxx.llvm.org/docs/UsingLibcxx.html

g++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-nostdinc++ -I/usr/lib/llvm-7/include/c++/v1 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o bar.o -c bar.cc

g++ -fPIC \
-nostdinc++ -I/usr/lib/llvm-7/include/c++/v1 \
-flto -shared \
-nodefaultlibs -L/usr/lib/llvm-7/lib -lc++ -lc++abi -lm -lc -lgcc_s -lgcc \
-o bar.so bar.o

g++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-nostdinc++ -I/usr/lib/llvm-7/include/c++/v1 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o _foo.o -c foo.cc

g++ -fPIC \
-nostdinc++ -I/usr/lib/llvm-7/include/c++/v1 \
-flto -shared \
-nodefaultlibs -L/usr/lib/llvm-7/lib -lc++ -lc++abi -lm -lc -lgcc_s -lgcc \
-o _foo.so _foo.o

python -c "import foo; foo.bar.thr()" || true


g++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o bar.o -c bar.cc

g++ -fPIC \
-flto -shared \
-o bar.so bar.o

g++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o _foo.o -c foo.cc

g++ -fPIC \
-flto -shared \
-o _foo.so _foo.o

python -c "import foo; foo.bar.thr()" || true

rm -rf *.o *.so

@oraluben
Copy link
Contributor Author

oraluben commented Feb 6, 2021

I also verified that simply combing the default Python (libstdc++) and a binding built with libc++ works fine. So this seems not a compatibility issue between different STL.

Thus I wonder what's the expected behavior here.

@bstaletic
Copy link
Collaborator

You're using gcc to include libc++ and then aren't linking the standard library at all? At least when compiling _foo.o? I don't think gcc ever claimed support for that. At least I've never seen or attempted that. Why not use clang++ and its -stdlib=libc++?

@YannickJadoul
Copy link
Collaborator

Specifically, what exactly are you trying to accomplish? There's weird things going on with standard libs, so I can't see how pybind11 would be to blame. By playing around with things, you probably just ended up with 2 different types?

@oraluben
Copy link
Contributor Author

oraluben commented Feb 8, 2021

Thanks for your reply!

use clang++ and its -stdlib=libc++

That's really a very good point. I didn't try it and it turns out to have no differrence:

clang++ and -stdlib=libc++

clang++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-stdlib=libc++ \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o bar.o -c bar.cc

clang++ -fPIC \
-stdlib=libc++ \
-flto -shared \
-o bar.so bar.o

clang++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-stdlib=libc++ \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o _foo.o -c foo.cc

clang++ -fPIC \
-stdlib=libc++ \
-flto -shared \
-o _foo.so _foo.o

python -c "import foo; foo.bar.thr()" || true


clang++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o bar.o -c bar.cc

clang++ -fPIC \
-flto -shared \
-o bar.so bar.o

clang++ -I/usr/include/pybind11/ -I/usr/include/python2.7 \
-fPIC -fvisibility=hidden -flto -fno-fat-lto-objects -std=gnu++11 \
-o _foo.o -c foo.cc

clang++ -fPIC \
-flto -shared \
-o _foo.so _foo.o

python -c "import foo; foo.bar.thr()" || true

still gives (warning omitted)

Traceback (most recent call last):
  File "<string>", line 1, in <module>
RuntimeError

Traceback (most recent call last):
  File "<string>", line 1, in <module>
KeyError: ''

I'm not trying to blame pybind11 at all.
What I'm trying to ask is, is the libstdc++ behavior intensional in pybind11, if yes, is there any special implement detail you achieve that.
And since libc++ claims to have ABI compatibility with libstdc++, and so do it do when building just one package will not have the inconsistency in my demo, do you think, from the perspective of the pybind11's developer, it could be a potential bug in either of the STL/compiler?

@bstaletic
Copy link
Collaborator

bstaletic commented Feb 8, 2021

Okay, I can repro with just clang++ -stdlib=libc++ -fPIC -shared -o thing.so thing.cpp. bar behaves as if there was no translation for e registered at all. Indeed, when importing foo, I can see it registering the exception, here: https://github.com/pybind/pybind11/blob/master/include/pybind11/pybind11.h#L1991

But then, when bar.thr() gets called, here it says the list of translators is empty... NOT empty, as expected! My bad.

@YannickJadoul
Copy link
Collaborator

Oh, that rings a bell. Could this be related to pybind11 explicitly taking the stdlib into account for the internals ABI version?

#ifndef PYBIND11_STDLIB
# if defined(_LIBCPP_VERSION)
# define PYBIND11_STDLIB "_libcpp"
# elif defined(__GLIBCXX__) || defined(__GLIBCPP__)
# define PYBIND11_STDLIB "_libstdcpp"
# else
# define PYBIND11_STDLIB ""
# endif
#endif

@bstaletic
Copy link
Collaborator

Okay... I don't get this at all.

    py::register_exception_translator([](std::exception_ptr p) {
        puts("called");
        try {
            puts(p?"has exception":"no exception");
            puts("Name of struct e:");
            puts(typeid(struct e).name());
            if (p) std::rethrow_exception(p);
        } catch (const e &_e) {
            puts("caught e");
            PyErr_SetString(PyExc_KeyError, _e.what());
        } catch (const std::exception& ee) {
            puts("caught something");
            puts("Name of something:");
            puts(typeid(ee).name());
            throw;
        }
    });

As you might expect (but probably not), the output of bar.thr() is:

called
has exception
Name of struct e:
1e
caught something
Name of something:
1e
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError

@bstaletic
Copy link
Collaborator

Oh, that rings a bell. Could this be related to pybind11 explicitly taking the stdlib into account for the internals ABI version?

But I'm compiling both modules with libc++...

@oraluben
Copy link
Contributor Author

oraluben commented Feb 8, 2021

But I'm compiling both modules with libc++...

Could it be the ABI mismatch between modules and Python interpreter?

@bstaletic
Copy link
Collaborator

Not likely? CPython doesn't link against any C++ library. I also tried to do something like this:

/// foo.cpp
void catch_e(std::exception_ptr p) {
        try {
                if(p) std::rethrow_exception(p);
        } catch(const e& e) {
                puts("e");
        } catch(const std::exception&) {
                puts("not e");
        }
}
/// bar.cpp
auto throw_e() {
        throw e("");
}
/// main.cpp
int main() {
        std::exception_ptr e;
        try {
                throw_e();
        } catch(...) {
                e = std::current_exception();
        }
        catch_e(e);
}

With foo.cpp compiled into libfoo.so, bar.cpp into libbar.so. I couldn't repro.

@oraluben
Copy link
Contributor Author

oraluben commented Feb 9, 2021

I confirm that's also the case on macOS, so seems related to libc++.

@XZiar
Copy link

XZiar commented May 10, 2021

The issue may be caused by the way libc++ handles hidden visibility and type_info.

With -fvisibility=hidden and struct e not being marked as visible, compiler may generate 2 copies of e for 2 modules, and libc++ is going to generate different type_info & hash_code for them, causing exception not being catched correctly.

See this

Maybe try add __attribute__((visibility("default"))) to see if it can fix the issue:

struct __attribute__((visibility("default"))) e : public std::runtime_error {

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 a pull request may close this issue.

4 participants