-
-
Notifications
You must be signed in to change notification settings - Fork 715
Add a helper for ctypes function annotations #18534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
@LeonarddeR I've just skimmed the code here. I've not provided a thorough review because I know you said this was very much a prototype. Here are my thoughts (I've not included nitpick comments or anything):
@seanbudd and @michaelDCurran any thoughts here? We have spoken about wanting something like this internally. Do you think this approach is viable? |
There are third-party modules like py-win32more, which has Python bindings for most of the Win32 APIs. The function prototypes are generated from win32metadata, and are categorized into several .py files, with decorators that parse the annotation and turn the "placeholder" functions into callable functions. Some of the placeholder functions inside `win32more.Windows.Win32.Storage.FileSystem`@winfunctype('KERNEL32.dll')
def CreateDirectoryA(lpPathName: win32more.Windows.Win32.Foundation.PSTR, lpSecurityAttributes: POINTER(win32more.Windows.Win32.Security.SECURITY_ATTRIBUTES)) -> win32more.Windows.Win32.Foundation.BOOL: ...
@winfunctype('KERNEL32.dll')
def CreateDirectoryW(lpPathName: win32more.Windows.Win32.Foundation.PWSTR, lpSecurityAttributes: POINTER(win32more.Windows.Win32.Security.SECURITY_ATTRIBUTES)) -> win32more.Windows.Win32.Foundation.BOOL: ...
CreateDirectory = UnicodeAlias('CreateDirectoryW')
@winfunctype('KERNEL32.dll')
def CreateFileA(lpFileName: win32more.Windows.Win32.Foundation.PSTR, dwDesiredAccess: UInt32, dwShareMode: win32more.Windows.Win32.Storage.FileSystem.FILE_SHARE_MODE, lpSecurityAttributes: POINTER(win32more.Windows.Win32.Security.SECURITY_ATTRIBUTES), dwCreationDisposition: win32more.Windows.Win32.Storage.FileSystem.FILE_CREATION_DISPOSITION, dwFlagsAndAttributes: win32more.Windows.Win32.Storage.FileSystem.FILE_FLAGS_AND_ATTRIBUTES, hTemplateFile: win32more.Windows.Win32.Foundation.HANDLE) -> win32more.Windows.Win32.Foundation.HANDLE: ...
@winfunctype('KERNEL32.dll')
def CreateFileW(lpFileName: win32more.Windows.Win32.Foundation.PWSTR, dwDesiredAccess: UInt32, dwShareMode: win32more.Windows.Win32.Storage.FileSystem.FILE_SHARE_MODE, lpSecurityAttributes: POINTER(win32more.Windows.Win32.Security.SECURITY_ATTRIBUTES), dwCreationDisposition: win32more.Windows.Win32.Storage.FileSystem.FILE_CREATION_DISPOSITION, dwFlagsAndAttributes: win32more.Windows.Win32.Storage.FileSystem.FILE_FLAGS_AND_ATTRIBUTES, hTemplateFile: win32more.Windows.Win32.Foundation.HANDLE) -> win32more.Windows.Win32.Foundation.HANDLE: ...
CreateFile = UnicodeAlias('CreateFileW')
@winfunctype('KERNEL32.dll')
def DefineDosDeviceW(dwFlags: win32more.Windows.Win32.Storage.FileSystem.DEFINE_DOS_DEVICE_FLAGS, lpDeviceName: win32more.Windows.Win32.Foundation.PWSTR, lpTargetPath: win32more.Windows.Win32.Foundation.PWSTR) -> win32more.Windows.Win32.Foundation.BOOL: ...
DefineDosDevice = UnicodeAlias('DefineDosDeviceW')
@winfunctype('KERNEL32.dll')
def DeleteFileA(lpFileName: win32more.Windows.Win32.Foundation.PSTR) -> win32more.Windows.Win32.Foundation.BOOL: ...
@winfunctype('KERNEL32.dll')
def DeleteFileW(lpFileName: win32more.Windows.Win32.Foundation.PWSTR) -> win32more.Windows.Win32.Foundation.BOOL: ...
DeleteFile = UnicodeAlias('DeleteFileW')
But here are some caveats:
So in order not to make VS Code "smart" enough to use the real underlying type, it might be better to check for |
This is a good idea, I was already considering that. Should at least improve performance during the type checking itself. |
@SaschaCowley wrote:
I don't think it will cost that much performance. We'll probably have to benchmark it if we want to know.
Most of them are documented, see for example _SimpleCData. I just discovered that
Thanks @gexgd0419 for your answer on this. I don't have much to add. |
See test results for failed build of commit bb1e129e3a |
Added tests and example in the screenCurtain module should give you an idea on how this is supposed to work. |
Temporarily marking this as ready to trigger a copilot review |
I remembered that See the comment where I discovered that problem: #17592 (comment) In that case, I ended up using something like the following: (the stackoverflow answer) if TYPE_CHECKING:
LP_c_ubyte = _Pointer[c_ubyte]
else:
LP_c_ubyte = POINTER(c_ubyte) But this is not generic enough, and requires a separate alias for each pointer type. So later in #18300, I tried something like this: if TYPE_CHECKING:
from ctypes import _Pointer
else:
# ctypes._Pointer is not available at run time.
# A custom class is created to make `_Pointer[type]` still work.
class _Pointer:
def __class_getitem__(cls, item: type) -> type:
return POINTER(item) I hope there's a unified way of annotating ctypes pointer types. |
Goot point @gexgd0419
We might have to choose between either one or the other to decrease complexity. |
@gexgd0419 wrote:
I actually like this a lot, and since the folks at python/ctypes are pretty unwilling to fix this themselves, we might want to offer this by default in ctypesUtils. syntactically, |
@SaschaCowley Wrote:
I'm only considering it now you're asking it, but honestly, I'm not sure whether I have time to maintain such a project. That said, If you prefer a third party package, I'm happy to consider it further. |
Re the discussion on pywin32more. After investigation, the following is probably of relevance to us.
Point 5 should be easy to fix with a pr and will probably improve type hints, but then there's still the issue with As for the automatic generation of functions, may be copilot agent mode can play nicely here when connected to the MPC for Microsoft Learn. It ensures that AI has all the function information at hand when writing. I haven't played with this yet. |
To get win32more to allow VS Code to correctly handle type info for the functions, I think win32more's winfunctype needs to be typed itself to to understand it returns a Callable with the same signature as what it received:
My VS Code now can see the correct param and return types for MessageBoxW for example. It would be good if we could do this in a type stub file, but I can't seem to get that to work. |
I edited my above comment to be more accurate:
|
If we use out parameters, will the out parameters be annotated on the original function pointer as well? I think that the code usually assumes there's no out parameter if |
I personally don't really like the automatic handling of out parameters here. I think that becomes a little too pythonic. I think if you want that, then write specific friendly wrappers at a higher level. At the lowest level, the goals should really just be data size / bitness compliance (E.g. 32 and 64 bit will work fine) and type checking will be accurate. But no shaddowing returns with outs or making signatures more friendly. I'd prefer it to match Microsoft docs to the letter. But just my feeling. |
I'm in agreement with Mick here. We want these to be fairly direct access to the Windows API. Pythonic wrappers should be handled separately. |
This PR can be used as a low level wrapper, and has the option to create a high level wrapper, which is just utilizing the features of If raw, direct access is preferred, we can just use this without any out parameters, and still have the option to use this to create higher level wrappers where needed. I think that error checking is a good addition though. In C/C++, we usually have to check the return value of API calls with ifs, but it's not so Pythonic, so converting them to exceptions might be good to avoid writing a bunch of ifs in Python code, or forgetting to check the return value and letting errors slip away. Unless most of the developers are still accustomed to checking the return values manually according to Microsoft docs, in which case this can be confusing. |
@michaelDCurran wrote:
Have you played with That said, as pointed out by @gexgd0419, win32more is still very strict about the types it communicates to the type checker. only c_int, not int, only c_wchar_p, not str. @gexgd0419 wrote:
No. The original function pointer is already constructed, and you can only provide the parameter spec at construction time of the function pointer. So even if you provide out parameters on the function prototype and you specify
This is exactly how I intended it to be: offering the broadest possible flexibility. If you want the wrappers to be c like, just don't provide out parameters and you're fine. @michaelDCurran wrote:
Note that ctypes does shadowing returns al over the place. Specify a restype of c_int and ctypes will return an python int, I believe it will even do so with c_void_p. The example in #18534 (comment)
Note that changing restype and argtypes is exactly what winbindings is doing in the current pull requests that show it, #18571 and #18207. |
What's the recommended way to pass a pointer to a structure (or anything else)? @dllFunc(ctypes.windll.user32)
def GetDesktopWindow() -> Annotated[int, HWND]: ...
@dllFunc(ctypes.windll.user32)
def GetWindowRect(hWnd: int | HWND, lpRect: Pointer[RECT]) -> Annotated[int, BOOL]: ...
hwnd = GetDesktopWindow()
rc = RECT()
GetWindowRect(hwnd, rc) # type checker error
GetWindowRect(hwnd, ctypes.pointer(rc)) # pass
GetWindowRect(hwnd, ctypes.byref(rc)) # type checker error Here only We might need to have some kind of coding standard for this. |
As input parameter: This tells the decorator to use the first type in argtypes. it tells the type checker that passing a c_int is also valid, since it is valid ctypes behavior to automatically byref c_int.
Should be solved by
Good catch. I think I can fix that. How do you check those at runtime? |
You can use something like Printing the type shows However, there's neither So the only way I can think of, is to use something like if TYPE_CHECKING:
byrefType = ctypes._CArgObject # type: ignore
else:
byrefType = type(ctypes.byref(ctypes.c_int())) |
@gexgd0419 This should work now. I hope that static type checking with byref is also fine now. |
Currently many of the Pyright rules are disabled, see #17744. The goal is to improve the existing code and turn on rules over time, but as for now, even simple type errors such as If you want to check how well it will work with type checkers in the future, you can try removing the "Bad rules" lines in the pyproject.toml file, so that it will check everything. |
I tried the above to let pyright do its work, and it started complaining. For example, this line: @dllFunc(windll.user32, restype=BOOL, annotateOriginalCFunc=True)
def GetClientRect(hWnd: Annotated[int, HWND]) -> Annotated[RECT, OutParam("lpRect", 1)]: ... Pylance warns that I think that it failed to associate So how does the original # in _ctypes.pyi
@type_check_only
class _CData:
...
class _SimpleCData(_CData, Generic[_T], metaclass=_PyCSimpleType):
...
_CDataType: TypeAlias = _SimpleCData[Any] | _Pointer[Any] | CFuncPtr | Union | Structure | Array[Any]
class CFuncPtr(_PointerLike, _CData, metaclass=_PyCFuncPtrType):
restype: type[_CDataType] | Callable[[int], Any] | None
... I guess that if we want to make the type checker happy, we might have to use those private, type-check-only types in .pyi files, and only during type checking. I wonder, do we still need run-time type information in this case? Or can we just deal with the static type checkers, like using a .pyi file? |
@gexgd0419 I changed code to use these internals. Runtime type checking is less important indeed, but with using an ABC like done now, it should also work. |
In 52f291b, I added some examples To make it more tangible how CtypesUtils works at a lower level. |
Link to issue number:
Related to #11125
Summary of the issue:
Windows DLL functions are a headache for type checkers, as these functions are not annotated with types.
Description of user facing changes:
None
Description of developer facing changes:
Better type checking for functions exported from dlls.
Description of development approach:
Create a function decorator that ensures the following:
Testing strategy:
visionEnhancementProviders.screenCurtain.Magnification
.Known issues with pull request:
None
Code Review Checklist:
@coderabbitai summary