You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何借助objc2将Rust函数作为Selector传入NSMenuItem.action?

解决objc2中Rust函数作为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);
}

关键说明

  1. Objective-C Action签名:Action方法必须符合- (void)methodName:(id)sender的签名,参数中sender是触发Action的NSMenuItem实例。
  2. Selector的使用:sel!(menuItemAction:)是objc2提供的宏,用来生成对应方法的Selector。
  3. 回调存储:通过Tag+哈希表的方式关联菜单项和Rust回调,避免了复杂的关联对象操作,内存管理更直观。

内容的提问来源于stack exchange,提问作者Denis Steinman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 09:35:55