Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/dotnet-cswin32-interop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 28% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 31% | 0% |
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 fromfixed (char* p = managedString). Add to NativeMethods.txt if not yet generated.
not out T` for pointer outputs. out triggers marshaling + a fixed round-tripat 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. UsePreserveSig = 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.
csharp#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.
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.
csharpusing 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:
shell# 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:
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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→pass | 12,289 | 5,632 | -54% | 1 | 1 | 0% | 2,334 | 5,061 | +117% | 0 | 0 | — |
case-01 | fail→pass | 17,635 | 34,457 | +95% | 1 | 1 | 0% | 3,607 | 6,856 | +90% | 0 | 0 | — |
case-02 | fail→pass | 25,742 | 12,248 | -52% | 1 | 1 | 0% | 5,003 | 6,415 | +28% | 0 | 0 | — |
case-03 | fail→pass | 21,730 | 15,109 | -30% | 1 | 1 | 0% | 4,426 | 7,082 | +60% | 0 | 0 | — |
case-04 | pass→pass | 15,992 | 7,839 | -51% | 1 | 1 | 0% | 2,691 | 5,061 | +88% | 0 | 0 | — |
case-06 | fail→pass | 22,663 | 3,126 | -86% | 1 | 1 | 0% | 3,372 | 4,427 | +31% | 0 | 0 | — |
case-07 | pass→pass | 12,764 | 8,298 | -35% | 1 | 1 | 0% | 1,969 | 5,189 | +164% | 0 | 0 | — |
case-08 | fail→pass | 17,954 | 7,981 | -56% | 1 | 1 | 0% | 2,643 | 5,432 | +106% | 0 | 0 | — |
case-09 | pass→pass | 12,981 | 4,347 | -67% | 1 | 1 | 0% | 2,184 | 4,539 | +108% | 0 | 0 | — |
case-10 | fail→pass | 12,101 | 3,104 | -74% | 1 | 1 | 0% | 2,571 | 4,463 | +74% | 0 | 0 | — |
case-11 | fail→pass | 12,312 | 4,255 | -65% | 1 | 1 | 0% | 2,311 | 4,653 | +101% | 0 | 0 | — |
case-12 | fail→pass | 17,641 | 8,841 | -50% | 1 | 1 | 0% | 3,390 | 5,736 | +69% | 0 | 0 | — |
case-13 | fail→pass | 12,818 | 10,541 | -18% | 1 | 1 | 0% | 2,149 | 5,431 | +153% | 0 | 0 | — |
case-14 | pass→pass | 22,976 | 4,944 | -78% | 1 | 1 | 0% | 3,420 | 4,699 | +37% | 0 | 0 | — |
case-15 | fail→pass | 16,118 | 6,141 | -62% | 1 | 1 | 0% | 2,405 | 4,927 | +105% | 0 | 0 | — |
case-16 | pass→pass | 12,817 | 8,648 | -33% | 1 | 1 | 0% | 2,129 | 5,500 | +158% | 0 | 0 | — |
case-17 | pass→pass | 17,765 | 11,077 | -38% | 1 | 1 | 0% | 2,895 | 5,649 | +95% | 0 | 0 | — |
case-18 | pass→pass | 10,462 | 6,257 | -40% | 1 | 1 | 0% | 1,980 | 4,824 | +144% | 0 | 0 | — |
case-19 | fail→pass | 13,455 | 10,467 | -22% | 1 | 1 | 0% | 2,379 | 6,248 | +163% | 0 | 0 | — |
case-20 | pass→pass | 20,733 | 16,183 | -22% | 1 | 1 | 0% | 3,117 | 6,763 | +117% | 0 | 0 | — |
case-21 | pass→pass | 25,622 | 18,356 | -28% | 1 | 1 | 0% | 4,977 | 7,257 | +46% | 0 | 0 | — |
case-22 | pass→pass | 15,745 | 7,106 | -55% | 1 | 1 | 0% | 2,553 | 5,083 | +99% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +55 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.