Skip to content

Allow using enums for typedefs #1478

@JeremyKuhne

Description

@JeremyKuhne

Is your feature request related to a problem? Please describe.

Using structs for typedefs (HWND, HBRUSH, etc.) is convenient and flexible but is not optimal for performance and creates blockers when trying to properly define member functions. Normally structs like this are treated like primitives, but this isn't technically correct and, in fact, breaks calling some methods that return structs (as DirectX does in some cases). [MemberFunction] was introduced to allow proper parameter handling, but it no longer treats these typedef structs like primitives.

Describe the solution you'd like

An option to generate typedefs as enum. In NativeMethods.json:

{
  "$schema": "https://aka.ms/CsWin32.schema.json",
  "useEnumTypedefs": true
}

When this is defined anything that shows up with [NativeTypedef] in the winmd would be defined as an enum. In addition, this would require using C#14 or higher and not building for AnyCPU (the AnyCPU requirement may go away if C# exposes nint backed enums).

Here is a sample of types:

[AlsoUsableFor("HANDLE")]
[NativeTypedef]
public struct HWND
{
	public unsafe void* Value;
}

[RAIIFree("CloseHandle")]
[InvalidHandleValue(-1L)]
[InvalidHandleValue(0L)]
[NativeTypedef]
public struct HANDLE
{
	public unsafe void* Value;
}

If useEnumTypedefs is enabled, they would be defined as (on 64bit):

namespace Windows.Win32.Foundation;

public enum HWND : long {}
public enum HANDLE: long {}

Currently HWND is generated as:

public unsafe readonly partial struct HWND : IEquatable<HWND>
{
    public readonly void* Value;
    public HWND(void* value) => Value = value;
    public HWND(IntPtr value) : this((void*)value) {}
    public static HWND Null => default;
    public bool IsNull => Value == default;
    public static implicit operator void*(HWND value) => value.Value;
    public static explicit operator HWND(void* value) => new(value);
    public static bool operator ==(HWND left, HWND right) => left.Value == right.Value;
    public static bool operator !=(HWND left, HWND right) => !(left == right);
    public bool Equals(HWND other) => Value == other.Value;
    public override bool Equals(object obj) => obj is HWND other && Equals(other);
    public override int GetHashCode() => unchecked((int)Value);
    public override string ToString() => $"0x{(nuint)Value:x}";
    public static implicit operator IntPtr(HWND value) => new(value.Value);
    public static explicit operator HWND(IntPtr value) => new(value.ToPointer());
    public static explicit operator HWND(UIntPtr value) => new(value.ToPointer());
    public static implicit operator HANDLE(HWND value) => new(value.Value);
    public static readonly HWND HWND_BROADCAST = (HWND)(IntPtr)(65535);
    public static readonly HWND HWND_MESSAGE = (HWND)(-3);
    public static readonly HWND HWND_DESKTOP = (HWND)(IntPtr)(0);
    public static readonly HWND HWND_TOP = ( HWND)(IntPtr)(0);
    public static readonly HWND HWND_BOTTOM = (HWND)(IntPtr)(1);
    public static readonly HWND HWND_TOPMOST = (HWND)(-1);
    public static readonly HWND HWND_NOTOPMOST = (HWND)(-2);
}

What this would look like as an enum.

// Ideally this would derive from nint, but that currently isn't supported by C#, but is supported by the CLI.
// This means that this approach is not usable in `AnyCPU` scenarios currently.
public enum HWND : long
{
    HWND_BROADCAST = 65535,
    HWND_MESSAGE = -3,
    HWND_DESKTOP = 0,
    HWND_TOP = 0,
    HWND_BOTTOM = 1,
    HWND_TOPMOST = -1,
    HWND_NOTOPMOST = -2
}

public unsafe static partial class HWNDExtensions
{
    extension(HWND hwnd)
    {
        public static HWND Null => default;
        public bool IsNull => hwnd == default;
        public void* ToPointer() => (void*)(nint)hwnd;
        public HANDLE ToHANDLE() => (HANDLE)(nint)hwnd;
    }
}

public unsafe static class SampleUsage
{
    public static void Sample(HWND hwnd)
    {
        if (hwnd.IsNull)
        {
            hwnd = HWND.HWND_TOPMOST;
        }

        void* ptr = hwnd.ToPointer();
        HWND copy = (HWND)(nint)ptr;
    }
}

Here is another example:

// Besides being technically correct, this can be much faster as we are now using
// constants for values instead of static readonly fields.
public enum HRESULT : int
{
    CLASS_E_NOTLICENSED = unchecked((int)0x80040112),
}

public static class HresultExtensions
{
    extension(HRESULT hr)
    {
        public bool Failed => hr < 0;
        public bool Succeeded => hr >= 0;

        public HRESULT ThrowOnFailure(IntPtr errorInfo = default)
        {
            Marshal.ThrowExceptionForHR((int)hr, errorInfo);
            return hr;
        }
    }
}

public static partial class CurrentPInvoke
{
    extension(HRESULT hr)
    {
        // How we can "compose" additional values onto an enum already defined in a referenced assembly.
        // Currently this isn't even possible.
        public static HRESULT E_FAIL => unchecked((HRESULT)0x80004005);
    }
}

Describe alternatives you've considered

The only other option would be to generate wrappers every time we have one of these that unwrap the actual primitive value. That would work but would incur performance and bloat penalties.

Additional context

Extension cast operators are being considered for a future version of C# and would give a super clean model.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions