Coverage for cuda/pathfinder/_dynamic_libs/load_dl_windows.py: 83.12%
77 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6import ctypes
7import ctypes.wintypes
8import os
9import struct
10import sys
11import warnings
12from typing import TYPE_CHECKING
14from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
16if TYPE_CHECKING:
17 from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
19# Mirrors WinBase.h (unfortunately not defined already elsewhere)
20WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100
21WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000
23POINTER_ADDRESS_SPACE = 2 ** (struct.calcsize("P") * 8)
25# Set up kernel32 functions with proper types
26windll = getattr(ctypes, "windll", None)
27if windll is None:
28 raise RuntimeError("ctypes.windll is required on Windows")
29kernel32 = windll.kernel32
31# GetModuleHandleW
32kernel32.GetModuleHandleW.argtypes = [ctypes.wintypes.LPCWSTR]
33kernel32.GetModuleHandleW.restype = ctypes.wintypes.HMODULE
35# LoadLibraryExW
36kernel32.LoadLibraryExW.argtypes = [
37 ctypes.wintypes.LPCWSTR, # lpLibFileName
38 ctypes.wintypes.HANDLE, # hFile (reserved, must be NULL)
39 ctypes.wintypes.DWORD, # dwFlags
40]
41kernel32.LoadLibraryExW.restype = ctypes.wintypes.HMODULE
43# GetModuleFileNameW
44kernel32.GetModuleFileNameW.argtypes = [
45 ctypes.wintypes.HMODULE, # hModule
46 ctypes.wintypes.LPWSTR, # lpFilename
47 ctypes.wintypes.DWORD, # nSize
48]
49kernel32.GetModuleFileNameW.restype = ctypes.wintypes.DWORD
52# GetLastError
53kernel32.GetLastError.argtypes = []
54kernel32.GetLastError.restype = ctypes.wintypes.DWORD
57def ctypes_handle_to_unsigned_int(handle: ctypes.wintypes.HMODULE) -> int:
58 """Convert ctypes HMODULE to unsigned int."""
59 handle_uint = int(handle) 1abcefd
60 if handle_uint < 0: 1abcefd
61 # Convert from signed to unsigned representation
62 handle_uint += POINTER_ADDRESS_SPACE
63 return handle_uint 1abcefd
66def add_dll_directory(dll_abs_path: str) -> None:
67 """Add a DLL directory to the search path and update PATH environment variable.
69 Args:
70 dll_abs_path: Absolute path to the DLL file
72 Raises:
73 AssertionError: If the directory containing the DLL does not exist
74 """
75 dirpath = os.path.dirname(dll_abs_path)
76 assert os.path.isdir(dirpath), dll_abs_path
78 # Add the DLL directory to the native search path via the stdlib wrapper
79 # around AddDllDirectory. This only affects the LOAD_LIBRARY_SEARCH_USER_DIRS
80 # search; PATH is updated unconditionally below to also cover legacy
81 # dependent-DLL resolution. The returned handle is intentionally discarded:
82 # the directory must stay on the search path for the process lifetime, and
83 # the handle has no finalizer, so dropping it does not remove the directory.
84 try:
85 if sys.platform == "win32":
86 os.add_dll_directory(dirpath)
87 except OSError as e:
88 # Warn instead of failing silently; the PATH update below is a weaker
89 # fallback that newer loaders may ignore.
90 warnings.warn(
91 f"os.add_dll_directory({dirpath!r}) failed ({e}); "
92 "falling back to process-global PATH mutation for dependent-DLL resolution.",
93 RuntimeWarning,
94 stacklevel=2,
95 )
97 # Update PATH as a fallback for dependent DLL resolution
98 curr_path = os.environ.get("PATH")
99 os.environ["PATH"] = dirpath if curr_path is None else os.pathsep.join((curr_path, dirpath))
102def abs_path_for_dynamic_library(libname: str, handle: ctypes.wintypes.HMODULE) -> str:
103 """Get the absolute path of a loaded dynamic library on Windows."""
104 # Create buffer for the path
105 buffer = ctypes.create_unicode_buffer(260) # MAX_PATH 1abc
106 length = kernel32.GetModuleFileNameW(handle, buffer, len(buffer)) 1abc
108 if length == 0: 1abc
109 error_code = kernel32.GetLastError()
110 raise RuntimeError(f"GetModuleFileNameW failed for {libname!r} (error code: {error_code})")
112 # If buffer was too small, try with larger buffer
113 if length == len(buffer): 1abc
114 buffer = ctypes.create_unicode_buffer(32768) # Extended path length
115 length = kernel32.GetModuleFileNameW(handle, buffer, len(buffer))
116 if length == 0:
117 error_code = kernel32.GetLastError()
118 raise RuntimeError(f"GetModuleFileNameW failed for {libname!r} (error code: {error_code})")
120 return buffer.value 1abc
123def check_if_already_loaded_from_elsewhere(desc: LibDescriptor) -> LoadedDL | None:
124 for dll_name in desc.windows_dlls: 1abcefd
125 handle = kernel32.GetModuleHandleW(dll_name) 1abcefd
126 if handle: 1abcefd
127 abs_path = abs_path_for_dynamic_library(desc.name, handle) 1aefd
128 if desc.requires_add_dll_directory: 1aefd
129 # Match load_with_abs_path(): lazy component DLLs need the directory
130 # of the module that is actually loaded, regardless of how it arrived.
131 add_dll_directory(abs_path) 1d
132 return LoadedDL(abs_path, True, ctypes_handle_to_unsigned_int(handle), "was-already-loaded-from-elsewhere") 1aefd
133 return None 1abc
136def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None:
137 """Try to load a DLL using the native Windows process DLL search path.
139 This calls ``LoadLibraryExW(dll_name, NULL, 0)`` directly. Under Python
140 3.8+, CPython configures the process with
141 ``SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)``, so this
142 search does **not** include the system ``PATH``. Directories added via
143 ``AddDllDirectory()`` still participate.
145 Args:
146 desc: Descriptor for the library to load
148 Returns:
149 A LoadedDL object if successful, None if the library cannot be loaded
150 """
151 for dll_name in desc.windows_dlls: 1abcg
152 handle = kernel32.LoadLibraryExW(dll_name, None, 0) 1abcg
153 if handle: 1abcg
154 abs_path = abs_path_for_dynamic_library(desc.name, handle) 1abc
155 return LoadedDL(abs_path, False, ctypes_handle_to_unsigned_int(handle), "system-search") 1abc
157 return None 1ag
160def load_with_abs_path(desc: LibDescriptor, found_path: str, found_via: str | None = None) -> LoadedDL:
161 """Load a dynamic library from the given path.
163 Args:
164 desc: Descriptor for the library to load.
165 found_path: The absolute path to the DLL file.
166 found_via: Label indicating how the path was discovered.
168 Returns:
169 A LoadedDL object representing the loaded library.
171 Raises:
172 RuntimeError: If the DLL cannot be loaded.
173 """
174 if desc.requires_add_dll_directory:
175 add_dll_directory(found_path)
177 flags = WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
178 handle = kernel32.LoadLibraryExW(found_path, None, flags)
180 if not handle:
181 error_code = kernel32.GetLastError()
182 raise RuntimeError(f"Failed to load DLL at {found_path}: Windows error {error_code}")
184 return LoadedDL(found_path, False, ctypes_handle_to_unsigned_int(handle), found_via)