docs(c++): FileOriginPython bridge — Python-to-C++ origin adapter
Document the FileOriginPython bridge: registration via FreeCADGui.addOrigin()/removeOrigin(), method dispatch pattern with GIL management, callStringMethod/callBoolMethod/callMethod helpers, document argument marshalling (App::Document* <-> App.Document), enum mapping tables for OriginType and ConnectionState, required vs optional Python interface methods with defaults, error handling (never propagates Python exceptions into C++), icon resolution via BitmapFactory, and lifetime/ ownership model between Py::Object, _instances vector, and OriginManager unique_ptr. Closes #135
This commit is contained in:
@@ -52,3 +52,4 @@
|
||||
- [FileOrigin Interface](./reference/cpp-file-origin.md)
|
||||
- [OriginManager](./reference/cpp-origin-manager.md)
|
||||
- [CommandOrigin](./reference/cpp-command-origin.md)
|
||||
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)
|
||||
|
||||
257
docs/src/reference/cpp-file-origin-python.md
Normal file
257
docs/src/reference/cpp-file-origin-python.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# FileOriginPython — Python-to-C++ Bridge
|
||||
|
||||
> **Header:** `src/Gui/FileOriginPython.h`
|
||||
> **Implementation:** `src/Gui/FileOriginPython.cpp`
|
||||
> **Python binding:** `src/Gui/ApplicationPy.cpp`
|
||||
> **Namespace:** `Gui`
|
||||
|
||||
`FileOriginPython` is an Adapter that wraps a Python object and presents
|
||||
it as a C++ [FileOrigin](./cpp-file-origin.md). This is how Python
|
||||
addons (like SiloOrigin) integrate with the C++
|
||||
[OriginManager](./cpp-origin-manager.md) without compiling any C++ code.
|
||||
|
||||
## Registration from Python
|
||||
|
||||
The bridge is exposed to Python through three `FreeCADGui` module
|
||||
functions:
|
||||
|
||||
```python
|
||||
import FreeCADGui
|
||||
|
||||
FreeCADGui.addOrigin(obj) # register a Python origin
|
||||
FreeCADGui.removeOrigin(obj) # unregister it
|
||||
FreeCADGui.getOrigin(obj) # look up the wrapper (or None)
|
||||
```
|
||||
|
||||
### `FreeCADGui.addOrigin(obj)`
|
||||
|
||||
1. Checks that `obj` is not already registered (duplicate check via
|
||||
`findOrigin`).
|
||||
2. Constructs a `FileOriginPython` wrapper around `obj`.
|
||||
3. Calls `obj.id()` immediately and caches the result — the ID must be
|
||||
non-empty or registration fails.
|
||||
4. Passes the wrapper to
|
||||
`OriginManager::instance()->registerOrigin(wrapper)`.
|
||||
5. If `registerOrigin` fails (e.g., duplicate ID), the wrapper is removed
|
||||
from the internal instances list (OriginManager already deleted it).
|
||||
|
||||
### `FreeCADGui.removeOrigin(obj)`
|
||||
|
||||
1. Finds the wrapper via `findOrigin(obj)`.
|
||||
2. Removes it from the internal `_instances` vector.
|
||||
3. Calls `OriginManager::instance()->unregisterOrigin(id)`, which deletes
|
||||
the wrapper.
|
||||
|
||||
### Typical Usage (from SiloOrigin)
|
||||
|
||||
```python
|
||||
# mods/silo/freecad/silo_origin.py
|
||||
|
||||
class SiloOrigin:
|
||||
def id(self): return "silo"
|
||||
def name(self): return "Kindred Silo"
|
||||
# ... remaining interface methods ...
|
||||
|
||||
_silo_origin = SiloOrigin()
|
||||
|
||||
def register_silo_origin():
|
||||
FreeCADGui.addOrigin(_silo_origin)
|
||||
|
||||
def unregister_silo_origin():
|
||||
FreeCADGui.removeOrigin(_silo_origin)
|
||||
```
|
||||
|
||||
This is called from `InitGui.py` via a deferred `QTimer.singleShot`
|
||||
(1500 ms after GUI init) to ensure the Gui module is fully loaded.
|
||||
|
||||
## Method Dispatch
|
||||
|
||||
Every C++ `FileOrigin` virtual method is implemented by delegating to the
|
||||
corresponding Python method on the wrapped object. Three internal
|
||||
helpers handle the marshalling:
|
||||
|
||||
| Helper | Signature | Used For |
|
||||
|--------|-----------|----------|
|
||||
| `callStringMethod` | `(name, default) → std::string` | `id()`, `name()`, `nickname()`, `icon()` |
|
||||
| `callBoolMethod` | `(name, default) → bool` | All `supportsXxx()`, `tracksExternally()`, etc. |
|
||||
| `callMethod` | `(name, args) → Py::Object` | Everything else (document ops, state queries) |
|
||||
|
||||
### Dispatch Pattern
|
||||
|
||||
Each overridden method follows the same structure:
|
||||
|
||||
```
|
||||
1. Acquire GIL ← Base::PyGILStateLocker lock
|
||||
2. Check if Python object has attr ← _inst.hasAttr("methodName")
|
||||
3. If missing → return default ← graceful degradation
|
||||
4. Build Py::Tuple args ← marshal C++ args to Python
|
||||
5. Call Python method ← func.apply(args)
|
||||
6. Convert result ← Py::String/Boolean/Long → C++ type
|
||||
7. Return ← release GIL on scope exit
|
||||
```
|
||||
|
||||
If the Python method is missing, the bridge returns sensible defaults
|
||||
(empty string, `false`, `nullptr`). If the Python method raises an
|
||||
exception, it is caught, reported to the FreeCAD console via
|
||||
`Base::PyException::reportException()`, and the default is returned.
|
||||
|
||||
### Document Argument Marshalling
|
||||
|
||||
Methods that take `App::Document*` convert it to a Python object using:
|
||||
|
||||
```cpp
|
||||
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
|
||||
{
|
||||
if (!doc) return Py::None();
|
||||
return Py::asObject(doc->getPyObject());
|
||||
}
|
||||
```
|
||||
|
||||
This returns the standard `App.Document` Python wrapper so the Python
|
||||
origin can call `doc.Objects`, `doc.FileName`, `doc.save()`, etc.
|
||||
|
||||
Methods that **return** an `App::Document*` (like `newDocument`,
|
||||
`openDocument`) check the return value with:
|
||||
|
||||
```cpp
|
||||
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
|
||||
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
|
||||
}
|
||||
```
|
||||
|
||||
If the Python method returns `None` or a non-Document object, the bridge
|
||||
returns `nullptr`.
|
||||
|
||||
## GIL Management
|
||||
|
||||
Every method acquires the GIL (`Base::PyGILStateLocker`) before touching
|
||||
any Python objects. This is critical because the C++ origin methods can
|
||||
be called from:
|
||||
|
||||
- The main Qt event loop (menu clicks, toolbar actions)
|
||||
- `OriginManager::findOwningOrigin()` during document operations
|
||||
- Signal handlers (fastsignals callbacks)
|
||||
|
||||
All of these may run on the main thread while the GIL is not held.
|
||||
|
||||
## Enum Mapping
|
||||
|
||||
Python origins return integers for enum values:
|
||||
|
||||
### `OriginType` (from `type()`)
|
||||
|
||||
| Python `int` | C++ Enum |
|
||||
|-------------|----------|
|
||||
| `0` | `OriginType::Local` |
|
||||
| `1` | `OriginType::PLM` |
|
||||
| `2` | `OriginType::Cloud` |
|
||||
| `3` | `OriginType::Custom` |
|
||||
|
||||
### `ConnectionState` (from `connectionState()`)
|
||||
|
||||
| Python `int` | C++ Enum |
|
||||
|-------------|----------|
|
||||
| `0` | `ConnectionState::Disconnected` |
|
||||
| `1` | `ConnectionState::Connecting` |
|
||||
| `2` | `ConnectionState::Connected` |
|
||||
| `3` | `ConnectionState::Error` |
|
||||
|
||||
Out-of-range values fall back to `OriginType::Custom` and
|
||||
`ConnectionState::Connected` respectively.
|
||||
|
||||
## Icon Resolution
|
||||
|
||||
The `icon()` override calls `callStringMethod("icon")` to get a string
|
||||
name, then passes it to `BitmapFactory().iconFromTheme(name)`. The
|
||||
Python origin returns an icon name (e.g., `"silo"`) rather than a QIcon
|
||||
object.
|
||||
|
||||
## Required Python Interface
|
||||
|
||||
Methods the Python object **must** implement (called unconditionally):
|
||||
|
||||
| Method | Signature | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `id()` | `→ str` | Unique origin ID (cached on registration) |
|
||||
| `name()` | `→ str` | Display name |
|
||||
| `nickname()` | `→ str` | Short toolbar label |
|
||||
| `icon()` | `→ str` | Icon name for `BitmapFactory` |
|
||||
| `type()` | `→ int` | `OriginType` enum value |
|
||||
| `tracksExternally()` | `→ bool` | Whether origin syncs externally |
|
||||
| `requiresAuthentication()` | `→ bool` | Whether login is needed |
|
||||
| `ownsDocument(doc)` | `→ bool` | Ownership detection |
|
||||
| `documentIdentity(doc)` | `→ str` | Immutable tracking key |
|
||||
| `documentDisplayId(doc)` | `→ str` | Human-readable ID |
|
||||
|
||||
Methods the Python object **may** implement (checked with `hasAttr`):
|
||||
|
||||
| Method | Signature | Default |
|
||||
|--------|-----------|---------|
|
||||
| `supportsRevisions()` | `→ bool` | `False` |
|
||||
| `supportsBOM()` | `→ bool` | `False` |
|
||||
| `supportsPartNumbers()` | `→ bool` | `False` |
|
||||
| `supportsAssemblies()` | `→ bool` | `False` |
|
||||
| `connectionState()` | `→ int` | `2` (Connected) |
|
||||
| `connect()` | `→ bool` | `True` |
|
||||
| `disconnect()` | `→ None` | no-op |
|
||||
| `syncProperties(doc)` | `→ bool` | `True` |
|
||||
| `newDocument(name)` | `→ Document` | `None` |
|
||||
| `openDocument(identity)` | `→ Document` | `None` |
|
||||
| `openDocumentInteractive()` | `→ Document` | `None` |
|
||||
| `saveDocument(doc)` | `→ bool` | `False` |
|
||||
| `saveDocumentAs(doc, id)` | `→ bool` | `False` |
|
||||
| `saveDocumentAsInteractive(doc)` | `→ bool` | `False` |
|
||||
| `commitDocument(doc)` | `→ bool` | `False` |
|
||||
| `pullDocument(doc)` | `→ bool` | `False` |
|
||||
| `pushDocument(doc)` | `→ bool` | `False` |
|
||||
| `showInfo(doc)` | `→ None` | no-op |
|
||||
| `showBOM(doc)` | `→ None` | no-op |
|
||||
|
||||
## Error Handling
|
||||
|
||||
All Python exceptions are caught and reported to the FreeCAD console.
|
||||
The bridge **never** propagates a Python exception into C++ — callers
|
||||
always receive a safe default value.
|
||||
|
||||
```
|
||||
[Python exception in SiloOrigin.ownsDocument]
|
||||
→ Base::PyException::reportException()
|
||||
→ FreeCAD Console: "Python error: ..."
|
||||
→ return false
|
||||
```
|
||||
|
||||
This prevents a buggy Python origin from crashing the application.
|
||||
|
||||
## Lifetime and Ownership
|
||||
|
||||
```
|
||||
FreeCADGui.addOrigin(py_obj)
|
||||
│
|
||||
├─ FileOriginPython(py_obj) ← wraps, holds Py::Object ref
|
||||
│ └─ _inst = py_obj ← prevents Python GC
|
||||
│
|
||||
├─ _instances.push_back(wrapper) ← static vector for findOrigin()
|
||||
│
|
||||
└─ OriginManager::registerOrigin(wrapper)
|
||||
└─ unique_ptr<FileOrigin> ← OriginManager owns the wrapper
|
||||
```
|
||||
|
||||
- `FileOriginPython` holds a `Py::Object` reference to the Python
|
||||
instance, preventing garbage collection.
|
||||
- `OriginManager` owns the wrapper via `std::unique_ptr`.
|
||||
- The static `_instances` vector provides `findOrigin()` lookup but does
|
||||
**not** own the pointers.
|
||||
- On `removeOrigin()`: the wrapper is removed from `_instances`, then
|
||||
`OriginManager::unregisterOrigin()` deletes the wrapper, which releases
|
||||
the `Py::Object` reference.
|
||||
|
||||
## See Also
|
||||
|
||||
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
|
||||
being adapted
|
||||
- [OriginManager](./cpp-origin-manager.md) — where the wrapped origin
|
||||
gets registered
|
||||
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch
|
||||
through this bridge
|
||||
- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md)
|
||||
— step-by-step guide using this bridge
|
||||
Reference in New Issue
Block a user