aboutsummaryrefslogtreecommitdiff
path: root/libs/plugin_python/src/mobase/README.md
blob: b4d184e267e107189546041d9cdf8aac57b4aaac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# mobase

`mobase` is a ModOrganizer2 Python API.
It provides access to the part of
[`uibase`](https://github.com/ModOrganizer2/modorganizer-uibase) C++ API from
Python through [`pybind11`](https://github.com/pybind/pybind11).

## Organization

**Important:** All (most) files should include `pybind11_all.h` (either directly
or through another header) to get proper `type_caster` available.

- `mobase.cpp` contains the `PYBIND11_MODULE` definition of `mobase` but is otherwise
  the entrypoint for other functions.
- `wrappers.h` contains the declaration of most functions implemented under
  `wrappers/`.
- The other files under `wrappers/` contains bindings and trampoline classes (see
  below) for `uibase` classes.
  - `basic_classes.cpp` contains the bindings for most classes that cannot be extended
    in Python (`IOrganizer`, `IModInterface`, etc.)
  - `game_features.cpp` contains the bindings and trampoline classes for game features.
  - `pyfiletree.h` and `pyfiletree.cpp` contains bindings for the `IFileTree`-related
    classes.
  - `pyplugins.h` contains the trampoline classes for the `IPluginXXX` classes and
    `pyplugins.cpp` the bindings.
    - `pyplugins.h` is required since the trampoline classes are tagged with `Q_OBJECT`,
      and MOC does not work if the classes are declared in a C++ file.
    - `pyplugins.cpp` also contains the `extract_plugins` function in `mobase.private`
      that is used to extract plugins from Python object in the runner.
  - `widgets.cpp` contains the bindings for the widget classes.
  - `wrappers.cpp` contains the trampoline and bindings for non-plugin classes that can
    be extended through Python.

## Updating mobase

### Classes that cannot be extended through Python

Updating or adding classes that cannot be extended through Python is quite easily.
One simply needs to declare the appropriate `py::class_` or add new `.def()`.

See below for things to remember when creating pybind11 bindings.

### Free functions

Similar to classes that cannot be extended through Python, see above.

### Classes that can be extended through Python: Plugins

To extend plugins, simply update the trampoline classes in `pyplugins.h` and the
bindings in `pyplugins.cpp`.

**Note:** For new plugins, simply look at the existing one.

### Classes that can be extended through Python: Game Features

To extend or expose game features:

- Create (if there is not already one) a trampoline class for the feature in
  `game_features.cpp`.
  - Add implementation of missing functions if required.
- Add the bindings in `add_game_feature_bindings` in `game_features.cpp`.
- For new feature, add the feature type to `GameFeaturesHelper::GameFeatures` in
  `game_features.cpp`.

### Classes that can be extended through Python: Others

Non-plugin classes should be added to the `wrappers.cpp` file and should be exposed
with `std::shared_ptr<>` or `qobject_holder<>` holders.

- If the classes extends `QObject`, use a `qobject_holder`.
- Otherwise use a `std::shared_ptr<>` holder and add a `MO2_PYBIND11_SHARED_CPP_HOLDER`
  declaration in `pybind11_all.h`.

Trampoline can be defined directly in `wrappers.cpp`, and bindings in the appropriate
function.
See the existing classes for example.

**Important:**
You need to make sure that `uibase` manipulates such classes through
`std::shared_ptr<>` (unless those inherit `QObject`).
Using `std::unique_ptr<>` is not possible since `std::unique_ptr<>` cannot have custom
runtime-specified deleters.

## Things to remember

Here are a few things to remember when creating bindings:

- If a function has multiple overloads that can conflict in Python, the more complex
  one must be defined first as pybind11 will try calling them in order.
- If a C++ function expect a `QString`, `QFileInfo` or `QDir` that represents a file or
  a directory, wrapping the function with `wrap_for_filepath` or `wrap_for_directory` is
  a good idea. This allows Python to call the function with `pathlib.Path`.
- Most of the C++ function taking a reference to modify in C++ cannot be directly
  exposed in Python since Python cannot modify reference to simple type (e.g.
  `QString&` or `int&`).
  The best way to expose such function is to bind a lambda that returns a variant from
  Python, e.g.

```cpp
// assume the C++ function is QString fn(QString& foo, QString const& bar, int& maz);

m.def("function", [](QString& foo, QString const& bar, int& maz) {
  // call the function
  auto ret = function(foo, bar, maz);

  // make a tuple containing the return value (if there is one), and the modified
  // values passed by reference
  return std::make_tuple(ret, foo, maz);
});
```