Back to Files

Converting Marshaled Interop to CsWin32 Unmarshaled Interop

docs/interop-unmarshaled-conversion.md

4.1.35.6 KB
Original Source

Converting Marshaled Interop to CsWin32 Unmarshaled Interop

This repository is moving trim-unsafe manual interop toward source-generated CsWin32 interop. When a user asks to convert marshaled interop code into unmarshaled interop, prefer updating NativeMethods.txt and the call sites instead of preserving manual DllImport/LibraryImport declarations or wrapping them with more local P/Invoke code.

Removing Vanara interop usage is also part of this direction since it internally has trim-unsafe interop code.

Goal

Remove interop code that relies on runtime marshalling, especially declarations using DllImport, ComImport and the Vanara package.

The target shape is:

  • Add the native API, COM interface, enum, or struct name to src/Files.App.CsWin32/NativeMethods.txt.
  • Use Windows.Win32.PInvoke and generated CsWin32 types at the call site.
  • Delete the manual declarations and Vanara references.
  • Keep Win32PInvoke only for definitions that are not yet converted or cannot be generated by CsWin32.

Workflow

  1. Locate manual interop:

    powershell
    git grep -n "DllImport\|MarshalAs\|StringBuilder" -- src
    git grep -n "Win32PInvoke\." -- src/Files.App
    git grep -n "using Vanara\|Vanara\.PInvoke\|Kernel32\.\|Shell32\.\|User32\." -- src/Files.App
    
  2. Add the API and related generated types to src/Files.App.CsWin32/NativeMethods.txt.

    Include both the function and any dependent structs, enums, or COM interfaces that the call site needs. For example:

    text
    RmStartSession
    RmRegisterResources
    RmGetList
    RmEndSession
    RM_PROCESS_INFO
    SHBrowseForFolder
    BROWSEINFOW
    SHGetPathFromIDList
    SHCreateItemFromParsingName
    SHCreateStreamOnFileEx
    
  3. Build the CsWin32 project or the app project to refresh generated signatures:

    powershell
    dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64
    
  4. Update callers to use generated APIs directly.

    Prefer generated safe overloads when they exist, such as Span<char>, SafeHandle, ComPtr<T>, generated enums, and generated structs. Use unsafe raw overloads only when the generated API naturally exposes pointers or when COM pointer identity is required.

  5. Remove the manual definition only after all callers have moved.

    Use targeted checks:

    powershell
    git grep -n "Win32PInvoke\.RmStartSession\|Win32PInvoke\.SHBrowseForFolder" -- src/Files.App
    git grep -n "RM_PROCESS_INFO\|BROWSEINFO" -- src/Files.App
    
  6. Build and confirm there are no C# errors.

    In this repo, WinUI XAML compiler failures may appear independently of interop changes. Separate error CS* failures from MSB3073 XAML compiler failures when reporting verification.

Manual Definition Conversion Notes

  • StringBuilder output buffers should usually become Span<char> or fixed char* buffers.

  • IntPtr handles should become SafeFileHandle, SafeHandle, HANDLE, or generated handle structs where practical.

  • Some APIs such as CoCreateInstance and SHCreateItemFromParsingName often require unsafe pointer overloads:

    csharp
    void* raw;
    Guid iid = SomeInterfaceIid;
    HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, &raw);
    IntPtr instance = (IntPtr)raw;
    
  • Shell APIs that return PIDLs or allocated strings still require explicit lifetime management, but the API declaration should come from CsWin32:

    csharp
    var pidl = PInvoke.SHBrowseForFolder(ref browseInfo);
    Marshal.FreeCoTaskMem((nint)pidl);
    
  • Restart Manager APIs can use generated RM_PROCESS_INFO and Span<char> session keys instead of local struct definitions.

  • Do not keep a local LibraryImport copy when the API can be represented in NativeMethods.txt. The requested direction is to update callees to CsWin32-generated interop.

Vanara Conversion Notes

Treat Vanara removal the same way as manual P/Invoke removal when Vanara is only wrapping a native API. Add the API to NativeMethods.txt, inspect the generated CsWin32 signature, then update the call site to generated types.

Example conversion:

csharp
// Before
var lib = Kernel32.LoadLibrary(file);
StringBuilder result = new(2048);
_ = User32.LoadString(lib, number, result, result.Capacity);
Kernel32.FreeLibrary(lib);
return result.ToString();

// After
using var lib = PInvoke.LoadLibrary(file);
Span<char> result = stackalloc char[2048];
int length = PInvoke.LoadString(lib, (uint)number, result, result.Length);
return result[..length].ToString();

Useful heuristics:

  • Start with simple Kernel32.*, User32.*, or Shell32.* calls that map directly to one Win32 function.
  • Remove using Vanara.PInvoke only when no remaining types in the file depend on it.
  • Prefer generated Span<T> overloads over StringBuilder buffers when available.
  • Prefer generated handle types and SafeHandle overloads where CsWin32 provides them.
  • Watch for CsWin32 convenience overloads. Some APIs generate safer shapes than the underlying Win32 signature; for example, LoadLibrary returns a disposable FreeLibrarySafeHandle, so the call site can use using var instead of a separate FreeLibrary call.

Verification Checklist

  • NativeMethods.txt contains every newly used API/type.
  • No caller remains for the deleted manual method or struct.
  • The generated CsWin32 types are used at call sites.
  • dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64 succeeds.
  • dotnet build src/Files.App/Files.App.csproj -c Debug -p:Platform=x64 has no new error CS* errors.
  • Any remaining build failure is called out explicitly, especially XAML compiler failures unrelated to interop.