Last active
August 14, 2025 22:19
-
-
Save PaulDance/413c40d2e05fc1138ca6d23e8f1d7f48 to your computer and use it in GitHub Desktop.
Sample Rust implementation of an IContextMenu-based Windows Shell extension DLL.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Windows Registry Editor Version 5.00 | |
| [HKEY_CLASSES_ROOT\CLSID\{12345678-9ABC-DEF1-2345-6789ABCDEF42}] | |
| @="SampleShellExtension" | |
| "Version"="1.2.3.4" | |
| [HKEY_CLASSES_ROOT\CLSID\{12345678-9ABC-DEF1-2345-6789ABCDEF42}\InprocServer32] | |
| @="C:\\path\\to\\sample-shellext.dll" | |
| "ThreadingModel"="Apartment" | |
| [HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\SampleShellExtensionVerb] | |
| @="{12345678-9ABC-DEF1-2345-6789ABCDEF42}" | |
| [HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\SampleShellExtensionVerb] | |
| @="{12345678-9ABC-DEF1-2345-6789ABCDEF42}" | |
| [HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\SampleShellExtensionVerb] | |
| @="{12345678-9ABC-DEF1-2345-6789ABCDEF42}" | |
| [HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\SampleShellExtensionVerb] | |
| @="{12345678-9ABC-DEF1-2345-6789ABCDEF42}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| fn main() { | |
| embed_resource::compile( | |
| "resources.rc", | |
| embed_resource::NONE, | |
| ) | |
| .manifest_required() | |
| .unwrap(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [package] | |
| name = "sample-shellext" | |
| description = "Sample IContextMenu-based Windows Shell extension" | |
| version = "0.1.0" | |
| edition = "2024" | |
| rust-version = "1.89.0" | |
| [lib] | |
| crate-type = ["cdylib"] | |
| [target.'cfg(windows)'.dependencies] | |
| log = "0.4.27" | |
| parking_lot = "0.12.4" | |
| windows = { version = "0.61.3", features = [ | |
| "Win32", | |
| "Win32_Graphics", | |
| "Win32_Graphics_Gdi", | |
| "Win32_Graphics_GdiPlus", | |
| "Win32_System", | |
| "Win32_System_Com", | |
| "Win32_System_Com_StructuredStorage", | |
| "Win32_System_Memory", | |
| "Win32_System_Ole", | |
| "Win32_System_Registry", | |
| "Win32_System_SystemServices", | |
| "Win32_UI", | |
| "Win32_UI_Shell", | |
| "Win32_UI_Shell_Common", | |
| "Win32_UI_WindowsAndMessaging", | |
| ] } | |
| windows-core = "0.61.2" | |
| [target.'cfg(windows)'.build-dependencies] | |
| embed-resource = "3.0.5" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! Utilities to manipulate resource icons and bitmaps. | |
| use std::collections::HashMap; | |
| use std::mem::{self, MaybeUninit}; | |
| use std::ptr; | |
| use std::sync::LazyLock; | |
| use parking_lot::RwLock; | |
| use windows::Win32::Foundation::{E_FAIL, HINSTANCE}; | |
| use windows::Win32::Graphics::Gdi::HBITMAP; | |
| use windows::Win32::Graphics::GdiPlus::{ | |
| Color as GpColor, GdipCreateBitmapFromHICON, GdipCreateHBITMAPFromBitmap, GdiplusShutdown, | |
| GdiplusStartup, GdiplusStartupInput, GpBitmap, Status as GpStatus, | |
| }; | |
| use windows::Win32::UI::WindowsAndMessaging::{ | |
| GetSystemMetrics, HICON, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW, SM_CXSMICON, SM_CYSMICON, | |
| }; | |
| use windows::core::{Owned, PCWSTR, Result as WinResult}; | |
| /// Global cache of icon [`PCWSTR`] names to [`HBITMAP`]s in raw values. | |
| static ICON_TO_BITMAP_CACHE: LazyLock<RwLock<HashMap<usize, usize>>> = | |
| LazyLock::new(Default::default); | |
| /// Loads the given `icon_name` from `dll` and converts it to a bitmap. | |
| pub fn resource_icon_to_bitmap(dll: HINSTANCE, icon_name: PCWSTR) -> WinResult<HBITMAP> { | |
| if let Some(&bmp_addr) = ICON_TO_BITMAP_CACHE | |
| .read() | |
| .get(&icon_name.as_ptr().expose_provenance()) | |
| { | |
| Ok(HBITMAP(ptr::with_exposed_provenance_mut(bmp_addr))) | |
| } else { | |
| let icon = load_small_icon(dll, icon_name).inspect_err(|err| { | |
| log::error!( | |
| "Failed to load icon {:?} from DLL {:?}: {:?}", | |
| icon_name, | |
| dll, | |
| err, | |
| ); | |
| })?; | |
| let icon_bmp = icon_to_bitmap(*icon).map_err(|err| { | |
| log::error!("Failed to convert the icon to a bitmap: {:?}", err); | |
| E_FAIL | |
| })?; | |
| ICON_TO_BITMAP_CACHE.write().insert( | |
| icon_name.as_ptr().expose_provenance(), | |
| icon_bmp.0.expose_provenance(), | |
| ); | |
| Ok(icon_bmp) | |
| } | |
| } | |
| /// Loads the given `icon_name` from `dll` in small size. | |
| /// | |
| /// `icon_name` can be an actual string pointer or a special value constructed | |
| /// from the `MAKEINTRESOURCE` macro. The handle is returned in an owned | |
| /// fashion for immediate [`Drop`] compatibility. | |
| fn load_small_icon(dll: HINSTANCE, icon_name: PCWSTR) -> WinResult<Owned<HICON>> { | |
| // `LoadIconWithScaleDown` is basically not available to us due to: | |
| // https://developercommunity.visualstudio.com/t/LoadIconWithScaleDown-not-in-the-default/10646099?sort=newest&topics=Known+Issue+in%3A+Visual+Studio+2017+Version+15.5 | |
| // SAFETY: always safe to call supposing the arguments are valid. | |
| unsafe { | |
| LoadImageW( | |
| Some(dll), | |
| icon_name, | |
| IMAGE_ICON, | |
| GetSystemMetrics(SM_CXSMICON), | |
| GetSystemMetrics(SM_CYSMICON), | |
| LR_DEFAULTCOLOR, | |
| ) | |
| } | |
| // SAFETY: the handle has just been created, so is owned by us. | |
| .map(|h| unsafe { Owned::new(HICON(h.0)) }) | |
| } | |
| /// Converts the given icon to a bitmap. | |
| /// | |
| /// Currently implemented using the GDI+ library because the regular GDI did | |
| /// not yield good results: the icon was very badly rendered. The handle is | |
| /// returned directly in a raw fashion so its ownership may be passed onto the | |
| /// system without releasing the resources by mistake. | |
| fn icon_to_bitmap(icon: HICON) -> GpResult<HBITMAP> { | |
| let gp_token = | |
| GpToken::new().inspect_err(|err| log::error!("Failed to initialize GDI+: {:?}", err))?; | |
| let mut gp_bmp: MaybeUninit<*mut GpBitmap> = MaybeUninit::uninit(); | |
| // SAFETY: both pointers are valid, respectively for reading and for writing. | |
| gp_status_ok(unsafe { GdipCreateBitmapFromHICON(icon, gp_bmp.as_mut_ptr()) }) | |
| .inspect_err(|err| log::error!("GdipCreateBitmapFromHICON failed: {:?}", err))?; | |
| // SAFETY: errors are checked for, so the pointer is valid at this point. | |
| let gp_bmp = unsafe { gp_bmp.assume_init() }; | |
| // `GpBitmap` does not have a destructor. | |
| let mut bmp: MaybeUninit<HBITMAP> = MaybeUninit::uninit(); | |
| // SAFETY: | |
| // * the GDI+ bitmap pointer comes from the API; | |
| // * the GDI bitmap pointer is valid for writing; | |
| gp_status_ok(unsafe { | |
| GdipCreateHBITMAPFromBitmap( | |
| gp_bmp, | |
| bmp.as_mut_ptr(), | |
| GpColor::Transparent.cast_unsigned(), | |
| ) | |
| }) | |
| .inspect_err(|err| log::error!("GdipCreateHBITMAPFromBitmap failed: {:?}", err))?; | |
| // SAFETY: errors are checked for, so the pointer is valid at this point. | |
| let bmp = unsafe { bmp.assume_init() }; | |
| // This value is returned, so does not need releasing. | |
| // Explicit drop to show the role of the token guard. | |
| mem::drop(gp_token); | |
| Ok(bmp) | |
| } | |
| /// RAII wrapper around a GDI+ session token with an adequate [`Drop`]. | |
| #[repr(transparent)] | |
| struct GpToken(usize); | |
| impl GpToken { | |
| /// Initializes a GDI+ session by calling [`GdiplusStartup`] with default values. | |
| pub fn new() -> GpResult<Self> { | |
| let input = GdiplusStartupInput { | |
| // > Must be 1. | |
| GdiplusVersion: 1, | |
| // Not useful here. | |
| // > The default value is NULL. | |
| DebugEventCallback: 0, | |
| // For easier API interaction: | |
| // > If you don't want to be responsible for calling the hook and | |
| // > unhook functions, then set this member to `FALSE`. | |
| SuppressBackgroundThread: false.into(), | |
| // For extra safety, although: | |
| // > GDI+ version 1.0 doesn't support external image codecs, so | |
| // > this field is ignored. | |
| SuppressExternalCodecs: true.into(), | |
| }; | |
| let mut token: MaybeUninit<usize> = MaybeUninit::uninit(); | |
| // SAFETY: | |
| // * the token pointer is valid for writing; | |
| // * the input pointer is valid for reading; | |
| // * the output pointer can be null because `SuppressBackgroundThread` | |
| // is set to `FALSE`, as per the documentation; | |
| gp_status_ok(unsafe { | |
| GdiplusStartup(token.as_mut_ptr(), &raw const input, ptr::null_mut()) | |
| }) | |
| .inspect_err(|err| log::error!("GdiplusStartup failed: {:?}", err))?; | |
| // SAFETY: errors are checked for, so the pointer is valid at this point. | |
| Ok(Self(unsafe { token.assume_init() })) | |
| } | |
| } | |
| /// Calls [`GdiplusShutdown`] with the stored token value. | |
| impl Drop for GpToken { | |
| fn drop(&mut self) { | |
| // SAFETY: the passed value comes from the API. | |
| unsafe { GdiplusShutdown(self.0) }; | |
| } | |
| } | |
| /// Shortcut to [`Result`] with a GDI+ error type set. | |
| type GpResult<T> = Result<T, GpStatus>; | |
| /// Maps GDI+ statuses to [`GpResult`]s. | |
| #[inline] | |
| fn gp_status_ok(status: GpStatus) -> GpResult<()> { | |
| if status.0 == 0 { Ok(()) } else { Err(status) } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #![cfg(windows)] | |
| use std::ffi::c_void; | |
| use std::ptr; | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | |
| use windows::Win32::Foundation::{ | |
| CLASS_E_CLASSNOTAVAILABLE, E_NOINTERFACE, E_POINTER, HINSTANCE, S_FALSE, S_OK, | |
| }; | |
| use windows::Win32::System::Com::IClassFactory; | |
| use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; | |
| use windows::core::{BOOL, GUID, HRESULT, Interface}; | |
| mod icon; | |
| mod shellext; | |
| use shellext::{INSTANCE_COUNT, ShellExtension, ShellextClassFactory}; | |
| static DLL_HMODULE: AtomicUsize = AtomicUsize::new(0); | |
| #[unsafe(no_mangle)] | |
| extern "system" fn DllMain(dll: HINSTANCE, reason: u32, _reserved: *const c_void) -> BOOL { | |
| log::debug!("@DllMain => dll: {:?}, reason: {}", dll, reason); | |
| if reason == DLL_PROCESS_ATTACH { | |
| DLL_HMODULE.store(dll.0.expose_provenance(), Ordering::SeqCst); | |
| } | |
| true.into() | |
| } | |
| #[unsafe(no_mangle)] | |
| extern "system" fn DllCanUnloadNow() -> HRESULT { | |
| log::debug!("@DllCanUnloadNow"); | |
| if INSTANCE_COUNT.load(Ordering::SeqCst) == 0 { | |
| S_OK | |
| } else { | |
| S_FALSE | |
| } | |
| } | |
| #[unsafe(no_mangle)] | |
| extern "system" fn DllGetClassObject( | |
| cls_id: *const GUID, | |
| iface_id: *const GUID, | |
| obj_out: *mut *mut c_void, | |
| ) -> HRESULT { | |
| log::debug!( | |
| "@DllGetClassObject => cls_id: {:?}, iface_id: {:?}, obj_out: {:?}", | |
| cls_id, | |
| iface_id, | |
| obj_out, | |
| ); | |
| let obj_out = if obj_out.is_null() { | |
| return E_POINTER; | |
| } else { | |
| // SAFETY: the pointer was just checked. | |
| unsafe { &mut *obj_out } | |
| }; | |
| // > If an error occurs, the interface pointer is `NULL`. | |
| *obj_out = ptr::null_mut(); | |
| let cls_id = if cls_id.is_null() { | |
| return E_POINTER; | |
| } else { | |
| // SAFETY: the pointer was just checked. | |
| unsafe { *cls_id } | |
| }; | |
| // Ensure `cls_id` matches the expected `CLSID` value. | |
| if cls_id != ShellExtension::CLS_ID { | |
| // > The DLL does not support the class (object definition). | |
| return CLASS_E_CLASSNOTAVAILABLE; | |
| } | |
| let iface_id = if iface_id.is_null() { | |
| return E_POINTER; | |
| } else { | |
| // SAFETY: the pointer was just checked. | |
| unsafe { *iface_id } | |
| }; | |
| // Ensure `iface_id` matches the expected `IID_IClassFactory` value. | |
| if iface_id != IClassFactory::IID { | |
| return E_NOINTERFACE; | |
| } | |
| // Finally, create and return the desired factory. | |
| *obj_out = IClassFactory::from(ShellextClassFactory {}).into_raw(); | |
| S_OK | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- See https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> | |
| <assembly | |
| manifestVersion="1.0" | |
| xmlns="urn:schemas-microsoft-com:asm.v1" | |
| xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" | |
| > | |
| <assemblyIdentity | |
| type="win32" | |
| name="WhateverInc.SampleShellext" | |
| version="1.2.3.4" | |
| /> | |
| <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> | |
| <application> | |
| <!-- Windows 7 --> | |
| <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> | |
| <!-- Windows 8 --> | |
| <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> | |
| <!-- Windows 8.1 --> | |
| <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> | |
| <!-- Windows 10 and 11 --> | |
| <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> | |
| </application> | |
| </compatibility> | |
| </assembly> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // In order to embed an icon in the assembly. | |
| #include <winuser.h> | |
| #include <verrsrc.h> | |
| 1 RT_MANIFEST "manifest.xml" | |
| IDI_MYAPP_ICON ICON "whatever.ico" | |
| IDI_MYAPP_ICON_SMALL ICON "whatever.ico" | |
| VS_VERSION_INFO VERSIONINFO | |
| FILEVERSION 1,2,3,4 | |
| PRODUCTVERSION 1,2,3,4 | |
| FILEFLAGSMASK VS_FFI_FILEFLAGSMASK | |
| FILEFLAGS 0 | |
| FILEOS VOS__WINDOWS32 | |
| FILETYPE VFT_APP | |
| FILESUBTYPE VFT2_UNKNOWN | |
| BEGIN | |
| BLOCK "StringFileInfo" | |
| BEGIN | |
| // US English UTF-16. | |
| BLOCK "040904B0" | |
| BEGIN | |
| VALUE "CompanyName", "WhateverInc" | |
| VALUE "FileDescription", "Sample Shell extension" | |
| VALUE "FileVersion", 1.2.3.4 | |
| VALUE "InternalName", "sample-shellext.dll" | |
| VALUE "OriginalFilename", "sample-shellext.dll" | |
| VALUE "ProductName", "SampleShellext" | |
| VALUE "ProductVersion", 1.2.3.4 | |
| VALUE "LegalCopyright", "Hello World" | |
| END | |
| END | |
| BLOCK "VarFileInfo" | |
| BEGIN | |
| VALUE "Translation", 0x0409, 0x04b0 | |
| END | |
| END |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! [`IContextMenu`]-based Shell extension core. | |
| //! | |
| //! Exports [`ShellExtension`] that implements the expected COM interfaces. | |
| //! [`execute_command`] can be seen as the entrypoint run on every path. | |
| use std::cell::RefCell; | |
| use std::ffi::{CStr, OsString, c_void}; | |
| use std::mem::{ManuallyDrop, MaybeUninit}; | |
| use std::os::windows::ffi::OsStringExt; | |
| use std::path::{Path, PathBuf}; | |
| use std::sync::atomic::{AtomicU32, Ordering}; | |
| use std::{fmt, mem, ptr}; | |
| use windows::Win32::Foundation::{ | |
| CLASS_E_NOAGGREGATION, E_FAIL, E_NOINTERFACE, E_NOTIMPL, E_POINTER, HINSTANCE, S_OK, | |
| SEVERITY_SUCCESS, | |
| }; | |
| use windows::Win32::System::Com::{ | |
| DVASPECT_CONTENT, FORMATETC, IClassFactory, IClassFactory_Impl, IDataObject, TYMED_HGLOBAL, | |
| }; | |
| use windows::Win32::System::Memory::{GlobalLock, GlobalUnlock}; | |
| use windows::Win32::System::Ole::CF_HDROP; | |
| use windows::Win32::System::Registry::HKEY; | |
| use windows::Win32::UI::Shell::Common::{ITEMIDLIST, STRRET, STRRET_WSTR}; | |
| use windows::Win32::UI::Shell::{ | |
| CMF_DEFAULTONLY, CMINVOKECOMMANDINFO, DROPFILES, GCS_HELPTEXTA, GCS_HELPTEXTW, GCS_VALIDATEA, | |
| GCS_VALIDATEW, GCS_VERBA, GCS_VERBW, IContextMenu, IContextMenu_Impl, IShellExtInit, | |
| IShellExtInit_Impl, SHGDN_FORPARSING, SHGDN_NORMAL, SHGDNF, SHGetDesktopFolder, StrRetToStrW, | |
| }; | |
| use windows::Win32::UI::WindowsAndMessaging::{ | |
| HMENU, InsertMenuItemW, MENUITEMINFOW, MFS_ENABLED, MIIM_BITMAP, MIIM_ID, MIIM_STATE, | |
| MIIM_STRING, | |
| }; | |
| use windows::core::{ | |
| BOOL, GUID, HRESULT, IUnknown, Interface, Owned, PCWSTR, PSTR, PWSTR, Ref as WinRef, | |
| Result as WinResult, implement, | |
| }; | |
| use crate::{DLL_HMODULE, icon}; | |
| pub(crate) static INSTANCE_COUNT: AtomicU32 = AtomicU32::new(0); | |
| #[implement(IShellExtInit, IContextMenu)] | |
| pub struct ShellExtension { | |
| /// List of paths set in [`<ShellExtension_Impl as IShellExtInit_Impl>::Initialize`]. | |
| item_paths: RefCell<Vec<PathBuf>>, | |
| } | |
| impl ShellExtension { | |
| pub const CLS_ID: GUID = GUID::from_u128(0x12345678_9abc_def1_2345_6789abcdef42); | |
| } | |
| impl Default for ShellExtension { | |
| fn default() -> Self { | |
| log::trace!("@ShellExtension::default"); | |
| INSTANCE_COUNT.fetch_add(1, Ordering::SeqCst); | |
| Self { | |
| item_paths: Default::default(), | |
| } | |
| } | |
| } | |
| impl Drop for ShellExtension { | |
| fn drop(&mut self) { | |
| log::trace!("@ShellExtension::drop"); | |
| INSTANCE_COUNT.fetch_sub(1, Ordering::SeqCst); | |
| } | |
| } | |
| /// Required for [`IContextMenu`]. | |
| impl IShellExtInit_Impl for ShellExtension_Impl { | |
| fn Initialize( | |
| &self, | |
| folder_idl: *const ITEMIDLIST, | |
| data_obj: WinRef<'_, IDataObject>, | |
| prog_id: HKEY, | |
| ) -> WinResult<()> { | |
| log::debug!( | |
| "@IShellExtInit_Impl::Initialize => folder_idl: {:?}, data_obj: {:?}, prog_id: {:?}", | |
| folder_idl, | |
| &*data_obj, | |
| prog_id, | |
| ); | |
| // > For shortcut menu extensions, `pdtobj` identifies the selected | |
| // > file objects, `hkeyProgID` identifies the file type of the object | |
| // > with focus, and `pidlFolder` is either `NULL` (for file objects) | |
| // > or specifies the folder for which the shortcut menu is being | |
| // > requested (for folder background shortcut menus). | |
| let paths = if let Some(data_obj) = &*data_obj { | |
| log::debug!("Receiving file paths from IShellExtInit."); | |
| let fmt = FORMATETC { | |
| cfFormat: CF_HDROP.0, | |
| dwAspect: DVASPECT_CONTENT.0, | |
| // > The most common value is -1, which identifies all of the data. | |
| lindex: -1, | |
| tymed: TYMED_HGLOBAL.0.cast_unsigned(), | |
| // > A NULL value is used whenever the specified data | |
| // > format is independent of the target device or when the | |
| // > caller doesn't care what device is used. | |
| ptd: ptr::null_mut(), | |
| }; | |
| // SAFETY: the format value is valid. | |
| let storage = unsafe { data_obj.GetData(&raw const fmt) } | |
| .inspect_err(|err| log::error!("IDataObject::GetData failed: {:?}", err))?; | |
| let _storage_rel = ManuallyDrop::into_inner(storage.pUnkForRelease); | |
| if storage.tymed != TYMED_HGLOBAL.0.cast_unsigned() { | |
| log::error!("Received tymed is not HGLOBAL: {:?}", storage.tymed); | |
| return E_FAIL.ok(); | |
| } | |
| // SAFETY: | |
| // * the variant is checked just above, so the union value is correct; | |
| // * the global is returned to us, so we have ownership of it; | |
| let global = unsafe { Owned::new(storage.u.hGlobal) }; | |
| if global.is_invalid() { | |
| log::error!("Received global is null."); | |
| return E_POINTER.ok(); | |
| } | |
| // SAFETY: the passed pointer cannot be null at this point. | |
| let lock = unsafe { GlobalLock(*global) }; | |
| if lock.is_null() { | |
| log::error!("Received global lock pointer is null."); | |
| return E_POINTER.ok(); | |
| }; | |
| // SAFETY: | |
| // * the pointer cannot be null at this point; | |
| // * `CF_HDROP` is requested, so the cast is valid; | |
| let files = unsafe { &*lock.cast_const().cast::<DROPFILES>() }; | |
| // SAFETY: the value is received from the API, so must be valid. | |
| let files_list = unsafe { dropfiles_to_paths(files) }; | |
| // SAFETY: | |
| // * the pointer is still valid; | |
| // * a lock is still held at this point; | |
| unsafe { GlobalUnlock(*global) } | |
| .inspect_err(|err| log::error!("GlobalUnlock failed: {:?}", err))?; | |
| files_list | |
| } else { | |
| log::debug!("Receiving a folder path from IShellExtInit."); | |
| if folder_idl.is_null() { | |
| log::error!("Folder path is null as well."); | |
| return E_POINTER.ok(); | |
| } | |
| vec![itemidlist_to_path(folder_idl).inspect_err(|err| { | |
| log::error!("Failed to convert the ITEMIDLIST to a path: {:?}", err); | |
| })?] | |
| }; | |
| log::debug!("Files list received from IShellExtInit: {:#?}", paths); | |
| self.this.item_paths.replace(paths); | |
| Ok(()) | |
| } | |
| } | |
| /// Extracts paths from the given files container. | |
| /// | |
| /// For use with [`IContextMenu`]. | |
| /// See <https://learn.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop>. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The passed value must have a valid files offset pointing to a string list. | |
| unsafe fn dropfiles_to_paths(files: &DROPFILES) -> Vec<PathBuf> { | |
| let mut res = Vec::new(); | |
| let is_wide = files.fWide.as_bool(); | |
| log::trace!("DROPFILES is wide encoded: {}.", is_wide); | |
| // SAFETY: `pFiles` is the offset to the string data. | |
| let mut str_ptr = unsafe { | |
| ptr::from_ref(files) | |
| .cast::<u8>() | |
| .add((&raw const files.pFiles).read_unaligned() as _) | |
| }; | |
| #[expect( | |
| clippy::cast_ptr_alignment, | |
| reason = "Only for wide; safety upheld by caller." | |
| )] | |
| // SAFETY: | |
| // * the validity of the offset is upheld by the caller; | |
| // * the read is performed with the right type depending on `is_wide`; | |
| while is_wide && unsafe { str_ptr.cast::<u16>().read() != 0 } || unsafe { str_ptr.read() } != 0 | |
| { | |
| log::trace!("str_ptr: {:?}", str_ptr); | |
| let (bytes_shift, path) = if is_wide { | |
| let s = PCWSTR(str_ptr.cast::<u16>()); | |
| // SAFETY: the pointer is valid at this point. | |
| ( | |
| 2 * (unsafe { s.len() } + 1), | |
| PathBuf::from(OsString::from_wide(unsafe { s.as_wide() })), | |
| ) | |
| } else { | |
| // SAFETY: the pointer is valid at this point. | |
| let s = unsafe { CStr::from_ptr(str_ptr.cast()) }; | |
| ( | |
| s.count_bytes() + 1, | |
| PathBuf::from(s.to_string_lossy().into_owned()), | |
| ) | |
| }; | |
| log::trace!("path: {}", path.display()); | |
| res.push(path); | |
| log::trace!("bytes_shift: {}", bytes_shift); | |
| // SAFETY: the strings are concatenated. | |
| str_ptr = unsafe { str_ptr.add(bytes_shift) }; | |
| } | |
| res | |
| } | |
| /// Extracts paths from the given item ID list. | |
| /// | |
| /// For use with [`IContextMenu`]. | |
| /// See <https://learn.microsoft.com/en-us/windows/win32/shell/namespace-intro#item-id-lists>. | |
| fn itemidlist_to_path(item_list: *const ITEMIDLIST) -> WinResult<PathBuf> { | |
| // SAFETY: always safe to call. | |
| let shell_folder = unsafe { SHGetDesktopFolder() } | |
| .inspect_err(|err| log::error!("SHGetDesktopFolder failed: {:?}", err))?; | |
| let mut name = STRRET { | |
| uType: STRRET_WSTR.0.cast_unsigned(), | |
| ..Default::default() | |
| }; | |
| // SAFETY: the name pointer is valid. | |
| unsafe { | |
| shell_folder.GetDisplayNameOf( | |
| item_list, | |
| SHGDNF(SHGDN_NORMAL.0 | SHGDN_FORPARSING.0), | |
| &raw mut name, | |
| ) | |
| } | |
| .inspect_err(|err| log::error!("IShellFolder::GetDisplayNameOf failed: {:?}", err))?; | |
| let mut path = MaybeUninit::uninit(); | |
| // SAFETY: both pointers are valid. | |
| unsafe { StrRetToStrW(&raw mut name, None, path.as_mut_ptr()) } | |
| .inspect_err(|err| log::error!("StrRetToStrW failed: {:?}", err))?; | |
| // SAFETY: errors are checked for, so the value is correct at this point. | |
| let path = unsafe { path.assume_init() }; | |
| // SAFETY: same. | |
| Ok(OsString::from_wide(unsafe { path.as_wide() }).into()) | |
| } | |
| /// API for WinXP+ Shell integration (effectively deprecated starting from Win11). | |
| impl IContextMenu_Impl for ShellExtension_Impl { | |
| fn GetCommandString( | |
| &self, | |
| cmd_id: usize, | |
| flags: u32, | |
| _reserved: *const u32, | |
| name_out: PSTR, | |
| name_out_len: u32, | |
| ) -> WinResult<()> { | |
| log::debug!( | |
| "@IContextMenu_Impl::GetCommandString => cmd_id: {}, flags: {:x?}, name_out: {:?}, name_out_len: {}", | |
| cmd_id, | |
| flags, | |
| name_out, | |
| name_out_len, | |
| ); | |
| unsafe fn write_out<T: Default + Copy + fmt::Debug>(val: &[T], out: *mut T, max_len: u32) { | |
| let len = val.len().min(max_len as usize - 1); | |
| // SAFETY: upheld by caller. | |
| unsafe { | |
| out.copy_from_nonoverlapping(val.as_ptr(), len); | |
| out.add(len).write(T::default()); | |
| } | |
| } | |
| match flags { | |
| // The menu is always enabled. | |
| GCS_VALIDATEA | GCS_VALIDATEW => S_OK, | |
| GCS_VERBA | GCS_VERBW | GCS_HELPTEXTA | GCS_HELPTEXTW if name_out.is_null() => { | |
| E_POINTER | |
| } | |
| // The verb is a "language-independent" value identifying the command. | |
| // SAFETY: the out pointer is not null at this point. | |
| GCS_VERBA => unsafe { | |
| write_out( | |
| "SampleShellextVerb".as_bytes(), | |
| name_out.as_ptr(), | |
| name_out_len, | |
| ); | |
| S_OK | |
| }, | |
| // SAFETY: same. | |
| GCS_VERBW => unsafe { | |
| let s = windows::core::w!("SampleShellextVerb"); | |
| write_out(s.as_wide(), name_out.as_ptr().cast(), name_out_len); | |
| S_OK | |
| }, | |
| // SAFETY: same. | |
| GCS_HELPTEXTA => unsafe { | |
| write_out( | |
| "Sample Shell extension verb".as_bytes(), | |
| name_out.as_ptr(), | |
| name_out_len, | |
| ); | |
| S_OK | |
| }, | |
| // SAFETY: same. | |
| GCS_HELPTEXTW => unsafe { | |
| let s = windows::core::w!("Sample Shell extension verb"); | |
| write_out(s.as_wide(), name_out.as_ptr().cast(), name_out_len); | |
| S_OK | |
| }, | |
| _ => { | |
| log::error!("Unknown requested command flags: {:x?}", flags); | |
| E_NOTIMPL | |
| } | |
| } | |
| .ok() | |
| } | |
| fn QueryContextMenu( | |
| &self, | |
| menu: HMENU, | |
| menu_index: u32, | |
| min_cmd_id: u32, | |
| max_cmd_id: u32, | |
| flags: u32, | |
| ) -> HRESULT { | |
| log::debug!( | |
| "@IContextMenu_Impl::QueryContextMenu => menu: {:?}, menu_index: {}, min_cmd_id: {}, max_cmd_id: {}, flags: {:x?}", | |
| menu, | |
| menu_index, | |
| min_cmd_id, | |
| max_cmd_id, | |
| flags, | |
| ); | |
| // > This flag provides a hint for the shortcut menu extension to add | |
| // > nothing if it does not modify the default item in the menu. | |
| if flags & CMF_DEFAULTONLY != 0 { | |
| return S_OK; | |
| } | |
| if menu.is_invalid() { | |
| return E_POINTER; | |
| } | |
| let item_text = windows::core::w!("Sample Shell extension verb"); | |
| let dll_icon = match icon::resource_icon_to_bitmap( | |
| HINSTANCE(ptr::with_exposed_provenance_mut( | |
| DLL_HMODULE.load(Ordering::SeqCst), | |
| )), | |
| // As configured in the `resource.rc`. | |
| windows::core::w!("IDI_MYAPP_ICON"), | |
| ) { | |
| Ok(i) => i, | |
| // This method returns `HRESULT` instead of `Result` in order to be | |
| // able to embed non-error information into it, so do this manually. | |
| Err(err) => return err.into(), | |
| }; | |
| let menu_item = MENUITEMINFOW { | |
| cbSize: mem::size_of::<MENUITEMINFOW>() as _, | |
| fMask: MIIM_ID | MIIM_STATE | MIIM_STRING | MIIM_BITMAP, | |
| wID: min_cmd_id, | |
| fState: MFS_ENABLED, | |
| dwTypeData: PWSTR::from_raw(item_text.as_ptr().cast_mut()), | |
| // SAFETY: the value is just above, so valid. | |
| cch: unsafe { item_text.len() } as _, | |
| hbmpItem: dll_icon, | |
| // We're only ever setting a single entry, so no need for the rest. | |
| ..Default::default() | |
| }; | |
| // SAFETY: | |
| // * the validity of the menu handle is checked above; | |
| // * the menu item is built here and referenced, so valid; | |
| if let Err(err) = unsafe { InsertMenuItemW(menu, menu_index, true, &raw const menu_item) } { | |
| log::error!("InsertMenuItemW failed: {:?}", err); | |
| return err.into(); | |
| } | |
| // > If successful, returns an `HRESULT` value that has its severity | |
| // > value set to `SEVERITY_SUCCESS` and its code value set to the | |
| // > offset of the largest command identifier that was assigned, plus one. | |
| // Here, there is only one, of ID `min_id`, so `min_id - min_id + 1 == 1`. | |
| MAKE_HRESULT(SEVERITY_SUCCESS.cast_signed(), 0, 1) | |
| } | |
| fn InvokeCommand(&self, info: *const CMINVOKECOMMANDINFO) -> WinResult<()> { | |
| log::debug!("@IContextMenu_Impl::InvokeCommand => info: {:?}", info); | |
| if info.is_null() { | |
| log::error!("Info pointer is null."); | |
| return E_POINTER.ok(); | |
| } | |
| // As set by `IShellExtInit::Initialize`. | |
| execute_command(self.this.item_paths.borrow().as_slice()); | |
| Ok(()) | |
| } | |
| } | |
| /// Executes the extension's command on the given paths. | |
| fn execute_command(paths: &[impl AsRef<Path>]) { | |
| log::info!( | |
| "Executing command for the following paths:\n{:#?}", | |
| paths.iter().map(AsRef::as_ref).collect::<Vec<_>>(), | |
| ); | |
| } | |
| #[implement(IClassFactory)] | |
| #[derive(Default)] | |
| pub struct ShellextClassFactory; | |
| impl IClassFactory_Impl for ShellextClassFactory_Impl { | |
| fn CreateInstance( | |
| &self, | |
| outer: WinRef<'_, IUnknown>, | |
| iface_id: *const GUID, | |
| obj_out: *mut *mut c_void, | |
| ) -> WinResult<()> { | |
| log::debug!( | |
| "@ShellextClassFactory_Impl::CreateInstance => outer: {:?}, iface_id: {:?}, obj_out: {:?}", | |
| &*outer, | |
| iface_id, | |
| obj_out, | |
| ); | |
| if outer.is_some() { | |
| log::error!("Outer parameter is non-null."); | |
| // > The `pUnkOuter` parameter was non-`NULL` and the object does | |
| // > not support aggregation. | |
| return Err(CLASS_E_NOAGGREGATION.into()); | |
| } | |
| let iface_id = if iface_id.is_null() { | |
| log::error!("Interface ID pointer is null."); | |
| return Err(E_POINTER.into()); | |
| } else { | |
| // SAFETY: the pointer was just checked. | |
| unsafe { *iface_id } | |
| }; | |
| log::debug!("Queried interface ID: {:?}", iface_id); | |
| let object = if obj_out.is_null() { | |
| log::error!("Object return pointer is null."); | |
| return Err(E_POINTER.into()); | |
| } else { | |
| // SAFETY: the pointer was just checked. | |
| unsafe { &mut *obj_out } | |
| }; | |
| // > If an error occurs, the interface pointer is `NULL`. | |
| *object = ptr::null_mut(); | |
| match iface_id { | |
| IUnknown::IID => { | |
| log::debug!("Requested interface is IUnknown."); | |
| *object = IUnknown::from(ShellExtension::default()).into_raw(); | |
| } | |
| IShellExtInit::IID => { | |
| log::debug!("Requested interface is IShellExtInit."); | |
| *object = IShellExtInit::from(ShellExtension::default()).into_raw(); | |
| } | |
| IContextMenu::IID => { | |
| log::debug!("Requested interface is IContextMenu."); | |
| *object = IContextMenu::from(ShellExtension::default()).into_raw(); | |
| } | |
| // > The object that `ppvObject` points to does not support the | |
| // > interface identified by `riid`. | |
| _ => { | |
| log::error!("Unsupported interface."); | |
| return Err(E_NOINTERFACE.into()); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn LockServer(&self, lock: BOOL) -> WinResult<()> { | |
| log::debug!("@ShellextClassFactory_Impl::LockServer => lock: {:?}", lock); | |
| if lock.as_bool() { | |
| INSTANCE_COUNT.fetch_add(1, Ordering::SeqCst); | |
| } else { | |
| INSTANCE_COUNT.fetch_sub(1, Ordering::SeqCst); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| /// Taken from `winapi`. | |
| #[expect(non_snake_case)] | |
| #[inline] | |
| pub fn MAKE_HRESULT(sev: i32, fac: i32, code: i32) -> HRESULT { | |
| HRESULT((sev << 31) | (fac << 16) | code) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment