Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
41 changes: 41 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,32 @@ 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.


.. data:: dllist()
:module: ctypes.util
:noindex:

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. If the
function is not able to determine the list of loaded libraries (either because
the current platform does not support it, or because an error occurred), it
returns ``None``.

The exact functionality is system dependent.

.. _ctypes-loading-shared-libraries:

Loading shared libraries
Expand Down Expand Up @@ -2083,6 +2109,21 @@ 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. If the
function is not able to determine the list of loaded libraries (either because
the current platform is not Windows, MacOS, or Linux and does not support the
the ``dl_iterate_phdr`` API in libc, or because an error occurred), it
returns ``None``.

The exact functionality is system dependent.

.. versionadded:: 3.14


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

* Added :func:`ctypes.util.dllist` for listing the shared libraries
loaded by the current process.
(Contributed by Brian Ward in :gh:`119349`.)

datetime
--------
Expand Down
124 changes: 124 additions & 0 deletions Lib/ctypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,68 @@ def find_library(name):
return fname
return None

import ctypes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly worried about circular imports ruining our day. I don't think there's anything we can do about that though, I'm just bringing it to attention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's certainly a possible problem in the future, but currently it seems to stay out of tying itself it knots

from ctypes import wintypes

# https://learn.microsoft.com/windows/win32/api/psapi/nf-psapi-enumprocessmodules
# https://learn.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew

_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():
try:
modules = _get_module_handles()
except OSError:
return None
# skip first entry, which is the executable itself
libraries = [name for h in modules[1:]
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 +142,21 @@ def find_library(name):
continue
return None

import ctypes

# https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
_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():
num_images = _libc._dyld_image_count()
# start at 1 to skip executable
libraries = [os.fsdecode(name) for i in range(1, 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 +418,51 @@ def find_library(name):
return _findSoname_ldconfig(name) or \
_get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))


# other systems use functions common to Linux and a few other Unix-like systems
# 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
# this relies on find_library, which is why it is defined at the end
if (os.name == "posix" and
sys.platform not in {"darwin", "ios", "tvos", "watchos"}):
import ctypes
_libc_path = find_library("c")
if (_libc_path is None or
not hasattr((_libc := ctypes.CDLL(_libc_path)), "dl_iterate_phdr")):
def dllist():
return None
else:
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),
]


@ctypes.CFUNCTYPE(
ctypes.c_int,
ctypes.POINTER(_dl_phdr_info),
ctypes.c_size_t,
ctypes.POINTER(ctypes.py_object),
)
def _info_callback(info, _size, data):
libraries = data.contents.value
name = os.fsdecode(info.contents.dlpi_name)
libraries.append(name)
return 0


def dllist():
libraries = []
_libc.dl_iterate_phdr(_info_callback,
ctypes.byref(ctypes.py_object(libraries)))
# remove the first entry, which is the executable itself
return libraries[1:]

################################################################
# test code

Expand Down Expand Up @@ -384,5 +506,7 @@ def test():
print(cdll.LoadLibrary("libcrypt.so"))
print(find_library("crypt"))

print(dllist())

if __name__ == "__main__":
test()
61 changes: 61 additions & 0 deletions Lib/test/test_ctypes/test_dllist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import sys
import test.support
import unittest
from ctypes import CDLL
from ctypes.util import dllist
from test.support import import_helper


WINDOWS = os.name == 'nt'
APPLE = sys.platform in {"darwin", "ios", "tvos", "watchos"}
AIX = sys.platform.startswith("aix")

if WINDOWS:
SYSTEM_LIBRARY = 'KERNEL32.DLL'
elif APPLE:
SYSTEM_LIBRARY = 'libSystem.B.dylib'
elif AIX:
SYSTEM_LIBRARY = None
else:
SYSTEM_LIBRARY = 'libc.so'

class ListSharedLibraries(unittest.TestCase):

def test_lists_system(self):
dlls = dllist()

if SYSTEM_LIBRARY is not None:
self.assertIsNotNone(dlls)
self.assertGreater(len(dlls), 0, f'loaded={dlls}')
self.assertTrue(any(SYSTEM_LIBRARY in dll for dll in dlls), f'loaded={dlls}')
else:
# unsupported platform
self.assertIsNone(dlls)

def test_lists_updates(self):
dlls = dllist()

if dlls is not None:
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 = dllist()
self.assertIsNotNone(dlls2)

dlls1 = set(dlls)
dlls2 = set(dlls2)
if test.support.verbose:
print("Newly loaded shared libraries:")
for dll in (dlls2 - dlls1):
print("\t", dll)

self.assertGreater(dlls2, dlls1)
self.assertTrue(any("_ctypes_test" in dll for dll in 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 @@ -1990,6 +1990,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 @@
Added the :func:`ctypes.util.dllist` function to list the loaded shared
libraries for the current process.
Loading