10 KiB
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:
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
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
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 detectionsrc/Wpf.Ui/Appearance/- Theme managerssrc/Wpf.Ui/Controls/FluentWindow/- Window chromesrc/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.
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.
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 failuresWin32Exception- Native API errorsEntryPointNotFoundException- API not available on OS versionDllNotFoundException- DLL not present
Conditional Compilation
Framework-Specific Code
#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
// 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
- Add new Win32 APIs to NativeMethods.txt (never manual P/Invoke unless CsWin32 fails)
- Validate handles before all native calls (IntPtr.Zero + IsWindow)
- Return bool from wrapper methods indicating success
- Suppress exceptions in interop layer for compatibility
- Use unsafe keyword explicitly for pointer operations
- Feature-gate by OS version for version-specific APIs
- Use HRESULT == S_OK pattern for success checking
- Keep generated code private (internal/private visibility)
MUST NOT Do
- Never call PInvoke directly from high-level code (use UnsafeNativeMethods wrappers)
- Never skip handle validation (even if "guaranteed" valid)
- Never throw exceptions from interop wrappers (return false instead)
- Never assume API availability across Windows versions
- Never use var for native types (HRESULT, HWND, etc. - explicit types required)
- 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_*andHT*) - 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
-
Add to NativeMethods.txt:
DwmGetColorizationColor -
Rebuild project (CsWin32 generates code)
-
Create managed wrapper in UnsafeNativeMethods.cs:
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; } } -
Consume from high-level code:
if (UnsafeNativeMethods.GetColorizationColor(out Color color)) { // Use color }