From abfc0b196ae8992795587276cdb30ccc5173889b Mon Sep 17 00:00:00 2001
From: Peter Steneteg <peter@steneteg.se>
Date: Tue, 27 May 2025 13:52:17 +0200
Subject: [PATCH 01/14] test: Added test case for visibility of common symbols
 across shared libraries

---
 tests/CMakeLists.txt                      |  3 ++
 tests/test_visibility/CMakeLists.txt      | 63 +++++++++++++++++++++++
 tests/test_visibility/bindings.cpp        | 20 +++++++
 tests/test_visibility/catch.cpp           | 22 ++++++++
 tests/test_visibility/lib.cpp             | 13 +++++
 tests/test_visibility/lib.h               | 30 +++++++++++
 tests/test_visibility/test_visibility.cpp | 48 +++++++++++++++++
 7 files changed, 199 insertions(+)
 create mode 100644 tests/test_visibility/CMakeLists.txt
 create mode 100644 tests/test_visibility/bindings.cpp
 create mode 100644 tests/test_visibility/catch.cpp
 create mode 100644 tests/test_visibility/lib.cpp
 create mode 100644 tests/test_visibility/lib.h
 create mode 100644 tests/test_visibility/test_visibility.cpp

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 069b4f6f38..2546d1d372 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -649,4 +649,7 @@ if(NOT PYBIND11_CUDA_TESTS)
 
   # Test CMake build using functions and targets from subdirectory or installed location
   add_subdirectory(test_cmake_build)
+
+  # Test visibility of common symbols across shared libraries
+  add_subdirectory(test_visibility)
 endif()
diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
new file mode 100644
index 0000000000..21da974d4c
--- /dev/null
+++ b/tests/test_visibility/CMakeLists.txt
@@ -0,0 +1,63 @@
+possibly_uninitialized(PYTHON_MODULE_EXTENSION Python_INTERPRETER_ID)
+
+if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
+   OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
+   OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")
+  message(STATUS "Skipping embed test on PyPy or GraalPy")
+  add_custom_target(cpptest) # Dummy target on PyPy or GraalPy. Embedding is not supported.
+  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
+  return()
+endif()
+
+if(TARGET Python::Module AND NOT TARGET Python::Python)
+  message(STATUS "Skipping embed test since no embed libs found")
+  add_custom_target(cpptest) # Dummy target since embedding is not supported.
+  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
+  return()
+endif()
+
+find_package(Catch 2.13.10)
+
+if(CATCH_FOUND)
+  message(STATUS "Building interpreter tests using Catch v${CATCH_VERSION}")
+else()
+  message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers"
+                 " manually or use `cmake -DDOWNLOAD_CATCH=ON` to fetch them automatically.")
+  return()
+endif()
+
+include(GenerateExportHeader)
+
+add_library(test_visibility_lib SHARED lib.h lib.cpp)
+add_library(test_visibility_lib::test_visibility_lib ALIAS test_visibility_lib)
+target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
+target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+generate_export_header(test_visibility_lib)
+
+pybind11_add_module(test_visibility_bindings SHARED bindings.cpp)
+target_link_libraries(test_visibility_bindings PUBLIC test_visibility_lib::test_visibility_lib)
+
+add_executable(test_visibility_main catch.cpp test_visibility.cpp)
+target_link_libraries(test_visibility_main PUBLIC
+  test_visibility_lib::test_visibility_lib
+  pybind11::embed
+  Catch2::Catch2)
+
+# Ensure that we have built the python bindings since we load them in main
+add_dependencies(test_visibility_main test_visibility_bindings)
+
+pybind11_enable_warnings(test_visibility_main)
+pybind11_enable_warnings(test_visibility_bindings)
+pybind11_enable_warnings(test_visibility_lib)
+
+add_custom_target(
+  test_visibility
+  COMMAND "$<TARGET_FILE:test_visibility_main>"
+  DEPENDS test_visibility_main
+  WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_visibility_main>")
+
+set_target_properties(test_visibility_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY
+                                                 "${CMAKE_CURRENT_BINARY_DIR}")
+
+add_dependencies(check test_visibility)
diff --git a/tests/test_visibility/bindings.cpp b/tests/test_visibility/bindings.cpp
new file mode 100644
index 0000000000..ccd78e2808
--- /dev/null
+++ b/tests/test_visibility/bindings.cpp
@@ -0,0 +1,20 @@
+#include <pybind11/pybind11.h>
+
+#include <lib.h>
+
+class BaseTrampoline : public lib::Base, public pybind11::trampoline_self_life_support {
+public:
+    using lib::Base::Base;
+    int get() const override { PYBIND11_OVERLOAD(int, lib::Base, get); }
+};
+
+PYBIND11_MODULE(test_visibility_bindings, m) {
+    pybind11::classh<lib::Base, BaseTrampoline>(m, "Base")
+        .def(pybind11::init<int, int>())
+        .def_readwrite("a", &lib::Base::a)
+        .def_readwrite("b", &lib::Base::b);
+
+    m.def("get_foo", [](int a, int b) -> std::shared_ptr<lib::Base> {
+        return std::make_shared<lib::Foo>(a, b);
+    });
+}
diff --git a/tests/test_visibility/catch.cpp b/tests/test_visibility/catch.cpp
new file mode 100644
index 0000000000..2debc5ff17
--- /dev/null
+++ b/tests/test_visibility/catch.cpp
@@ -0,0 +1,22 @@
+// The Catch implementation is compiled here. This is a standalone
+// translation unit to avoid recompiling it for every test change.
+
+#include <pybind11/embed.h>
+
+// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
+// catch 2.0.1; this should be fixed in the next catch release after 2.0.1).
+PYBIND11_WARNING_DISABLE_MSVC(4996)
+
+// Catch uses _ internally, which breaks gettext style defines
+#ifdef _
+#    undef _
+#endif
+
+#define CATCH_CONFIG_RUNNER
+#include <catch.hpp>
+
+int main(int argc, char *argv[]) {
+    pybind11::scoped_interpreter guard{};
+    auto result = Catch::Session().run(argc, argv);
+    return result < 0xff ? result : 0xff;
+}
diff --git a/tests/test_visibility/lib.cpp b/tests/test_visibility/lib.cpp
new file mode 100644
index 0000000000..13b254d445
--- /dev/null
+++ b/tests/test_visibility/lib.cpp
@@ -0,0 +1,13 @@
+#include <lib.h>
+
+namespace lib {
+
+Base::Base(int a, int b) : a(a), b(b) {}
+
+int Base::get() const { return a + b; }
+
+Foo::Foo(int a, int b) : Base{a, b} {}
+
+int Foo::get() const { return 2 * a + b; }
+
+}  // namespace lib
diff --git a/tests/test_visibility/lib.h b/tests/test_visibility/lib.h
new file mode 100644
index 0000000000..3a82436dd2
--- /dev/null
+++ b/tests/test_visibility/lib.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <test_visibility_lib_export.h>
+#include <memory>
+
+#if defined(_MSC_VER)
+__pragma(warning(disable : 4251))
+#endif
+
+namespace lib {
+
+class TEST_VISIBILITY_LIB_EXPORT Base : public std::enable_shared_from_this<Base> {
+public:
+    Base(int a, int b);
+    virtual ~Base() = default;
+
+    virtual int get() const;
+
+    int a;
+    int b;
+};
+
+class TEST_VISIBILITY_LIB_EXPORT Foo : public Base {
+public:
+    Foo(int a, int b);
+
+    int get() const override;
+};
+
+}  // namespace lib
diff --git a/tests/test_visibility/test_visibility.cpp b/tests/test_visibility/test_visibility.cpp
new file mode 100644
index 0000000000..f5254be1e9
--- /dev/null
+++ b/tests/test_visibility/test_visibility.cpp
@@ -0,0 +1,48 @@
+
+#include <lib.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/embed.h>
+
+#include <catch.hpp>
+
+static constexpr auto script = R"(
+import test_visibility_bindings
+
+class Bar(test_visibility_bindings.Base):
+    def __init__(self, a, b):
+        test_visibility_bindings.Base.__init__(self, a, b)
+    
+    def get(self):
+        return 4 * self.a + self.b
+
+
+def get_bar(a, b):
+    return Bar(a, b)
+
+)";
+
+
+TEST_CASE("Simple case where without alias") {
+    // "Simple" case this will not have "python_instance_is_alias" set in type_cast_base.h:771
+    auto bindings = pybind11::module_::import("test_visibility_bindings");
+    auto holder = bindings.attr("get_foo")(1,2);
+    auto foo = holder.cast<std::shared_ptr<lib::Base>>();
+    REQUIRE(foo->get() == 4); // 2 * 1 + 2 = 4
+}
+
+TEST_CASE("Complex case where with alias") {
+    // "Complex" case this will have "python_instance_is_alias" set in type_cast_base.h:771
+    pybind11::exec(script);
+    auto main = pybind11::module::import("__main__");
+    auto holder2 = main.attr("get_bar")(1, 2);
+
+    // this will trigger "std::get_deleter<memory::guarded_delete>" in type_cast_base.h:772
+    // This will fail since the program will see two different typeids for "memory::guarded_delete"
+    // on from the bindings module and one from "main", which will both have
+    // "__is_type_name_unique" as true and but still have different values. Hence we will not find
+    // the deleter and the cast fill fail. See "__eq(__type_name_t __lhs, __type_name_t __rhs)" in
+    // typeinfo in libc++
+    auto bar = holder2.cast<std::shared_ptr<lib::Base>>();
+    REQUIRE(bar->get() == 6); // 4 * 1 + 2 = 6
+
+}

From 8637068ac817799b146fbd834f7ec20b38ab67f8 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 27 May 2025 13:19:20 +0000
Subject: [PATCH 02/14] style: pre-commit fixes

---
 tests/test_visibility/CMakeLists.txt      |  8 +++---
 tests/test_visibility/lib.cpp             |  2 +-
 tests/test_visibility/lib.h               | 32 +++++++++++------------
 tests/test_visibility/test_visibility.cpp | 10 +++----
 4 files changed, 24 insertions(+), 28 deletions(-)

diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
index 21da974d4c..05dafd410b 100644
--- a/tests/test_visibility/CMakeLists.txt
+++ b/tests/test_visibility/CMakeLists.txt
@@ -39,10 +39,8 @@ pybind11_add_module(test_visibility_bindings SHARED bindings.cpp)
 target_link_libraries(test_visibility_bindings PUBLIC test_visibility_lib::test_visibility_lib)
 
 add_executable(test_visibility_main catch.cpp test_visibility.cpp)
-target_link_libraries(test_visibility_main PUBLIC
-  test_visibility_lib::test_visibility_lib
-  pybind11::embed
-  Catch2::Catch2)
+target_link_libraries(test_visibility_main PUBLIC test_visibility_lib::test_visibility_lib
+                                                  pybind11::embed Catch2::Catch2)
 
 # Ensure that we have built the python bindings since we load them in main
 add_dependencies(test_visibility_main test_visibility_bindings)
@@ -58,6 +56,6 @@ add_custom_target(
   WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_visibility_main>")
 
 set_target_properties(test_visibility_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY
-                                                 "${CMAKE_CURRENT_BINARY_DIR}")
+                                                          "${CMAKE_CURRENT_BINARY_DIR}")
 
 add_dependencies(check test_visibility)
diff --git a/tests/test_visibility/lib.cpp b/tests/test_visibility/lib.cpp
index 13b254d445..927ed044bc 100644
--- a/tests/test_visibility/lib.cpp
+++ b/tests/test_visibility/lib.cpp
@@ -10,4 +10,4 @@ Foo::Foo(int a, int b) : Base{a, b} {}
 
 int Foo::get() const { return 2 * a + b; }
 
-}  // namespace lib
+} // namespace lib
diff --git a/tests/test_visibility/lib.h b/tests/test_visibility/lib.h
index 3a82436dd2..8275b85b5c 100644
--- a/tests/test_visibility/lib.h
+++ b/tests/test_visibility/lib.h
@@ -1,30 +1,30 @@
 #pragma once
 
-#include <test_visibility_lib_export.h>
 #include <memory>
+#include <test_visibility_lib_export.h>
 
 #if defined(_MSC_VER)
 __pragma(warning(disable : 4251))
 #endif
 
-namespace lib {
+    namespace lib {
 
-class TEST_VISIBILITY_LIB_EXPORT Base : public std::enable_shared_from_this<Base> {
-public:
-    Base(int a, int b);
-    virtual ~Base() = default;
+    class TEST_VISIBILITY_LIB_EXPORT Base : public std::enable_shared_from_this<Base> {
+    public:
+        Base(int a, int b);
+        virtual ~Base() = default;
 
-    virtual int get() const;
+        virtual int get() const;
 
-    int a;
-    int b;
-};
+        int a;
+        int b;
+    };
 
-class TEST_VISIBILITY_LIB_EXPORT Foo : public Base {
-public:
-    Foo(int a, int b);
+    class TEST_VISIBILITY_LIB_EXPORT Foo : public Base {
+    public:
+        Foo(int a, int b);
 
-    int get() const override;
-};
+        int get() const override;
+    };
 
-}  // namespace lib
+} // namespace lib
diff --git a/tests/test_visibility/test_visibility.cpp b/tests/test_visibility/test_visibility.cpp
index f5254be1e9..2412add3c1 100644
--- a/tests/test_visibility/test_visibility.cpp
+++ b/tests/test_visibility/test_visibility.cpp
@@ -1,9 +1,9 @@
 
-#include <lib.h>
-#include <pybind11/pybind11.h>
 #include <pybind11/embed.h>
+#include <pybind11/pybind11.h>
 
 #include <catch.hpp>
+#include <lib.h>
 
 static constexpr auto script = R"(
 import test_visibility_bindings
@@ -11,7 +11,7 @@ import test_visibility_bindings
 class Bar(test_visibility_bindings.Base):
     def __init__(self, a, b):
         test_visibility_bindings.Base.__init__(self, a, b)
-    
+
     def get(self):
         return 4 * self.a + self.b
 
@@ -21,11 +21,10 @@ def get_bar(a, b):
 
 )";
 
-
 TEST_CASE("Simple case where without alias") {
     // "Simple" case this will not have "python_instance_is_alias" set in type_cast_base.h:771
     auto bindings = pybind11::module_::import("test_visibility_bindings");
-    auto holder = bindings.attr("get_foo")(1,2);
+    auto holder = bindings.attr("get_foo")(1, 2);
     auto foo = holder.cast<std::shared_ptr<lib::Base>>();
     REQUIRE(foo->get() == 4); // 2 * 1 + 2 = 4
 }
@@ -44,5 +43,4 @@ TEST_CASE("Complex case where with alias") {
     // typeinfo in libc++
     auto bar = holder2.cast<std::shared_ptr<lib::Base>>();
     REQUIRE(bar->get() == 6); // 4 * 1 + 2 = 6
-
 }

From d866a8aff28206718ed79c7ff34d6d7026f93942 Mon Sep 17 00:00:00 2001
From: Peter Steneteg <peter@steneteg.se>
Date: Tue, 27 May 2025 15:46:17 +0200
Subject: [PATCH 03/14] tests: cmake target name fix

---
 tests/test_visibility/CMakeLists.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
index 05dafd410b..79efae0822 100644
--- a/tests/test_visibility/CMakeLists.txt
+++ b/tests/test_visibility/CMakeLists.txt
@@ -4,14 +4,14 @@ if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
    OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
    OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")
   message(STATUS "Skipping embed test on PyPy or GraalPy")
-  add_custom_target(cpptest) # Dummy target on PyPy or GraalPy. Embedding is not supported.
+  add_custom_target(test_visibility) # Dummy target on PyPy or GraalPy. Embedding is not supported.
   set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
   return()
 endif()
 
 if(TARGET Python::Module AND NOT TARGET Python::Python)
   message(STATUS "Skipping embed test since no embed libs found")
-  add_custom_target(cpptest) # Dummy target since embedding is not supported.
+  add_custom_target(test_visibility) # Dummy target since embedding is not supported.
   set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
   return()
 endif()

From 6b81c9e9b032c3adfecc9706fa8c3ee85fae6ddf Mon Sep 17 00:00:00 2001
From: Peter Steneteg <peter@steneteg.se>
Date: Tue, 27 May 2025 18:47:25 +0200
Subject: [PATCH 04/14] tests: Added visibility test to ci

---
 .github/workflows/ci.yml                  | 43 +++++++++++++++++++++++
 .github/workflows/reusable-standard.yml   |  3 ++
 tests/test_visibility/CMakeLists.txt      |  4 +--
 tests/test_visibility/test_visibility.cpp | 18 ++++++----
 4 files changed, 59 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7d7d73a665..683000ec14 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -230,6 +230,9 @@ jobs:
     - name: Interface test
       run: cmake --build . --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build . --target test_visibility
+
 
   manylinux:
     name: Manylinux on 🐍 3.13t • GIL
@@ -328,6 +331,9 @@ jobs:
     - name: C++ tests
       run: cmake --build --preset default --target cpptest
 
+    - name: Visibility test
+      run: cmake --build --preset default --target test_visibility
+
     - name: Run Valgrind on Python tests
       if: matrix.valgrind
       run: cmake --build --preset default --target memcheck
@@ -386,6 +392,8 @@ jobs:
     - name: Interface test
       run: cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build build --target test_visibility
 
   # Testing NVCC; forces sources to behave like .cu files
   cuda:
@@ -505,6 +513,8 @@ jobs:
     - name: Interface test
       run: cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build build --target test_visibility
 
   # Testing on GCC using the GCC docker images (only recent images supported)
   gcc:
@@ -556,6 +566,9 @@ jobs:
     - name: Interface test
       run: cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build build --target test_visibility
+
     - name: Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE
       if: matrix.gcc == '12'
       shell: bash
@@ -638,6 +651,11 @@ jobs:
         set +e; source /opt/intel/oneapi/setvars.sh; set -e
         cmake --build build-11 --target test_cmake_build
 
+    - name: Visibility test
+      run: |
+        set +e; source /opt/intel/oneapi/setvars.sh; set -e
+        cmake --build build-11 --target test_visibility
+
     - name: Configure C++17
       run: |
         set +e; source /opt/intel/oneapi/setvars.sh; set -e
@@ -670,6 +688,10 @@ jobs:
         set +e; source /opt/intel/oneapi/setvars.sh; set -e
         cmake --build build-17 --target test_cmake_build
 
+    - name: Visibility test
+      run: |
+        set +e; source /opt/intel/oneapi/setvars.sh; set -e
+        cmake --build build-17 --target test_visibility
 
   # Testing on CentOS (manylinux uses a centos base).
   centos:
@@ -732,6 +754,9 @@ jobs:
     - name: Interface test
       run: cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build build --target test_visibility
+
 
   # This tests an "install" with the CMake tools
   install-classic:
@@ -961,6 +986,9 @@ jobs:
     - name: Interface test C++20
       run: cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: cmake --build build --target test_visibility
+
     - name: Configure C++20 - Exercise cmake -DPYBIND11_TEST_OVERRIDE
       run: >
         cmake -S . -B build_partial
@@ -1034,6 +1062,9 @@ jobs:
     - name: Interface test C++11
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target test_cmake_build
 
+    - name: Visibility test
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target test_visibility
+
     - name: Clean directory
       run: git clean -fdx
 
@@ -1055,6 +1086,9 @@ jobs:
     - name: Interface test C++14
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target test_cmake_build
 
+    - name: Visibility test
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target test_visibility
+
     - name: Clean directory
       run: git clean -fdx
 
@@ -1076,6 +1110,9 @@ jobs:
     - name: Interface test C++17
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cmake_build
 
+    - name: Visibility test
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_visibility
+
   windows_clang:
     if: github.event.pull_request.draft == false
 
@@ -1143,6 +1180,9 @@ jobs:
       - name: Interface test
         run: cmake --build . --target test_cmake_build -j 2
 
+      - name: Visibility test
+        run: cmake --build . --target test_visibility -j 2
+
       - name: Clean directory
         run: git clean -fdx
 
@@ -1210,6 +1250,9 @@ jobs:
       - name: Interface test
         run: cmake --build . --target test_cmake_build -j 2
 
+      - name: Visibility test
+        run: cmake --build . --target test_visibility -j 2
+
       - name: CMake Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE
         run: >
           cmake -S . -B build_partial
diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml
index b59949316b..270479f4f5 100644
--- a/.github/workflows/reusable-standard.yml
+++ b/.github/workflows/reusable-standard.yml
@@ -74,6 +74,9 @@ jobs:
       - name: Interface test
         run: cmake --build build --target test_cmake_build
 
+      - name: Visibility test
+        run: cmake --build . --target test_visibility
+
       - name: Setuptools helpers test
         run: |
           uv pip install --python=python --system setuptools
diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
index 79efae0822..cba1dd62d0 100644
--- a/tests/test_visibility/CMakeLists.txt
+++ b/tests/test_visibility/CMakeLists.txt
@@ -3,14 +3,14 @@ possibly_uninitialized(PYTHON_MODULE_EXTENSION Python_INTERPRETER_ID)
 if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
    OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
    OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")
-  message(STATUS "Skipping embed test on PyPy or GraalPy")
+  message(STATUS "Skipping visibility test on PyPy or GraalPy")
   add_custom_target(test_visibility) # Dummy target on PyPy or GraalPy. Embedding is not supported.
   set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
   return()
 endif()
 
 if(TARGET Python::Module AND NOT TARGET Python::Python)
-  message(STATUS "Skipping embed test since no embed libs found")
+  message(STATUS "Skipping visibility test since no embed libs found")
   add_custom_target(test_visibility) # Dummy target since embedding is not supported.
   set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
   return()
diff --git a/tests/test_visibility/test_visibility.cpp b/tests/test_visibility/test_visibility.cpp
index 2412add3c1..fe3caed2e5 100644
--- a/tests/test_visibility/test_visibility.cpp
+++ b/tests/test_visibility/test_visibility.cpp
@@ -21,24 +21,28 @@ def get_bar(a, b):
 
 )";
 
-TEST_CASE("Simple case where without alias") {
-    // "Simple" case this will not have "python_instance_is_alias" set in type_cast_base.h:771
+TEST_CASE("Simple case where without is_alias") {
+    // "Simple" case this will not have `python_instance_is_alias` set in type_cast_base.h:771
     auto bindings = pybind11::module_::import("test_visibility_bindings");
     auto holder = bindings.attr("get_foo")(1, 2);
     auto foo = holder.cast<std::shared_ptr<lib::Base>>();
     REQUIRE(foo->get() == 4); // 2 * 1 + 2 = 4
 }
 
-TEST_CASE("Complex case where with alias") {
-    // "Complex" case this will have "python_instance_is_alias" set in type_cast_base.h:771
+TEST_CASE("Complex case where with it_alias") {
+    // "Complex" case this will have `python_instance_is_alias` set in type_cast_base.h:771
     pybind11::exec(script);
     auto main = pybind11::module::import("__main__");
+
+    // The critical part of "Bar" is that it will have the `is_alias` `instance` flag set.
+    // I'm not quite sure what is required to get that flag, this code is derived from a
+    // larger code where this issue was observed.
     auto holder2 = main.attr("get_bar")(1, 2);
 
-    // this will trigger "std::get_deleter<memory::guarded_delete>" in type_cast_base.h:772
-    // This will fail since the program will see two different typeids for "memory::guarded_delete"
+    // this will trigger `std::get_deleter<memory::guarded_delete>` in type_cast_base.h:772
+    // This will fail since the program will see two different typeids for `memory::guarded_delete`
     // on from the bindings module and one from "main", which will both have
-    // "__is_type_name_unique" as true and but still have different values. Hence we will not find
+    // `__is_type_name_unique` as true and but still have different values. Hence we will not find
     // the deleter and the cast fill fail. See "__eq(__type_name_t __lhs, __type_name_t __rhs)" in
     // typeinfo in libc++
     auto bar = holder2.cast<std::shared_ptr<lib::Base>>();

From 4cb2f8352518a4145a46981e700e5ac83e2e23c1 Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Tue, 27 May 2025 14:46:15 -0400
Subject: [PATCH 05/14] tests: set the default visibility to hidden

---
 tests/test_visibility/CMakeLists.txt | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
index cba1dd62d0..3d724ca28d 100644
--- a/tests/test_visibility/CMakeLists.txt
+++ b/tests/test_visibility/CMakeLists.txt
@@ -1,5 +1,7 @@
 possibly_uninitialized(PYTHON_MODULE_EXTENSION Python_INTERPRETER_ID)
 
+set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+
 if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
    OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
    OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")

From 4205d06a4de25e51fb9ac487cafea05dcbec19c9 Mon Sep 17 00:00:00 2001
From: "Ralf W. Grosse-Kunstleve" <rgrossekunst@nvidia.com>
Date: Tue, 27 May 2025 13:50:46 -0700
Subject: [PATCH 06/14] prototype/proof-of-concept fix:
 PYBIND11_EXPORT_GUARDED_DELETE

---
 include/pybind11/detail/struct_smart_holder.h | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index 758182119c..d1b33665d6 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -58,6 +58,18 @@ High-level aspects:
 #include <typeinfo>
 #include <utility>
 
+// IMPORTANT: This code block must stay BELOW the #include <stdexcept> above.
+#if !defined(PYBIND11_EXPORT_GUARDED_DELETE)
+#    if defined(_LIBCPP_EXCEPTION)
+#        if defined(WIN32) || defined(_WIN32)
+#            error "UNEXPECTED: defined(_LIBCPP_EXCEPTION) && (defined(WIN32) || defined(_WIN32))"
+#        endif
+#        define PYBIND11_EXPORT_GUARDED_DELETE __attribute__((visibility("default")))
+#    else
+#        define PYBIND11_EXPORT_GUARDED_DELETE
+#    endif
+#endif
+
 PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
 PYBIND11_NAMESPACE_BEGIN(memory)
 

From 267e01e8bfa1b5932758675a11699461aec21ef0 Mon Sep 17 00:00:00 2001
From: "Ralf W. Grosse-Kunstleve" <rgrossekunst@nvidia.com>
Date: Tue, 27 May 2025 14:09:42 -0700
Subject: [PATCH 07/14] Fix silly oversight: actually use
 PYBIND11_EXPORT_GUARDED_DELETE

---
 include/pybind11/detail/struct_smart_holder.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index d1b33665d6..1a08a12e88 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -90,7 +90,7 @@ static constexpr bool type_has_shared_from_this(const void *) {
     return false;
 }
 
-struct guarded_delete {
+struct PYBIND11_EXPORT_GUARDED_DELETE guarded_delete {
     std::weak_ptr<void> released_ptr;    // Trick to keep the smart_holder memory footprint small.
     std::function<void(void *)> del_fun; // Rare case.
     void (*del_ptr)(void *);             // Common case.

From e9ee4a226f447ecfced31e135b7df0ed0e6915c5 Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Thu, 29 May 2025 10:45:32 -0400
Subject: [PATCH 08/14] Update struct_smart_holder.h

---
 include/pybind11/detail/struct_smart_holder.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index 1a08a12e88..94950210d2 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -59,11 +59,11 @@ High-level aspects:
 #include <utility>
 
 // IMPORTANT: This code block must stay BELOW the #include <stdexcept> above.
+// This is only requried on some builds with libc++ (one of three implementations
+// in https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276
+// requiere it)
 #if !defined(PYBIND11_EXPORT_GUARDED_DELETE)
-#    if defined(_LIBCPP_EXCEPTION)
-#        if defined(WIN32) || defined(_WIN32)
-#            error "UNEXPECTED: defined(_LIBCPP_EXCEPTION) && (defined(WIN32) || defined(_WIN32))"
-#        endif
+#    if defined(__libcpp_version) && !defined(WIN32) && !defined(_WIN32)
 #        define PYBIND11_EXPORT_GUARDED_DELETE __attribute__((visibility("default")))
 #    else
 #        define PYBIND11_EXPORT_GUARDED_DELETE

From c5eac1b53c1d1610846e8f5a752a2f42c0a86f27 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 29 May 2025 14:47:31 +0000
Subject: [PATCH 09/14] style: pre-commit fixes

---
 include/pybind11/detail/struct_smart_holder.h | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index 94950210d2..f34ca94712 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -60,7 +60,8 @@ High-level aspects:
 
 // IMPORTANT: This code block must stay BELOW the #include <stdexcept> above.
 // This is only requried on some builds with libc++ (one of three implementations
-// in https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276
+// in
+// https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276
 // requiere it)
 #if !defined(PYBIND11_EXPORT_GUARDED_DELETE)
 #    if defined(__libcpp_version) && !defined(WIN32) && !defined(_WIN32)

From a19920c5407a95ea62ed960d3b907eb75e808bed Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Thu, 29 May 2025 11:29:27 -0400
Subject: [PATCH 10/14] Update include/pybind11/detail/struct_smart_holder.h

---
 include/pybind11/detail/struct_smart_holder.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index f34ca94712..9747225f96 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -64,7 +64,7 @@ High-level aspects:
 // https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276
 // requiere it)
 #if !defined(PYBIND11_EXPORT_GUARDED_DELETE)
-#    if defined(__libcpp_version) && !defined(WIN32) && !defined(_WIN32)
+#    if defined(_LIBCPP_VERSION) && !defined(WIN32) && !defined(_WIN32)
 #        define PYBIND11_EXPORT_GUARDED_DELETE __attribute__((visibility("default")))
 #    else
 #        define PYBIND11_EXPORT_GUARDED_DELETE

From 48f2e94da476ec71d26f2c5b14a915dec73b50fe Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Thu, 29 May 2025 11:50:49 -0400
Subject: [PATCH 11/14] Update struct_smart_holder.h

---
 include/pybind11/detail/struct_smart_holder.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index 9747225f96..9b2da87837 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -59,10 +59,10 @@ High-level aspects:
 #include <utility>
 
 // IMPORTANT: This code block must stay BELOW the #include <stdexcept> above.
-// This is only requried on some builds with libc++ (one of three implementations
+// This is only required on some builds with libc++ (one of three implementations
 // in
 // https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276
-// requiere it)
+// require it)
 #if !defined(PYBIND11_EXPORT_GUARDED_DELETE)
 #    if defined(_LIBCPP_VERSION) && !defined(WIN32) && !defined(_WIN32)
 #        define PYBIND11_EXPORT_GUARDED_DELETE __attribute__((visibility("default")))

From 29b2853f6d3bc555473f980087a2fe92828359ce Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Sun, 1 Jun 2025 01:37:54 -0400
Subject: [PATCH 12/14] ci: fix addition to reusable-standard.yml

---
 .github/workflows/reusable-standard.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml
index 270479f4f5..e124b65bd4 100644
--- a/.github/workflows/reusable-standard.yml
+++ b/.github/workflows/reusable-standard.yml
@@ -75,7 +75,7 @@ jobs:
         run: cmake --build build --target test_cmake_build
 
       - name: Visibility test
-        run: cmake --build . --target test_visibility
+        run: cmake --build build --target test_visibility
 
       - name: Setuptools helpers test
         run: |

From 896b3af749feba6e37ae29966d4c860ccde3b467 Mon Sep 17 00:00:00 2001
From: Henry Schreiner <HenrySchreinerIII@gmail.com>
Date: Sun, 1 Jun 2025 03:15:26 -0400
Subject: [PATCH 13/14] Update CMakeLists.txt

---
 tests/test_visibility/CMakeLists.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
index 3d724ca28d..7d045f3f9f 100644
--- a/tests/test_visibility/CMakeLists.txt
+++ b/tests/test_visibility/CMakeLists.txt
@@ -34,6 +34,7 @@ add_library(test_visibility_lib SHARED lib.h lib.cpp)
 add_library(test_visibility_lib::test_visibility_lib ALIAS test_visibility_lib)
 target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
 target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_compile_features(test_visibility_lib PUBLIC cxx_std_11)
 
 generate_export_header(test_visibility_lib)
 

From f2a5001c1d58d7621a74d35e220380018b52d711 Mon Sep 17 00:00:00 2001
From: Henry Schreiner <henryschreineriii@gmail.com>
Date: Tue, 3 Jun 2025 01:54:46 -0400
Subject: [PATCH 14/14] refactor: rename tests to test_cross_module_rtti

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
---
 .github/workflows/ci.yml                      | 28 ++++----
 .github/workflows/reusable-standard.yml       |  2 +-
 CMakePresets.json                             |  4 +-
 tests/CMakeLists.txt                          |  2 +-
 tests/test_cross_module_rtti/CMakeLists.txt   | 68 +++++++++++++++++++
 .../bindings.cpp                              |  2 +-
 .../catch.cpp                                 |  0
 .../lib.cpp                                   |  0
 .../lib.h                                     |  6 +-
 .../test_cross_module_rtti.cpp}               |  8 +--
 tests/test_visibility/CMakeLists.txt          | 64 -----------------
 11 files changed, 94 insertions(+), 90 deletions(-)
 create mode 100644 tests/test_cross_module_rtti/CMakeLists.txt
 rename tests/{test_visibility => test_cross_module_rtti}/bindings.cpp (91%)
 rename tests/{test_visibility => test_cross_module_rtti}/catch.cpp (100%)
 rename tests/{test_visibility => test_cross_module_rtti}/lib.cpp (100%)
 rename tests/{test_visibility => test_cross_module_rtti}/lib.h (64%)
 rename tests/{test_visibility/test_visibility.cpp => test_cross_module_rtti/test_cross_module_rtti.cpp} (87%)
 delete mode 100644 tests/test_visibility/CMakeLists.txt

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 683000ec14..9e8d3b6fe5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -231,7 +231,7 @@ jobs:
       run: cmake --build . --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build . --target test_visibility
+      run: cmake --build . --target test_cross_module_rtti
 
 
   manylinux:
@@ -332,7 +332,7 @@ jobs:
       run: cmake --build --preset default --target cpptest
 
     - name: Visibility test
-      run: cmake --build --preset default --target test_visibility
+      run: cmake --build --preset default --target test_cross_module_rtti
 
     - name: Run Valgrind on Python tests
       if: matrix.valgrind
@@ -393,7 +393,7 @@ jobs:
       run: cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build build --target test_visibility
+      run: cmake --build build --target test_cross_module_rtti
 
   # Testing NVCC; forces sources to behave like .cu files
   cuda:
@@ -514,7 +514,7 @@ jobs:
       run: cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build build --target test_visibility
+      run: cmake --build build --target test_cross_module_rtti
 
   # Testing on GCC using the GCC docker images (only recent images supported)
   gcc:
@@ -567,7 +567,7 @@ jobs:
       run: cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build build --target test_visibility
+      run: cmake --build build --target test_cross_module_rtti
 
     - name: Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE
       if: matrix.gcc == '12'
@@ -654,7 +654,7 @@ jobs:
     - name: Visibility test
       run: |
         set +e; source /opt/intel/oneapi/setvars.sh; set -e
-        cmake --build build-11 --target test_visibility
+        cmake --build build-11 --target test_cross_module_rtti
 
     - name: Configure C++17
       run: |
@@ -691,7 +691,7 @@ jobs:
     - name: Visibility test
       run: |
         set +e; source /opt/intel/oneapi/setvars.sh; set -e
-        cmake --build build-17 --target test_visibility
+        cmake --build build-17 --target test_cross_module_rtti
 
   # Testing on CentOS (manylinux uses a centos base).
   centos:
@@ -755,7 +755,7 @@ jobs:
       run: cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build build --target test_visibility
+      run: cmake --build build --target test_cross_module_rtti
 
 
   # This tests an "install" with the CMake tools
@@ -987,7 +987,7 @@ jobs:
       run: cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: cmake --build build --target test_visibility
+      run: cmake --build build --target test_cross_module_rtti
 
     - name: Configure C++20 - Exercise cmake -DPYBIND11_TEST_OVERRIDE
       run: >
@@ -1063,7 +1063,7 @@ jobs:
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target test_cmake_build
 
     - name: Visibility test
-      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target test_visibility
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target test_cross_module_rtti
 
     - name: Clean directory
       run: git clean -fdx
@@ -1087,7 +1087,7 @@ jobs:
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target test_cmake_build
 
     - name: Visibility test
-      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target test_visibility
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target test_cross_module_rtti
 
     - name: Clean directory
       run: git clean -fdx
@@ -1111,7 +1111,7 @@ jobs:
       run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cmake_build
 
     - name: Visibility test
-      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_visibility
+      run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cross_module_rtti
 
   windows_clang:
     if: github.event.pull_request.draft == false
@@ -1181,7 +1181,7 @@ jobs:
         run: cmake --build . --target test_cmake_build -j 2
 
       - name: Visibility test
-        run: cmake --build . --target test_visibility -j 2
+        run: cmake --build . --target test_cross_module_rtti -j 2
 
       - name: Clean directory
         run: git clean -fdx
@@ -1251,7 +1251,7 @@ jobs:
         run: cmake --build . --target test_cmake_build -j 2
 
       - name: Visibility test
-        run: cmake --build . --target test_visibility -j 2
+        run: cmake --build . --target test_cross_module_rtti -j 2
 
       - name: CMake Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE
         run: >
diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml
index e124b65bd4..d3c769449a 100644
--- a/.github/workflows/reusable-standard.yml
+++ b/.github/workflows/reusable-standard.yml
@@ -75,7 +75,7 @@ jobs:
         run: cmake --build build --target test_cmake_build
 
       - name: Visibility test
-        run: cmake --build build --target test_visibility
+        run: cmake --build build --target test_cross_module_rtti
 
       - name: Setuptools helpers test
         run: |
diff --git a/CMakePresets.json b/CMakePresets.json
index c967c25a37..42bf3ade9d 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -61,13 +61,13 @@
       "name": "tests",
       "displayName": "Tests (for workflow)",
       "configurePreset": "default",
-      "targets": ["pytest", "cpptest", "test_cmake_build"]
+      "targets": ["pytest", "cpptest", "test_cmake_build", "test_cross_module_rtti"]
     },
     {
       "name": "testsvenv",
       "displayName": "Tests Venv (for workflow)",
       "configurePreset": "venv",
-      "targets": ["pytest", "cpptest", "test_cmake_build"]
+      "targets": ["pytest", "cpptest", "test_cmake_build", "test_cross_module_rtti"]
     }
   ],
   "workflowPresets": [
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2546d1d372..0e76e68786 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -651,5 +651,5 @@ if(NOT PYBIND11_CUDA_TESTS)
   add_subdirectory(test_cmake_build)
 
   # Test visibility of common symbols across shared libraries
-  add_subdirectory(test_visibility)
+  add_subdirectory(test_cross_module_rtti)
 endif()
diff --git a/tests/test_cross_module_rtti/CMakeLists.txt b/tests/test_cross_module_rtti/CMakeLists.txt
new file mode 100644
index 0000000000..97d2c780cb
--- /dev/null
+++ b/tests/test_cross_module_rtti/CMakeLists.txt
@@ -0,0 +1,68 @@
+possibly_uninitialized(PYTHON_MODULE_EXTENSION Python_INTERPRETER_ID)
+
+set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+
+if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
+   OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
+   OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")
+  message(STATUS "Skipping visibility test on PyPy or GraalPy")
+  add_custom_target(test_cross_module_rtti
+  )# Dummy target on PyPy or GraalPy. Embedding is not supported.
+  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
+  return()
+endif()
+
+if(TARGET Python::Module AND NOT TARGET Python::Python)
+  message(STATUS "Skipping visibility test since no embed libs found")
+  add_custom_target(test_cross_module_rtti) # Dummy target since embedding is not supported.
+  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
+  return()
+endif()
+
+find_package(Catch 2.13.10)
+
+if(CATCH_FOUND)
+  message(STATUS "Building interpreter tests using Catch v${CATCH_VERSION}")
+else()
+  message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers"
+                 " manually or use `cmake -DDOWNLOAD_CATCH=ON` to fetch them automatically.")
+  return()
+endif()
+
+include(GenerateExportHeader)
+
+add_library(test_cross_module_rtti_lib SHARED lib.h lib.cpp)
+add_library(test_cross_module_rtti_lib::test_cross_module_rtti_lib ALIAS
+            test_cross_module_rtti_lib)
+target_include_directories(test_cross_module_rtti_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
+target_include_directories(test_cross_module_rtti_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_compile_features(test_cross_module_rtti_lib PUBLIC cxx_std_11)
+
+generate_export_header(test_cross_module_rtti_lib)
+
+pybind11_add_module(test_cross_module_rtti_bindings SHARED bindings.cpp)
+target_link_libraries(test_cross_module_rtti_bindings
+                      PUBLIC test_cross_module_rtti_lib::test_cross_module_rtti_lib)
+
+add_executable(test_cross_module_rtti_main catch.cpp test_cross_module_rtti.cpp)
+target_link_libraries(
+  test_cross_module_rtti_main PUBLIC test_cross_module_rtti_lib::test_cross_module_rtti_lib
+                                     pybind11::embed Catch2::Catch2)
+
+# Ensure that we have built the python bindings since we load them in main
+add_dependencies(test_cross_module_rtti_main test_cross_module_rtti_bindings)
+
+pybind11_enable_warnings(test_cross_module_rtti_main)
+pybind11_enable_warnings(test_cross_module_rtti_bindings)
+pybind11_enable_warnings(test_cross_module_rtti_lib)
+
+add_custom_target(
+  test_cross_module_rtti
+  COMMAND "$<TARGET_FILE:test_cross_module_rtti_main>"
+  DEPENDS test_cross_module_rtti_main
+  WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_cross_module_rtti_main>")
+
+set_target_properties(test_cross_module_rtti_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY
+                                                                 "${CMAKE_CURRENT_BINARY_DIR}")
+
+add_dependencies(check test_cross_module_rtti)
diff --git a/tests/test_visibility/bindings.cpp b/tests/test_cross_module_rtti/bindings.cpp
similarity index 91%
rename from tests/test_visibility/bindings.cpp
rename to tests/test_cross_module_rtti/bindings.cpp
index ccd78e2808..94fa6874f8 100644
--- a/tests/test_visibility/bindings.cpp
+++ b/tests/test_cross_module_rtti/bindings.cpp
@@ -8,7 +8,7 @@ class BaseTrampoline : public lib::Base, public pybind11::trampoline_self_life_s
     int get() const override { PYBIND11_OVERLOAD(int, lib::Base, get); }
 };
 
-PYBIND11_MODULE(test_visibility_bindings, m) {
+PYBIND11_MODULE(test_cross_module_rtti_bindings, m) {
     pybind11::classh<lib::Base, BaseTrampoline>(m, "Base")
         .def(pybind11::init<int, int>())
         .def_readwrite("a", &lib::Base::a)
diff --git a/tests/test_visibility/catch.cpp b/tests/test_cross_module_rtti/catch.cpp
similarity index 100%
rename from tests/test_visibility/catch.cpp
rename to tests/test_cross_module_rtti/catch.cpp
diff --git a/tests/test_visibility/lib.cpp b/tests/test_cross_module_rtti/lib.cpp
similarity index 100%
rename from tests/test_visibility/lib.cpp
rename to tests/test_cross_module_rtti/lib.cpp
diff --git a/tests/test_visibility/lib.h b/tests/test_cross_module_rtti/lib.h
similarity index 64%
rename from tests/test_visibility/lib.h
rename to tests/test_cross_module_rtti/lib.h
index 8275b85b5c..0925b084ca 100644
--- a/tests/test_visibility/lib.h
+++ b/tests/test_cross_module_rtti/lib.h
@@ -1,7 +1,7 @@
 #pragma once
 
 #include <memory>
-#include <test_visibility_lib_export.h>
+#include <test_cross_module_rtti_lib_export.h>
 
 #if defined(_MSC_VER)
 __pragma(warning(disable : 4251))
@@ -9,7 +9,7 @@ __pragma(warning(disable : 4251))
 
     namespace lib {
 
-    class TEST_VISIBILITY_LIB_EXPORT Base : public std::enable_shared_from_this<Base> {
+    class TEST_CROSS_MODULE_RTTI_LIB_EXPORT Base : public std::enable_shared_from_this<Base> {
     public:
         Base(int a, int b);
         virtual ~Base() = default;
@@ -20,7 +20,7 @@ __pragma(warning(disable : 4251))
         int b;
     };
 
-    class TEST_VISIBILITY_LIB_EXPORT Foo : public Base {
+    class TEST_CROSS_MODULE_RTTI_LIB_EXPORT Foo : public Base {
     public:
         Foo(int a, int b);
 
diff --git a/tests/test_visibility/test_visibility.cpp b/tests/test_cross_module_rtti/test_cross_module_rtti.cpp
similarity index 87%
rename from tests/test_visibility/test_visibility.cpp
rename to tests/test_cross_module_rtti/test_cross_module_rtti.cpp
index fe3caed2e5..64988b77a1 100644
--- a/tests/test_visibility/test_visibility.cpp
+++ b/tests/test_cross_module_rtti/test_cross_module_rtti.cpp
@@ -6,11 +6,11 @@
 #include <lib.h>
 
 static constexpr auto script = R"(
-import test_visibility_bindings
+import test_cross_module_rtti_bindings
 
-class Bar(test_visibility_bindings.Base):
+class Bar(test_cross_module_rtti_bindings.Base):
     def __init__(self, a, b):
-        test_visibility_bindings.Base.__init__(self, a, b)
+        test_cross_module_rtti_bindings.Base.__init__(self, a, b)
 
     def get(self):
         return 4 * self.a + self.b
@@ -23,7 +23,7 @@ def get_bar(a, b):
 
 TEST_CASE("Simple case where without is_alias") {
     // "Simple" case this will not have `python_instance_is_alias` set in type_cast_base.h:771
-    auto bindings = pybind11::module_::import("test_visibility_bindings");
+    auto bindings = pybind11::module_::import("test_cross_module_rtti_bindings");
     auto holder = bindings.attr("get_foo")(1, 2);
     auto foo = holder.cast<std::shared_ptr<lib::Base>>();
     REQUIRE(foo->get() == 4); // 2 * 1 + 2 = 4
diff --git a/tests/test_visibility/CMakeLists.txt b/tests/test_visibility/CMakeLists.txt
deleted file mode 100644
index 7d045f3f9f..0000000000
--- a/tests/test_visibility/CMakeLists.txt
+++ /dev/null
@@ -1,64 +0,0 @@
-possibly_uninitialized(PYTHON_MODULE_EXTENSION Python_INTERPRETER_ID)
-
-set(CMAKE_CXX_VISIBILITY_PRESET hidden)
-
-if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy"
-   OR "${Python_INTERPRETER_ID}" STREQUAL "PyPy"
-   OR "${PYTHON_MODULE_EXTENSION}" MATCHES "graalpy")
-  message(STATUS "Skipping visibility test on PyPy or GraalPy")
-  add_custom_target(test_visibility) # Dummy target on PyPy or GraalPy. Embedding is not supported.
-  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
-  return()
-endif()
-
-if(TARGET Python::Module AND NOT TARGET Python::Python)
-  message(STATUS "Skipping visibility test since no embed libs found")
-  add_custom_target(test_visibility) # Dummy target since embedding is not supported.
-  set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
-  return()
-endif()
-
-find_package(Catch 2.13.10)
-
-if(CATCH_FOUND)
-  message(STATUS "Building interpreter tests using Catch v${CATCH_VERSION}")
-else()
-  message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers"
-                 " manually or use `cmake -DDOWNLOAD_CATCH=ON` to fetch them automatically.")
-  return()
-endif()
-
-include(GenerateExportHeader)
-
-add_library(test_visibility_lib SHARED lib.h lib.cpp)
-add_library(test_visibility_lib::test_visibility_lib ALIAS test_visibility_lib)
-target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
-target_include_directories(test_visibility_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_compile_features(test_visibility_lib PUBLIC cxx_std_11)
-
-generate_export_header(test_visibility_lib)
-
-pybind11_add_module(test_visibility_bindings SHARED bindings.cpp)
-target_link_libraries(test_visibility_bindings PUBLIC test_visibility_lib::test_visibility_lib)
-
-add_executable(test_visibility_main catch.cpp test_visibility.cpp)
-target_link_libraries(test_visibility_main PUBLIC test_visibility_lib::test_visibility_lib
-                                                  pybind11::embed Catch2::Catch2)
-
-# Ensure that we have built the python bindings since we load them in main
-add_dependencies(test_visibility_main test_visibility_bindings)
-
-pybind11_enable_warnings(test_visibility_main)
-pybind11_enable_warnings(test_visibility_bindings)
-pybind11_enable_warnings(test_visibility_lib)
-
-add_custom_target(
-  test_visibility
-  COMMAND "$<TARGET_FILE:test_visibility_main>"
-  DEPENDS test_visibility_main
-  WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_visibility_main>")
-
-set_target_properties(test_visibility_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY
-                                                          "${CMAKE_CURRENT_BINARY_DIR}")
-
-add_dependencies(check test_visibility)