Skip to content

Instantly share code, notes, and snippets.

@PaulDance
Last active August 14, 2025 22:19
Show Gist options
  • Select an option

  • Save PaulDance/413c40d2e05fc1138ca6d23e8f1d7f48 to your computer and use it in GitHub Desktop.

Select an option

Save PaulDance/413c40d2e05fc1138ca6d23e8f1d7f48 to your computer and use it in GitHub Desktop.
Sample Rust implementation of an IContextMenu-based Windows Shell extension DLL.
// This shows an example of how to implement the `IContextMenu` interface
// in Rust for Windows Shell context menu integration.
// The build results in a DLL that should be placed somewhere on the file
// system and then referenced in the registry. `activation.reg` shows the
// registry keys and values to set; can be used with `reg.exe import`.
// The Rust files should be moved into place as such:
// * `Cargo.toml`
// * `build.rs`
// * `src/lib.rs`
// * `src/shellext.rs`
// * `src/icon.rs`
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}"
fn main() {
embed_resource::compile(
"resources.rc",
embed_resource::NONE,
)
.manifest_required()
.unwrap();
}
[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"
//! 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) }
}
#![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
}
<!-- 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>
// 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
//! [`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