| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0f128b9 commit 421ea12
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1406,6 +1406,28 @@ the shared library name at development time, and hardcode that into the wrapper | |||
| 1406 | 1406 | module instead of using :func:`~ctypes.util.find_library` to locate the library at runtime. | |
| 1407 | 1407 | ||
| 1408 | 1408 | ||
| 1409 | + .. _ctypes-listing-loaded-shared-libraries: | ||
| 1410 | + | ||
| 1411 | + Listing loaded shared libraries | ||
| 1412 | + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| 1413 | + | ||
| 1414 | + When writing code that relies on code loaded from shared libraries, it can be | ||
| 1415 | + useful to know which shared libraries have already been loaded into the current | ||
| 1416 | + process. | ||
| 1417 | + | ||
| 1418 | + The :mod:`!ctypes.util` module provides the :func:`~ctypes.util.dllist` function, | ||
| 1419 | + which calls the different APIs provided by the various platforms to help determine | ||
| 1420 | + which shared libraries have already been loaded into the current process. | ||
| 1421 | + | ||
| 1422 | + The exact output of this function will be system dependent. On most platforms, | ||
| 1423 | + the first entry of this list represents the current process itself, which may | ||
| 1424 | + be an empty string. | ||
| 1425 | + For example, on glibc-based Linux, the return may look like:: | ||
| 1426 | + | ||
| 1427 | + >>> from ctypes.util import dllist | ||
| 1428 | + >>> dllist() | ||
| 1429 | + ['', 'linux-vdso.so.1', '/lib/x86_64-linux-gnu/libm.so.6', '/lib/x86_64-linux-gnu/libc.so.6', ... ] | ||
| 1430 | + | ||
| 1409 | 1431 | .. _ctypes-loading-shared-libraries: | |
| 1410 | 1432 | ||
| 1411 | 1433 | Loading shared libraries | |
@@ -2083,6 +2105,20 @@ Utility functions | |||
| 2083 | 2105 | .. availability:: Windows | |
| 2084 | 2106 | ||
| 2085 | 2107 | ||
| 2108 | + .. function:: dllist() | ||
| 2109 | + :module: ctypes.util | ||
| 2110 | + | ||
| 2111 | + Try to provide a list of paths of the shared libraries loaded into the current | ||
| 2112 | + process. These paths are not normalized or processed in any way. The function | ||
| 2113 | + can raise :exc:`OSError` if the underlying platform APIs fail. | ||
| 2114 | + The exact functionality is system dependent. | ||
| 2115 | + | ||
| 2116 | + On most platforms, the first element of the list represents the current | ||
| 2117 | + executable file. It may be an empty string. | ||
| 2118 | + | ||
| 2119 | + .. availability:: Windows, macOS, iOS, glibc, BSD libc, musl | ||
| 2120 | + .. versionadded:: next | ||
| 2121 | + | ||
| 2086 | 2122 | .. function:: FormatError([code]) | |
| 2087 | 2123 | ||
| 2088 | 2124 | Returns a textual description of the error code *code*. If no error code is | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -389,6 +389,9 @@ ctypes | |||
| 389 | 389 | complex C types. | |
| 390 | 390 | (Contributed by Sergey B Kirpichev in :gh:`61103`). | |
| 391 | 391 | ||
| 392 | + * Add :func:`ctypes.util.dllist` for listing the shared libraries | ||
| 393 | + loaded by the current process. | ||
| 394 | + (Contributed by Brian Ward in :gh:`119349`.) | ||
| 392 | 395 | ||
| 393 | 396 | datetime | |
| 394 | 397 | -------- | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -67,6 +67,65 @@ def find_library(name): | |||
| 67 | 67 | return fname | |
| 68 | 68 | return None | |
| 69 | 69 | ||
| 70 | + # Listing loaded DLLs on Windows relies on the following APIs: | ||
| 71 | + # https://learn.microsoft.com/windows/win32/api/psapi/nf-psapi-enumprocessmodules | ||
| 72 | + # https://learn.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew | ||
| 73 | + import ctypes | ||
| 74 | + from ctypes import wintypes | ||
| 75 | + | ||
| 76 | + _kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) | ||
| 77 | + _get_current_process = _kernel32["GetCurrentProcess"] | ||
| 78 | + _get_current_process.restype = wintypes.HANDLE | ||
| 79 | + | ||
| 80 | + _k32_get_module_file_name = _kernel32["GetModuleFileNameW"] | ||
| 81 | + _k32_get_module_file_name.restype = wintypes.DWORD | ||
| 82 | + _k32_get_module_file_name.argtypes = ( | ||
| 83 | + wintypes.HMODULE, | ||
| 84 | + wintypes.LPWSTR, | ||
| 85 | + wintypes.DWORD, | ||
| 86 | + ) | ||
| 87 | + | ||
| 88 | + _psapi = ctypes.WinDLL('psapi', use_last_error=True) | ||
| 89 | + _enum_process_modules = _psapi["EnumProcessModules"] | ||
| 90 | + _enum_process_modules.restype = wintypes.BOOL | ||
| 91 | + _enum_process_modules.argtypes = ( | ||
| 92 | + wintypes.HANDLE, | ||
| 93 | + ctypes.POINTER(wintypes.HMODULE), | ||
| 94 | + wintypes.DWORD, | ||
| 95 | + wintypes.LPDWORD, | ||
| 96 | + ) | ||
| 97 | + | ||
| 98 | + def _get_module_filename(module: wintypes.HMODULE): | ||
| 99 | + name = (wintypes.WCHAR * 32767)() # UNICODE_STRING_MAX_CHARS | ||
| 100 | + if _k32_get_module_file_name(module, name, len(name)): | ||
| 101 | + return name.value | ||
| 102 | + return None | ||
| 103 | + | ||
| 104 | + | ||
| 105 | + def _get_module_handles(): | ||
| 106 | + process = _get_current_process() | ||
| 107 | + space_needed = wintypes.DWORD() | ||
| 108 | + n = 1024 | ||
| 109 | + while True: | ||
| 110 | + modules = (wintypes.HMODULE * n)() | ||
| 111 | + if not _enum_process_modules(process, | ||
| 112 | + modules, | ||
| 113 | + ctypes.sizeof(modules), | ||
| 114 | + ctypes.byref(space_needed)): | ||
| 115 | + err = ctypes.get_last_error() | ||
| 116 | + msg = ctypes.FormatError(err).strip() | ||
| 117 | + raise ctypes.WinError(err, f"EnumProcessModules failed: {msg}") | ||
| 118 | + n = space_needed.value // ctypes.sizeof(wintypes.HMODULE) | ||
| 119 | + if n <= len(modules): | ||
| 120 | + return modules[:n] | ||
| 121 | + | ||
| 122 | + def dllist(): | ||
| 123 | + """Return a list of loaded shared libraries in the current process.""" | ||
| 124 | + modules = _get_module_handles() | ||
| 125 | + libraries = [name for h in modules | ||
| 126 | + if (name := _get_module_filename(h)) is not None] | ||
| 127 | + return libraries | ||
| 128 | + | ||
| 70 | 129 | elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}: | |
| 71 | 130 | from ctypes.macholib.dyld import dyld_find as _dyld_find | |
| 72 | 131 | def find_library(name): | |
@@ -80,6 +139,22 @@ def find_library(name): | |||
| 80 | 139 | continue | |
| 81 | 140 | return None | |
| 82 | 141 | ||
| 142 | + # Listing loaded libraries on Apple systems relies on the following API: | ||
| 143 | + # https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html | ||
| 144 | + import ctypes | ||
| 145 | + | ||
| 146 | + _libc = ctypes.CDLL(find_library("c")) | ||
| 147 | + _dyld_get_image_name = _libc["_dyld_get_image_name"] | ||
| 148 | + _dyld_get_image_name.restype = ctypes.c_char_p | ||
| 149 | + | ||
| 150 | + def dllist(): | ||
| 151 | + """Return a list of loaded shared libraries in the current process.""" | ||
| 152 | + num_images = _libc._dyld_image_count() | ||
| 153 | + libraries = [os.fsdecode(name) for i in range(num_images) | ||
| 154 | + if (name := _dyld_get_image_name(i)) is not None] | ||
| 155 | + | ||
| 156 | + return libraries | ||
| 157 | + | ||
| 83 | 158 | elif sys.platform.startswith("aix"): | |
| 84 | 159 | # AIX has two styles of storing shared libraries | |
| 85 | 160 | # GNU auto_tools refer to these as svr4 and aix | |
@@ -341,6 +416,55 @@ def find_library(name): | |||
| 341 | 416 | return _findSoname_ldconfig(name) or \ | |
| 342 | 417 | _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name)) | |
| 343 | 418 | ||
| 419 | + | ||
| 420 | + # Listing loaded libraries on other systems will try to use | ||
| 421 | + # functions common to Linux and a few other Unix-like systems. | ||
| 422 | + # See the following for several platforms' documentation of the same API: | ||
| 423 | + # https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html | ||
| 424 | + # https://man.freebsd.org/cgi/man.cgi?query=dl_iterate_phdr | ||
| 425 | + # https://man.openbsd.org/dl_iterate_phdr | ||
| 426 | + # https://docs.oracle.com/cd/E88353_01/html/E37843/dl-iterate-phdr-3c.html | ||
| 427 | + if (os.name == "posix" and | ||
| 428 | + sys.platform not in {"darwin", "ios", "tvos", "watchos"}): | ||
| 429 | + import ctypes | ||
| 430 | + if hasattr((_libc := ctypes.CDLL(None)), "dl_iterate_phdr"): | ||
| 431 | + | ||
| 432 | + class _dl_phdr_info(ctypes.Structure): | ||
| 433 | + _fields_ = [ | ||
| 434 | + ("dlpi_addr", ctypes.c_void_p), | ||
| 435 | + ("dlpi_name", ctypes.c_char_p), | ||
| 436 | + ("dlpi_phdr", ctypes.c_void_p), | ||
| 437 | + ("dlpi_phnum", ctypes.c_ushort), | ||
| 438 | + ] | ||
| 439 | + | ||
| 440 | + _dl_phdr_callback = ctypes.CFUNCTYPE( | ||
| 441 | + ctypes.c_int, | ||
| 442 | + ctypes.POINTER(_dl_phdr_info), | ||
| 443 | + ctypes.c_size_t, | ||
| 444 | + ctypes.POINTER(ctypes.py_object), | ||
| 445 | + ) | ||
| 446 | + | ||
| 447 | + @_dl_phdr_callback | ||
| 448 | + def _info_callback(info, _size, data): | ||
| 449 | + libraries = data.contents.value | ||
| 450 | + name = os.fsdecode(info.contents.dlpi_name) | ||
| 451 | + libraries.append(name) | ||
| 452 | + return 0 | ||
| 453 | + | ||
| 454 | + _dl_iterate_phdr = _libc["dl_iterate_phdr"] | ||
| 455 | + _dl_iterate_phdr.argtypes = [ | ||
| 456 | + _dl_phdr_callback, | ||
| 457 | + ctypes.POINTER(ctypes.py_object), | ||
| 458 | + ] | ||
| 459 | + _dl_iterate_phdr.restype = ctypes.c_int | ||
| 460 | + | ||
| 461 | + def dllist(): | ||
| 462 | + """Return a list of loaded shared libraries in the current process.""" | ||
| 463 | + libraries = [] | ||
| 464 | + _dl_iterate_phdr(_info_callback, | ||
| 465 | + ctypes.byref(ctypes.py_object(libraries))) | ||
| 466 | + return libraries | ||
| 467 | + | ||
| 344 | 468 | ################################################################ | |
| 345 | 469 | # test code | |
| 346 | 470 | ||
@@ -384,5 +508,12 @@ def test(): | |||
| 384 | 508 | print(cdll.LoadLibrary("libcrypt.so")) | |
| 385 | 509 | print(find_library("crypt")) | |
| 386 | 510 | ||
| 511 | + try: | ||
| 512 | + dllist | ||
| 513 | + except NameError: | ||
| 514 | + print('dllist() not available') | ||
| 515 | + else: | ||
| 516 | + print(dllist()) | ||
| 517 | + | ||
| 387 | 518 | if __name__ == "__main__": | |
| 388 | 519 | test() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,59 @@ | |||
| 1 | + import os | ||
| 2 | + import sys | ||
| 3 | + import unittest | ||
| 4 | + from ctypes import CDLL | ||
| 5 | + import ctypes.util | ||
| 6 | + from test.support import import_helper | ||
| 7 | + | ||
| 8 | + | ||
| 9 | + WINDOWS = os.name == "nt" | ||
| 10 | + APPLE = sys.platform in {"darwin", "ios", "tvos", "watchos"} | ||
| 11 | + | ||
| 12 | + if WINDOWS: | ||
| 13 | + KNOWN_LIBRARIES = ["KERNEL32.DLL"] | ||
| 14 | + elif APPLE: | ||
| 15 | + KNOWN_LIBRARIES = ["libSystem.B.dylib"] | ||
| 16 | + else: | ||
| 17 | + # trickier than it seems, because libc may not be present | ||
| 18 | + # on musl systems, and sometimes goes by different names. | ||
| 19 | + # However, ctypes itself loads libffi | ||
| 20 | + KNOWN_LIBRARIES = ["libc.so", "libffi.so"] | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + @unittest.skipUnless( | ||
| 24 | + hasattr(ctypes.util, "dllist"), | ||
| 25 | + "ctypes.util.dllist is not available on this platform", | ||
| 26 | + ) | ||
| 27 | + class ListSharedLibraries(unittest.TestCase): | ||
| 28 | + | ||
| 29 | + def test_lists_system(self): | ||
| 30 | + dlls = ctypes.util.dllist() | ||
| 31 | + | ||
| 32 | + self.assertGreater(len(dlls), 0, f"loaded={dlls}") | ||
| 33 | + self.assertTrue( | ||
| 34 | + any(lib in dll for dll in dlls for lib in KNOWN_LIBRARIES), f"loaded={dlls}" | ||
| 35 | + ) | ||
| 36 | + | ||
| 37 | + def test_lists_updates(self): | ||
| 38 | + dlls = ctypes.util.dllist() | ||
| 39 | + | ||
| 40 | + # this test relies on being able to import a library which is | ||
| 41 | + # not already loaded. | ||
| 42 | + # If it is (e.g. by a previous test in the same process), we skip | ||
| 43 | + if any("_ctypes_test" in dll for dll in dlls): | ||
| 44 | + self.skipTest("Test library is already loaded") | ||
| 45 | + | ||
| 46 | + _ctypes_test = import_helper.import_module("_ctypes_test") | ||
| 47 | + test_module = CDLL(_ctypes_test.__file__) | ||
| 48 | + dlls2 = ctypes.util.dllist() | ||
| 49 | + self.assertIsNotNone(dlls2) | ||
| 50 | + | ||
| 51 | + dlls1 = set(dlls) | ||
| 52 | + dlls2 = set(dlls2) | ||
| 53 | + | ||
| 54 | + self.assertGreater(dlls2, dlls1, f"newly loaded libraries: {dlls2 - dlls1}") | ||
| 55 | + self.assertTrue(any("_ctypes_test" in dll for dll in dlls2), f"loaded={dlls2}") | ||
| 56 | + | ||
| 57 | + | ||
| 58 | + if __name__ == "__main__": | ||
| 59 | + unittest.main() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1993,6 +1993,7 @@ Edward C Wang | |||
| 1993 | 1993 | Jiahua Wang | |
| 1994 | 1994 | Ke Wang | |
| 1995 | 1995 | Liang-Bo Wang | |
| 1996 | + Brian Ward | ||
| 1996 | 1997 | Greg Ward | |
| 1997 | 1998 | Tom Wardill | |
| 1998 | 1999 | Zachary Ware | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,2 @@ | |||
| 1 | + Add the :func:`ctypes.util.dllist` function to list the loaded shared | ||
| 2 | + libraries for the current process. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments