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

Global main thread ID #1045

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

TitanNano
Copy link
Contributor

In #1043 I need to identify the main thread without accessing the related engine API.

By storing the current thread ID during initialization, we can identify the main thread from that point on. I only found one other use of the main thread ID in godot-ffi but having access to the ThreadId of the main-thread could also be useful to user code.

@GodotRust
Copy link

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

@TitanNano TitanNano mentioned this pull request Feb 10, 2025
3 tasks
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, good idea!

I'm not sure if I'm doing premature optimization here, but since initialization happens exactly once, I'm not a huge fan of all future code needing to obtain a lock to read the thread ID. This might even be slower than thread::current().id(), depending on how that is implemented/cached.

In the library we have ManualInitCell<T> for this purpose. The invariant is that the value is only accessed after initialization, which means it needs to be unsafe, which is a bit annoying for ergonomics. In Debug mode, misuse would still be caught.

Thoughts? If it's not used in hot paths, we could keep things simple with OnceLock.

godot-core/src/init/mod.rs Outdated Show resolved Hide resolved
godot-core/src/meta/mod.rs Outdated Show resolved Hide resolved
@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: ffi Low-level components and interaction with GDExtension API labels Feb 10, 2025
@TitanNano
Copy link
Contributor Author

Thoughts? If it's not used in hot paths, we could keep things simple with OnceLock.

Yeah, I think that is a good idea. I definitely need to check the main thread ID every time a new async task is created, and it's hard to say if that will end up in someone's hot path. It being unsafe is unfortunate, but manageable.

@TitanNano
Copy link
Contributor Author

@Bromeon this probably also fits better into the global module than meta right?

@TitanNano TitanNano force-pushed the jovan/main_thread_id branch 2 times, most recently from 04484f3 to 4d3d3ee Compare February 10, 2025 22:56
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.

@Bromeon this probably also fits better into the global module than meta right?

Not really, that module is for global symbols from Godot itself.

Maybe init/mod.rs?
This functionality is only needed in godot-core, not in godot-ffi?

godot-ffi/src/toolbox.rs Outdated Show resolved Hide resolved
Comment on lines 88 to 95
/// Get the [`ThreadId`](std::thread::ThreadId) of the main thread.
#[cfg(not(wasm_nothreads))]
pub fn main_thread_id() -> std::thread::ThreadId {
// SAFETY: We initialized the cell during library initialization, before any other code is executed.
let thread_id = unsafe { MAIN_THREAD_ID.get_unchecked() };

*thread_id
}
Copy link
Member

Choose a reason for hiding this comment

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

As much as I'd like it, I'm not sure if we can make this function safe -- the function doesn't have more information than MAIN_THREAD_ID.get_unchecked() directly, and a caller could technically invoke this before the initialization.

This whole topic deserves some more discussion at some point, because we have the same problem for all the Godot APIs: they too are only valid after the binding has been initialized, but that happens as the very first thing. And yet the accessor functions all remain unsafe.

Maybe there's eventually some API design that allows us to centralize the unsafe parts related to late initialization, instead of requiring a thousand // SAFETY: binding is initialized statements scattered through the codebase. But it needs a bit more thought 🤔

cc @lilizoey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a caller could technically invoke this before the initialization.

How would that realistically happen?

Copy link
Member

@lilizoey lilizoey Feb 11, 2025

Choose a reason for hiding this comment

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

a caller could technically invoke this before the initialization.

How would that realistically happen?

just call it from some context outside of running it as a dynamic library. for instance if you call this function from a unit test then the dynamic library initialization code wont have run yet. i dont think that's too unrealistic a scenario for people to do, like they wanna unit test their code and accidentally call something which checks the main thread.

so yeah this cant really be safe, you should probably even add a unit test to check this behavior explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lilizoey @Bromeon thanks for all the feedback. I have now added a
debug_assert!(MAIN_THREAD_ID.is_initialized()) to main_thread_id() like it's done in BindingStorage. This should make the behavior consistent with, i.e., calling RefCounted::new_gd() from a unit test.

Copy link
Member

@lilizoey lilizoey Feb 11, 2025

Choose a reason for hiding this comment

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

the function still can't be safe and public as you've written it currently (though even if private it'd be better if it was unsafe). if you are claiming here that RefCounted::new_gd() would trigger UB in a unit test just like this function would then that's a bug and should be reported as such. not used as justification for adding more UB to the library.

in practice it may be hard to fix the UB in RefCounted::new_gd() if that's the case, but still it shouldnt be used to justify this function having UB.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apologies, that was not the intention.

I was thinking that there is no real opportunity to use anything in a unit test. So I checked the simplest example I could think of, which is creating a new object instance. In a unit test, this panicked in the multithreaded BindingStorage::get_binding_unchecked() which made sense to me. Therefore, I replicated the behavior in main_thread_id by calling the safe method ManualInitCell::is_initialized first and then panicking should the cell not be initialized. If we panic before going into the unsafe code, we shouldn't have any UB, or not?

Let me know what I missed.

Copy link
Member

@lilizoey lilizoey Feb 12, 2025

Choose a reason for hiding this comment

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

debug_assert doesn't panic if debug assertions are turned off, so they can't be used to ensure safety. debug_assert is only useful for sanity checks, and when checking that a safety condition isn't broken it's a soundness bug if it is ever triggered.

like it is possible to run unit tests without debug assertions being turned on for instance by running cargo test --release. see here for instance. so adding a debug_assert to check that the value is initialized wont fix the issue that unit tests can call this function before it's initialized.

additionally, you can also just run it as a normal rust binary, for instance by calling the function from fn main(). there are many ways to call functions in this library without the library initialization code being run.

@TitanNano
Copy link
Contributor Author

Maybe init/mod.rs?
This functionality is only needed in godot-core, not in godot-ffi?

Hmm, yes, I think it could be used in godot-ffi as well. I'll move it there and expose the fn main_thread_id() from godot-core::init as well? I think this could be useful for user-code too, so I think it should be public.

@TitanNano TitanNano force-pushed the jovan/main_thread_id branch from 4d3d3ee to f0485c1 Compare February 11, 2025 19:32
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.

4 participants