mirror of
https://github.com/python/cpython.git
synced 2026-01-08 00:12:42 +00:00
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:
parent
0f128b9435
commit
421ea1291d
6 changed files with 232 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
--------
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
59
Lib/test/test_ctypes/test_dllist.py
Normal file
59
Lib/test/test_ctypes/test_dllist.py
Normal 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()
|
||||
|
|
@ -1993,6 +1993,7 @@ Edward C Wang
|
|||
Jiahua Wang
|
||||
Ke Wang
|
||||
Liang-Bo Wang
|
||||
Brian Ward
|
||||
Greg Ward
|
||||
Tom Wardill
|
||||
Zachary Ware
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Add the :func:`ctypes.util.dllist` function to list the loaded shared
|
||||
libraries for the current process.
|
||||
Loading…
Add table
Add a link
Reference in a new issue