[AOT][CmdPal] Enable NativeAOT Compatibility for CmdPal Apps Extension (#39678)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces necessary changes to make the CmdPal.Apps extension
compatible with NativeAOT publishing. The main updates include:

1. Project configuration updates: Added NativeAOT-related properties to
the .csproj.

2. Native interop adjustments:

> - Introduced NativeMethods.json and used
[CsWin32](https://github.com/microsoft/cswin32) to generate P/Invoke
bindings.
> - Replaced some DllImport declarations with source-generated
[LibraryImport] for improved AOT support.
This commit is contained in:
leileizhang
2025-06-04 19:30:11 +08:00
committed by GitHub
parent ddbb6161e3
commit 79958975b4
26 changed files with 477 additions and 700 deletions

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
public static class ComFreeHelper
{
internal static unsafe string GetStringAndFree(HRESULT hr, PWSTR ptr)
{
hr.ThrowOnFailure();
try
{
return ptr.ToString();
}
finally
{
PInvoke.CoTaskMemFree(ptr);
}
}
public static unsafe void ComObjectRelease<T>(T* comPtr)
where T : unmanaged
{
if (comPtr != null)
{
((IUnknown*)comPtr)->Release();
}
}
}

View File

@@ -1,157 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")]
public sealed class Native
{
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, nint ppvReserved);
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string path, nint pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern HRESULT SHCreateStreamOnFileEx(string fileName, STGM grfMode, uint attributes, bool create, System.Runtime.InteropServices.ComTypes.IStream reserved, out System.Runtime.InteropServices.ComTypes.IStream stream);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern HRESULT SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, nint ppvReserved);
public enum HRESULT : uint
{
/// <summary>
/// Operation successful.
/// </summary>
S_OK = 0x00000000,
/// <summary>
/// Operation successful. (negative condition/no operation)
/// </summary>
S_FALSE = 0x00000001,
/// <summary>
/// Not implemented.
/// </summary>
E_NOTIMPL = 0x80004001,
/// <summary>
/// No such interface supported.
/// </summary>
E_NOINTERFACE = 0x80004002,
/// <summary>
/// Pointer that is not valid.
/// </summary>
E_POINTER = 0x80004003,
/// <summary>
/// Operation aborted.
/// </summary>
E_ABORT = 0x80004004,
/// <summary>
/// Unspecified failure.
/// </summary>
E_FAIL = 0x80004005,
/// <summary>
/// Unexpected failure.
/// </summary>
E_UNEXPECTED = 0x8000FFFF,
/// <summary>
/// General access denied error.
/// </summary>
E_ACCESSDENIED = 0x80070005,
/// <summary>
/// Handle that is not valid.
/// </summary>
E_HANDLE = 0x80070006,
/// <summary>
/// Failed to allocate necessary memory.
/// </summary>
E_OUTOFMEMORY = 0x8007000E,
/// <summary>
/// One or more arguments are not valid.
/// </summary>
E_INVALIDARG = 0x80070057,
/// <summary>
/// The operation was canceled by the user. (Error source 7 means Win32.)
/// </summary>
/// <SeeAlso href="https://learn.microsoft.com/windows/win32/debug/system-error-codes--1000-1299-"/>
/// <SeeAlso href="https://en.wikipedia.org/wiki/HRESULT"/>
E_CANCELLED = 0x800704C7,
}
public static class ShellItemTypeConstants
{
/// <summary>
/// Guid for type IShellItem.
/// </summary>
public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe");
/// <summary>
/// Guid for type IShellItem2.
/// </summary>
public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93");
}
/// <summary>
/// The following are ShellItem DisplayName types.
/// </summary>
[Flags]
public enum SIGDN : uint
{
NORMALDISPLAY = 0,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000,
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
public interface IShellItem
{
void BindToHandler(
nint pbc,
[MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
out nint ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
/// <summary>
/// <see href="https://learn.microsoft.com/windows/win32/stg/stgm-constants">see all STGM values</see>
/// </summary>
[Flags]
public enum STGM : long
{
READ = 0x00000000L,
WRITE = 0x00000001L,
READWRITE = 0x00000002L,
CREATE = 0x00001000L,
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
public partial class SafeComHandle : SafeHandle
{
public SafeComHandle()
: base(IntPtr.Zero, ownsHandle: true)
{
}
public SafeComHandle(IntPtr handle)
: base(IntPtr.Zero, ownsHandle: true)
{
SetHandle(handle);
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
var count = Marshal.Release(handle);
return true;
}
}

View File

@@ -3,124 +3,17 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using ManagedCommon;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
public class ShellLinkHelper : IShellLinkHelper
{
[Flags]
private enum SLGP_FLAGS
{
SLGP_SHORTPATH = 0x1,
SLGP_UNCPRIORITY = 0x2,
SLGP_RAWPATH = 0x4,
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching COM")]
private struct WIN32_FIND_DATAW
{
public uint dwFileAttributes;
public long ftCreationTime;
public long ftLastAccessTime;
public long ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[Flags]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
public enum SLR_FLAGS
{
SLR_NO_UI = 0x1,
SLR_ANY_MATCH = 0x2,
SLR_UPDATE = 0x4,
SLR_NOUPDATE = 0x8,
SLR_NOSEARCH = 0x10,
SLR_NOTRACK = 0x20,
SLR_NOLINKINFO = 0x40,
SLR_INVOKE_MSI = 0x80,
}
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
// The IShellLink interface allows Shell links to be created, modified, and resolved
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
/// <summary>Retrieves the path and file name of a Shell link object</summary>
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
void GetIDList(out nint ppidl);
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
void SetIDList(nint pidl);
/// <summary>Retrieves the description string for a Shell link object</summary>
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
/// <summary>Sets the name of the working directory for a Shell link object</summary>
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
/// <summary>Sets the command-line arguments for a Shell link object</summary>
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
/// <summary>Retrieves the hot key for a Shell link object</summary>
void GetHotkey(out short pwHotkey);
/// <summary>Sets a hot key for a Shell link object</summary>
void SetHotkey(short wHotkey);
/// <summary>Retrieves the show command for a Shell link object</summary>
void GetShowCmd(out int piShowCmd);
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
void SetShowCmd(int iShowCmd);
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
/// <summary>Sets the relative path to the Shell link object</summary>
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
void Resolve(ref nint hwnd, SLR_FLAGS fFlags);
/// <summary>Sets the path and file name of a Shell link object</summary>
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink
{
}
// Contains the description of the app
public string Description { get; set; } = string.Empty;
@@ -130,60 +23,63 @@ public class ShellLinkHelper : IShellLinkHelper
public bool HasArguments { get; set; }
// Retrieve the target path using Shell Link
public string RetrieveTargetPath(string path)
public unsafe string RetrieveTargetPath(string path)
{
var link = new ShellLink();
const int STGM_READ = 0;
try
{
((IPersistFile)link).Load(path, STGM_READ);
}
catch (System.IO.FileNotFoundException ex)
{
Logger.LogError(ex.Message);
return string.Empty;
}
var hwnd = default(nint);
((IShellLinkW)link).Resolve(ref hwnd, 0);
var target = string.Empty;
const int MAX_PATH = 260;
var buffer = new StringBuilder(MAX_PATH);
IShellLinkW* link = null;
var data = default(WIN32_FIND_DATAW);
((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH);
var target = buffer.ToString();
PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out link).ThrowOnFailure();
using var linkHandle = new SafeComHandle((IntPtr)link);
const int STGMREAD = 0;
IPersistFile* persistFile = null;
Guid iid = typeof(IPersistFile).GUID;
((IUnknown*)link)->QueryInterface(&iid, (void**)&persistFile);
if (persistFile != null)
{
using var persistFileHandle = new SafeComHandle((IntPtr)persistFile);
try
{
persistFile->Load(path, STGMREAD);
}
catch (System.IO.FileNotFoundException)
{
// Log.Exception($"Failed to load {path}, {e.Message}", e, GetType());
return string.Empty;
}
}
var hwnd = HWND.Null;
const uint SLR_NO_UI = 0x1;
link->Resolve(hwnd, SLR_NO_UI);
var buffer = stackalloc char[MAX_PATH];
var hr = link->GetPath((PWSTR)buffer, MAX_PATH, null, 0x1);
target = hr.Succeeded ? new string(buffer) : string.Empty;
// To set the app description
if (!string.IsNullOrEmpty(target))
{
buffer = new StringBuilder(MAX_PATH);
try
{
((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
Description = buffer.ToString();
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
Description = string.Empty;
}
var descBuffer = stackalloc char[MAX_PATH];
var desHr = link->GetDescription(descBuffer, MAX_PATH);
Description = desHr.Succeeded ? new string(descBuffer) : string.Empty;
var argumentBuffer = new StringBuilder(MAX_PATH);
((IShellLinkW)link).GetArguments(argumentBuffer, argumentBuffer.Capacity);
Arguments = argumentBuffer.ToString();
var argsBuffer = stackalloc char[MAX_PATH];
var argHr = link->GetArguments(argsBuffer, MAX_PATH);
Arguments = argHr.Succeeded ? new string(argsBuffer) : string.Empty;
// Set variable to true if the program takes in any arguments
if (argumentBuffer.Length != 0)
if (Arguments.Length != 0)
{
HasArguments = true;
}
}
// To release unmanaged memory
Marshal.ReleaseComObject(link);
return target;
}
}

View File

@@ -4,7 +4,8 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
using Windows.Win32;
using Windows.Win32.UI.Shell;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
@@ -23,7 +24,7 @@ public class ShellLocalization
/// </summary>
/// <param name="path">Path to the shell item (e. g. shortcut 'File Explorer.lnk').</param>
/// <returns>The localized name as string or <see cref="string.Empty"/>.</returns>
public string GetLocalizedName(string path)
public unsafe string GetLocalizedName(string path)
{
var lowerInvariantPath = path.ToLowerInvariant();
@@ -33,18 +34,29 @@ public class ShellLocalization
return value;
}
var shellItemType = ShellItemTypeConstants.ShellItemGuid;
var retCode = SHCreateItemFromParsingName(path, nint.Zero, ref shellItemType, out var shellItem);
if (retCode != 0)
void* shellItemPtrVoid = null;
try
{
var retCode = PInvoke.SHCreateItemFromParsingName(path, null, typeof(IShellItem).GUID, out shellItemPtrVoid).ThrowOnFailure();
using var shellItemHandle = new SafeComHandle((IntPtr)shellItemPtrVoid);
IShellItem* shellItemPtr = (IShellItem*)shellItemPtrVoid;
var hr = shellItemPtr->GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY, out var filenamePtr);
var filename = ComFreeHelper.GetStringAndFree(hr, filenamePtr);
if (filename == null)
{
return string.Empty;
}
_ = _localizationCache.TryAdd(lowerInvariantPath, filename);
return filename;
}
catch (Exception)
{
return string.Empty;
}
shellItem.GetDisplayName(SIGDN.NORMALDISPLAY, out var filename);
_ = _localizationCache.TryAdd(lowerInvariantPath, filename);
return filename;
}
/// <summary>