Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d9c92c3
Add function to list the currently loaded libraries to ctypes.util
WardBrian Aug 12, 2024
89a3c71
PEP-8 compilance
WardBrian Aug 13, 2024
c29b348
Detect availability at runtime on non-Apple POSIX
WardBrian Aug 13, 2024
97d2eb1
use os.fsdecode for library paths
WardBrian Aug 14, 2024
2e12e22
Test improvements
WardBrian Aug 14, 2024
6e18c36
Add What's New entry
WardBrian Aug 14, 2024
2ce1cb9
Remove warning code
WardBrian Aug 14, 2024
de2c048
Lint fix
WardBrian Aug 14, 2024
8728393
Avoid editing cached versions of symbols
WardBrian Aug 14, 2024
dd97ba0
use ctypes.WinError in _get_module_handles
WardBrian Aug 16, 2024
297c71e
Format WinError message
WardBrian Aug 16, 2024
a059573
Remove unnecessary error handling
WardBrian Aug 16, 2024
586f37c
Tighten except statement
WardBrian Aug 19, 2024
efb7c1c
Mention reasoning for lack of support in doc
WardBrian Nov 20, 2024
4aba7d6
Merge branch 'main' into ctypes-util-list-libraries
WardBrian Jan 8, 2025
fc54910
Lint fix
WardBrian Jan 8, 2025
ab739d2
Merge branch 'main' into ctypes-util-list-libraries
WardBrian Jan 27, 2025
1626a25
Merge branch 'main' into ctypes-util-list-libraries
WardBrian Jan 31, 2025
a23b9c1
Raise if list cannot be produced
WardBrian Jan 31, 2025
58edf0f
Stop removing first entry
WardBrian Jan 31, 2025
c91849e
Set argtypes on dl_iterate_phdr call
WardBrian Jan 31, 2025
9c351d1
Leave dllist undefined if platform doesn't support it
WardBrian Jan 31, 2025
eedf35e
Documentation updates
WardBrian Jan 31, 2025
3918675
Move availability above versionadded
WardBrian Jan 31, 2025
5ab1b02
Update to work on musl systems
WardBrian Jan 31, 2025
ca0a8c6
Merge branch 'main' into ctypes-util-list-libraries
WardBrian Feb 4, 2025
94b1157
Update test code
WardBrian Feb 4, 2025
5dc0ecd
Update documentation
WardBrian Feb 4, 2025
43ff300
Lint fix
WardBrian Feb 4, 2025
80be2ab
Fix test failing on RHEL8
WardBrian Feb 4, 2025
e1dae92
test_dllist: check for one of several known libraries
WardBrian Feb 5, 2025
cb22479
test updates per review
WardBrian Feb 5, 2025
1c3594e
Various documentation updates per reviews
WardBrian Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,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
--------
Expand Down
131 changes: 131 additions & 0 deletions Lib/ctypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 changes: 59 additions & 0 deletions Lib/test/test_ctypes/test_dllist.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,7 @@ Edward C Wang
Jiahua Wang
Ke Wang
Liang-Bo Wang
Brian Ward
Greg Ward
Tom Wardill
Zachary Ware
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add the :func:`ctypes.util.dllist` function to list the loaded shared
libraries for the current process.
Loading