Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove usage of static mut #581

Merged
merged 2 commits into from
Feb 4, 2024

Conversation

lilizoey
Copy link
Member

Replaces the usage of static mut with single and multi-threaded version of a binding storage. The main difference being whether they use OnceLock or OnceCell.

From what i can tell, the only real performance difference between the two lie in initialization, with the multi-threaded one being slower. Once the values have been initialized they seem basically identical in most cases. The multi-threaded one often has around like 1-3% more lines of assembly when a ffi call is made. But i'm not sure if that actually creates a performance impact, i haven't been able to confidently measure it at least.

I made the various tables and such not directly implement Sync/Send, instead the components that make up the table do. This will hopefully avoid a situation where the tables are changed for some reason, in a way that makes them non-sync or non-send, but we dont notice because we just manually implement Sync and Send for the table.

@lilizoey lilizoey added the c: ffi Low-level components and interaction with GDExtension API label Jan 25, 2024
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-581

@Bromeon Bromeon added the quality-of-life No new functionality, but improves ergonomics/internals label Jan 25, 2024
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks a lot! 🙂

There is quite a bit of duplication across single/multi threaded bindings which I think could be extracted: namely the common "data" (all the method tables, library, config etc). Wouldn't it be enough to just have the thread access and thread-ID validation different, but store the data in a single struct?

We should avoid nested locks/cells, see also comments.

godot-ffi/src/binding/single_threaded.rs Outdated Show resolved Hide resolved
Comment on lines 94 to 103
pub(super) interface: GDExtensionInterface,
pub(super) library: GDExtensionClassLibraryPtr,
pub(super) global_method_table: BuiltinLifecycleTable,
pub(super) class_server_method_table: OnceCell<ClassServersMethodTable>,
pub(super) class_scene_method_table: OnceCell<ClassSceneMethodTable>,
pub(super) class_editor_method_table: OnceCell<ClassEditorMethodTable>,
pub(super) builtin_method_table: OnceCell<BuiltinMethodTable>,
pub(super) utility_function_table: UtilityFunctionTable,
pub(super) runtime_metadata: GdextRuntimeMetadata,
pub(super) config: GdextConfig,
Copy link
Member

Choose a reason for hiding this comment

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

Those may as well be pub(crate), usually less hassle when refactoring.

godot-ffi/src/binding/single_threaded.rs Outdated Show resolved Hide resolved
godot-ffi/src/binding/single_threaded.rs Outdated Show resolved Hide resolved
godot-ffi/src/binding/multi_threaded.rs Outdated Show resolved Hide resolved
godot-ffi/src/toolbox.rs Show resolved Hide resolved
godot-ffi/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 80 to 84
// Ensure both crates are checked regardless of cfg, for the sake of development convenience.
#[cfg_attr(not(feature = "experimental-threads"), allow(dead_code))]
mod multi_threaded;
#[cfg_attr(feature = "experimental-threads", allow(dead_code))]
mod single_threaded;
Copy link
Member

Choose a reason for hiding this comment

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

Not a huge deal, but the problem is that this allocates additional statics even in the unused configuration.

Or do you know if those are optimized out?

Copy link
Member Author

Choose a reason for hiding this comment

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

They should be optimized out if they're not accessed (which they aren't), i can try to check if that is the case though.

Copy link
Member Author

Choose a reason for hiding this comment

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

From what i can tell, it seems like there are no references at all to the module that is not active in the assembly, barring debug info if that is enabled. (which it isn't for release builds by default). So yes it seems that they are actually optimized out.

@Bromeon
Copy link
Member

Bromeon commented Jan 31, 2024

Thanks for the answers! I think using OnceCell is fine if we access it through .get().unwrap_unchecked().
What is still open is this part:

There is quite a bit of duplication across single/multi threaded bindings which I think could be extracted: namely the common "data" (all the method tables, library, config etc). Wouldn't it be enough to just have the thread access and thread-ID validation different, but store the data in a single struct?

Using OnceCell in both single- and multi-threaded implementations would facilitate this. See also comment.

@Bromeon
Copy link
Member

Bromeon commented Feb 1, 2024

Just for my understanding: what does UnsafeOnceCell as proposed here concretely improve over static mut? Preventing handing-out of &mut references?

Because that alone can also be achieved by encapsulating in a private module. Do you think we can reuse the cell in many places?

@lilizoey
Copy link
Member Author

lilizoey commented Feb 3, 2024

Just for my understanding: what does UnsafeOnceCell as proposed here concretely improve over static mut? Preventing handing-out of &mut references?

Because that alone can also be achieved by encapsulating in a private module. Do you think we can reuse the cell in many places?

  • Arguably more idiomatic, which means that many of rust's features work better together with such a solution.
  • We can statically ensure the relevant structs implement Sync and Send. With static mut we just bypass that entire thing, making it easy to accidentally add something thread-unsafe.
  • I can easily add in some debug-only safety checks, like ensuring things are/aren't initialized when appropriate.
  • Easier to reason about safety invariants which must be upheld when initializing things like this.
  • Harder to mess up in the future, as static mut doesn't really provide any guarantees beyond requiring unsafe.

I'm not sure if we can reuse them much, but maybe. Currently i added an explicit caveat that we should discuss more and possibly upgrade it to more of a first-class feature if we want to do that. I think this is the main place where we need this kind of unsafe initialization.

@lilizoey lilizoey force-pushed the fix/static-mut-safety branch 4 times, most recently from 75962ed to 4877b0b Compare February 3, 2024 15:47
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks a lot! Also, really nice work on the safety documentations all over the place 👍

Comment on lines 35 to 49
/// Initialize the binding storage, this must be called before any other public functions.
///
/// # Safety
///
/// - Must only be called once.
/// - Must not be called concurrently with [`get_binding_unchecked`](BindingStorage::get_binding_unchecked).
pub unsafe fn initialize(binding: GodotBinding) -> Result<(), ()> {
let storage = Self::storage();

// SAFETY: `initialize` is only called once, and is not called concurrently with `get_binding_unchecked`, which is the
// only place where other methods are called on the binding.
unsafe { storage.binding.set(binding) }

Ok(())
}
Copy link
Member

Choose a reason for hiding this comment

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

This only ever returns Ok -- I wonder also if any violates should be detected internally (via debug_assert!) and thus panic directly?

There is not much point in returning Result to the caller, as there is no way to react to this properly.

Copy link
Member Author

Choose a reason for hiding this comment

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

i guess since the performance of the initialization function isn't too important, then i could just check that the storage isn't initialized before initializing it. it shouldn't ever fail but we also only call the function once during the entire lifetime of the library.

Comment on lines 36 to 40
// This struct is generic over `Config`, this is only to make it possible to use two different implementations of `GdextConfig`.
// As we cannot safely use the same `GdextConfig` in both single and multi-threaded code without requiring extra unsafe in `godot-core`.
// This is because we'd need to implement `Sync` for `GdextConfig`, since it'd be used in the multi-threaded version. But then you could
// share references to it between threads, which is not safe to do.
pub(crate) struct GodotBindingRaw<Config> {
Copy link
Member

Choose a reason for hiding this comment

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

It looks to me like the generic parameter could be replaced with a a #[cfg] on the config field. You have the #[cfg] dispatching on thread support just a few lines above, so they might as well be here.

This would also remove the need for yet another indirection, namely type GodotBinding = GodotBindingRaw<GdextConfig> (which btw looks the same in all places, although it uses types from different modules). Thus simplifying code a bit, with 2 less names one needs to be aware of.

Copy link
Member Author

Choose a reason for hiding this comment

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

If i do that then i have to #[cfg] out the multi_threaded module entirely when experimental-threads is disabled, as otherwise we get an error in there since GodotBinding doesn't implement Sync. Which i can do but then we do lose the development convenience of having both modules available at the same time during development.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see.

Do you think having both #[cfg]'s available at the same time will still be that useful in the future? We don't generally do this in other places (or for other features), and we still have CI as a backup 🤔

Just asking because it seems now that this convenience doesn't come entirely for free...

Copy link
Member Author

@lilizoey lilizoey Feb 4, 2024

Choose a reason for hiding this comment

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

it could be, in that it'd make a change that makes GodotBinding not sync (even when GdextConfig is sync) become an error before hitting CI. but it's probably not a very common scenario to do that. so i'll just cfg them for now.

// SAFETY: It is safe to have access to the library pointer from any thread, as we ensure any access is thread-safe through other means.
// Even without "experimental-threads", there is no way without `unsafe` to cause UB by accessing the library from different threads.
unsafe impl Sync for ClassLibraryPtr {}
// SAFETY: See `Sync` impl safety doc.
unsafe impl Send for ClassLibraryPtr {}
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this could mention that "accessing" here refers to read/write the data on a pure Rust level.

Calling functions in Godot is not automatically thread-safe, but that's beyond the responsibility of Send/Sync here and needs to be enforced by higher-level APIs.

(I guess that's what you meant with "other means"; I guess it doesn't hurt to be explicit though).

godot-ffi/src/binding/mod.rs Outdated Show resolved Hide resolved
godot-ffi/src/binding/single_threaded.rs Outdated Show resolved Hide resolved
godot-ffi/src/toolbox.rs Outdated Show resolved Hide resolved
godot-ffi/src/toolbox.rs Outdated Show resolved Hide resolved
@lilizoey lilizoey force-pushed the fix/static-mut-safety branch 3 times, most recently from 94736ca to 9cddf56 Compare February 4, 2024 13:27
Make binding use `UnsafeOnceCell`
Remove most code duplication
@Bromeon Bromeon added this pull request to the merge queue Feb 4, 2024
@Bromeon
Copy link
Member

Bromeon commented Feb 4, 2024

Thanks a lot! 🚀

Merged via the queue into godot-rust:master with commit b4a91a6 Feb 4, 2024
16 checks passed
@Bromeon Bromeon mentioned this pull request Feb 9, 2024
8 tasks
TCROC added a commit to Lange-Studios/gdext that referenced this pull request Feb 25, 2024
…t-safety"

This reverts commit b4a91a6, reversing
changes made to 6243fc6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: ffi Low-level components and interaction with GDExtension API quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants