cswin32-interop

star 5.5k

Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, BufferScope<T>, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.

dotnet By dotnet schedule Updated 6/12/2026

name: cswin32-interop description: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, BufferScope, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.' argument-hint: 'Describe the Windows API or interop code you are migrating or adding.'

CsWin32 Interop Guide

CsWin32 replaces [DllImport] with source-generated PInvoke.* calls. FEATURE_WINDOWSINTEROP is the compile-time gate; source builds disable it.

Paired skill: cswin32-com covers struct-based COM interop on top of CsWin32 (ComScope<T>, AgileComPointer<T>, delegate* unmanaged vtables, IComIID, CoCreateInstance, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules.

Rules

  1. Replace [DllImport] with PInvoke.*. Delete old declarations and hand-written structs/enums/constants.
  2. Gate with #if FEATURE_WINDOWSINTEROP, add runtime IsWindows check inside. Both required.
  3. Use CsWin32 types directly (HANDLE, HMODULE, HRESULT.S_OK, FILE_FLAGS_AND_ATTRIBUTES, etc.).
  4. Call PInvoke.* directly — no wrappers. Types flow via InternalsVisibleTo.
  5. Prefer CsWin32 for Windows APIs. Use [LibraryImport] only for non-Windows native calls (e.g. libc), guarded with #if NET.
  6. Preserve the old error-handling contract. Check the original [DllImport] for PreserveSig / SetLastError / BOOL / HRESULT semantics and reproduce them: PreserveSig=false.ThrowOnFailure(); SetLastError=true + failed BOOLthrow new Win32Exception(). Silently returning where the old code threw is a behavior change. See cswin32-com's parity table for the COM-side equivalent.

Blittable signatures

CsWin32 is configured with allowMarshaling: false, so every [DllImport] and every manual COM vtable method must be blittable — no marshalling at the boundary. These rules apply to both. For COM-vtable-only additions, see cswin32-com.

  • Return HRESULT from HRESULT-returning APIs (not int). Blittable (single int field), exposes .Succeeded / .Failed / .ThrowOnFailure(). Use HRESULT.S_OK over 0; cast e.HResult to (HRESULT) when wrapping. AddRef / Release return uint.
  • Call .ThrowOnFailure() instead of if (hr.Failed) Marshal.ThrowExceptionForHR(hr) — same exception, IErrorInfo-enriched, one-line call site: iface->Method(...).ThrowOnFailure();. Branch on hr only when handling a specific HRESULT (e.g. ERROR_INSUFFICIENT_BUFFER) before throwing. See cswin32-com's parity table for the migration contract.
  • Use PCWSTR / PWSTR for wide strings, never managed string. Implicit conversion from fixed (char* p = managedString). Add to NativeMethods.txt if not yet generated.
  • T** not out T* for pointer outputs. out triggers marshaling + a fixed round-trip at every call site.
  • void* for opaque / reserved params — never IntPtr.Zero; pass null literally. IntPtr is fine at boundaries with the wider .NET surface (Marshal.*, SafeHandle.DangerousGetHandle, public API).
  • Prefer nint / nuint over IntPtr / UIntPtr for native-sized integers — no boxing, better cast semantics, no IntPtr.Zero ceremony.
  • No managed reference types (string, StringBuilder, arrays) in blittable signatures.
  • Don't specify PreserveSig = true on [DllImport] — it's the default. Use PreserveSig = false only to force marshaller throw-on-failure (rare; prefer returning HRESULT and .ThrowOnFailure()). [ComImport] defaults the opposite way, but struct-based COM uses raw delegate* and isn't affected — see cswin32-com.
  • Constrain flag / option parameters to a typed enum. When a native DWORD / ULONG / int is documented as a typedef enum or #define set, declare a C# [Flags] enum Foo : uint (matching the underlying type) and use it in the signature (and delegate* cast for COM). Mirror the constraint even when the native side has no named enum. Self-documenting at the call site: OpenScope(path, CorOpenFlags.ofRead, ...) vs OpenScope(path, 0, ...). Co-locate the enum next to its consumer. See CorOpenFlags.cs and CorAssemblyFlags.cs.

Dual Guard Pattern

#if FEATURE_WINDOWSINTEROP
    if (IsWindows)
    {
        PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);
    }
#endif
    // Cross-platform fallback

WRONG: if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif } — dead code in source builds.

Windows-only files are excluded via <Compile Remove> instead — no #if inside needed.

Infrastructure

Define: src/Directory.BeforeCommon.targets sets FEATURE_WINDOWSINTEROP + $(FeatureWindowsInterop) when DotNetBuildSourceOnly != true. Use $(FeatureWindowsInterop) in .csproj for <Compile Remove>/<Compile Include>.

CsWin32 config: src/Framework/NativeMethods.txt (API list) + NativeMethods.json (allowMarshaling: false, useSafeHandles: false). Lives in Framework; other projects consume via InternalsVisibleTo. Do not add CsWin32 to other projects.

Guard selection:

Guard When Runtime check?
#if FEATURE_WINDOWSINTEROP Multi-TFM Windows calls Yes
#if FEATURE_WINDOWSINTEROP && NET Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via ComScope<T> work on net472 too — the generator emits IComIID on every TFM (see cswin32-com) Yes
#if FEATURE_WINDOWSINTEROP && !NETSTANDARD CsWin32 types without static abstract (net472 + net10) Yes
#if !NET / #if FEATURE_MSCOREE net472-only = inherently Windows No

Namespace imports must be inside #if FEATURE_WINDOWSINTEROP. WDK APIs use Windows.Wdk namespace.

Files: src/Framework/Windows/ (CsWin32 partials), src/Shared/Win32/ (COM helpers), src/Framework/Utilities/Wmi/ (.NET-only COM structs).

Constant Replacements

NativeMethodsShared.S_OKHRESULT.S_OK, InvalidHandleHANDLE.INVALID_HANDLE_VALUE, FILE_ATTRIBUTE_DIRECTORYFILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY, STD_OUTPUT_HANDLESTD_HANDLE.STD_OUTPUT_HANDLE, GENERIC_READFILE_ACCESS_RIGHTS.FILE_GENERIC_READ. Pattern: CsWin32EnumType.ORIGINAL_NAME — check generated types in obj/.

Always prefer the generated enum over a local copy. Before defining private const int ERROR_* / private enum FooFlags, grep the CsWin32 metadata:

  • ERROR_* (Win32 error codes) → WIN32_ERROR.ERROR_* (uint enum)
  • HRESULT codes → HRESULT.S_OK, etc.
  • Restart Manager status/type → RM_APP_STATUS / RM_APP_TYPE
  • File flags → FILE_FLAGS_AND_ATTRIBUTES, FILE_ACCESS_RIGHTS, FILE_SHARE_MODE, FILE_CREATION_DISPOSITION
  • Process flags → PROCESS_CREATION_FLAGS, STARTUPINFOW_FLAGS, PROCESS_ACCESS_RIGHTS
  • Memory mapping → PAGE_PROTECTION_FLAGS, FILE_MAP
  • Shell folder → KNOWN_FOLDER_FLAG

Cast int/uint return codes via (WIN32_ERROR)res for switch and equality. Add the enum to NativeMethods.txt if not yet generated, then check obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32.<EnumName>.g.cs.

Some flag values are standalone constants, not enum members. A Win32 #define outside a typedef enum generates as an internal const on PInvoke. Example: the LoadTypeLibEx flags are PInvoke.LOAD_TLB_AS_32BIT / _64BIT (uint), not members of REGKIND (which has only REGKIND_DEFAULT/REGISTER/NONE). Add the constant name to NativeMethods.txt like any API. OR it onto an enum at the constant's width and cast back: (REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT). Don't reintroduce a local const CsWin32 already emits.

Match local types to the CsWin32 type. Instead of int res = (int)PInvoke.RmStartSession(...) and casting at every comparison, declare WIN32_ERROR res = PInvoke.RmStartSession(...) and let helpers like GetException(WIN32_ERROR res, ...) take the typed value. Cast to int/uint only at the boundary where a non-CsWin32 API needs it (e.g. new Win32Exception((int)res, ...)). The same applies to HRESULT, BOOL, HANDLE, PROCESS_CREATION_FLAGS, etc.

Delete local mirror enums that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth.

FILETIME Conversions

Use the helpers in src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs:

  • fileTime.ToLong() → 64-bit ticks
  • fileTime.ToDateTime() → local DateTime (FILETIME values returned as local time, e.g. RM_PROCESS_INFO.ProcessStartTime)
  • fileTime.ToDateTimeUtc() → UTC DateTime (FILETIME values returned as UTC, e.g. WIN32_FILE_ATTRIBUTE_DATA.ftLastWriteTime)

Do not hand-roll DateTime.FromFileTime((long)hi << 32 | lo) — use the helpers for consistency. Note CsWin32-generated structs use ComTypes.FILETIME (int fields) for COM members and Windows.Win32.Foundation.FILETIME (uint fields) for kernel ones; the extension covers ComTypes.FILETIME.

BufferScope

BufferScope<T> (src/Framework/Utilities/BufferScope.cs) — stackalloc initial buffer with ArrayPool<T> fallback. Lives in Framework, available to all projects via InternalsVisibleTo.

using BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);
int length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());
if (length > buffer.Length)
{
    buffer.EnsureCapacity(length);
    length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());
}
if (length > 0) path = buffer.Slice(0, length).ToString();
  • ref struct — always use with using. Never stack-allocate more than 1024 bytes.
  • Check CsWin32 convenience overloads (e.g. GetShortPathName(string, Span<char>)) before writing fixed blocks.

Gotchas

CA1416 Platform Compatibility

No blanket NoWarn — handle semantically:

  • if (IsWindows) satisfies [SupportedOSPlatform] — no pragma needed
  • if (IsUnixLike) satisfies [UnsupportedOSPlatform("windows")]
  • Never use !IsWindows — use else if (IsUnixLike). See documentation/specs/CA1416-analyzer-analysis.md
  • Use versioned [SupportedOSPlatform("windows6.1")] on methods calling CsWin32 APIs
  • #pragma warning disable CA1416 only for static local functions (analyzer limitation)
  • CS0592 prevents [SupportedOSPlatform] on partial struct — put on individual members instead

Type Conversions

  • HANDLE ↔ IntPtr: (HANDLE)intPtr / (IntPtr)h.Value. Sentinels: HANDLE.Null, HANDLE.INVALID_HANDLE_VALUE
  • FILETIME conversion: data.ftLastWriteTime.ToLong(), .ToDateTime() (local), .ToDateTimeUtc() — see "FILETIME Conversions" above. CsWin32 uses ComTypes.FILETIME (int fields), not Win32.Foundation.FILETIME
  • SafeFileHandle: new SafeFileHandle((IntPtr)h.Value, true), pass with (HANDLE)handle.DangerousGetHandle()
  • Nullable structs: (SECURITY_ATTRIBUTES?)null
  • Enum flags: use bitwise &HasFlag() boxes on .NET Framework
  • Anonymous unions: systemInfo.Anonymous.Anonymous.wProcessorArchitecture — check generated source in obj/

Source-Build Verification (REQUIRED before pushing)

Source builds (DotNetBuildSourceOnly=true) disable FEATURE_WINDOWSINTEROP. CI treats all warnings as errors. Run both builds before every push:

# Normal build
dotnet msbuild MSBuild.Dev.slnf -v:q

# Source-build — catches unused usings/members/docs from #if guards
dotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q

Everything only referenced inside #if FEATURE_WINDOWSINTEROP must also be guarded:

  • IDE0005: using directives — most common failure
  • IDE0051/IDE0052: Private members (methods, fields, including helpers like StringToByteArray, constants like ERROR_SHARING_VIOLATION)
  • CA1823: Unused private fields (e.g. constants only consumed inside the guard)
  • CS1587: XML doc comments (move inside #if, not before)

The same applies when adding polyfills in src/Framework/Polyfills/ (e.g. SpanExtensions, IndexOfAnyExcept): polyfills usually live behind #if !NET (or similar TFM guards) but are still consumed from #if FEATURE_WINDOWSINTEROP code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds.

Install via CLI
npx skills add https://github.com/dotnet/msbuild --skill cswin32-interop
Repository Details
star Stars 5,519
call_split Forks 1,431
navigation Branch main
article Path SKILL.md
More from Creator