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 <eryksun@gmail.com>
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
This commit is contained in:
Brian Ward 2025-02-08 08:02:36 -05:00 committed by GitHub
parent 0f128b9435
commit 421ea1291d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 232 additions and 0 deletions

View 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

View 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
--------

View file

@ -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()

View file

@ -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()

View file

@ -1993,6 +1993,7 @@ Edward C Wang
Jiahua Wang
Ke Wang
Liang-Bo Wang
Brian Ward
Greg Ward
Tom Wardill
Zachary Ware

View file

@ -0,0 +1,2 @@
Add the :func:`ctypes.util.dllist` function to list the loaded shared
libraries for the current process.