Back to Wpfui

ADR-003: Win32 Interop via CsWin32

docs/architecture/decisions/ADR-003-win32-interop-via-cswin32.md

4.3.010.3 KB
Original Source

ADR-003: Win32 Interop via CsWin32

Status

Accepted

Context

WPF UI requires extensive Win32 API access for features unavailable in standard WPF:

  • Desktop Window Manager (DWM) effects (Mica, Acrylic backdrops)
  • Window corner rounding (Windows 11)
  • Dark mode title bars
  • System tray icon management
  • System theme detection
  • Taskbar progress indicators

Traditional P/Invoke requires:

  • Manual function signature declarations
  • COM interface definitions
  • Struct layout definitions
  • Constant value definitions
  • Maintaining cross-architecture compatibility (x86/x64/ARM64)

Decision

Use CsWin32 Source Generator

Package: Microsoft.Windows.CsWin32 (build-time only, PrivateAssets="all")

Declaration File: src/Wpf.Ui/NativeMethods.txt

CsWin32 generates P/Invoke bindings at compile-time from Win32 metadata.

NativeMethods.txt Format

# DWM Functions
DwmIsCompositionEnabled
DwmSetWindowAttribute
DwmExtendFrameIntoClientArea
S_OK
SetWindowThemeAttribute
DWM_SYSTEMBACKDROP_TYPE
DWM_WINDOW_CORNER_PREFERENCE
DWMWA_COLOR_NONE
WTA_OPTIONS

# Window Management
GetDpiForWindow
GetForegroundWindow
IsWindowVisible
SetWindowRgn
GetWindowRect
GetSystemMetrics
WINDOW_STYLE

# COM Interfaces
ITaskbarList4
TaskbarList

# Wildcard Patterns
WM_*
HT*

CsWin32 automatically generates:

  • Function P/Invoke declarations
  • Struct definitions with correct layout
  • Enum types
  • COM interface wrappers
  • Foundation types (HWND, HRESULT, BOOL, etc.)

Generated Code Location

Namespace: Windows.Win32 and Windows.Win32.Foundation Physical Location: obj/ directory (not committed to source control)

Usage:

csharp
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;

// Generated type-safe P/Invoke
HRESULT result = PInvoke.DwmSetWindowAttribute(
    new HWND(windowHandle),
    DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
    &darkMode,
    sizeof(BOOL)
);

Three-Layer Architecture

Layer 1: CsWin32 Generated Code

Purpose: Auto-generated P/Invoke declarations

Characteristics:

  • Compile-time generated
  • Type-safe API surface
  • Cross-architecture compatible
  • Not committed to source control

Layer 2: Managed Wrappers

Purpose: Safe, validated native API access

Location: src/Wpf.Ui/Interop/

UnsafeNativeMethods.cs

csharp
internal static class UnsafeNativeMethods
{
    public static unsafe bool ApplyWindowCornerPreference(
        IntPtr handle,
        WindowCornerPreference cornerPreference)
    {
        // Validation layer
        if (handle == IntPtr.Zero)
            return false;

        if (!PInvoke.IsWindow(new HWND(handle)))
            return false;

        // Type conversion
        DWM_WINDOW_CORNER_PREFERENCE pvAttribute =
            UnsafeReflection.Cast(cornerPreference);

        // Native call with exception handling
        try
        {
            HRESULT hr = PInvoke.DwmSetWindowAttribute(
                new HWND(handle),
                DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
                &pvAttribute,
                (uint)sizeof(DWM_WINDOW_CORNER_PREFERENCE)
            );

            return hr == HRESULT.S_OK;
        }
        catch
        {
            // Graceful degradation for unsupported OS versions
            return false;
        }
    }
}

Responsibilities:

  • Handle validation (IntPtr.Zero, IsWindow checks)
  • Exception suppression for cross-version compatibility
  • HRESULT → bool conversion
  • Type-safe enum conversions

Custom PInvoke.cs

Purpose: Supplement CsWin32 for missing/incorrect signatures

csharp
namespace Windows.Win32;

internal static partial class PInvoke
{
    // CsWin32 doesn't generate correct SetWindowLongPtr for 32/64-bit
    [DllImport("USER32.dll",
        ExactSpelling = true,
        EntryPoint = "SetWindowLongPtrW",
        SetLastError = true)]
    internal static extern nint SetWindowLongPtr(
        HWND hWnd,
        WINDOW_LONG_PTR_INDEX nIndex,
        nint dwNewLong
    );
}

Layer 3: High-Level Utilities

Purpose: Business logic and feature implementation

Locations:

  • src/Wpf.Ui/Win32/Utilities.cs - OS version detection
  • src/Wpf.Ui/Appearance/ - Theme managers
  • src/Wpf.Ui/Controls/FluentWindow/ - Window chrome
  • src/Wpf.Ui/Tray/ - System tray management

Characteristics:

  • Consumes Layer 2 safe wrappers
  • OS version feature gating
  • Business logic and state management

Handle Validation Pattern

Critical Requirement: All native calls MUST validate handles.

csharp
public static bool NativeOperation(IntPtr handle)
{
    // Step 1: Null check
    if (handle == IntPtr.Zero)
    {
        return false;
    }

    // Step 2: Verify window exists
    if (!PInvoke.IsWindow(new HWND(handle)))
    {
        return false;
    }

    // Step 3: Perform operation
    HRESULT hr = PInvoke.SomeWin32Function(new HWND(handle), ...);
    return hr == HRESULT.S_OK;
}

Rationale:

  • Handles can become invalid between retrieval and use
  • Window may be destroyed on background thread
  • Invalid handles cause native crashes
  • IsWindow is inexpensive (single User32 call)

Exception Handling Strategy

Philosophy: Native APIs fail silently across Windows versions. Prefer graceful degradation over exceptions.

csharp
try
{
    HRESULT hr = PInvoke.DwmSetWindowAttribute(...);
    return hr == HRESULT.S_OK;
}
catch (COMException)
{
    // API not available on this Windows version
    return false;
}
catch
{
    // Unexpected failure, degrade gracefully
    return false;
}

Suppressed Exceptions:

  • COMException - COM API failures
  • Win32Exception - Native API errors
  • EntryPointNotFoundException - API not available on OS version
  • DllNotFoundException - DLL not present

Conditional Compilation

Framework-Specific Code

csharp
#if NET5_0_OR_GREATER
    // Modern API available
    var version = Environment.OSVersion;
#else
    // Fallback for .NET Framework
    var version = GetVersionFromRegistry();
#endif

OS Version Feature Gating

csharp
// Windows 11+ only features
if (Win32.Utilities.IsOSWindows11OrNewer)
{
    UnsafeNativeMethods.ApplyWindowCornerPreference(
        handle,
        WindowCornerPreference.Round
    );
}

// DWM composition required
if (Win32.Utilities.IsCompositionEnabled)
{
    UnsafeNativeMethods.ApplyWindowBackdrop(
        handle,
        WindowBackdropType.Acrylic
    );
}

Enforcement

MUST Follow

  1. Add new Win32 APIs to NativeMethods.txt (never manual P/Invoke unless CsWin32 fails)
  2. Validate handles before all native calls (IntPtr.Zero + IsWindow)
  3. Return bool from wrapper methods indicating success
  4. Suppress exceptions in interop layer for compatibility
  5. Use unsafe keyword explicitly for pointer operations
  6. Feature-gate by OS version for version-specific APIs
  7. Use HRESULT == S_OK pattern for success checking
  8. Keep generated code private (internal/private visibility)

MUST NOT Do

  1. Never call PInvoke directly from high-level code (use UnsafeNativeMethods wrappers)
  2. Never skip handle validation (even if "guaranteed" valid)
  3. Never throw exceptions from interop wrappers (return false instead)
  4. Never assume API availability across Windows versions
  5. Never use var for native types (HRESULT, HWND, etc. - explicit types required)
  6. Never commit obj/ directory (contains generated code)

Verification

  • WpfAnalyzers enforces correct patterns
  • Code review checks handle validation
  • Multi-version testing (Windows 7, 8.1, 10, 11)

Consequences

Positive

  • Type Safety: CsWin32 generates correct signatures from metadata
  • Maintenance: Win32 metadata updates automatically benefit project
  • Cross-Platform: ARM64, x64, x86 handled automatically
  • Correctness: Struct layouts, calling conventions verified by Microsoft
  • Discoverability: IntelliSense for all Windows APIs
  • Build-Time Only: Zero runtime dependencies

Negative

  • Build-Time Dependency: Requires CsWin32 NuGet package
  • Opaque Generation: Generated code in obj/, harder to debug
  • NativeMethods.txt Maintenance: Must manually add new APIs (currently 34 lines, including wildcard patterns like WM_* and HT*)
  • Supplements Needed: Some APIs require manual P/Invoke (SetWindowLongPtr)
  • Learning Curve: Developers must understand NativeMethods.txt format

Performance Considerations

CsWin32 Performance

  • Zero overhead: Generated code identical to hand-written P/Invoke
  • Inlined by JIT: Same JIT optimization as manual declarations
  • No reflection: Compile-time code generation

Handle Validation Cost

  • IsWindow: Single User32 API call (~1-2 μs)
  • Negligible: Compared to DWM/window operations (hundreds of μs)
  • Essential: Prevents native crashes worth the cost

Alternatives Considered

Manual P/Invoke

Rejected:

  • High maintenance burden (200+ Win32 functions across project)
  • Error-prone struct layout definitions
  • Cross-architecture compatibility issues
  • No automatic updates from Windows SDK

ComWrappers

Rejected:

  • Only solves COM interop, not general P/Invoke
  • More complex than CsWin32
  • Limited to .NET 5+

PInvoke.net Snippets

Rejected:

  • Community-maintained, not authoritative
  • Copy-paste errors common
  • No compile-time verification
  • Inconsistent signature styles

Migration Guide

Adding New Win32 API

  1. Add to NativeMethods.txt:

    DwmGetColorizationColor
    
  2. Rebuild project (CsWin32 generates code)

  3. Create managed wrapper in UnsafeNativeMethods.cs:

    csharp
    public static bool GetColorizationColor(out Color color)
    {
        try
        {
            HRESULT hr = PInvoke.DwmGetColorizationColor(out uint colorValue, out BOOL opaque);
            color = Color.FromArgb(...);
            return hr == HRESULT.S_OK;
        }
        catch
        {
            color = default;
            return false;
        }
    }
    
  4. Consume from high-level code:

    csharp
    if (UnsafeNativeMethods.GetColorizationColor(out Color color))
    {
        // Use color
    }
    

References