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.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
[DllImport] with PInvoke.*. Delete old declarations and hand-written structs/enums/constants.#if FEATURE_WINDOWSINTEROP, add runtime IsWindows check inside. Both required.HANDLE, HMODULE, HRESULT.S_OK, FILE_FLAGS_AND_ATTRIBUTES, etc.).PInvoke.* directly — no wrappers. Types flow via InternalsVisibleTo.[LibraryImport] only for non-Windows native calls (e.g. libc), guarded with #if NET.[DllImport] for PreserveSig / SetLastError / BOOL / HRESULT semantics and reproduce them: PreserveSig=false → .ThrowOnFailure(); SetLastError=true + failed BOOL → throw new Win32Exception(). Silently returning where the old code threw is a behavior change. See cswin32-com's parity table for the COM-side equivalent.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.
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..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.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).nint / nuint over IntPtr / UIntPtr for native-sized integers — no boxing,
better cast semantics, no IntPtr.Zero ceremony.string, StringBuilder, arrays) in blittable signatures.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.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.#if FEATURE_WINDOWSINTEROP
if (IsWindows)
{
PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);
}
#endif
// Cross-platform fallbackWRONG: if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif } — dead code in source builds.
Windows-only files are excluded via <Compile Remove> instead — no #if inside needed.
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).
NativeMethodsShared.S_OK → HRESULT.S_OK, InvalidHandle → HANDLE.INVALID_HANDLE_VALUE, FILE_ATTRIBUTE_DIRECTORY → FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY, STD_OUTPUT_HANDLE → STD_HANDLE.STD_OUTPUT_HANDLE, GENERIC_READ → FILE_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.RM_APP_STATUS / RM_APP_TYPEFILE_FLAGS_AND_ATTRIBUTES, FILE_ACCESS_RIGHTS, FILE_SHARE_MODE, FILE_CREATION_DISPOSITIONPROCESS_CREATION_FLAGS, STARTUPINFOW_FLAGS, PROCESS_ACCESS_RIGHTSPAGE_PROTECTION_FLAGS, FILE_MAPKNOWN_FOLDER_FLAGCast 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.
Use the helpers in src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs:
fileTime.ToLong() → 64-bit ticksfileTime.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<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.GetShortPathName(string, Span<char>)) before writing fixed blocks.No blanket NoWarn — handle semantically:
if (IsWindows) satisfies [SupportedOSPlatform] — no pragma neededif (IsUnixLike) satisfies [UnsupportedOSPlatform("windows")]!IsWindows — use else if (IsUnixLike). See documentation/specs/CA1416-analyzer-analysis.md[SupportedOSPlatform("windows6.1")] on methods calling CsWin32 APIs#pragma warning disable CA1416 only for static local functions (analyzer limitation)[SupportedOSPlatform] on partial struct — put on individual members insteadHANDLE ↔ IntPtr: (HANDLE)intPtr / (IntPtr)h.Value. Sentinels: HANDLE.Null, HANDLE.INVALID_HANDLE_VALUEFILETIME conversion: data.ftLastWriteTime.ToLong(), .ToDateTime() (local), .ToDateTimeUtc() — see "FILETIME Conversions" above. CsWin32 uses ComTypes.FILETIME (int fields), not Win32.Foundation.FILETIMESafeFileHandle: new SafeFileHandle((IntPtr)h.Value, true), pass with (HANDLE)handle.DangerousGetHandle()(SECURITY_ATTRIBUTES?)null& — HasFlag() boxes on .NET FrameworksystemInfo.Anonymous.Anonymous.wProcessorArchitecture — check generated source in obj/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:qEverything only referenced inside #if FEATURE_WINDOWSINTEROP must also be guarded:
using directives — most common failureStringToByteArray, constants like ERROR_SHARING_VIOLATION)#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.
c88db8e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.