]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119349: Add ctypes.util.dllist -- list loaded shared libraries (GH-122946)
authorBrian Ward <brianmward99@gmail.com>
Sat, 8 Feb 2025 13:02:36 +0000 (08:02 -0500)
committerGitHub <noreply@github.com>
Sat, 8 Feb 2025 13:02:36 +0000 (14:02 +0100)
Add function to list the currently loaded libraries to ctypes.util

The dllist() function calls platform-specific APIs in order to
list the runtime libraries loaded by Python and any imported modules.
On unsupported platforms the function may be missing.

Co-authored-by: Eryk Sun <eryksun@gmail.com>
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Doc/library/ctypes.rst
Doc/whatsnew/3.14.rst
Lib/ctypes/util.py
Lib/test/test_ctypes/test_dllist.py [new file with mode: 0644]
Misc/ACKS
Misc/NEWS.d/next/Library/2024-08-12-11-58-15.gh-issue-119349.-xTnHl.rst [new file with mode: 0644]

index 615138302e13799461893a18687ec8c81d128035..cb02fd33a6e741941e8634e89a692c8678544ad4 100644 (file)
@@ -1406,6 +1406,28 @@ the shared library name at development time, and hardcode that into the wrapper
 module instead of using :func:`~ctypes.util.find_library` to locate the library at runtime.
 
 
+.. _ctypes-listing-loaded-shared-libraries:
+
+Listing loaded shared libraries
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When writing code that relies on code loaded from shared libraries, it can be
+useful to know which shared libraries have already been loaded into the current
+process.
+
+The :mod:`!ctypes.util` module provides the :func:`~ctypes.util.dllist` function,
+which calls the different APIs provided by the various platforms to help determine
+which shared libraries have already been loaded into the current process.
+
+The exact output of this function will be system dependent. On most platforms,
+the first entry of this list represents the current process itself, which may
+be an empty string.
+For example, on glibc-based Linux, the return may look like::
+
+   >>> from ctypes.util import dllist
+   >>> dllist()
+   ['', 'linux-vdso.so.1', '/lib/x86_64-linux-gnu/libm.so.6', '/lib/x86_64-linux-gnu/libc.so.6', ... ]
+
 .. _ctypes-loading-shared-libraries:
 
 Loading shared libraries
@@ -2083,6 +2105,20 @@ Utility functions
    .. availability:: Windows
 
 
+.. function:: dllist()
+   :module: ctypes.util
+
+   Try to provide a list of paths of the shared libraries loaded into the current
+   process.  These paths are not normalized or processed in any way.  The function
+   can raise :exc:`OSError` if the underlying platform APIs fail.
+   The exact functionality is system dependent.
+
+   On most platforms, the first element of the list represents the current
+   executable file. It may be an empty string.
+
+   .. availability:: Windows, macOS, iOS, glibc, BSD libc, musl
+   .. versionadded:: next
+
 .. function:: FormatError([code])
 
    Returns a textual description of the error code *code*.  If no error code is
index 9ac0e6ed2a6d404a87c0794218b35dd2aa708d68..9c4922308b7f2db1a45e7d594aa23bdb36e434dd 100644 (file)
@@ -389,6 +389,9 @@ ctypes
   complex C types.
   (Contributed by Sergey B Kirpichev in :gh:`61103`).
 
+* Add :func:`ctypes.util.dllist` for listing the shared libraries
+  loaded by the current process.
+  (Contributed by Brian Ward in :gh:`119349`.)
 
 datetime
 --------
index 117bf06cb01013b221a94282a3bf4cf8878488c8..99504911a3dbe01a5b6eb122abed27e47c1950c6 100644 (file)
@@ -67,6 +67,65 @@ if os.name == "nt":
                 return fname
         return None
 
+    # Listing loaded DLLs on Windows relies on the following APIs:
+    # https://learn.microsoft.com/windows/win32/api/psapi/nf-psapi-enumprocessmodules
+    # https://learn.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew
+    import ctypes
+    from ctypes import wintypes
+
+    _kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
+    _get_current_process = _kernel32["GetCurrentProcess"]
+    _get_current_process.restype = wintypes.HANDLE
+
+    _k32_get_module_file_name = _kernel32["GetModuleFileNameW"]
+    _k32_get_module_file_name.restype = wintypes.DWORD
+    _k32_get_module_file_name.argtypes = (
+        wintypes.HMODULE,
+        wintypes.LPWSTR,
+        wintypes.DWORD,
+    )
+
+    _psapi = ctypes.WinDLL('psapi', use_last_error=True)
+    _enum_process_modules = _psapi["EnumProcessModules"]
+    _enum_process_modules.restype = wintypes.BOOL
+    _enum_process_modules.argtypes = (
+        wintypes.HANDLE,
+        ctypes.POINTER(wintypes.HMODULE),
+        wintypes.DWORD,
+        wintypes.LPDWORD,
+    )
+
+    def _get_module_filename(module: wintypes.HMODULE):
+        name = (wintypes.WCHAR * 32767)() # UNICODE_STRING_MAX_CHARS
+        if _k32_get_module_file_name(module, name, len(name)):
+            return name.value
+        return None
+
+
+    def _get_module_handles():
+        process = _get_current_process()
+        space_needed = wintypes.DWORD()
+        n = 1024
+        while True:
+            modules = (wintypes.HMODULE * n)()
+            if not _enum_process_modules(process,
+                                         modules,
+                                         ctypes.sizeof(modules),
+                                         ctypes.byref(space_needed)):
+                err = ctypes.get_last_error()
+                msg = ctypes.FormatError(err).strip()
+                raise ctypes.WinError(err, f"EnumProcessModules failed: {msg}")
+            n = space_needed.value // ctypes.sizeof(wintypes.HMODULE)
+            if n <= len(modules):
+                return modules[:n]
+
+    def dllist():
+        """Return a list of loaded shared libraries in the current process."""
+        modules = _get_module_handles()
+        libraries = [name for h in modules
+                        if (name := _get_module_filename(h)) is not None]
+        return libraries
+
 elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}:
     from ctypes.macholib.dyld import dyld_find as _dyld_find
     def find_library(name):
@@ -80,6 +139,22 @@ elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}
                 continue
         return None
 
+    # Listing loaded libraries on Apple systems relies on the following API:
+    # https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
+    import ctypes
+
+    _libc = ctypes.CDLL(find_library("c"))
+    _dyld_get_image_name = _libc["_dyld_get_image_name"]
+    _dyld_get_image_name.restype = ctypes.c_char_p
+
+    def dllist():
+        """Return a list of loaded shared libraries in the current process."""
+        num_images = _libc._dyld_image_count()
+        libraries = [os.fsdecode(name) for i in range(num_images)
+                        if (name := _dyld_get_image_name(i)) is not None]
+
+        return libraries
+
 elif sys.platform.startswith("aix"):
     # AIX has two styles of storing shared libraries
     # GNU auto_tools refer to these as svr4 and aix
@@ -341,6 +416,55 @@ elif os.name == "posix":
             return _findSoname_ldconfig(name) or \
                    _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))
 
+
+# Listing loaded libraries on other systems will try to use
+# functions common to Linux and a few other Unix-like systems.
+# See the following for several platforms' documentation of the same API:
+# https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html
+# https://man.freebsd.org/cgi/man.cgi?query=dl_iterate_phdr
+# https://man.openbsd.org/dl_iterate_phdr
+# https://docs.oracle.com/cd/E88353_01/html/E37843/dl-iterate-phdr-3c.html
+if (os.name == "posix" and
+    sys.platform not in {"darwin", "ios", "tvos", "watchos"}):
+    import ctypes
+    if hasattr((_libc := ctypes.CDLL(None)), "dl_iterate_phdr"):
+
+        class _dl_phdr_info(ctypes.Structure):
+            _fields_ = [
+                ("dlpi_addr", ctypes.c_void_p),
+                ("dlpi_name", ctypes.c_char_p),
+                ("dlpi_phdr", ctypes.c_void_p),
+                ("dlpi_phnum", ctypes.c_ushort),
+            ]
+
+        _dl_phdr_callback = ctypes.CFUNCTYPE(
+            ctypes.c_int,
+            ctypes.POINTER(_dl_phdr_info),
+            ctypes.c_size_t,
+            ctypes.POINTER(ctypes.py_object),
+        )
+
+        @_dl_phdr_callback
+        def _info_callback(info, _size, data):
+            libraries = data.contents.value
+            name = os.fsdecode(info.contents.dlpi_name)
+            libraries.append(name)
+            return 0
+
+        _dl_iterate_phdr = _libc["dl_iterate_phdr"]
+        _dl_iterate_phdr.argtypes = [
+            _dl_phdr_callback,
+            ctypes.POINTER(ctypes.py_object),
+        ]
+        _dl_iterate_phdr.restype = ctypes.c_int
+
+        def dllist():
+            """Return a list of loaded shared libraries in the current process."""
+            libraries = []
+            _dl_iterate_phdr(_info_callback,
+                             ctypes.byref(ctypes.py_object(libraries)))
+            return libraries
+
 ################################################################
 # test code
 
@@ -384,5 +508,12 @@ def test():
             print(cdll.LoadLibrary("libcrypt.so"))
             print(find_library("crypt"))
 
+    try:
+        dllist
+    except NameError:
+        print('dllist() not available')
+    else:
+        print(dllist())
+
 if __name__ == "__main__":
     test()
diff --git a/Lib/test/test_ctypes/test_dllist.py b/Lib/test/test_ctypes/test_dllist.py
new file mode 100644 (file)
index 0000000..15603dc
--- /dev/null
@@ -0,0 +1,59 @@
+import os
+import sys
+import unittest
+from ctypes import CDLL
+import ctypes.util
+from test.support import import_helper
+
+
+WINDOWS = os.name == "nt"
+APPLE = sys.platform in {"darwin", "ios", "tvos", "watchos"}
+
+if WINDOWS:
+    KNOWN_LIBRARIES = ["KERNEL32.DLL"]
+elif APPLE:
+    KNOWN_LIBRARIES = ["libSystem.B.dylib"]
+else:
+    # trickier than it seems, because libc may not be present
+    # on musl systems, and sometimes goes by different names.
+    # However, ctypes itself loads libffi
+    KNOWN_LIBRARIES = ["libc.so", "libffi.so"]
+
+
+@unittest.skipUnless(
+    hasattr(ctypes.util, "dllist"),
+    "ctypes.util.dllist is not available on this platform",
+)
+class ListSharedLibraries(unittest.TestCase):
+
+    def test_lists_system(self):
+        dlls = ctypes.util.dllist()
+
+        self.assertGreater(len(dlls), 0, f"loaded={dlls}")
+        self.assertTrue(
+            any(lib in dll for dll in dlls for lib in KNOWN_LIBRARIES), f"loaded={dlls}"
+        )
+
+    def test_lists_updates(self):
+        dlls = ctypes.util.dllist()
+
+        # this test relies on being able to import a library which is
+        # not already loaded.
+        # If it is (e.g. by a previous test in the same process), we skip
+        if any("_ctypes_test" in dll for dll in dlls):
+            self.skipTest("Test library is already loaded")
+
+        _ctypes_test = import_helper.import_module("_ctypes_test")
+        test_module = CDLL(_ctypes_test.__file__)
+        dlls2 = ctypes.util.dllist()
+        self.assertIsNotNone(dlls2)
+
+        dlls1 = set(dlls)
+        dlls2 = set(dlls2)
+
+        self.assertGreater(dlls2, dlls1, f"newly loaded libraries: {dlls2 - dlls1}")
+        self.assertTrue(any("_ctypes_test" in dll for dll in dlls2), f"loaded={dlls2}")
+
+
+if __name__ == "__main__":
+    unittest.main()
index 27480a1f3131bd087de84b2840e2220a7972f2fe..2a68b69f161041321abcca00cfda89d1dc01e743 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1993,6 +1993,7 @@ Edward C Wang
 Jiahua Wang
 Ke Wang
 Liang-Bo Wang
+Brian Ward
 Greg Ward
 Tom Wardill
 Zachary Ware
diff --git a/Misc/NEWS.d/next/Library/2024-08-12-11-58-15.gh-issue-119349.-xTnHl.rst b/Misc/NEWS.d/next/Library/2024-08-12-11-58-15.gh-issue-119349.-xTnHl.rst
new file mode 100644 (file)
index 0000000..5dd8264
--- /dev/null
@@ -0,0 +1,2 @@
+Add the :func:`ctypes.util.dllist` function to list the loaded shared
+libraries for the current process.