Skip to content

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

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
541bfb3
Add a helper for ctypes function annotations
LeonarddeR Jul 21, 2025
d04874c
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Jul 22, 2025
79399a2
Add tests
LeonarddeR Jul 22, 2025
88ce14f
Add more tests and errcheck support
LeonarddeR Jul 22, 2025
3d0d8fc
Apply ctypes helper to Magnification API in screen curtain
LeonarddeR Jul 22, 2025
c53eab3
Several fixes
LeonarddeR Jul 22, 2025
5c70bd9
Allow getting the func name from the placeholder python func
LeonarddeR Jul 22, 2025
df66b92
Update tests/unit/test_util/test_ctypesUtils.py
LeonarddeR Jul 23, 2025
1cd366a
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Jul 23, 2025
12267df
Additional checks
LeonarddeR Jul 23, 2025
96fe65a
Provide our own pointer annotations
LeonarddeR Jul 23, 2025
d79962e
Register more pointer types
LeonarddeR Jul 23, 2025
94abec7
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Jul 25, 2025
52fda43
Improve documentation in doc string
LeonarddeR Jul 25, 2025
e2ebbe3
Enhance outparam
LeonarddeR Jul 25, 2025
2497409
Finish documentation
LeonarddeR Jul 25, 2025
76a5f05
Typo and test fixes
LeonarddeR Jul 25, 2025
fafe5fd
Apply suggestions from code review
LeonarddeR Jul 31, 2025
c8f6570
Apply suggestions from code review
LeonarddeR Jul 31, 2025
85d9746
Use typing.Annotated and typing.Annotated directly, clarify IN_OUT
LeonarddeR Jul 31, 2025
3f84458
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Jul 31, 2025
3ec34a4
Additional docs, add windowsErrCheck
LeonarddeR Jul 31, 2025
80fb559
Improve decorator type hints
LeonarddeR Aug 1, 2025
51b6395
annotateOriginalCFunc=False by default
LeonarddeR Aug 1, 2025
5d56bcf
Support None as restype because it is valid in ctypes, similar to c void
LeonarddeR Aug 1, 2025
e64f5b3
Hopefully satisfy type checkers for byref
LeonarddeR Aug 1, 2025
a947ad8
Docmentation
LeonarddeR Aug 1, 2025
9f2ee6c
Improved doc string formatting
LeonarddeR Aug 1, 2025
c2909ee
Improve types
LeonarddeR Aug 1, 2025
961b6e1
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Aug 6, 2025
52f291b
Some winBindings example conversions
LeonarddeR Aug 6, 2025
5746f59
Undo submodule change
LeonarddeR Aug 6, 2025
6f31213
Merge remote-tracking branch 'origin/master' into winBindings
LeonarddeR Aug 8, 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
79 changes: 79 additions & 0 deletions projectDocs/dev/codingStandards.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,85 @@ Type hints make reasoning about code much easier, and allow static analysis tool
* Prefer union shorthand (`X | Y`) over explicitly using `typing.Union`.
* Corollary: prefer `T | None` over `typing.Optional[T]`.

### Calling non-python code

When using parts of the Windows API, or parts of NVDA implemented in C++, it is necessary to use the [ctypes](https://docs.python.org/3/library/ctypes.html) library.

* When providing ctypes type information for foreign functions, structures and data types, prefer to use the same name as used in the external library.
* E.g. `GetModuleFileName` not `getModuleFileName`, even though the latter is a more Pythonic function name.
* Pythonic names should be reserved for wrappers that provide more pythonic access to functions.
* All Windows API functions, types and data structures should be defined in the `winBindings` package, in modules named according to the DLL which exports the function.
* E.g. `winBindings.kernel32`.
* Ctypes code for nvdaHelper should be defined in the `NVDAHelper.localLib` module.

#### using the `ctypesUtils` module

The goal of the `utils.ctypesUtils` module is to make calling external C functions easier.
It does so by aiding in two areas:

1. Annotating the ctypes C function pointer with type information (restype and argtypes) so that parameters passed to the function are properly validated by ctypes.
2. Providing type hints to the several parameters of the function.

The decorator is best explained by a basic example.

```python
from ctypes import windll
from ctypes.wintypes import BOOL, HANDLE
from typing import Annotated
from utils.ctypesUtils import dllFunc

@dllFunc(windll.kernel32)
def CloseHandle(hObject: int | HANDLE) -> Annotated[int, BOOL]:
...
```

A properly annotated function is a function of the form:
`def FunctionNameINDll(param1: hint1, param2: hint2) -> ReturnHint:`

* By default, the `dllFunc` decorator infers the function name from the name of the Python function.
It can be overridden by passing an additional parameter to the decorator.
* Parameter type hints can be of several forms, but should at least reference a ctypes compatible type.
* Form 1: Just a ctypes type, e.g. `HWND`
* Form 2: `int | HWND`. Both `int` and `HWND` are reported as valid by type checkers.
The first ctypes type found (`HWND`) is used in `restypes`.
This is the preferred approach.
* Form 3: `typing.Annotated[int, HWND]`. Only `int` is reported as valid by type checkers.
The annotation (i.e. `HWND`) is used in `restypes`.
This can be used when the desired ctypes type might be incompatible with type checkers.
* Return type hints can also be of several forms.
* Form 1: Just a ctypes type, e.g. `BOOL`.
It will be used as `restype`.
* Form 2: `typing.Annotated[int, BOOL]`. Preferred, because ctypes will automatically convert a `BOOL` to an `int`, whereas `BOOL` will be the `restype`.
* Note that the `int | BOOL` form (e.g. input parameter form 2) is not supported here, since a ctypes function will never return a `BOOL`, it will always create a more pythonic value whenever possible.

Output parameters are more complex.
When ctypes knows that a certain parameter is an output parameter, it will automatically create an object and passes a pointer to that object to the C function.
Therefore, output parameters are defined in a `typing.Annotated` hint as a `OutParam` object. e.g.

```python
from ctypes import windll
from ctypes.wintypes import BOOL, HWND, RECT
from typing import Annotated
from utils.ctypesUtils import dllFunc, OutParam, Pointer

@dllFunc(windll.user32, restype=BOOL)
def GetClientRect(hWnd: Annotated[int, HWND]) -> Annotated[RECT, OutParam("lpRect", 1)]: ...
...
```

Note that:

* Since specifying output parameters in ctypes swallows up the restype, `restype` needs to be defined on the `dllFunc` decorator explicitly.
Not doing so results in a `TypeError`.
* ctypes automatically returns the contained value of a pointer object.
So the return annotation here means:
* Treat `RECT` as the return type. Type checkers will communicate as such.
* Assume `ctypes.POINTER(RECT)` in `argtypes`, unless the return type is an array (e.g. `RECT * 1)`.
To override the pointer type, use the `type` parameter of the `OutParam` class.
* The out param is the second entry in the `argtypes` array, index=1.

For a function with multiple arg types, specify a type hint like `tuple[Annotated[RECT, OutParam(Pointer[RECT], "lpRect1", 1)], Annotated[RECT, OutParam(Pointer[RECT], "lpRect2", 2)]`.

### Language choices

The NVDA community is large and diverse, and we have a responsibility to make everyone feel welcome in it.
Expand Down
Loading