A `ctypes` function to list all loaded shared libraries

The implementation of dllist on Windows should use ctypes.WinDLL with use_last_error=True, and get the last error via ctypes.get_last_error(). One reason for this is that a package shouldn’t modify the state of the global loaders ctypes.windll and ctypes.cdll. Those are a convenience for scripts, which was never really a good idea. Modifying function prototypes from the loaders can lead to clashes between packages that define function prototypes differently. Regarding use_last_error=True, high-level Python code really shouldn’t depend on GetLastError(). The value needs to be captured in C as soon as the foreign function call returns. That’s the reason for use_last_error, get_last_error(), and set_last_error(). The same applies to use_errno, get_errno(), and set_errno() for C errno.

At the design level, I think the implementation in “dllist/windows.py” is more complicated that it needs to be. It also shouldn’t repeatedly incur the cost of define prototypes inside of the API wrappers. Also, since the use case is just for the current process, I’d use EnumProcessModules() instead of EnumProcessModulesEx(). One last note is that slicing an HMODULE array returns a list of integers, and the typing return type should reflect that.

Here’s a modified implementation for reference:

import ctypes
import warnings
from ctypes import wintypes
from typing import List, Optional

# 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)
_psapi = ctypes.WinDLL('psapi', use_last_error=True)

_kernel32.GetCurrentProcess.restype = wintypes.HANDLE

_kernel32.GetModuleFileNameW.restype = wintypes.DWORD
_kernel32.GetModuleFileNameW.argtypes = (
    wintypes.HMODULE,
    wintypes.LPWSTR,
    wintypes.DWORD,
)

_psapi.EnumProcessModules.restype = wintypes.BOOL
_psapi.EnumProcessModules.argtypes = (
    wintypes.HANDLE,
    ctypes.POINTER(wintypes.HMODULE),
    wintypes.DWORD,
    wintypes.LPDWORD,
)

def get_module_filename(hModule: wintypes.HMODULE) -> Optional[str]:
    name = (wintypes.WCHAR * 32767)() # UNICODE_STRING_MAX_CHARS
    if _kernel32.GetModuleFileNameW(hModule, name, len(name)):
        return name.value
    error = ctypes.get_last_error()
    warnings.warn(f"Failed to get module file name for module {hModule}: "
                  f"GetModuleFileNameW failed with error code {error}",
                  stacklevel=2)
    return None


def get_module_handles() -> List[int]:
    hProcess = _kernel32.GetCurrentProcess()
    cbNeeded = wintypes.DWORD()
    n = 1024
    while True:
        modules = (wintypes.HMODULE * n)()
        if not _psapi.EnumProcessModules(hProcess,
                                         modules,
                                         ctypes.sizeof(modules),
                                         ctypes.byref(cbNeeded)):
            break
        n = cbNeeded.value // ctypes.sizeof(wintypes.HMODULE)
        if n <= len(modules):
            return modules[:n]
    error = ctypes.get_last_error()
    warnings.warn("Unable to list loaded libraries: EnumProcessModules "
                  f"failed with error code {error}",
                  stacklevel=2)
    return []


def _platform_specific_dllist() -> List[str]:
    # skip first entry, which is the executable itself
    modules = get_module_handles()[1:]
    libraries = [name for h in modules
                    if (name := get_module_filename(h)) is not None]
    return libraries
3 Likes