-
-
Notifications
You must be signed in to change notification settings - Fork 2
chore: migrate P/Invoke declarations to [LibraryImport] (MODERN-002) #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,7 @@ namespace SysManager.Services; | |
| /// • NVIDIA GPU subkey is auto-detected (not hardcoded to 0000). | ||
| /// • Visual effects use SystemParametersInfo (instant), not registry-only. | ||
| /// </summary> | ||
| public sealed class PerformanceService : IDisposable | ||
| public sealed partial class PerformanceService : IDisposable | ||
| { | ||
| private readonly PowerShellRunner _ps; | ||
| private readonly SemaphoreSlim _psGate = new(1, 1); | ||
|
|
@@ -46,15 +46,15 @@ public sealed class PerformanceService : IDisposable | |
| private const uint SPI_SETUIEFFECTS = 0x103F; | ||
| private const uint SPIF_SENDCHANGE = 0x02; | ||
|
|
||
| [DllImport("user32.dll", SetLastError = true)] | ||
| [LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)] | ||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| private static extern bool SystemParametersInfo( | ||
| uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni); | ||
| private static partial bool SystemParametersInfoGet( | ||
| uint uiAction, uint uiParam, ref int pvParam, uint fWinIni); | ||
|
|
||
| [DllImport("user32.dll", SetLastError = true)] | ||
| [LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)] | ||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| private static extern bool SystemParametersInfo( | ||
| uint uiAction, uint uiParam, bool pvParam, uint fWinIni); | ||
| private static partial bool SystemParametersInfoSet( | ||
| uint uiAction, uint uiParam, int pvParam, uint fWinIni); | ||
|
|
||
| public PerformanceService(PowerShellRunner ps) => _ps = ps; | ||
|
|
||
|
|
@@ -266,9 +266,9 @@ public async Task<string> EnsureUltimatePerformancePlanAsync(CancellationToken c | |
| /// <summary>Read whether UI effects are currently enabled.</summary> | ||
| internal static bool GetUiEffectsEnabled() | ||
| { | ||
| bool enabled = true; | ||
| SystemParametersInfo(SPI_GETUIEFFECTS, 0, ref enabled, 0); | ||
| return enabled; | ||
| int enabled = 1; | ||
| SystemParametersInfoGet(SPI_GETUIEFFECTS, 0, ref enabled, 0); | ||
| return enabled != 0; | ||
|
Comment on lines
+269
to
+271
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: When using.NET LibraryImport with SetLastError = true, you should primarily rely on the boolean return value of SystemParametersInfoW to determine if the call succeeded [1][2]. According to the Windows API documentation, SystemParametersInfo returns a nonzero value (TRUE) if the function succeeds and zero (FALSE) if it fails [1][2]. You should check this return value first. If the function returns FALSE, you can then use Marshal.GetLastPInvokeError to retrieve the extended error information [3][4]. This is the correct, modern.NET approach for accessing the last Win32 error code after a P/Invoke call configured with SetLastError = true [3][4]. It is important to note that GetLastError (and by extension, Marshal.GetLastPInvokeError) should only be consulted when the function's return value indicates a failure [5][6]. If the function succeeds, the value returned by GetLastPInvokeError is unreliable and may contain stale data from previous operations [5][6]. Therefore, explicitly checking the boolean return value is the standard and required practice for robust error handling with this API [5][6]. Citations:
🏁 Script executed: # Find and examine the PerformanceService.cs file
find . -name "PerformanceService.cs" -type fRepository: laurentiu021/SystemManager Length of output: 125 🏁 Script executed: # Once we locate it, examine the relevant lines and method signatures
cat -n SysManager/SysManager/Services/PerformanceService.cs | sed -n '250,300p'Repository: laurentiu021/SystemManager Length of output: 2423 🏁 Script executed: # Check the full file size to understand context
wc -l SysManager/SysManager/Services/PerformanceService.csRepository: laurentiu021/SystemManager Length of output: 127 🏁 Script executed: # Search for the SystemParametersInfoGet and SystemParametersInfoSet declarations
rg -n "SystemParametersInfo(Get|Set)" SysManager/SysManager/Services/PerformanceService.csRepository: laurentiu021/SystemManager Length of output: 356 🏁 Script executed: # Look for LibraryImport and SetLastError declarations
rg -n -B 2 "SystemParametersInfo" SysManager/SysManager/Services/PerformanceService.cs | head -50Repository: laurentiu021/SystemManager Length of output: 1283 Check the Both call sites discard the BOOL return. If the GET call fails, Line 271 reports Suggested fix internal static bool GetUiEffectsEnabled()
{
int enabled = 1;
- SystemParametersInfoGet(SPI_GETUIEFFECTS, 0, ref enabled, 0);
+ if (!SystemParametersInfoGet(SPI_GETUIEFFECTS, 0, ref enabled, 0))
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastPInvokeError());
return enabled != 0;
}
@@
public static void SetUiEffects(bool enabled)
{
- SystemParametersInfoSet(SPI_SETUIEFFECTS, 0, enabled ? 1 : 0, SPIF_SENDCHANGE);
+ if (!SystemParametersInfoSet(SPI_SETUIEFFECTS, 0, enabled ? 1 : 0, SPIF_SENDCHANGE))
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastPInvokeError());
Log.Information("System: Visual effects {Action}", enabled ? "enabled" : "reduced");
}Also applies to: 282-283 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -279,7 +279,7 @@ internal static bool GetUiEffectsEnabled() | |
| /// </summary> | ||
| public static void SetUiEffects(bool enabled) | ||
| { | ||
| SystemParametersInfo(SPI_SETUIEFFECTS, 0, enabled, SPIF_SENDCHANGE); | ||
| SystemParametersInfoSet(SPI_SETUIEFFECTS, 0, enabled ? 1 : 0, SPIF_SENDCHANGE); | ||
| Log.Information("System: Visual effects {Action}", enabled ? "enabled" : "reduced"); | ||
| } | ||
|
|
||
|
|
@@ -531,9 +531,9 @@ public async Task<bool> CreateRestorePointAsync(string description, Cancellation | |
| // RAM WORKING SET TRIM — via EmptyWorkingSet (instant) | ||
| // ═══════════════════════════════════════════════════════════════ | ||
|
|
||
| [DllImport("psapi.dll", SetLastError = true)] | ||
| [LibraryImport("psapi.dll", SetLastError = true)] | ||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| private static extern bool EmptyWorkingSet(IntPtr hProcess); | ||
| private static partial bool EmptyWorkingSet(IntPtr hProcess); | ||
|
|
||
| /// <summary> | ||
| /// Trim the working set of all accessible processes, freeing physical | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Does C# [LibraryImport] support automatic HRESULT-to-exception translation or a PreserveSig=false equivalent for Win32 APIs like SHGetKnownFolderPath? What is the recommended LibraryImport signature for SHGetKnownFolderPath when the native function returns HRESULT and allocates a PWSTR that must be freed by the caller?💡 Result:
Yes. When you use source-generated P/Invoke via LibraryImport (LibraryImportAttribute),.NET performs the same kind of HRESULT-to-exception translation that DllImportAttribute does when PreserveSig=false. In that mode, failing HRESULTs are automatically converted into exceptions, and successful S_OK (0) is treated as “no exception”; the managed signature becomes void (or the type of the last [out, retval] parameter) rather than returning the HRESULT directly. [1][2][3] For SHGetKnownFolderPath, the native signature is: HRESULT SHGetKnownFolderPath(..., PWSTR ppszPath); The output string is allocated by the callee and must always be freed by the caller with CoTaskMemFree once you’re done (even on failure). [4] Recommended LibraryImport signature (HRESULT-returning native, PWSTR allocated by callee freed by caller): [LibraryImport("shell32.dll", StringMarshalling = StringMarshalling.Utf16)] internal static partial int SHGetKnownFolderPath( ref readonly Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr ppszPath); Then, call it and translate HRESULT yourself (because you will typically keep the HRESULT return type as int). Example pattern: - declare IntPtr p; - int hr = SHGetKnownFolderPath(ref knownFolderId, flags, IntPtr.Zero, out p); - if (p!= IntPtr.Zero) { string path = Marshal.PtrToStringUni(p)!; Marshal.FreeCoTaskMem(p); } If you want automatic exception translation, you need the LibraryImport signature to be “PreserveSig=false equivalent” (i.e., have.NET treat the HRESULT as not directly returned). Microsoft documents that setting PreserveSig=false causes the managed signature to become void or the type of the last [out, retval] parameter, and failing HRESULTs throw automatically. [1][3] However, for SHGetKnownFolderPath specifically, because the last parameter is a pointer-out (PWSTR), there is no built-in way described in the cited docs to both (a) keep a success/failure automatic exception translation and (b) return the allocated pointer safely without also relying on correct source-generator handling of the parameter as the “retval”/translated return. The most robust, recommended approach grounded in the official allocation/freeing contract is therefore: keep an int HRESULT return and explicitly free the returned PWSTR with CoTaskMemFree/Marshal.FreeCoTaskMem. [4]
Citations:
🏁 Script executed:
Repository: laurentiu021/SystemManager
Length of output: 121
🏁 Script executed:
Repository: laurentiu021/SystemManager
Length of output: 3948
Preserve the HRESULT from
SHGetKnownFolderPathfor explicit error handling.The current declaration with
StringMarshalling = StringMarshalling.Utf16andvoidreturn type relies on automatic HRESULT-to-exception translation. However, this combination is not reliably specified forLibraryImportwith out-pointer parameters likePWSTR*. The native function allocates memory that must be freed withCoTaskMemFree; usingout stringobscures this contract. Return theintHRESULT, explicitly check it withMarshal.ThrowExceptionForHR(), and manually free the allocated pointer to ensure correct fallback behavior and memory safety.Suggested fix
private static string GetPath(Guid folderId) { try { - SHGetKnownFolderPath(folderId, 0, nint.Zero, out var path); - return path; + var hr = SHGetKnownFolderPath(folderId, 0, nint.Zero, out var pathPtr); + if (hr < 0) + Marshal.ThrowExceptionForHR(hr); + + try + { + return Marshal.PtrToStringUni(pathPtr) ?? string.Empty; + } + finally + { + Marshal.FreeCoTaskMem(pathPtr); + } } catch (COMException) {🤖 Prompt for AI Agents