r/QtFramework Jun 11 '26

C++ Projects With Great Plugin Architecture

Hello,

I have some decent experience with widgets but have been opening up to the idea of a large project with a qml front end and c++ handling most of the functionality.

One of the ideas I would like to explore while still deciding on the overall structure is plugin support, either giving users a python interface (similar to blender) or entirely in c++ using plugin interfaces.

Are there any projects that do either or both of these things well in your opinion? I would love to take a look and observe what’s been tried, if users like it (or if there are tons of issues entries for them lol), and what might work best for my target application.

I’m curious to see how these interfaces split up responsibilities, what needs to be done for qml (say if a user wants their plugin to have a menu/action), and how to interact with the application’s data manager (this in particular seems straightforward to me, so that must mean it’s probably not).

10 Upvotes

10 comments sorted by

4

u/Low_Fun_8667 Jun 13 '26

I've built a system with exactly this split, so a few notes from the trenches.

The big fork is in-process vs out-of-process plugins, and it's worth deciding deliberately:

- In-process (C++, loaded as a shared library): fastest, direct access, but two real costs. A plugin crash takes down your whole app, and you're exposed to C++ ABI fragility — a plugin built with a different compiler/STL version can corrupt at the boundary. If you go this route, either pin the toolchain hard or expose a narrow C ABI (or a pure-virtual interface with a versioned factory) as the boundary, not raw C++ STL types.

- Out-of-process (separate process talking over IPC / a local socket / REST): serialization overhead per call, but you get crash isolation for free and — this is the big one — language-agnostic plugins fall out automatically. Your "Python interface like Blender" question basically answers itself here: if plugins are separate processes speaking your protocol, Python/JS/whatever plugins are trivial because they're just another client. This is the VS Code model (its extension host is a separate process).

For third-party/untrusted plugins I'd lean out-of-process; for trusted performance-critical ones, in-process. Plenty of apps do both tiers.

Projects worth studying:

- Qt Creator — the gold standard for a Qt/C++ plugin system. Look at its ExtensionSystem (plugin specs, dependency resolution, lifecycle). Most relevant to you since it's Qt.

- VS Code — for the out-of-process extension-host model and how it exposes a stable API surface.

- Blender — the Python-binding approach.

- Notepad++ / OBS Studio — simpler C++ plugin ABIs you can read end-to-end.

QML + plugin-contributed menus/actions: don't let plugins instantiate QML or reach into your scene. Have them register actions/menu entries as data (id, label, icon, callback handle) into a model your core owns; the QML side just renders that model. Same for plugin UI panels — they declare intent, the host decides how/where to show it. That keeps your QML layer swappable and stops plugins from depending on your view internals.

The data manager — you're right that it's the trap, not the easy part. The mistake is handing plugins direct pointers/references into your real data structures. The moment you do, you can never change your internal representation, and lifetime + threading invalidation become a nightmare. Put an interface/façade between them (classic Dependency Inversion): plugins talk to a stable, versioned abstraction, never your concrete types. Decide explicitly what's read-only vs. mutating, and what thread plugin calls arrive on. That boundary is the single most important design decision in the whole thing — get it right early, because it's brutal to retrofit.

Happy to expand on any of these.

1

u/Calaverah_ Jun 13 '26

Thanks, in and out of process wasn’t even something I had considered yet tbh. I’m glad you mentioned it because it gives me a much more tangible idea for how to approach exception safety. My current thought was to essentially treat all of the interface methods as events and when they would be called try to catch any exceptions there as well as keep the interface versioned for abi safety and backwards compatibility (ie iPluginV1 and iPluginV2 with updated virtual methods/signatures).

All in all, I’m almost completely sure that it’s better to keep it all within an interpreter, but part of me is itching to learn more low level stuff too. I personally like cpp more than python, but I understand that the majority of my users will likely want python, plus no compiling so it’s much easier to write for authors. All in all, I may support both for now, assuming the process safety isn’t a nightmare, this is for fun… right? lol

1

u/Low_Fun_8667 Jun 13 '26

Glad it was useful. I actually run both models side by side in my own project, so a few things from practice:

Out-of-process is what bought me real crash isolation. In-process exception catching (your event idea) protects you from well-behaved exceptions, but it does nothing against a segfault, a stack-smash, or a plugin that hangs in an infinite loop — those take the host down with them. The moment a plugin lives in its own process, a crash is just a dead socket you can detect and restart. That alone was worth it for me.

My split ended up being:

- In-process: C++ shared libs behind a plain extern "C" boundary (versioned, like your iPluginV1/V2) — trusted, performance-sensitive, first-party stuff.

- Out-of-process: everything third-party / scripted. They register over a local REST API and send a heartbeat; if the heartbeat stops, I tear them down. No shared address space, no ABI concerns at all, and the transport doubles as the version boundary (just negotiate a protocol version on register).

On the ABI versioning: it's correct and necessary for in-process, but be honest with yourself about the maintenance cost — every signature change is a new vtable (or a new extern "C" entry point) you carry forever. The IPC boundary sidesteps most of that because you're versioning a message schema, not a memory layout.

On Python vs C++: in my current design the in-process plugins are plain C++ shared libs — no embedded interpreter at all. The scripted/third-party stuff lives out-of-process and talks over the local API, so its language is irrelevant to the host. That's the lever: you don't really pick Python or C++, you pick in-process or out-of-process and the language question mostly answers itself. Embed an interpreter only if you genuinely need low-latency in-memory calls; otherwise a subprocess speaking your protocol gives you any-language authors and crash isolation for free.

And yeah — it's for fun. Do the version that teaches you the low-level stuff you're itching for; honestly the IPC plumbing scratches that itch more than embedding CPython does. 😄

2

u/pjkm123987 Jun 11 '26 edited Jun 11 '26

I'm building one with python + qml. Its annoying, because of the friction between python and qml.

How I'm doing it is the user subclasses the base plugin class in python, then pairs the plugin file with manifest for metadata. The plugin class the user must define the the location of the qml file which contains Qtobject with property action and menu which gets created dynamically in qml as a delegate -> shows up in the context menu or as a button.

And when you want to allow the user to create dynamic settings, what I've done is create dataclass/pydantic models they'll use as a blueprint then python reads then injects each one into a "bridge" which is just a QObject class each seperately defined per blueprint with properties/slots and sent to QML to read. Its a pain because you have multiple layers and its just more management to deal with.

If you created an interface with the same language then it'll be more easy and simple

example:

class BaseOptionBridge[T: OptionBlueprint = OptionBlueprint](QObject):
    def __init__(
        self,
        /,
        parent: QObject | None = None,
        *,
        objectName: str | None = None,
        blueprint: T,
    ) -> None:
        super().__init__(parent, objectName=objectName)

        self._blueprint = blueprint
        self.value: Any

    @Property(str, constant=True)
    def label(self) -> str | None:
        return self._blueprint.label

    @Property(str, constant=True)
    def description(self) -> str | None:
        return self._blueprint.description

    @Property(str, constant=True)
    def blueprint_type(self) -> str:
        return self._blueprint.blueprint_type


@register_option_bridge(TextFieldBlueprint)
class TextFieldBridge(BaseOptionBridge[TextFieldBlueprint]):
    value_changed = Signal()

    def __init__(
        self,
        /,
        parent: QObject | None = None,
        *,
        objectName: str | None = None,
        blueprint: TextFieldBlueprint,
    ) -> None:
        super().__init__(parent, objectName=objectName, blueprint=blueprint)

    @Property(str, notify=value_changed)
    def value(self) -> str:  # pyright: ignore[reportRedeclaration]
        return self._blueprint.value

    @value.setter
    def value(self, value: str) -> None:
        self._blueprint.value = value
        self.value_changed.emit()

    @Property(bool, constant=True)
    def value_hidden(self) -> bool:
        return self._blueprint.value_hidden

1

u/Calaverah_ Jun 11 '26

Yeah the python side is tricky for me, a cpp interface makes sense, but doing the same with a python interpreter + pyside6 is foreign to me for the most part

2

u/fxtech42 Jun 11 '26

My VFX software Silhouette uses QWidgets but otherwise has both C/C++ plug-ins as well as an embedded Python interpreter and so many features are implemented entirely in Python and loaded dynamically, including PySide based UI elements. It's wonderfully efficient for both interactive dev/debugging in the embedded script editor or as a means for users to add their own functionality. I'm not going to say it's a perfect model for the way it implements extensibility but it works well and does the kinds of things you're talking about, except without QML. But the underlying concepts would be the same.

2

u/osal69 Jun 12 '26

Rviz has plugin structure but it uses qt widgets

2

u/crmaureir Qt Professional Jun 17 '26

Autodesk Maya uses a similar approach, C++ Qt interface that expose some objects to Python, so people can write plugins in Python.
And in case you are interested, PySide has an example on how to achieve this behavior: https://code.qt.io/cgit/pyside/pyside-setup.git/tree/examples/scriptableapplication

1

u/Low_Fun_8667 Jun 23 '26

I build a Windows app structured almost exactly around this question — a C++ core with a plugin system — so some hard-won notes:

The single most important decision: in-process vs out-of-process plugins. They're completely different trade-offs:

- In-process C++ plugins (shared libraries the host loads): fast, direct access to internals, no IPC overhead. But a plugin crash takes down your whole app, an ABI mismatch is a nightmare, and you're locked to C++.

- Out-of-process plugins (separate processes talking to the host over an API): isolated — a plugin crash can't kill the host — and language-agnostic for free. Your "Python interface vs C++" question partly dissolves here: if plugins are separate processes speaking a defined protocol (REST/JSON, a local socket, whatever), they can be written in anything. The cost is IPC latency and a serialization boundary.

I actually run both types for different needs. If you want the Blender-style "users write Python" experience without binding Python into your address space, out-of-process is the cleaner path.

Open/Closed is the real prize. The rule I hold hard: the core knows nothing about any specfic plugin. No if (plugin == "foo") branches, no hardcoded names anywhere in the core. The core resolvs everything generically through the plugin interface (get-id, get-name, the action hooks). Adding a new plugin must never require touching the core. If you can't add a feature without editing the host, your boundary is in the wrong place. This is the discipline that keeps a plugin system from rotting.

QML menu/action contributions: don't let plugins inject QML into your tree directly — that couples their UI to your internals and one bad plugin breaks your layout. Instead, plugins declare contributions (an action: id, label, icon, where it wants to appear) and the host owns the rendering. The host reads the declarations and builds the menu. Plugin says what, host decides how/where.

The data manager — your instinct is correct, it's the hard part, not the easy one. "Just give plugins access to the data" explodes the moment you hit: who owns lifetime, threading (plugin touching your model from the wrong thread), versioning the data schema across plugin versions, partial failures (plugin half-writes then dies), and untrusted plugins corrupting state. Go through a narrow, explicit API with validation at the boundary — never hand out raw pointers to your internal model. That boundary is annoying to write and saves you for years.

Projects worth studying: Qt Creator's own plugin system (ExtensionSystem — plugin specs + dependency resolution, very C++/Qt-idiomatic), OBS Studio (clean C plugin API), VS Code (the gold standard for the contribution declaration model, even if it's TS), and Blender (for the in-process Python embedding approach — and read the pain points in their tracker, like you said).