Skip to content
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

We need a way to distinguish handles from pointers from pointer-sized integers #21

Closed
AArnott opened this issue Nov 12, 2020 · 11 comments
Closed
Labels
usability Touch-up to improve the user experience for a language projection

Comments

@AArnott
Copy link
Member

AArnott commented Nov 12, 2020

Ryan said:

I've been operating from the assumption that our metadata maps "native int" and "native uint" to integer types, not pointer types. This is because they are being used for integer types like WPARAM, LPARAM (see WNDPROC), and SIZE_T (see D3D12CreateVersionedRootSignatureDeserializer).

I agree. We need to distinguish handles/pointers from pointer-sized integers for the best language experience.

In .NET, we want to use IntPtr for handles, void* for pointers, and nint for integers that the user may actually care to read/write.

@DefaultRyan
Copy link
Member

I hadn't heard of nint, but looking at https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/native-integers are we sure that's the right choice? That looks like a compiler keyword that uses System.IntPtr as its underlying type.

Also, after doing a little more reading, I learned that my hunch is probably still correct. Despite their names System.IntPtr and System.UIntPtr aren't pointers, but integers that are pointer-sized. So in this way, they are just like C++'s intptr_t and uintptr_t, with uintptr_t being synonymous with size_t (on nearly all platforms).

@AArnott
Copy link
Member Author

AArnott commented Nov 12, 2020

Yes, nint is just syntax sugar for IntPtr, but it allows the language to treat it like an integer, whereas IntPtr is very difficult to work with for numbers. That's why C# finally made it better with C#9. It's like size_t on C in that it can actually be used like an integer type.

@AArnott AArnott added the usability Touch-up to improve the user experience for a language projection label Jan 14, 2021
@sotteson1
Copy link
Contributor

This should be fixed with the work done for NativeTypedef structs. Let me know if there is still more here that doesn't look right.

@marler8997
Copy link
Contributor

Here's one issue I see with making handle types the integral type IntPtr. Alot of documentation and code examples talk about handle values being NULL rather than 0 (i.e. the return value of GetModuleHandle). If handles are integral types then NULL won't work for languages that make a type distinction between NULL and 0. All else being equal, I don't see any benefit to defining handle values as integral types but defining them as pointer types would allow handle values to continue to work with null pointer values.

@sotteson1
Copy link
Contributor

Here's one issue I see with making handle types the integral type IntPtr. Alot of documentation and code examples talk about handle values being NULL rather than 0 (i.e. the return value of GetModuleHandle). If handles are integral types then NULL won't work for languages that make a type distinction between NULL and 0. All else being equal, I don't see any benefit to defining handle values as integral types but defining them as pointer types would allow handle values to continue to work with null pointer values.

I think it's really up to the language projection authors and what they think works best for their languages. Notice what @AArnott (the author of CsWin32) said at the top:

In .NET, we want to use IntPtr for handles, void* for pointers, and nint for integers that the user may actually care to read/write.

It's easy for me to change handles from IntPtr to void* if it makes their projections better, but so far I haven't heard that from the projection authors.

@marler8997
Copy link
Contributor

marler8997 commented Mar 31, 2021

I think it's really up to the language projection authors and what they think works best for their languages.

The problem here is that we've established that IntPtr and UIntPtr are to be treated as integer types, which implies that you can assign them values like 0, 1, etc. Since this is the type we are using for GetWindowLongPtr and SetWindowLongPtr, this makes sense as that's how the original api worked as well (because it used the integer type LONG_PTR). But now that we've defined IntPtr and UIntPtr as integer types, the NULL (or nullptr value) is no longer compatible. In every location where NULL was previously being passed to a handle variable/parameter (see https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/begin/LearnWin32/HelloWorld/cpp/main.cpp#L34) the code must be modified to use 0 instead.

Note that this discrepancy is easy to workaround by simply replacing all NULL literals in a code base meant for a handle type with 0 instead, but this results in code that diverges from the win32 documentation which states that handle values use NULL (not 0).

As the author of the Zig projection I can attest that I'd prefer to use null rather than 0 for handles to remain consistent with the win32 documentation and all the existing C/C++ code out there. It looks like Rust also defines HANDLE as a void pointer type (https://docs.rs/winapi/0.3.9/winapi/um/winnt/type.HANDLE.html), I wonder what they'd have to say on this? Not sure what C# is doing either, would be interesting to see.

@jnm2
Copy link

jnm2 commented Mar 31, 2021

I think it would be worse to advertise "this opaque handle value is a pointer to something" than to be forced to use the syntax IntPtr.Zero instead of NULL.

@tannergooding
Copy link
Member

tannergooding commented Mar 31, 2021

Regardless of what people want to say about pointers (void*, HWND__*, etc) vs handles (HWND, HBRUSH, etc) vs integrals (size_t, LONG_PTR, etc); the fundamental truth is that C/C++ treat all three relatively the same due to implicit conversions and support for operations that many more modern languages treat as illegal by default.

NULL is not the same as nullptr, it is a macro define for the integer literal 0 and it is simply implicitly convertible to any of the above three and functions as nullptr when the target type is a pointer.

"Opaque handles" in Windows frequently behave as truly opaque values, but they equally have cases where values in the range 0-65535 are "well-known" or reserved constants. Take for example, COLOR_WINDOW and friends which are meant to be used in WNDCLASSW.hbrBackground as an HBRUSH by doing wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1): https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassw

For opaque handles, I see three options for the raw signatures:

  • Use void*, as is done for HANDLE and if STRICT is not defined
  • Use Name__* (such as HWND__*), as the underlying typedef actually uses when STRICT is defined (the default)
  • Use IntPtr to represent an opaque handle

The typical convention in .NET has been to define this as IntPtr because it doesn't require unsafe and so can be easily publicly exposed (such as is done in WinForms). It supports only the most trivial integer operations out of the box which fits the need for simple well-known handle values like COLOR_WINDOW, and it works with the rest of the ecosystem around SafeHandles and marshal APIs.

I think that using void* or Name__* provides a strictly worse experience out of the box and makes it harder to determine, generally speaking, if the thing is a pointer, integer, or opaque handle for when that distinction is important.

@AArnott
Copy link
Member Author

AArnott commented Apr 2, 2021

we've established that IntPtr and UIntPtr are to be treated as integer types

If that's true, it only applies where these types appear outside of a typedef. The typedef structs are (mostly) strongly-typed wrappers for handles.
In CsWin32 then, we map IntPtr from metadata to nint in C# except where it appears in a typedef, in which case we leave it as IntPtr. Perhaps Zig can do something similar.

@marler8997
Copy link
Contributor

marler8997 commented Apr 3, 2021

In .NET, we want to use IntPtr for handles, void* for pointers, and nint for integers that the user may actually care to read/write.

Is this the currently accepted behavior? If so, does that mean that GetWindowLongPtr and SetWindowLongPtr should be using nint instead of IntPtr?

@AArnott
Copy link
Member Author

AArnott commented Apr 5, 2021

Is this the currently accepted behavior?

Yes, I believe that is the behavior we're shooting for.

does that mean that GetWindowLongPtr and SetWindowLongPtr should be using nint instead of IntPtr?

I'm not sure which type you're referring to, as 3 types exist on those method signatures.

  • HWND should remain HWND.
  • int should remain int
  • LONG_PTR should probably be nint.

But keep in mind that there is no such thing as nint in metadata. It's called native int in metadata instead, and C# tends to interpret that as IntPtr historically. C# itself turns nint into native int when it compiles code as well, but it adds an attribute to the type reference as a hint that this native int should be interpreted as nint when C# later sees it rather than IntPtr.
However our metadata does not apply this attribute anywhere. The way we deal with this in CsWin32 is to translate all native int to nint when generating C# except for fields in a typedef struct, where we prefer IntPtr instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
usability Touch-up to improve the user experience for a language projection
Projects
None yet
Development

No branches or pull requests

6 participants