Skip to content

Commit 421ea12

Browse files
WardBrianeryksunZeroIntensity
authored
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]>
1 parent 0f128b9 commit 421ea12

File tree

6 files changed

+232
-0
lines changed

6 files changed

+232
-0
lines changed

Doc/library/ctypes.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,28 @@ the shared library name at development time, and hardcode that into the wrapper
14061406
module instead of using :func:`~ctypes.util.find_library` to locate the library at runtime.
14071407

14081408

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+
14091431
.. _ctypes-loading-shared-libraries:
14101432

14111433
Loading shared libraries
@@ -2083,6 +2105,20 @@ Utility functions
20832105
.. availability:: Windows
20842106

20852107

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+
20862122
.. function:: FormatError([code])
20872123

20882124
Returns a textual description of the error code *code*. If no error code is

Doc/whatsnew/3.14.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@ ctypes
389389
complex C types.
390390
(Contributed by Sergey B Kirpichev in :gh:`61103`).
391391

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`.)
392395

393396
datetime
394397
--------

Lib/ctypes/util.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,65 @@ def find_library(name):
6767
return fname
6868
return None
6969

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+
70129
elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}:
71130
from ctypes.macholib.dyld import dyld_find as _dyld_find
72131
def find_library(name):
@@ -80,6 +139,22 @@ def find_library(name):
80139
continue
81140
return None
82141

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+
83158
elif sys.platform.startswith("aix"):
84159
# AIX has two styles of storing shared libraries
85160
# GNU auto_tools refer to these as svr4 and aix
@@ -341,6 +416,55 @@ def find_library(name):
341416
return _findSoname_ldconfig(name) or \
342417
_get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))
343418

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+
344468
################################################################
345469
# test code
346470

@@ -384,5 +508,12 @@ def test():
384508
print(cdll.LoadLibrary("libcrypt.so"))
385509
print(find_library("crypt"))
386510

511+
try:
512+
dllist
513+
except NameError:
514+
print('dllist() not available')
515+
else:
516+
print(dllist())
517+
387518
if __name__ == "__main__":
388519
test()

Lib/test/test_ctypes/test_dllist.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff 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()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,7 @@ Edward C Wang
19931993
Jiahua Wang
19941994
Ke Wang
19951995
Liang-Bo Wang
1996+
Brian Ward
19961997
Greg Ward
19971998
Tom Wardill
19981999
Zachary Ware
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff 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.

0 commit comments

Comments
 (0)