Skip to content

Commit 6245529

Browse files
0x5bfaCopilot
andauthored
Code Quality: Remove manual interop definitions and add agent instructions for this (#18449)
Signed-off-by: terat <62196528+0x5bfa@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 48e97f2 commit 6245529

18 files changed

Lines changed: 321 additions & 229 deletions

AGENTS.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Repository Instructions
2+
3+
This repository contains the Files Windows desktop app, a WinUI-based file manager for Windows. The codebase includes the main app, reusable controls, storage layers, Win32/CsWin32 interop, packaging support, background/server components, and UI/interaction tests.
4+
5+
## Codebase Overview
6+
7+
```text
8+
/src
9+
├── Files.App // Main WinUI desktop app: startup, DI, views, view models, actions, services, dialogs, styles, assets, strings, and app helpers.
10+
├── Files.App.CsWin32 // CsWin32 source-generated Win32 interop. Add APIs to NativeMethods.txt here.
11+
├── Files.App.Controls // Reusable WinUI controls shared by the app.
12+
├── Files.App.Storage // App-facing storage abstractions and storage implementation pieces.
13+
├── Files.App.BackgroundTasks // Background task project.
14+
├── Files.App.Server // App service/server behavior.
15+
├── Files.App.Launcher // Launch-related entry points.
16+
├── Files.App.OpenDialog // File open dialog-specific app project/folder.
17+
├── Files.App.SaveDialog // File save dialog-specific app project/folder.
18+
├── Files.App (Package) // Packaging-related app project assets.
19+
├── Files.Core.Storage // Lower-level storage primitives that should not depend on the main WinUI app.
20+
├── Files.Core.SourceGenerator // Roslyn source generators used by the solution.
21+
└── Files.Shared // Shared models, helpers, and code used by multiple projects.
22+
```
23+
24+
```text
25+
/tests
26+
├── Files.App.UITests // UI test assets and views.
27+
├── Files.InteractionTests // Interaction tests used by CI automation.
28+
└── Files.App.UnitTests // Placeholder/stale in this checkout; verify project files before assuming unit tests exist here.
29+
```
30+
31+
## When Dealing With Interop Code
32+
33+
When the user asks to convert marshaled interop code into unmarshaled interop, or asks to remove trim-unsafe manual P/Invoke definitions, see [docs/interop-unmarshaled-conversion.md](docs/interop-unmarshaled-conversion.md).
34+
35+
Prefer adding APIs and related generated types to `src/Files.App.CsWin32/NativeMethods.txt`, then update the callees to use CsWin32-generated `Windows.Win32.PInvoke` APIs directly. Do not leave manual `DllImport` definitions in place or replace them with local `LibraryImport` declarations when CsWin32 can generate the API.
36+
37+
## When Building the App
38+
39+
Use `.github/workflows/ci.yml` as the source of truth for building.
40+
For normal local verification, build with MSBuild restore and explicit configuration/platform; packaging is not required.
41+
42+
```powershell
43+
msbuild -restore src\Files.App\Files.App.csproj /p:Configuration=Debug /p:Platform=x64
44+
```
45+
46+
## When Packaging the App
47+
48+
Use `.github/workflows/ci.yml` as the source of truth for packaging. Adjust `Configuration`, `Platform`, and `AppxBundlePlatforms` as needed.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Converting Marshaled Interop to CsWin32 Unmarshaled Interop
2+
3+
This repository is moving trim-unsafe manual interop toward source-generated CsWin32 interop.
4+
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.
5+
6+
Removing Vanara interop usage is also part of this direction since it internally has trim-unsafe interop code.
7+
8+
## Goal
9+
10+
Remove interop code that relies on runtime marshalling, especially declarations using `DllImport`, `ComImport` and the Vanara package.
11+
12+
The target shape is:
13+
14+
- Add the native API, COM interface, enum, or struct name to `src/Files.App.CsWin32/NativeMethods.txt`.
15+
- Use `Windows.Win32.PInvoke` and generated CsWin32 types at the call site.
16+
- Delete the manual declarations and Vanara references.
17+
- Keep `Win32PInvoke` only for definitions that are not yet converted or cannot be generated by CsWin32.
18+
19+
## Workflow
20+
21+
1. Locate manual interop:
22+
23+
```powershell
24+
git grep -n "DllImport\|MarshalAs\|StringBuilder" -- src
25+
git grep -n "Win32PInvoke\." -- src/Files.App
26+
git grep -n "using Vanara\|Vanara\.PInvoke\|Kernel32\.\|Shell32\.\|User32\." -- src/Files.App
27+
```
28+
29+
2. Add the API and related generated types to `src/Files.App.CsWin32/NativeMethods.txt`.
30+
31+
Include both the function and any dependent structs, enums, or COM interfaces that the call site needs. For example:
32+
33+
```text
34+
RmStartSession
35+
RmRegisterResources
36+
RmGetList
37+
RmEndSession
38+
RM_PROCESS_INFO
39+
SHBrowseForFolder
40+
BROWSEINFOW
41+
SHGetPathFromIDList
42+
SHCreateItemFromParsingName
43+
SHCreateStreamOnFileEx
44+
```
45+
46+
3. Build the CsWin32 project or the app project to refresh generated signatures:
47+
48+
```powershell
49+
dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64
50+
```
51+
52+
4. Update callers to use generated APIs directly.
53+
54+
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.
55+
56+
5. Remove the manual definition only after all callers have moved.
57+
58+
Use targeted checks:
59+
60+
```powershell
61+
git grep -n "Win32PInvoke\.RmStartSession\|Win32PInvoke\.SHBrowseForFolder" -- src/Files.App
62+
git grep -n "RM_PROCESS_INFO\|BROWSEINFO" -- src/Files.App
63+
```
64+
65+
6. Build and confirm there are no C# errors.
66+
67+
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.
68+
69+
## Manual Definition Conversion Notes
70+
71+
- `StringBuilder` output buffers should usually become `Span<char>` or fixed `char*` buffers.
72+
- `IntPtr` handles should become `SafeFileHandle`, `SafeHandle`, `HANDLE`, or generated handle structs where practical.
73+
- Some APIs such as `CoCreateInstance` and `SHCreateItemFromParsingName` often require unsafe pointer overloads:
74+
75+
```csharp
76+
void* raw;
77+
Guid iid = SomeInterfaceIid;
78+
HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, &raw);
79+
IntPtr instance = (IntPtr)raw;
80+
```
81+
82+
- Shell APIs that return PIDLs or allocated strings still require explicit lifetime management, but the API declaration should come from CsWin32:
83+
84+
```csharp
85+
var pidl = PInvoke.SHBrowseForFolder(ref browseInfo);
86+
Marshal.FreeCoTaskMem((nint)pidl);
87+
```
88+
89+
- Restart Manager APIs can use generated `RM_PROCESS_INFO` and `Span<char>` session keys instead of local struct definitions.
90+
- 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.
91+
92+
## Vanara Conversion Notes
93+
94+
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.
95+
96+
Example conversion:
97+
98+
```csharp
99+
// Before
100+
var lib = Kernel32.LoadLibrary(file);
101+
StringBuilder result = new(2048);
102+
_ = User32.LoadString(lib, number, result, result.Capacity);
103+
Kernel32.FreeLibrary(lib);
104+
return result.ToString();
105+
106+
// After
107+
using var lib = PInvoke.LoadLibrary(file);
108+
Span<char> result = stackalloc char[2048];
109+
int length = PInvoke.LoadString(lib, (uint)number, result, result.Length);
110+
return result[..length].ToString();
111+
```
112+
113+
Useful heuristics:
114+
115+
- Start with simple `Kernel32.*`, `User32.*`, or `Shell32.*` calls that map directly to one Win32 function.
116+
- Remove `using Vanara.PInvoke` only when no remaining types in the file depend on it.
117+
- Prefer generated `Span<T>` overloads over `StringBuilder` buffers when available.
118+
- Prefer generated handle types and `SafeHandle` overloads where CsWin32 provides them.
119+
- 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.
120+
121+
## Verification Checklist
122+
123+
- `NativeMethods.txt` contains every newly used API/type.
124+
- No caller remains for the deleted manual method or struct.
125+
- The generated CsWin32 types are used at call sites.
126+
- `dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64` succeeds.
127+
- `dotnet build src/Files.App/Files.App.csproj -c Debug -p:Platform=x64` has no new `error CS*` errors.
128+
- Any remaining build failure is called out explicitly, especially XAML compiler failures unrelated to interop.

src/Files.App.CsWin32/Extras.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ namespace UI.WindowsAndMessaging
2222

2323
public static partial class PInvoke
2424
{
25-
[DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)]
26-
static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong);
25+
[LibraryImport("User32", EntryPoint = "SetWindowLongW")]
26+
private static partial int _SetWindowLong(nint hWnd, int nIndex, int dwNewLong);
2727

28-
[DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)]
29-
static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong);
28+
[LibraryImport("User32", EntryPoint = "SetWindowLongPtrW")]
29+
private static partial nint _SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
3030

3131
// NOTE:
3232
// CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa.

src/Files.App.CsWin32/NativeMethods.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,37 @@ WNetAddConnection3
5050
CREDENTIALW
5151
CredWrite
5252
WNetConnectionDialog1
53+
WNetGetConnection
5354
CONNECTDLGSTRUCTW
55+
RmRegisterResources
56+
RmStartSession
57+
RmEndSession
58+
RmGetList
59+
RM_UNIQUE_PROCESS
60+
RM_PROCESS_INFO
61+
CreateEvent
62+
SetEvent
63+
CancelIoEx
64+
WaitForSingleObjectEx
65+
ReadDirectoryChangesW
66+
CreateFileFromAppW
67+
GetFileAttributesExFromApp
68+
SetFileAttributesFromApp
69+
ReadFile
70+
WriteFile
71+
WriteFileEx
72+
GetFileTime
73+
SetFileTime
74+
GetFileInformationByHandleEx
75+
FILE_ID_BOTH_DIR_INFO
76+
FindFirstStreamW
77+
FindNextStreamW
78+
WIN32_FIND_STREAM_DATA
79+
RegisterApplicationRestart
80+
BROWSEINFOW
81+
SHBrowseForFolder
82+
SHGetPathFromIDList
83+
SHCreateStreamOnFileEx
5484
DwmSetWindowAttribute
5585
WIN32_ERROR
5686
CoCreateInstance
@@ -116,9 +146,13 @@ WindowsDeleteString
116146
IPreviewHandler
117147
AssocQueryString
118148
GetModuleHandle
149+
LoadLibrary
150+
FreeLibrary
151+
LoadString
119152
SHEmptyRecycleBin
120153
SHFileOperation
121154
SHGetFolderPath
155+
SHGetKnownFolderPath
122156
SHGFP_TYPE
123157
SHGetKnownFolderItem
124158
SHQUERYRBINFO

src/Files.App/App.xaml.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.UI.Xaml;
88
using Microsoft.UI.Xaml.Controls;
99
using Microsoft.Windows.AppLifecycle;
10+
using Windows.Win32;
1011
using Windows.ApplicationModel;
1112
using Windows.ApplicationModel.DataTransfer;
1213
using Windows.Storage;
@@ -227,9 +228,8 @@ private async void Window_Closed(object sender, WindowEventArgs args)
227228
var results = items.Select(x => x.ItemPath).ToList();
228229
System.IO.File.WriteAllLines(OutputPath, results);
229230

230-
IntPtr eventHandle = Win32PInvoke.CreateEvent(IntPtr.Zero, false, false, "FILEDIALOG");
231-
Win32PInvoke.SetEvent(eventHandle);
232-
Win32PInvoke.CloseHandle(eventHandle);
231+
using var eventHandle = PInvoke.CreateEvent(null, false, false, "FILEDIALOG");
232+
PInvoke.SetEvent(eventHandle);
233233
}
234234

235235
// Continue running the app on the background

src/Files.App/Helpers/Win32/Win32Helper.Process.cs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using Windows.Win32;
5+
using Windows.Win32.Foundation;
6+
using Windows.Win32.System.RestartManager;
7+
48
namespace Files.App.Helpers
59
{
610
/// <summary>
@@ -68,36 +72,37 @@ public static async Task<bool> InvokeWin32ComponentsAsync(IEnumerable<string> ap
6872
/// </remarks>
6973
public static List<Process> WhoIsLocking(string[] resources)
7074
{
71-
string key = Guid.NewGuid().ToString();
75+
Span<char> key = stackalloc char[64];
76+
Guid.NewGuid().TryFormat(key, out var charsWritten);
77+
key = key[..charsWritten];
7278
List<Process> processes = [];
7379

74-
int res = Win32PInvoke.RmStartSession(out uint handle, 0, key);
75-
if (res != 0) throw new Exception("Could not begin restart session. Unable to determine file locker.");
80+
WIN32_ERROR res = PInvoke.RmStartSession(out uint handle, key);
81+
if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not begin restart session. Unable to determine file locker.");
7682

7783
try
7884
{
79-
const int ERROR_MORE_DATA = 234;
8085
uint pnProcInfo = 0;
81-
uint lpdwRebootReasons = Win32PInvoke.RmRebootReasonNone;
86+
uint lpdwRebootReasons;
8287

83-
res = Win32PInvoke.RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null);
88+
res = PInvoke.RmRegisterResources(handle, resources, [], []);
8489

85-
if (res != 0) throw new Exception("Could not register resource.");
90+
if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not register resource.");
8691

8792
// Note:
8893
// There's a race condition here -- the first call to RmGetList() returns the total number of process.
8994
// However, when we call RmGetList() again to get the actual processes this number may have increased.
90-
res = Win32PInvoke.RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons);
95+
res = PInvoke.RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, [], out lpdwRebootReasons);
9196

92-
if (res == ERROR_MORE_DATA)
97+
if (res == WIN32_ERROR.ERROR_MORE_DATA)
9398
{
9499
// Create an array to store the process results
95-
var processInfo = new Win32PInvoke.RM_PROCESS_INFO[pnProcInfoNeeded];
100+
var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
96101
pnProcInfo = pnProcInfoNeeded;
97102

98103
// Get the list
99-
res = Win32PInvoke.RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons);
100-
if (res == 0)
104+
res = PInvoke.RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, out lpdwRebootReasons);
105+
if (res == WIN32_ERROR.NO_ERROR)
101106
{
102107
processes = new List<Process>((int)pnProcInfo);
103108

@@ -107,19 +112,19 @@ public static List<Process> WhoIsLocking(string[] resources)
107112
{
108113
try
109114
{
110-
processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId));
115+
processes.Add(Process.GetProcessById((int)processInfo[i].Process.dwProcessId));
111116
}
112117
// catch the error -- in case the process is no longer running
113118
catch (ArgumentException) { }
114119
}
115120
}
116121
else throw new Exception("Could not list processes locking resource.");
117122
}
118-
else if (res != 0) throw new Exception("Could not list processes locking resource. Failed to get size of result.");
123+
else if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not list processes locking resource. Failed to get size of result.");
119124
}
120125
finally
121126
{
122-
_ = Win32PInvoke.RmEndSession(handle);
127+
_ = PInvoke.RmEndSession(handle);
123128
}
124129

125130
return processes;

src/Files.App/Helpers/Win32/Win32Helper.Shell.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.Runtime.InteropServices;
66
using Vanara.PInvoke;
77
using Vanara.Windows.Shell;
8+
using Windows.Win32;
9+
using Windows.Win32.Foundation;
10+
using Windows.Win32.UI.Shell;
811

912
namespace Files.App.Helpers
1013
{
@@ -78,12 +81,12 @@ public static partial class Win32Helper
7881
}, App.Logger);
7982
}
8083

81-
public static string GetFolderFromKnownFolderGUID(Guid guid)
84+
public static unsafe string GetFolderFromKnownFolderGUID(Guid guid)
8285
{
83-
nint pszPath;
84-
Win32PInvoke.SHGetKnownFolderPath(guid, 0, nint.Zero, out pszPath);
85-
string path = Marshal.PtrToStringUni(pszPath);
86-
Marshal.FreeCoTaskMem(pszPath);
86+
PWSTR pszPath;
87+
PInvoke.SHGetKnownFolderPath(ref guid, (KNOWN_FOLDER_FLAG)0, null, out pszPath);
88+
string path = pszPath.ToString();
89+
Marshal.FreeCoTaskMem((nint)pszPath.Value);
8790

8891
return path;
8992
}

0 commit comments

Comments
 (0)