Add Documentation about Plugins

This commit is contained in:
Florian Märkl 2019-03-16 17:14:54 +01:00
parent aa591e1a47
commit b8e9e37c86
9 changed files with 295 additions and 2 deletions

View File

@ -1,3 +1,5 @@
.. _api:
API Reference
===========

View File

@ -46,6 +46,11 @@ to know what you can do to help the project!
.. toctree::
:maxdepth: 2
:caption: Contents:
:glob:
*
shortcuts
building
common-errors
code
plugins
api

35
docs/source/plugins.rst Normal file
View File

@ -0,0 +1,35 @@
Plugins
=======
Cutter supports writing plugins in both C++ and Python.
If you are unsure which one to choose, we strongly suggest starting with Python since
it allows for a quicker and easier workflow.
If you plan to implement support for a new file format or architecture, Cutter plugins are not the correct approach.
Instead, you will want to implement a radare2 plugin, which is documented `here <https://radare.gitbooks.io/radare2book/plugins/intro.html>`_.
Loading and Overview
--------------------
Plugins are loaded from an OS-dependent user-level directory.
To get the location of this directory and a list of currently loaded plugins, navigate to Edit -> Preferences -> Plugins.
.. image:: plugins/preferences-plugins.png
The plugins directory contains two subdirectories, ``native`` and ``python`` for C++ and Python plugins respectively,
which will be created automatically by Cutter.
Note that support for Python plugins is only available if Cutter was built with the options ``CUTTER_ENABLE_PYTHON``
and ``CUTTER_ENABLE_PYTHON_BINDINGS`` enabled.
This is the case for all official builds from GitHub Releases starting with version 1.8.0.
Creating Plugins
---------------
.. toctree::
:glob:
:hidden:
plugins/*

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,251 @@
Getting started with Python Plugins
===================================
This article provides a step-by-step guide on how to write a simple Python plugin for Cutter.
Create a python file, called ``myplugin.py`` for example, and add the following contents:
.. code-block:: python
import cutter
class MyCutterPlugin(cutter.CutterPlugin):
name = "My Plugin"
description = "This plugin does awesome things!"
version = "1.0"
author = "1337 h4x0r"
def setupPlugin(self):
pass
def setupInterface(self, main):
pass
def create_cutter_plugin():
return MyCutterPlugin()
This is the most basic code that makes up a plugin.
Python plugins in Cutter are regular Python modules that are imported automatically on startup.
In order to load the plugin, Cutter will call the function ``create_cutter_plugin()`` located
in the root of the module and expects it to return an instance of ``cutter.CutterPlugin``.
Normally, you shouldn't have to do anything else in this function.
.. note::
The Cutter API is exposed through the ``cutter`` module.
This consists mostly of direct bindings of the original C++ classes, generated with Shiboken2.
For more detail about this API, see the Cutter C++ code or :ref:`api`.
The ``CutterPlugin`` subclass contains some meta-info and two callback methods:
* ``setupPlugin()`` is called right after the plugin is loaded and can be used to initialize the plugin itself.
* ``setupInterface()`` is called with the instance of MainWindow as an argument and should create and register any UI components.
Copy this file into the ``python`` subdirectory located under the plugins directory of Cutter and start the application.
You should see an entry for your plugin in the list under Edit -> Preferences -> Plugins.
Here, the absolute path to the plugins directory is shown too if you are unsure where to put your plugin:
.. image:: preferences-plugins.png
.. note::
As mentioned, plugins are Python modules. This means, instead of only a single .py file, you can also
use a directory containing multiple python files and an ``__init__.py`` file that defines or imports the
``create_cutter_plugin()`` function.
.. note::
If you are working on a Unix-like system, instead of copying, you can also symlink your plugin into the plugins
directory, which lets you store the plugin somewhere else without having to copy the files over and over again.
Creating a Widget
-----------------
Next, we are going to add a simple dock widget. Extend the code as follows:
.. code-block:: python
import cutter
from PySide2.QtWidgets import QAction, QLabel
class MyDockWidget(cutter.CutterDockWidget):
def __init__(self, parent, action):
super(MyDockWidget, self).__init__(parent, action)
self.setObjectName("MyDockWidget")
self.setWindowTitle("My cool DockWidget")
label = QLabel(self)
self.setWidget(label)
label.setText("Hello World")
class MyCutterPlugin(cutter.CutterPlugin):
# ...
def setupInterface(self, main):
action = QAction("My Plugin", main)
action.setCheckable(True)
widget = MyDockWidget(main, action)
main.addPluginDockWidget(widget, action)
# ...
We are subclassing ``cutter.CutterDockWidget``, which is the base class for all dock widgets in Cutter,
and adding a label to it.
.. note::
You can access the whole Qt5 API from Python, which is exposed by PySide2. For more information about this, refer to the
Documentation of `Qt <https://doc.qt.io/qt-5/reference-overview.html>`_ and `PySide2 <https://wiki.qt.io/Qt_for_Python>`_.
In our ``setupInterface()`` method, we create an instance of our dock widget and an action to be
added to the menu for showing and hiding the widget.
MainWindow provides a helper method called ``addPluginDockWidget()`` to easily register these.
When running Cutter now, you should see the widget:
.. image:: mydockwidget.png
... as well as the action:
.. image:: mydockwidget-action.png
Fetching Data
-------------
Next, we want to show some actual data from the binary in our widget.
As an example, we will display the instruction and instruction size at the current position.
Extend the code as follows:
.. code-block:: python
# ...
class MyDockWidget(cutter.CutterDockWidget):
def __init__(self, parent, action):
# ...
label = QLabel(self)
self.setWidget(label)
disasm = cutter.cmd("pd 1").strip()
instruction = cutter.cmdj("pdj 1")
size = instruction[0]["size"]
label.setText("Current disassembly:\n{}\nwith size {}".format(disasm, size))
# ...
We can access the data by calling radare2 commands and utilizing their output.
This is done by using the two functions ``cmd()`` and ``cmdj()``, which behave just as they
do in `r2pipe <https://radare.gitbooks.io/radare2book/scripting/r2pipe.html>`_.
Many commands in radare2 can be suffixed with a ``j`` to return JSON output.
``cmdj()`` will automatically deserialize the JSON into python dicts and lists, so the
information can be easily accessed.
In our case, we use the two commands ``pd`` (Print Disassembly) and ``pdj`` (Print Disassembly as JSON)
with a parameter of 1 to fetch a single line of disassembly.
.. note::
To try out commands, you can use the Console widget in Cutter. Almost all commands support a ``?`` suffix, like in
``pd?``, to show help and available sub-commands.
To get a general overview, enter a single ``?``.
The result will look like the following:
.. image:: disasm-static.png
Of course, since we only fetch the info once during the creation of the widget, the content never updates.
We are going to change that in the next section.
Reacting to Events
------------------
We want to update the content of our widget on every seek.
This can be done like the following:
.. code-block:: python
# ...
from PySide2.QtCore import QObject, SIGNAL
# ...
class MyDockWidget(cutter.CutterDockWidget):
def __init__(self, parent, action):
# ...
self._label = QLabel(self)
self.setWidget(self._label)
QObject.connect(cutter.core(), SIGNAL("seekChanged(RVA)"), self.update_contents)
self.update_contents()
def update_contents(self):
disasm = cutter.cmd("pd 1").strip()
instruction = cutter.cmdj("pdj 1")
size = instruction[0]["size"]
self._label.setText("Current disassembly:\n{}\nwith size {}".format(disasm, size))
First, we move the update code to a separate method.
Then we call ``cutter.core()``, which returns the global instance of ``CutterCore``.
This class provides the Qt signal ``seekChanged(RVA)``, which is emitted every time the current seek changes.
We can simply connect this signal to our method and our widget will update as we expect it to:
.. image:: disasm-dynamic.png
For more information about Qt signals and slots, refer to `<https://doc.qt.io/qt-5/signalsandslots.html>`_.
Full Code
---------
.. code-block:: python
import cutter
from PySide2.QtCore import QObject, SIGNAL
from PySide2.QtWidgets import QAction, QLabel
class MyDockWidget(cutter.CutterDockWidget):
def __init__(self, parent, action):
super(MyDockWidget, self).__init__(parent, action)
self.setObjectName("MyDockWidget")
self.setWindowTitle("My cool DockWidget")
self._label = QLabel(self)
self.setWidget(self._label)
QObject.connect(cutter.core(), SIGNAL("seekChanged(RVA)"), self.update_contents)
self.update_contents()
def update_contents(self):
disasm = cutter.cmd("pd 1").strip()
instruction = cutter.cmdj("pdj 1")
size = instruction[0]["size"]
self._label.setText("Current disassembly:\n{}\nwith size {}".format(disasm, size))
class MyCutterPlugin(cutter.CutterPlugin):
name = "My Plugin"
description = "This plugin does awesome things!"
version = "1.0"
author = "1337 h4x0r"
def setupPlugin(self):
pass
def setupInterface(self, main):
action = QAction("My Plugin", main)
action.setCheckable(True)
widget = MyDockWidget(main, action)
main.addPluginDockWidget(widget, action)
def create_cutter_plugin():
return MyCutterPlugin()