Skip to main content

win_context_menu/
ffi.rs

1//! C-compatible FFI layer for `win-context-menu`.
2//!
3//! Enable with the **`ffi`** feature flag. All functions in this module use
4//! C calling conventions and return null pointers on failure. Error details
5//! can be retrieved via [`wcm_last_error`].
6//!
7//! # Threading
8//!
9//! All functions **must** be called from a thread that has been initialized for
10//! COM STA mode (via [`wcm_com_init`]). These functions are **not thread-safe**.
11//!
12//! # Memory ownership
13//!
14//! Pointers returned by `wcm_*` functions are heap-allocated and must be freed
15//! by the corresponding `wcm_free_*` function. Failing to do so will leak
16//! memory.
17
18use std::cell::RefCell;
19use std::ffi::{CStr, CString};
20use std::os::raw::c_char;
21use std::path::PathBuf;
22
23use crate::com::ComGuard;
24use crate::context_menu::ContextMenu;
25use crate::menu_items::MenuItem;
26use crate::shell_item::ShellItems;
27
28thread_local! {
29    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
30}
31
32fn set_last_error(err: &crate::error::Error) {
33    let msg = CString::new(err.to_string()).unwrap_or_default();
34    LAST_ERROR.with(|e| {
35        *e.borrow_mut() = Some(msg);
36    });
37}
38
39/// Opaque handle for a COM guard.
40pub struct FfiComGuard(ComGuard);
41
42/// Information about a selected context menu item.
43///
44/// Returned by [`wcm_show_context_menu`]. Must be freed with
45/// [`wcm_free_selected`].
46#[repr(C)]
47pub struct FfiSelectedItem {
48    /// The command ID of the selected item.
49    pub command_id: u32,
50    /// The display label (UTF-8, heap-allocated). May be null.
51    pub label: *mut c_char,
52    /// The verb string (UTF-8, heap-allocated). Null if unavailable.
53    pub verb: *mut c_char,
54}
55
56/// Retrieve the last error message from the most recent failed `wcm_*` call
57/// on this thread.
58///
59/// Returns a pointer to a static thread-local C string, or null if no error
60/// has occurred. The pointer is valid until the next `wcm_*` call on the same
61/// thread. **Do not free this pointer.**
62#[no_mangle]
63pub extern "C" fn wcm_last_error() -> *const c_char {
64    LAST_ERROR.with(|e| {
65        e.borrow()
66            .as_ref()
67            .map(|s| s.as_ptr())
68            .unwrap_or(std::ptr::null())
69    })
70}
71
72/// Initialize COM in STA mode.
73///
74/// Must be called once per thread before any other `wcm_*` function.
75/// Returns an opaque handle that must be freed with [`wcm_com_uninit`].
76/// Returns null on failure (call [`wcm_last_error`] for details).
77#[no_mangle]
78pub extern "C" fn wcm_com_init() -> *mut FfiComGuard {
79    match ComGuard::new() {
80        Ok(guard) => Box::into_raw(Box::new(FfiComGuard(guard))),
81        Err(e) => {
82            set_last_error(&e);
83            std::ptr::null_mut()
84        }
85    }
86}
87
88/// Free a COM guard handle returned by [`wcm_com_init`].
89///
90/// # Safety
91///
92/// `guard` must be a pointer returned by [`wcm_com_init`], or null.
93/// Must not be called more than once for the same pointer.
94#[no_mangle]
95pub unsafe extern "C" fn wcm_com_uninit(guard: *mut FfiComGuard) {
96    if !guard.is_null() {
97        // SAFETY: Caller guarantees `guard` was returned by `wcm_com_init`
98        // and has not been freed yet.
99        let _ = unsafe { Box::from_raw(guard) };
100    }
101}
102
103/// Show a context menu for a single file path at the given screen coordinates.
104///
105/// Returns a [`FfiSelectedItem`] pointer if the user selected an item, or null
106/// if the menu was dismissed or an error occurred. On error, call
107/// [`wcm_last_error`] for details.
108///
109/// The returned pointer must be freed with [`wcm_free_selected`]. The
110/// selected command is automatically executed before this function returns.
111///
112/// # Safety
113///
114/// - `path` must be a valid null-terminated UTF-8 C string, or null.
115/// - Must be called from a thread with an active `wcm_com_init` guard.
116#[no_mangle]
117pub unsafe extern "C" fn wcm_show_context_menu(
118    path: *const c_char,
119    x: i32,
120    y: i32,
121) -> *mut FfiSelectedItem {
122    if path.is_null() {
123        return std::ptr::null_mut();
124    }
125
126    // SAFETY: Caller guarantees `path` is a valid null-terminated C string.
127    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
128        Ok(s) => s,
129        Err(_) => return std::ptr::null_mut(),
130    };
131
132    let items = match ShellItems::from_path(PathBuf::from(path_str)) {
133        Ok(i) => i,
134        Err(e) => {
135            set_last_error(&e);
136            return std::ptr::null_mut();
137        }
138    };
139
140    let menu = match ContextMenu::new(items) {
141        Ok(m) => m,
142        Err(e) => {
143            set_last_error(&e);
144            return std::ptr::null_mut();
145        }
146    };
147
148    match menu.show_at(x, y) {
149        Ok(Some(selected)) => {
150            let label = CString::new(selected.menu_item().label.as_str())
151                .unwrap_or_default()
152                .into_raw();
153            let verb = selected
154                .menu_item()
155                .command_string
156                .as_deref()
157                .and_then(|v| CString::new(v).ok())
158                .map(|c| c.into_raw())
159                .unwrap_or(std::ptr::null_mut());
160            let command_id = selected.command_id();
161
162            // Execute the command
163            if let Err(e) = selected.execute() {
164                set_last_error(&e);
165            }
166
167            Box::into_raw(Box::new(FfiSelectedItem {
168                command_id,
169                label,
170                verb,
171            }))
172        }
173        Ok(None) => std::ptr::null_mut(),
174        Err(e) => {
175            set_last_error(&e);
176            std::ptr::null_mut()
177        }
178    }
179}
180
181/// Free a [`FfiSelectedItem`] returned by [`wcm_show_context_menu`].
182///
183/// # Safety
184///
185/// `item` must be a pointer returned by [`wcm_show_context_menu`], or null.
186/// Must not be called more than once for the same pointer.
187#[no_mangle]
188pub unsafe extern "C" fn wcm_free_selected(item: *mut FfiSelectedItem) {
189    if !item.is_null() {
190        // SAFETY: Caller guarantees `item` was returned by
191        // `wcm_show_context_menu` and has not been freed yet.
192        let item = unsafe { Box::from_raw(item) };
193        if !item.label.is_null() {
194            // SAFETY: `label` was allocated by `CString::into_raw`.
195            let _ = unsafe { CString::from_raw(item.label) };
196        }
197        if !item.verb.is_null() {
198            // SAFETY: `verb` was allocated by `CString::into_raw`.
199            let _ = unsafe { CString::from_raw(item.verb) };
200        }
201    }
202}
203
204/// Enumerate context menu items as a JSON string.
205///
206/// Returns a heap-allocated null-terminated UTF-8 C string containing a JSON
207/// array, or null on failure (call [`wcm_last_error`] for details). The
208/// returned pointer must be freed with [`wcm_free_string`].
209///
210/// # Safety
211///
212/// - `path` must be a valid null-terminated UTF-8 C string, or null.
213/// - Must be called from a thread with an active `wcm_com_init` guard.
214#[no_mangle]
215pub unsafe extern "C" fn wcm_enumerate_menu(
216    path: *const c_char,
217    extended: bool,
218) -> *mut c_char {
219    if path.is_null() {
220        return std::ptr::null_mut();
221    }
222
223    // SAFETY: Caller guarantees `path` is a valid null-terminated C string.
224    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
225        Ok(s) => s,
226        Err(_) => return std::ptr::null_mut(),
227    };
228
229    let items = match ShellItems::from_path(PathBuf::from(path_str)) {
230        Ok(i) => i,
231        Err(e) => {
232            set_last_error(&e);
233            return std::ptr::null_mut();
234        }
235    };
236
237    let menu = match ContextMenu::new(items) {
238        Ok(m) => m.extended(extended),
239        Err(e) => {
240            set_last_error(&e);
241            return std::ptr::null_mut();
242        }
243    };
244
245    match menu.enumerate() {
246        Ok(items) => {
247            let json = menu_items_to_json(&items);
248            match CString::new(json) {
249                Ok(c) => c.into_raw(),
250                Err(_) => std::ptr::null_mut(),
251            }
252        }
253        Err(e) => {
254            set_last_error(&e);
255            std::ptr::null_mut()
256        }
257    }
258}
259
260/// Free a string returned by [`wcm_enumerate_menu`].
261///
262/// # Safety
263///
264/// `s` must be a pointer returned by a `wcm_*` function, or null.
265#[no_mangle]
266pub unsafe extern "C" fn wcm_free_string(s: *mut c_char) {
267    if !s.is_null() {
268        // SAFETY: Caller guarantees `s` was allocated by `CString::into_raw`.
269        let _ = unsafe { CString::from_raw(s) };
270    }
271}
272
273/// Serialize menu items to a JSON array string.
274fn menu_items_to_json(items: &[MenuItem]) -> String {
275    #[derive(serde::Serialize)]
276    struct JsonMenuItem<'a> {
277        id: u32,
278        label: &'a str,
279        #[serde(skip_serializing_if = "Option::is_none")]
280        verb: Option<&'a str>,
281        separator: bool,
282        disabled: bool,
283        #[serde(skip_serializing_if = "Option::is_none")]
284        submenu: Option<Vec<JsonMenuItem<'a>>>,
285    }
286
287    fn to_json_items(items: &[MenuItem]) -> Vec<JsonMenuItem<'_>> {
288        items
289            .iter()
290            .map(|item| JsonMenuItem {
291                id: item.id,
292                label: &item.label,
293                verb: item.command_string.as_deref(),
294                separator: item.is_separator,
295                disabled: item.is_disabled,
296                submenu: item.submenu.as_ref().map(|s| to_json_items(s)),
297            })
298            .collect()
299    }
300
301    serde_json::to_string(&to_json_items(items)).unwrap_or_else(|_| "[]".to_string())
302}