如何借助objc2将Rust函数作为Selector传入NSMenuItem.action?
核心问题是:Objective-C的setAction:接收的是Selector(对应Objective-C方法的标识),而Rust的fn()不能直接转换成Selector。你需要通过以下步骤将Rust回调和Objective-C的Action机制结合起来:
步骤1:调整菜单项的回调类型
将原来的fn()改为线程安全的可克隆回调类型,方便在Objective-C方法中调用:
use std::sync::Arc; #[derive(Debug)] struct MenuItem { name: String, // 用Arc包裹闭包,支持多线程和克隆 on_click: Option<Arc<dyn Fn()>>, submenu: Option<Menu>, } #[derive(Debug)] struct Menu { items: Vec<MenuItem>, }
步骤2:扩展AppDelegate的存储结构
在AppDelegate的Ivars中添加哈希表,用来关联菜单项的Tag和Rust回调:
use std::collections::HashMap; use objc2::MainThreadMarker; use objc2::rc::Id; struct Ivars { mtm: MainThreadMarker, menu: Option<Menu>, // 存储Tag到回调的映射 menu_callbacks: HashMap<usize, Arc<dyn Fn()>>, // 生成唯一Tag的计数器 next_tag: usize, }
步骤3:修改AppDelegate的初始化方法
初始化新增的存储字段:
impl AppDelegate { fn new(mtm: MainThreadMarker, menu: Option<Menu>) -> Id<Self> { let this = mtm.alloc(); let this = this.set_ivars(Ivars { mtm, menu, menu_callbacks: HashMap::new(), next_tag: 0, }); unsafe { msg_send_id![super(this), init] } }
步骤4:修改build_menu方法,关联回调和菜单项
给每个带回调的菜单项分配唯一Tag,设置Action的Selector和Target,并将回调存入哈希表:
use objc2::appkit::{NSMenu, NSMenuItem}; use objc2::sel; fn build_menu(&self, menu: &Menu, ns_menu: &Id<NSMenu>) { let ivars = self.ivars(); for item in menu.items.iter() { let ns_menu_item = NSMenuItem::new(ivars.mtm); let title = NSString::from_str(&item.name); unsafe { ns_menu_item.setTitle(&title) }; if let Some(callback) = &item.on_click { // 分配唯一Tag let tag = ivars.next_tag; ivars.next_tag += 1; // 存储回调 ivars.menu_callbacks.insert(tag, Arc::clone(callback)); // 设置Action为自定义的Objective-C方法Selector unsafe { ns_menu_item.setAction(Some(sel!(menuItemAction:))); // 设置Target为AppDelegate实例 ns_menu_item.setTarget(Some(self)); // 设置Tag,用于后续查找回调 ns_menu_item.setTag(tag as i32); } } if let Some(submenu) = &item.submenu { let ns_submenu = NSMenu::new(ivars.mtm); self.build_menu(submenu, &ns_submenu); ns_menu_item.setSubmenu(Some(&ns_submenu)); } ns_menu.addItem(&ns_menu_item); } }
步骤5:在AppDelegate中定义Objective-C Action方法
实现符合Objective-C签名的方法,通过Tag取出Rust回调并执行:
use objc2::declare::ClassDecl; use objc2::appkit::NSApplicationDelegate; use objc2::runtime::Sel; // 声明AppDelegate类时注册该方法 declare!( #[derive(Debug, PartialEq, Eq)] pub struct AppDelegate; unsafe impl ClassType for AppDelegate { type Super = NSObject; type Mutability = MainThreadOnly; const NAME: &'static str = "AppDelegate"; fn class_decl() -> &'static mut ClassDecl { let decl = ClassDecl::new(Self::NAME, Self::Super::NAME).unwrap(); decl.add_ivar::<Ivars>("ivars"); // 注册应用启动方法和自定义Action方法 decl.add_method(sel!("applicationDidFinishLaunching:"), application_did_finish_launching as extern "C" fn(&Self, Sel, &NSNotification)); decl.add_method(sel!("menuItemAction:"), menu_item_action_impl as extern "C" fn(&Self, Sel, &NSMenuItem)); decl } } ); // 实现Action方法的逻辑 fn menu_item_action(&self, sender: &NSMenuItem) { let tag = sender.tag() as usize; let ivars = self.ivars(); // 通过Tag取出回调并执行 if let Some(callback) = ivars.menu_callbacks.get(&tag) { callback(); } } // 转换为extern "C"函数供Objective-C调用 extern "C" fn menu_item_action_impl(this: &AppDelegate, _: Sel, sender: &NSMenuItem) { this.menu_item_action(sender); } // 应用启动时创建菜单的方法 extern "C" fn application_did_finish_launching(this: &AppDelegate, _: Sel, _: &NSNotification) { let app = unsafe { NSApplication::sharedApplication() }; this.create_menu(app); }
关键说明
- Objective-C Action签名:Action方法必须符合
- (void)methodName:(id)sender的签名,参数中sender是触发Action的NSMenuItem实例。 - Selector的使用:
sel!(menuItemAction:)是objc2提供的宏,用来生成对应方法的Selector。 - 回调存储:通过Tag+哈希表的方式关联菜单项和Rust回调,避免了复杂的关联对象操作,内存管理更直观。
内容的提问来源于stack exchange,提问作者Denis Steinman
相关产品推荐
相关产品推荐

