Windows Data Types says that a HANDLE is a *void, which is consistent with std. But by definition, a HANDLE is a kernel "object id", similar to a unix file descriptor, not a "memory address". It just happens to be the same size as a pointer. So maybe it shouldn't be represented by a pointer, and from that point of view windows-sys is right.
I remember that when std changed to windows-bindgen generated binding, this problem was mentioned and the reason was "to sidestep opsem concerns about mixing pointer types and integers between languages". Any details or previous discussions?
The only difference is that on CHERI-like systems, pointers have capability/provenance metadata, while usize doesn't.
So whether to use a pointer or usize would depend on how Windows was implementing handles on a given CHERI-like architecture. usize would be the default choice, but it would also possible to implement handles as capabilities, in which case using either a pointer or an "offset-less" pointer capability would be appropriate.
So it should probably be usize by default, but be open to depending on cfg directives in case Windows starts supporting a CHERI-like architecture and uses capabilities for handles there.
Talking to my friend who has experience with Windows API and internals, I realized that a HANDLE can be literally anything in various contexts and APIs, including but not limited to an NT object ID, a memory address, an offset, or even a thread ID! For example, the FindFirstFile function returns a HANDLE which is actually a memory address, the SYSTEM_PROCESS_INFORMATION struct contains a field named UniqueProcessId and has HANDLE type. In most Microsoft products, including the dotnet/C# stack, a HANDLE is represented as a pointer, as described in the document. So for normal cases, the pointer type is sufficient.
But for stuff that may or may not be a pointer, I'm not sure how to represent it in Rust is the most sufficient or correct, especially with possible systems as mentioned above that track metadata about "real" pointers.
I think having it as a pointer to an opaque type is a slightly safer choice, given that identity of usize in Rust is a bit confused, and often assumed to be size_t rather than the theoretical equivalence to intptr_t. But I don't know if Windows will ever support architectures with 128-bit pointers, so the distinction may not matter in practice.
I presume these pointers will never be dereferenced on the Rust side. It's legal in Rust to have dangling raw pointers. The opaque target type needs to have align=1, and then even arbitrary IDs cast to pointers will be fine.
The Rust projections for the Win32 SDK come from the win32metadata project. This encodes the APIs as .NET metadata. In the metadata, a HANDLE is defined as (when represented as C# for clarity):
public struct HANDLE
public IntPtr Value;
In the actual metadata that Value field is defined as:
.field public native int Value
A native int is a "signed integer, native size".
So the answer why windows-rs uses isize is "because that's what the metadata tells it". Of course that just moves the question to "why does the metadata say that". Here are some old issues that try to cover that ground:
What follows is my personal opinion alone and should not be taken as the opinion of the project, etc, etc.
I think it would be fine for std to internally use isize for kernel HANDLEs. The standard library supports a known set of targets where we know the rules and we know what kernel HANDLEs are. Also we talk directly to system libraries, we don't go through third parties. If a new target requires special consideration then we can special case it accordingly, though I'm unclear if it would require much effort on our part. If a platform requires stronger distinction between integers and pointers then the Windows headers themselves will also need to decide how to handle that.
However, the public API can't change for backward compatibility reasons. These will always require using pointer types. So std::os::windows::raw::HANDLE can never change on currently supported platforms.
One thing I didn't fully realize before is that since () is a ZST, a Rust program's access to *const () will be compiled as a no-op, so a *const () is guaranteed never to be dereferenced (except casting to other non-ZST types).
*const () (or equivalent) works reasonably well for void*, and can be made into a reference without any safety problems. It still doesn't prevent you from trying to read or write values, but at least it compiles to a no-op instead of Undefined Behavior.