Skip to content

bevy_dynamic_plugin: Don't leak memory#6705

Closed
soqb wants to merge 14 commits into
bevyengine:mainfrom
soqb:no-dynamic-loading-leak
Closed

bevy_dynamic_plugin: Don't leak memory#6705
soqb wants to merge 14 commits into
bevyengine:mainfrom
soqb:no-dynamic-loading-leak

Conversation

@soqb

@soqb soqb commented Nov 20, 2022

Copy link
Copy Markdown
Contributor

Objective

  • Currently bevy_dynamic_plugin::dynamically_load_plugin calls std::mem::forget on the libloading::Library instance. An escape hatch should be added to allow users to more easily handle the unloading of dynamic plugins.

Solution

  • Add a DynamicPlugin struct which can be dropped manually.

  • Add a new resource DynamicPluginLibraries which allows users to (unsafely) mark libraries for automatic unloading with DynamicPluginLibraries::mark_for_unloading. The library will be dropped when the associated DynamicPlugin is or when mark_for_unloading is called, whichever happens last.


Changelog

  • Added DynamicPlugin.
  • Added DynamicPluginLibraries.
  • Changed the return type of dynamically_load_plugin to DynamicPlugin.
  • Added bevy_ecs and bevy_utils dependencies for bevy_dynamic_plugin.

Migration Guide

  • dynamically_load_plugin now returns a DynamicPlugin instead of a tuple. Call DynamicPlugin::into_raw_parts and Arc::try_unwrap on the resulting library if you need the old behavior.

@alice-i-cecile alice-i-cecile added C-Code-Quality A section of code that is hard to understand or change A-App Bevy apps and plugins labels Nov 21, 2022
Comment thread crates/bevy_dynamic_plugin/src/loader.rs Outdated
@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Nov 21, 2022

@alice-i-cecile alice-i-cecile left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how much cleaner this is. And avoiding leaking memory could be useful for modding.

However, I'm definitely nervous about the long list of non-local invariants that must be upheld to avoid UB.

That's largely the story of this whole crate though, so my official stance on this PR is "neutral".

@soqb

soqb commented Nov 21, 2022

Copy link
Copy Markdown
Contributor Author

I'm definitely nervous about the long list of non-local invariants that must be upheld to avoid UB.

This is definitely an issue - as libloading states loading dynamic libraries is in theory always UB by definition but never in practice with a well-defined library that satisfies the relevant safety invariants.

The safety of the running an app with dynamic plugins relies on the safety calling dynamically_load_library. This PR fixes a memory leak (albeit intentional), but adds another safety invariant onto that function.

The safety notes on DynamicPlugin (ignoring that I now think they should be moved to dynamically_load_plugin for clarity and publicity) are unclear as they are now and could inadvertently cause UB. (e.g. An app defined in crate my_app loads a dynamic plugin defined in libdynamic.so which both depend on the my_dep crate. The dynamic plugin adds a resource defined in my_dep that contains a Box<dyn Fn()> and my_app retrieves it (because it's nameable through my_dep and may have the same TypeId if the same version of my_dep was built) and calls it after the app is dropped - inadvert UB but not a soundness hole.)

The only more complete solution i can conceive involves obfuscating dynamically-defined types and providing dynamic plugins with a more carefully controlled app scope.

I think for now I will change this PR to be leaky by default, with opt-in automatic dropping.

Comment thread crates/bevy_dynamic_plugin/src/loader.rs Outdated
@soqb soqb force-pushed the no-dynamic-loading-leak branch from b6a8662 to 80ab1b3 Compare November 21, 2022 21:39
@soqb

soqb commented Nov 21, 2022

Copy link
Copy Markdown
Contributor Author

anyone know what this ci failure is about?

@alice-i-cecile

Copy link
Copy Markdown
Member

Not your fault: there's duplicate dependencies in tree due to staggered updates. It won't block merging.

@soqb soqb force-pushed the no-dynamic-loading-leak branch from c035dde to e1ddf8d Compare December 3, 2022 16:55
@inodentry

Copy link
Copy Markdown
Contributor

Hmm. This is getting a little over my head now. I don't fully understand all the safety implications here. How is this intended to be used exactly?

If I'm understanding correctly, this PR adds a mechanism for unloading plugins. I cannot really imagine what that would look like in real user code.

@alice-i-cecile

Copy link
Copy Markdown
Member

If this was used for modding, unloading plugins would be useful for disabling mods without a game restart.

@inodentry

Copy link
Copy Markdown
Contributor

I understand the benefits of "unloading plugins" as a feature.

I meant that I don't understand how this particular API is to be used, and the safety implications of it.

@soqb

soqb commented Jan 4, 2023

Copy link
Copy Markdown
Contributor Author

I meant that I don't understand how this particular API is to be used, and the safety implications of it.

as it stands i'm not very happy with the api/implementation of this PR. i'm taking another look at the moment to see if i can come up with a better design.

@soqb soqb force-pushed the no-dynamic-loading-leak branch from 0daa0e7 to edc4488 Compare January 6, 2023 12:40
@soqb

soqb commented Jan 6, 2023

Copy link
Copy Markdown
Contributor Author

I meant that I don't understand how this particular API is to be used, and the safety implications of it.

an example might help:

use bevy_dynamic_plugin::{DynamicPluginLibraries, DynamicPluginExt};
use bevy_app::App;
use bevy_ecs::system::ResMut;

const LIB_PATH: &str = "./libmy_dyn_plugin.so";

let mut app = App::new();
unsafe { app.load_plugin(LIB_PATH)
app.add_system(remove_library);

fn remove_library(mut libs: ResMut<DynamicPluginLibraries>) {
    // use arbitrary logic to decide whether to unload the library:
    if 1 != 2 {
        // SAFETY: No resources or components that contain function pointers remain after the app is dropped
        // and so code from the library cannot be called after the app is dropped.
        // Additionally, we have checked './libmy_dyn_plugin.so' and ensured it does not cause UB when unloaded.
        unsafe { libs.mark_for_unloading(LIB_PATH) };
        // library is still loaded at this point.
    }
}

app.run();

// library is no longer loaded.

@ItsDoot

ItsDoot commented Aug 17, 2024

Copy link
Copy Markdown
Contributor

bevy_dynamic_plugin was deprecated in #13080 and subsequently removed in #14534.

@ItsDoot ItsDoot closed this Aug 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-App Bevy apps and plugins C-Code-Quality A section of code that is hard to understand or change D-Complex Quite challenging from either a design or technical perspective. Ask for help!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants