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

1# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 

2# SPDX-License-Identifier: Apache-2.0 

3 

4from __future__ import annotations 

5 

6import ctypes 

7import ctypes.wintypes 

8import os 

9import struct 

10import sys 

11import warnings 

12from typing import TYPE_CHECKING 

13 

14from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL 

15 

16if TYPE_CHECKING: 

17 from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor 

18 

19# Mirrors WinBase.h (unfortunately not defined already elsewhere) 

20WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100 

21WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000 

22 

23POINTER_ADDRESS_SPACE = 2 ** (struct.calcsize("P") * 8) 

24 

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 

30 

31# GetModuleHandleW 

32kernel32.GetModuleHandleW.argtypes = [ctypes.wintypes.LPCWSTR] 

33kernel32.GetModuleHandleW.restype = ctypes.wintypes.HMODULE 

34 

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 

42 

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 

50 

51 

52# GetLastError 

53kernel32.GetLastError.argtypes = [] 

54kernel32.GetLastError.restype = ctypes.wintypes.DWORD 

55 

56 

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

64 

65 

66def add_dll_directory(dll_abs_path: str) -> None: 

67 """Add a DLL directory to the search path and update PATH environment variable. 

68 

69 Args: 

70 dll_abs_path: Absolute path to the DLL file 

71 

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 

77 

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 ) 

96 

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

100 

101 

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

107 

108 if length == 0: 1abc

109 error_code = kernel32.GetLastError() 

110 raise RuntimeError(f"GetModuleFileNameW failed for {libname!r} (error code: {error_code})") 

111 

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})") 

119 

120 return buffer.value 1abc

121 

122 

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

134 

135 

136def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None: 

137 """Try to load a DLL using the native Windows process DLL search path. 

138 

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. 

144 

145 Args: 

146 desc: Descriptor for the library to load 

147 

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

156 

157 return None 1ag

158 

159 

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. 

162 

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. 

167 

168 Returns: 

169 A LoadedDL object representing the loaded library. 

170 

171 Raises: 

172 RuntimeError: If the DLL cannot be loaded. 

173 """ 

174 if desc.requires_add_dll_directory: 

175 add_dll_directory(found_path) 

176 

177 flags = WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 

178 handle = kernel32.LoadLibraryExW(found_path, None, flags) 

179 

180 if not handle: 

181 error_code = kernel32.GetLastError() 

182 raise RuntimeError(f"Failed to load DLL at {found_path}: Windows error {error_code}") 

183 

184 return LoadedDL(found_path, False, ctypes_handle_to_unsigned_int(handle), found_via)