- 应用安全
- 桌面应用
- 开发工具
【免费下载链接】cutter
Free and Open Source Reverse Engineering Platform powered by rizin
Cutter 的菜单栏中有一个特殊的Plugins(插件)菜单,它是已加载插件向用户暴露其窗口与视图的统一入口。本文基于 plugins-menu.rst 文档,结合 Cutter 仓库中的MainWindow、CutterPlugin、PluginManager等源码实现,深入讲解该菜单的默认行为、填充机制、插件加载流程,并给出 Python 与 C++ 插件的完整实战示例,帮助读者理解并亲手编写能在 Plugins 菜单中注册窗口的插件。
一、Plugins 菜单是什么
根据官方文档 plugins-menu.rst 的定义:
Plugins Sub-Menu(插件子菜单)描述:该菜单包含由已加载插件(loaded plugins)创建的窗口(windows)和视图(views)。默认情况下此菜单为空,除非插件主动将动作(actions)和条目(items)添加到该菜单。路径:Windows -> Plugins
也就是说,Plugins 菜单并不展示内置功能,而是完全由第三方插件驱动。插件通过注册 Dock Widget(停靠窗口)等 UI 组件,为每个窗口生成一个"切换显示/隐藏"的动作(toggleViewAction),这些动作会被统一收集到 Plugins 菜单中,使用户可以像操作 Cutter 内置窗口一样显示、隐藏、停靠插件提供的视图。
该菜单位于主菜单栏中,与 File、Edit、View、Windows、Debug、Help 等菜单并列。菜单栏的整体结构可参考 menu-bar.rst,其中plugins-menu是其固定的子页面之一。
二、默认状态:何时为空、何时被禁用
文档明确指出"默认情况下此菜单为空"。但"为空"有两种不同的表现,从源码中可以看得更细致。
在 MainWindow.cpp 中,Cutter 初始化主窗口时会根据插件情况设置 Plugins 菜单的状态:
if (plugins.empty()) { ui->menuPlugins->menuAction()->setToolTip( tr("No plugins are installed. Check the plugins section on Cutter documentation to " "learn more.")); ui->menuPlugins->setEnabled(false); } else if (ui->menuPlugins->isEmpty()) { ui->menuPlugins->menuAction()->setToolTip( tr("The installed plugins didn't add entries to this menu.")); ui->menuPlugins->setEnabled(false); }从这段代码可以归纳出三种典型状态:
| 状态 | 触发条件 | 表现 |
|---|---|---|
| 无插件 | 插件目录为空,或所有插件加载失败 | 菜单被禁用(置灰),tooltip 提示"未安装插件,请查阅文档" |
| 有插件但未添加条目 | 插件已加载,但没有通过addPluginDockWidget()等方式注册动作 | 菜单同样被禁用,tooltip 提示"已安装的插件没有向此菜单添加条目" |
| 正常显示 | 插件加载成功且注册了窗口/动作 | 菜单可用,列出所有插件窗口的切换动作 |
这一设计说明:Plugins 菜单的"空"是常态,只有真正注册了 UI 组件的插件才会让菜单变得可用。它既不会凭空出现假条目,也不会因为没有插件而报错。
三、菜单如何被填充:addPluginDockWidget()调用链
Plugins 菜单的填充机制集中在MainWindow::addPluginDockWidget()中。在 MainWindow.cpp:
void MainWindow::addPluginDockWidget(CutterDockWidget *dockWidget) { addWidget(dockWidget); ui->menuPlugins->addAction(dockWidget->toggleViewAction()); addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, dockWidget); pluginDocks.push_back(dockWidget); }该函数一次完成了四件事:
addWidget(dockWidget):将窗口注册到 Cutter 的窗口管理体系中(与内置窗口一致);ui->menuPlugins->addAction(dockWidget->toggleViewAction()):把该窗口的"切换视图"动作(Qt 内置的toggleViewAction(),勾选/取消勾选即可显示/隐藏窗口)追加到 Plugins 菜单中——这是填充菜单的关键一步;addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, dockWidget):将窗口添加到主窗口的顶部停靠区域;pluginDocks.push_back(dockWidget):把窗口指针记录到插件窗口列表中,便于统一管理。
此外 MainWindow.h 中还保留了一个带QAction*参数的旧版本重载,并标记为CUTTER_DEPRECATED(已废弃,但仍会转发到新版本),说明插件的菜单条目统一由窗口的 toggle 动作生成,插件无需自己手工往菜单里塞QAction。
getMenuByType()(MainWindow.cpp)还暴露了MenuType::Plugins枚举值,其他代码可以通过它直接拿到ui->menuPlugins这个QMenu*指针,例如:
enum class MenuType : ut8 { File, Edit, View, Windows, Debug, Help, Plugins }; QMenu *getMenuByType(MenuType type);从源码结构看,MenuType::Plugins是专为插件设计的菜单类型,允许插件或 Cutter 内部代码以统一的方式访问该菜单。
四、插件接口:CutterPlugin与生命周期回调
插件要往 Plugins 菜单注册窗口,首先必须实现插件基类CutterPlugin(CutterPlugin.h)。该接口定义了完整的插件生命周期:
class CUTTER_EXPORT CutterPlugin { public: virtual void setupPlugin() = 0; // 插件刚被加载时调用,用于初始化 virtual void setupInterface(MainWindow *main) = 0; // Cutter 核心 UI 初始化完成后调用,用于注册 UI 组件 virtual void registerDecompilers() {} // 可选:注册插件实现的反编译器 virtual void terminate() {}; // 插件被销毁前调用,用于清理资源 virtual QString getName() const = 0; virtual QString getAuthor() const = 0; virtual QString getDescription() const = 0; virtual QString getVersion() const = 0; };其中与 Plugins 菜单关系最密切的是setupInterface(MainWindow *main)——插件在此回调中创建自己的 Dock Widget,并调用main->addPluginDockWidget(widget)完成菜单注册。接口还通过 Qt 的元对象机制声明了CutterPlugin_iid "re.rizin.cutter.plugins.CutterPlugin",用于插件身份识别与加载校验。
整个插件生命周期由PluginManager(PluginManager.h)统一驱动:
loadPlugins(bool enablePlugins = true):应用启动时调用一次,扫描插件目录并加载所有插件;destroyPlugins():应用退出时调用,依次终止并销毁插件;getPluginDirectories()/getUserPluginsDirectory():提供插件目录信息(用于在"编辑 -> 首选项 -> 插件"中展示路径);- 内部通过
loadNativePlugins()与loadPythonPlugins()分别加载 C++ 与 Python 插件。
五、插件从哪里来:加载目录与 native/python 子目录
根据官方 plugins.rst 文档:
- 插件从与操作系统相关的用户级目录加载;
- 该目录下包含
native和python两个子目录,分别存放 C++ 与 Python 插件,由 Cutter 自动创建; - 若想知道插件目录的绝对路径以及当前已加载的插件列表,可打开Edit -> Preferences -> Plugins(编辑 -> 首选项 -> 插件)查看。
需要注意:Python 插件支持仅在 Cutter 以CUTTER_ENABLE_PYTHON和CUTTER_ENABLE_PYTHON_BINDINGS选项编译时才可用(官方发布版自 1.8.0 起均默认开启)。从 PluginManager.h 也可以看到,loadPythonPlugins()与loadPythonPlugin()都包在#ifdef CUTTER_ENABLE_PYTHON_BINDINGS中。
此外,文档还特别提醒:如果要做的是新文件格式或新架构支持,不应写 Cutter 插件,而应实现 Rizin 插件(见 plugins.rst)——Cutter 插件专注于 UI 集成。
六、实战:用 Python 插件在 Plugins 菜单注册窗口
Cutter 强烈建议新手从 Python 插件入手,因为其工作流更快、更简单。以下代码来自官方示例 sample_python.py,它演示了最核心的"注册 Dock Widget 到 Plugins 菜单"流程:
import cutter from PySide6.QtCore import Qt from PySide6.QtWidgets import QVBoxLayout, QLabel, QWidget, QSizePolicy, QPushButton class FortuneWidget(cutter.CutterDockWidget): def __init__(self, parent): super(FortuneWidget, self).__init__(parent) self.setObjectName("FancyDockWidgetFromCoolPlugin") self.setWindowTitle("Sample Python Plugin") content = QWidget() self.setWidget(content) layout = QVBoxLayout(content) content.setLayout(layout) self.text = QLabel(content) layout.addWidget(self.text) button = QPushButton(content) button.setText("Want a fortune?") layout.addWidget(button) layout.setAlignment(button, Qt.AlignHCenter) button.clicked.connect(self.generate_fortune) cutter.core().seekChanged.connect(self.generate_fortune) self.show() def generate_fortune(self): fortune = cutter.cmd("fortune").replace("\n", "") res = cutter.core().cmdRaw(f"?E {fortune}") self.text.setText(res) class CutterSamplePlugin(cutter.CutterPlugin): name = "Sample Plugin" description = "A sample plugin written in python." version = "1.2" author = "Cutter developers" def setupPlugin(self): pass def setupInterface(self, main): # Dock widget: 这一步会把窗口的 toggle 动作自动加到 Plugins 菜单 widget = FortuneWidget(main) main.addPluginDockWidget(widget) ... def create_cutter_plugin(): return CutterSamplePlugin()关键点拆解:
main.addPluginDockWidget(widget):在setupInterface()中调用,将FortuneWidget注册到 Cutter 主窗口,其toggleViewAction()会自动出现在Plugins菜单中(C++ 版示例 CutterSamplePlugin.cpp 的写法完全一致)。create_cutter_plugin()入口函数:Cutter 在启动时会导入插件模块,并调用模块根部的create_cutter_plugin()函数,期待其返回一个cutter.CutterPlugin实例。name/description/version/author:插件的元信息,会显示在"编辑 -> 首选项 -> 插件"列表中。
完整的从零编写 Python 插件教程(包括创建 Dock Widget、用cutter.cmd()/cutter.cmdj()获取数据、连接seekChanged信号实现动态刷新、最终完整代码)见 getting-started.rst,其运行效果如下两图所示:
将插件安装到加载目录
- 打开Edit -> Preferences -> Plugins,查看插件目录的绝对路径;
- 将
.py文件放入该目录下的python/子目录(C++ 插件放入native/); - 重启 Cutter,插件即被自动加载。
额外提示(来自 getting-started.rst):
- 插件本质是 Python 模块,因此也可以用包含多个
.py文件 +__init__.py的目录形式组织,只要__init__.py定义了create_cutter_plugin()即可; - 在 Unix 系系统上,可以用符号链接把插件链接到插件目录,避免反复拷贝;
- 当插件需要清理资源时,实现可选的
terminate()方法即可(Python 版示例 sample_python.py 展示了如何从上下文菜单中移除自己添加的动作)。
七、实战:C++ 插件注册 Dock Widget
C++ 插件的注册流程与 Python 完全对称。官方示例 CutterSamplePlugin.cpp 的核心代码:
void CutterSamplePlugin::setupInterface(MainWindow *main) { CutterSamplePluginWidget *widget = new CutterSamplePluginWidget(main); main->addPluginDockWidget(widget); } CutterSamplePluginWidget::CutterSamplePluginWidget(MainWindow *main) : CutterDockWidget(main) { this->setObjectName("CutterSamplePluginWidget"); this->setWindowTitle("Sample C++ Plugin"); // ... 构建 QLabel、QPushButton 等 UI 组件 ... connect(Core(), &CutterCore::seekChanged, this, &CutterSamplePluginWidget::on_seekChanged); }要点:
- 插件类继承
CutterPlugin,并实现setupPlugin()、setupInterface()、getName()、getAuthor()、getDescription()、getVersion()等纯虚函数; - 窗口类继承
CutterDockWidget,通过setObjectName()保证窗口布局状态可被持久化; - 同样只需一行
main->addPluginDockWidget(widget),窗口的切换动作便进入 Plugins 菜单。
从源码结构还可以推断:CutterDockWidget是所有可停靠插件窗口的基类,因此任何继承它的自定义窗口都能获得与内置窗口一致的行为(拖拽、停靠、布局保存、菜单开关等)。
八、插件还能往哪里加条目:上下文菜单扩展
除了 Plugins 菜单,插件还可以扩展 Cutter 的右键上下文菜单,机制是 MainWindow.h 中定义的getContextMenuExtensions():
enum class ContextMenuType : ut8 { Disassembly, Addressable }; QMenu *getContextMenuExtensions(ContextMenuType type);Python 示例 sample_python.py 展示了两种典型用法:
# 反汇编视图右键菜单 menu = main.getContextMenuExtensions(cutter.MainWindow.ContextMenuType.Disassembly) self.disas_action = menu.addAction("CutterSamplePlugin dissassembly action") self.disas_action.triggered.connect(self.handle_disassembler_action) # 可寻址条目列表(Flags/Functions/Strings/Search 结果等)的右键菜单 addressable_item_menu = main.getContextMenuExtensions(cutter.MainWindow.ContextMenuType.Addressable) self.addr_submenu = addressable_item_menu.addMenu("CutterSamplePlugin") adrr_action = self.addr_submenu.addAction("Action 1") self.addr_submenu.addSeparator() adrr_action2 = self.addr_submenu.addAction("Action 2")注意:当用户在上下文菜单中触发插件动作时,Cutter 会把当前条目的地址写入QAction的data()(见 sample_python.py 的回调),因此插件回调里可以通过action.data()拿到当前反汇编行或当前表格项对应的地址。
九、常见问题排查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| Plugins 菜单被禁用,tooltip 提示"未安装插件" | 插件目录为空或插件未放入正确子目录 | 通过Edit -> Preferences -> Plugins查看目录路径,将插件放入python/或native/子目录 |
| Plugins 菜单被禁用,tooltip 提示"插件未添加条目" | 插件已加载但setupInterface()未调用addPluginDockWidget() | 检查插件源码是否注册了 Dock Widget 或菜单动作 |
| Python 插件无法加载 | Cutter 未启用 Python 绑定 | 确认构建选项CUTTER_ENABLE_PYTHON与CUTTER_ENABLE_PYTHON_BINDINGS已开启(官方发布版默认开启) |
| 插件加载后未见于列表 | 插件缺少create_cutter_plugin()入口函数 | 确保模块根部定义了create_cutter_plugin()并返回插件实例 |
十、总结
Cutter 的Plugins 菜单是插件 UI 的"总开关面板":它默认为空、无插件时自动禁用;当插件通过setupInterface()回调调用addPluginDockWidget()注册窗口后,窗口的切换动作会以toggleViewAction()的形式自动出现在该菜单中。这套机制将插件窗口与内置窗口统一管理,用户无需学习任何额外操作即可显示、隐藏、停靠插件视图。
对开发者而言,只需掌握三点即可上手:实现CutterPlugin接口、在setupInterface()中创建CutterDockWidget窗口、调用main.addPluginDockWidget(widget)。更多细节可继续阅读仓库中的 plugins.rst、getting-started.rst 以及两个官方示例 sample_python.py 与 CutterSamplePlugin.cpp。
- 应用安全
- 桌面应用
- 开发工具
【免费下载链接】cutter
Free and Open Source Reverse Engineering Platform powered by rizin
相关推荐
x64dbg 插件菜单(Plugins Menu)完全指南:从插件加载到 Scylla 启动
x64dbg 插件菜单(Plugins Menu)完全指南:从插件加载到 Scylla 启动 本文围绕 x64dbg 的 Plugins(插件)菜单 展开,讲解
逆向工程调试器开发工具应用安全Cutter 反编译功能全解析:Decompiler 插件架构、反编译视图与上下文菜单实战指南
Cutter 反编译功能全解析:Decompiler 插件架构、反编译视图与上下文菜单实战指南 导读 本文以 Cutter 用户文档中的 Features 章节
应用安全桌面应用开发工具FaceSwap plugins 包解析:插件加载机制、三大任务域与全量插件清单
FaceSwap plugins 包解析:插件加载机制、三大任务域与全量插件清单 本文以官方文档 docs/full/plugins 目录为骨架,系统讲解 Fa
人工智能深度学习计算机视觉媒体生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考