# Declarative Plugins / "Plugins as Data" for Bevy
## Isolating `&mut App` and `&mut World` from Plugin registration
**Tl;dr: I'm arguing we introduce the invariant "plugin registration does not touch `&mut App` or `&mut World` directly"** so we can make a bunch of things work Just Fine eventually such as **plugin dependencies**, re-registration during runtime, more information for the editor, and better hot-patching. Reasoning about plugins that have direct access to `&mut World` will only continue to cause headaches over time as the possibilities of plugins keep being tied back to "this has `&mut App`/`&mut World` access." Plugins should be about building up a database and configuration of systems, resources, observers, etc. rather than sequential, potentially arbitrary computation on `&mut App` or `&mut World`.
---
## 1. Design Goals
- Move plugin registration in the direction of being "data" / configuration.
- Restricted operations on an opaque type.
- Operations that genuinely require World access get moved to startup/pre-startup systems.
- The final set of plugin data structures get "solved" between the end of plugin registration and the start of the app running.
- Make users worry less about the details of plugin development and registration.
- Expose data about game setup in a standard, reliable way to Editors.
- Provide users with a way to resolve contradictions between plugins on their own without necessarily needing to patch or vendor dependencies.
---
Bevy currently registers plugins in a procedural way by handing off a `&mut App` to all plugin authors. This is great for plugin author flexibility, but conflicts with both hot patching workflows and future potential for having plugins expose information in a consistent enough way to make an Editor-App split trivial to reason about. Additionally, we run into problems when two plugins add the same sub-plugin.
A plugin system where **we care about the information the user is providing** rather than the user caring about what they are being passed will let us reflect the needs of users, plugin developers, and editor developers better. Few users actually break into using `SubApps` etc.
The end result of this kind of API should look largely the same to most users, but being able to say "the registration function can only do certain operations" and "contradictions between registrations can be resolved by end users" will do a lot to make plugins closer to what people expect from the plugin infrastructure of other tools.
## 2. Why change?
We need a more consistent, repeatable way of reasoning about what we expose to bevy through the Plugin API as Bevy matures and projects grow. People will want access to a lot of information about running programs, as well as the ability to toggle what's getting run without touching their codebase.
### 2.1 Treating Registrations as Data
Registration is an act that introduces knowledge to Bevy. By registering an item we increase the number of systems, messages, observers etc. that Bevy knows about. By registering something, we say "This thing exists, I want you to know about it, here is its relevant information."
This is the core of what we do in `Plugins`, we provide type information, system information, observer information, relationship information etc. This is what the Plugin framework is for.
A declarative plugin architecture lets us put a barrier between the `App` (and therefore `World`) and the act of registration.
There is a major knowledge management benefit from this: we establish the invariant "**registration does not touch a `App` or `World` directly, it just produces a database[^database] of types, systems, and configurations**." This lets us extend the purpose of plugin registration past "set up the Bevy application" and into "**produce a database of Bevy-relevant objects and reason about how to turn that into a running app.**"
[^database]: I don't mean this in the sense of SQL but in the sense of "the relevant data is together in one place." We're not serializing functions :)
### 2.2 Expose more to the Editor, and more reliably.
We're reaching a point where "the editor" is becoming real. Once we have a BSN asset format, people will be writing editors for it (and some already are, see: [jackdaw](https://github.com/jbuehler23/jackdaw).)
With the existence of an editor, expectations wills grow. If common features for analysis and extensibility don't exist early, users will at the very least want to know if there is a path to implementation.
Declarative plugins will help us expose an API surface that will remove a lot of pain by eliminating an escape hatch that practically nobody uses, and few should be using.
Being able to reliably construct a database existing Bevy program and expose that data to an Editor without assuming deep integration is where we want to be. We can solve the same graph differently depending on if we're in Editor logic or App logic.
What's stopping us from doing this right now? We expose the same information, but we don't have the invariants that make this easy to reason about with confidence. There is no split between "Editor space" items and "App space" items because we do not have such a split. Introducing such a split is possible under current infrastructure, but we would always be stuck reasoning about things that can touch a `&mut World`!
#### 2.2.1 Editor Plugins in Plugin Registration
Given an Editor is an application that reasons about Bevy data for a Bevy program in a specific way without running the whole schedule of program, and users will want to extend editors with purpose-built design tooling, having a way to reliably separate things that matter to the editor from things that matter to the program without introducing a whole new plugin API would be reasonable.
Having an intermediary step between registration and application of items to the world lets us do things like select items to actually add to the world that are relevant to the execution context, such as _editor-specific behaviors_.
Editors have extra things to care about, the [Jackdaw Wing RFC](https://github.com/jbuehler23/jackdaw/issues/38) exposes an extra set of Editor Items that would be relevant to the context of viewing information in the editor but not in the context of running a Game/Program/etc. We can imagine how this slots into this API: the Editor-only information is discarded before the graph is applied to the App in a Game/Program setting, or the Game/Program information is discarded before the graph is applied in an Editor-only setting. Items are inspected for there relevance for a given context, and for Game/Programs vs Editors there is a relatively easy split. This was published today (2026/04/14), so I've not turned it around in my head too much
### 2.3 Solve dependencies
One of the big problems in the current plugin infrastructure is Dependencies. This isn't a big issue right now because the Bevy ecosystem is _shallow and wide_, but this risks being a chicken-and-egg situation. If dependencies between plugins isn't solved, then how do we author plugins that depend on other plugins?
A plugin becomes a graph with the following items:
- **Items** (anything we register)
- **Dependencies** ("We want plugin X" or "We want resource Y")
- **Resolution callbacks** to choose between defaults
With this we have enough information to construct a full dependency graph of a program by merging these individual plugins and solving it through **topological sorting**, panicking only in the case where plugins have deeply contradictory and highly opinionated needs.
### 2.4 Allow overriding AKA Data driven system ordering
Some of the items we introduce to Bevy are _Defaults_ about a piece of information, such as system schedule labels.
We do not need to treat some of this _default information_ as the source of truth all the time.
In the case of System schedules, we get to map out an entire Bevy application's system graph through the information exposed in plugin registration calls. Then, we have a an opportunity between solving that graph and running the Bevy app to read data not defined in code that lets us re-order systems however the designer etc. sees fit.
### 2.5 Better hot-patching experience AKA Idempotent plugin registration
We got hot-patching in Bevy 0.17 via subsecond + dioxus's `dx` tooling. This has been a game changer for development iteration times in some areas, but it's lacking in others.
Plugin registration functions are not re-run on hot-patching. This means that new types, systems, global observers etc. are not made visible to a running Bevy application during these processes.
We cannot rerun plugin registration functions as it is. Re-registering something that already exists either 1. duplicates existing information (a system gets registered twice) or 2. causes a panic (plugin re-registering).
We are encouraged to create plugins in a tree-like hierarchy. This means that higher-level plugins often end up causing many smaller plugins to be registered too. This means plugin re-registration being
Being able to safely re-run a plugin registration function by having the full dependency graph and its diff between hot-patches available to us puts us in a position where we have enough information to rebuild and replace / disable everything we need to with a higher degree of safety. Or, as much as we can in a code hot-patching context.
### 2.6 ...Dynamic plugins?
If we have a general data structure that can hold arbitrary registration information (types, reflected type information, systems, observers, schedule labels, etc.) and a consistent entry point for getting that information (`extern "C"` a named plugin registration function) then we have something that, if you squint, looks like a Bevy-specific ABI.
But! Don't we already have these prerequisites with the existing plugin infrastructure? Kind of. Existing infrastructure is fragile and its purpose ends up more procedural. There's always the potential for access to `World`! This makes registration non-idempotent.
An entry point that is "this function will establish a database for information about a plugin and throw it back to you to solve" is a more stable foundation to build dynamic plugins on than what we have currently, and is much closer to how ABIs work across language boundaries.
While not a perfect solution this still puts us in a position where we can produce dynamic libraries with an obvious entry point for Bevy applications. TBD TBH, this is not my strong suit. I imagine that there will be a lot of ignoring whatever Rust type ID information exists for items and instead referring to whatever Bevy's reflect information comes to, as well as some complication surrounding how the VTables of these things will work across a FFI boundary.
Dynamic plugins are still a solid "want this" to a lot of people. People who want to make proprietary Bevy apps extensible with rust code or make proprietary extensions for things like the editor, modders who want "full access" to inserting new information to bevy programs etc.
### 2.7 Plugin groups are just plugins that have dependencies and register no new information.
Plugin groups serve as the existing solution to "a bunch of plugins depend on each other."
## 3. Existing / Possible Solutions
1. Don't Do Anything
- Plugins work fine
- A major change would touch every single part of bevy after all, and its entire ecosystem.
2. Async plugins
- Solves Plugin dependencies, but doesn't step away from "procedural" APIs
- This is a bit too imperative for my liking
- Solves 1 problem, but doesn't address others.
- Assumes a context where we still want to do arbitrary operations on `App` or an async-focused indirection of `App`, which may be true for some plugins.
3. Keep everything the same but make adding plugins & other registration operations idempotent.
- Puts extra checking-if-present logic in registration operations.
- Doesn't expose trivial & extra control to alternative execution contexts (like the editor)
- Maintains the current reasoning model.
### 3.1 Don't do anything
### 3.2 Async plugins
This solves plugin dependencies but maintains the procedural model of plugin registration. This also maintains the idea that plugins need access to `&mut App` et al.
## 4. Potential Future API
1. `Plugin`
- `#[deprecated]` alias for `ProceduralPlugin`/`FoundationalPlugin`.
2. `ProceduralPlugin`/`FoundationalPlugin`
- We **cannot** rerun this on hot-patching
- This is what the current plugin API is.
- We can do anything here, we get a `&mut App`
- Still useful in some circumstances!
- Could still be `async`
- We should, over time, make this looks more like a Scary Engine Internal rather than the preferred entry point for users.
3. `DeclarativePlugin`
- We **can** rerun this on hot-patching (hypothetically)
- A plugin system that makes bevy "aware" of functions, methods, types at run time.
- Limited operations:
- Register systems with a schedule label _preference_ or _default_
- Register schedule labels
- Register messages
- Ask for resources
- Add observers
- Register a dependency on another plugin, with a preferred "plugin state" input.
- Focuses on handing a coherent data structure to bevy to "solve"
- We can perform "dry runs" on the information this gives to bevy in contexts like i.e. the editor.
- **Produces a graph-like data structure** that can be merged with other outputs of other `DeclarativePlugin` implementations.
## 5. In Practice
This is a sketch of what could be possible from a declarative API. This isn't meant to be much different from the existing API, but the semantics are closer to suggesting defaults than setting values.
```rust
pub struct MyDeclrPlugin;
impl DeclarativePlugin for MyDeclrPlugin {
fn build(&self, &app: &mut DeclarativePluginOutput) {
// Identical to current system add/registration API
app.add_systems(Update, my_normal_system);
app.register_system(my_other_normal_system);
// Registering systems, only adding some of them to schedules.
app.add_systems_but_more_datay((
// Let the ECS know about system_1, don't put it in any given
// schedule.
system_1,
// Let the ECS know about system_2 + put it in the update
// schedulelabel.
(system_2, Update),
system_3,
system_4,
(system_5, Last),
// Let the ECS know about system_6, and put it after system_2
(system_6, After(system_2)),
));
// Adding schedule labels
app.add_schedules((
// Schedule labels with a default of happening after startup
After(Startup, (
MyCoolStartup,
MyCoolPostStartup
)),
// Schedule labels with a default of happening before each
// update
Before(Update, (
MyPreUpdate,
MyPostPreUpdate,
)),
// A schedule label that isn't assigned to anywhere in the
// existing schedule.
MyUnplacedSheduleLabel
));
// Add an observer and default to it being a global observer
app.add_observer(my_observer);
// Register an observer, but don't put it anywhere and give it a
// different name than its function name for i.e editors
app.register_observer(
// Observer
my_named_observer,
// Name it's registered under
Some("giving_it_a_special_name".into())
);
// Register a message type
app.add_message::<MyMessageType>();
// Register a plugin dependency
app.require_plugin::<OtherPlugin>();
// Register a plugin dependency, offering up an initialization value
// for it, and requiring that its `internal` value be exactly 7.
app.require_plugin_with_value(
// Proposed init value
AnotherPlugin{ internal: 7 },
// Conflict resolution function
|another_plugin| another_plugin.internal == 7
);
// Require a resource, no matter what its value is.
app.require_resource::<SomeResource>();
// Require a resource, but its `another_resource` should be 10 or less.
app.require_resource_with_value::<AnotherResource>(
// Proposed init value
AnotherResource{ value: 4.0 },
// Conflict resolution function
|another_resource| another_resource.value < 10.0
);
}
}
```
The API we want to establish internally is elaborated on a bit further down at [Design Data Structures](#2-Design-data-structures-and-Operations)
### 5.1 Non-Contradicting Cycles
One of the benefits of a more "deferred" plugin registration process is that we get to deal with duplications and cycles in some categories of registration. Systems can be registered in multiple schedules. Components only need to be registered once. Messages are a trivial thing to register.
### 5.2 True Contradiction Points
One way we can tackle deadlocks is by giving the plugin authors a way to evaluate, via callback, if a resource, plugin, or schedule deviates too far from their assumptions. We expect most conflict resolution callbacks to be of the form "`|_| true`" – they do not care what their dependency is initialized to, only that it exists. For these "everything's fine as long as it exists" dependencies, conflict resolution is trivial.
The other ways we could solve this is with a static "preference precedence" value, or we could solve this simply by crashing.
We will want some kind of precedence, as we want to be able to "override" things like resources the closer we are to the true running app logic. We can maybe consider depth-of-dependency here.
### 5.3 Resource Contradictions
What happens if two plugins both have opinions about something like a `Resource`? Multiple plugins could register them
There's a couple of ways to solve this
1. An additional callback of type `&R -> bool` when registering a "sensitive" type like a resource that "asks" if a resource with a specific config is "okay" and go through all the different potential `Resource` values one by one until all plugins return "true" on one of the resources
- If we can't find a suitable resource for all plugins that want that resource, crash!
- Problems: requires exposing resource internals for users to evaluate. But this is unlikely to be a super frequent case, as plugins from different crates don't usually have strong opinions on common resources.
- It could also be that a plugin would want to evaluate resources in n-tuples i.e. "if this database exists and looks like this AND if this other resources matches my expectations, then we're all good." But this could also be the kind of logic that exists at Startup system time.
3. Pick one and let the program report errors, or crash if bevy is configured to crash on this kind of thing.
4. Each registration of a resource comes with an `Ord` "precedence" value and the one with the highest precedence wins out.
This would have `O(N * M)` complexity where N is the number of resources registered and M is the number of plugins that registered that resource. These numbers are likely to be low, but it's something to keep in mind.
### 5.4 Plugin Dependency Contradictions
One can imagine a circular dependency between three plugins. `Game` wants `A` who wants `B` wants `C` wants `A`, and maybe the initialization of plugin `A` from `Game` is different from the one `C` prefers!
Once again, we have a couple of ways to solve this:
1. Like with resources, for a clash in dependencies between plugins of type `A` we have a callback of type `&A -> Bool` that takes the plugin with conflicting values and evaluates if a plugin will work just fine with that value.
3. Pick one and let the program report errors, or crash if bevy is configured to crash on this kind of thing.
4. Each registration of a plugin comes with an `Ord` "precedence" value and the one with the highest precedence wins out.
Plugins in a declarative plugin "dependency graph" can be expanded to the point where we can solve for the particular items registered in each plugin.
### 5.5 Schedule Contradictions
I personally do not know if you can place a `ScheduleLabel` into two places in the overall `Schedule`. If you can't, then two plugins registering the same schedule label at different points is something to consider having the program crash at.
## 6 Path to Implementation
### 6.1. Change the input type of `Plugin` to a newtype over `App`
Most of the internal changes don't need to happen right away. The simplest change is newtyping `App` before passing it to plugin `build` methods for the declarative plugin system.
```rust
#[deprecated("Use `DeclarativePlugin`, or `FoundationalPlugin`")]
pub trait Plugin { /* Old API */ }
// Newtype App to change the reasoning model for users.
pub struct DeclarativePluginOutput(App);
impl DeclarativePluginOutput {
// Expose a similar/identical API as App wrt Plugins, but don't
// expose `App`, `SubApp`, or `World`.
}
pub trait DeclarativePlugin: ... {
fn build(&self, &mut DeclarativePluginOutput) {
// Identical API usage to current model, for most users.
}
}
```
This establishes the main invariant we want, but only doing this wouldn't make plugins declarative. What it does do is prompt developers to figure out if they _really do need access to internals during registration_ and punts them to the "more internal" plugin system if they do.
In this position, we've hidden `App` but a lot of operations still aren't idempotent. Additionally, nothing has changed wrt the ability to hot-patch registration. But because we've set up the invariant we need, replacing `App` with an opaque-to-the-user type, we can start to build up the declarative properties.
### 6.2. Design data structures and Operations
We need the following data structures (names illustrative, not final):
1. `DeclarativePluginOutput`: Plugin Output
2. `PluginOutputSet`: Set of Plugin Output
3. `PluginGraph`: Graph of merged Plugin Outputs
4. `PluginGraphDiff`: Diff on Graph of merged Plugin Outputs
So we can have the following operations:
```rust
fn build_declr(&self: impl DeclarativePlugin, out: &mut DeclarativePluginOutput);
// Plugin output can be added to the Plugin Set easily.
fn add_plugin(self: PluginOutputSet, output: impl DeclarativePlugin)
-> PluginOutputSet;
// The plugin output set can be transformed into a graph.
fn solve_set(self: &PluginOutputSet)
-> Result<PluginGraph, Err>;
// The graph can be diffed.
fn graph_diff(self: &PluginGraph, other: &PluginGraph)
-> Option<PluginGraphDiff>;
// The graph can be applied to an app, directly replacing existing items.
fn apply_graph(self: &mut App, graph: &PluginGraph)
-> Result<(), Err>;
```
This could be missing things, as always. To be iterated on.
Once these data structures are built we can replace the `App` in the newtype with a data structure that holds these pieces of data directly.
#### 6.2.1 Individual plugin data structure (`DeclarativePluginOutput`)
`DeclarativePluginOutput` is the "unit" of data that declarative plugin build functions output. It manages the following for **single plugin build functions**:
1. Plugin data structure init data.
2. Plugin dependencies.
3. Resources and the default preferences of those resources.
4. Conflict resolution callbacks for plugins, resources (and maybe schedule labels?)
5. Schedule labels and where to put them.
6. Message types.
7. Reflect information? TypeId info?
We already store most of this information in Bevy apps somewhere.
#### 6.2.2 Intermediary structure (`PluginSet`)
This just needs to hold many instances of `DeclarativePluginOutput` and associate them with type information or any other uniquely-identifying information.
#### 6.2.3 Wider data structure (`PluginGraph`)
Once we have the data structure from individual plugins, we can build a more broad graph-like structure that contains:
1. Plugins (the rust data structure) and the plugins that it depends on / that depend on it.
2. Resources and the plugin source that initialized / requested them.
3. Pointers to systems, their bevy-ecs relevant information, their "default" schedule label, and which plugin inserted them.
4. Pointers to observers, their relevant ecs information, if they're registered under a specific name.
5. Messages that need to be initialized.
6. Schedule labels, and which plugin inserted them.
From here we can build a graph, establish where conflicts exist, run conflict resolution callbacks to see if there's a solution, and if a solution is possible we can register & replace information in an `App` in the order we get from a topsort of the final dependency graph.
#### 6.2.4 Graph Diff (`PluginGraphDiff`)
Needed so we know what has changed, and how to minimally replace items in a running `World`.
### 6.3. Bridge gap in current Bevy API, if it exists. Draw the rest of the owl, etc.
There's unstated API needs in this document, and I don't think all of them are currently met by the existing internal Bevy API. Downside of writing a technical document instead of a prototype.
I don't think there's much to bridge, but this will depend on specific missing components from Bevy's internals.
More here TBD.
---
<details>
<summary>Glossary</summary>
- Items: Things that are registered by a plugin. Systems, global observers, messages, dependencies, etc.
- Hot-patching / hot-patching: Code recompilation & re-injection at runtime, via the dioxus toolchain.
- Idempotent/Idempotence: The ability to do something more than once without the duplicate action changing the state of things. i.e. `f(f(x)) == f(x) && f(x) != x`, or "The wall was green before I painted it red. Then I painted it red a second time, this didn't cause the application to crash because painting the wall the color it already was is an idempotent operation."
</details>