https://github.com/python/cpython/commit/421ea1291d9b8ebfe5eaa72ab041338073fb67d0
commit: 421ea1291d9b8ebfe5eaa72ab041338073fb67d0
branch: main
author: Brian Ward <[email protected]>
committer: encukou <[email protected]>
date: 2025-02-08T14:02:36+01:00
summary:

gh-119349: Add ctypes.util.dllist -- list loaded shared libraries (GH-122946)

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 <[email protected]>
Co-authored-by: Peter Bierma <[email protected]>

files:
A Lib/test/test_ctypes/test_dllist.py
A Misc/NEWS.d/next/Library/2024-08-12-11-58-15.gh-issue-119349.-xTnHl.rst
M Doc/library/ctypes.rst
M Doc/whatsnew/3.14.rst
M Lib/ctypes/util.py
M Misc/ACKS

diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst
index 615138302e1379..cb02fd33a6e741 100644
--- a/Doc/library/ctypes.rst
+++ b/Doc/library/ctypes.rst
@@ -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
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 9ac0e6ed2a6d40..9c4922308b7f2d 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -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
 --------
diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py
index 117bf06cb01013..99504911a3dbe0 100644
--- a/Lib/ctypes/util.py
+++ b/Lib/ctypes/util.py
@@ -67,6 +67,65 @@ def find_library(name):
                 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 @@ def find_library(name):
                 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 @@ def find_library(name):
             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
index 00000000000000..15603dc3d77972
--- /dev/null
+++ b/Lib/test/test_ctypes/test_dllist.py
@@ -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"]
+
+
[email protected](
+    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()
diff --git a/Misc/ACKS b/Misc/ACKS
index 27480a1f3131bd..2a68b69f161041 100644
--- 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
index 00000000000000..5dd8264a608dfa
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-12-11-58-15.gh-issue-119349.-xTnHl.rst
@@ -0,0 +1,2 @@
+Add the :func:`ctypes.util.dllist` function to list the loaded shared
+libraries for the current process.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to