-
-
Notifications
You must be signed in to change notification settings - Fork 198
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
Conversation
fd5f593
to
a76dd19
Compare
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-581 |
…version of bindings and interface manager
a76dd19
to
157f8e0
Compare
There was a problem hiding this 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.
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, |
There was a problem hiding this comment.
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/lib.rs
Outdated
// 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; |
There was a problem hiding this comment.
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 static
s even in the unused configuration.
Or do you know if those are optimized out?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Thanks for the answers! I think using
Using |
Just for my understanding: what does Because that alone can also be achieved by encapsulating in a private module. Do you think we can reuse the cell in many places? |
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. |
75962ed
to
4877b0b
Compare
There was a problem hiding this 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 👍
/// 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(()) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
godot-ffi/src/binding/mod.rs
Outdated
// 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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
godot-ffi/src/binding/mod.rs
Outdated
// 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 {} |
There was a problem hiding this comment.
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).
94736ca
to
9cddf56
Compare
Make binding use `UnsafeOnceCell` Remove most code duplication
9cddf56
to
fdf5ae0
Compare
Thanks a lot! 🚀 |
Replaces the usage of
static mut
with single and multi-threaded version of a binding storage. The main difference being whether they useOnceLock
orOnceCell
.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 implementSync
andSend
for the table.