Repository navigation
Conversation
1e43f6c to
28c83fc
Compare
|
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1524 |
7a094fa to
967028a
Compare
There was a problem hiding this comment.
Thanks a lot for the effort, very interesting approach!
Several of the symbols (UniqueType, Array::to_unique, ...) do not appear in generated docs, which makes the PR a bit hard to introspect. Please make sure the full public API is visible, including required bounds 🙂
Since engine methods currently take &AnyArray/&AnyDictionary rather than impl AsArg<...>, the implicit conversions from Unique to array/dict arguments won't work. I'm not sure about making everything generic, as it increases complexity and compile time -- for now there could be an escape hook (explicit conversion) maybe? Although that might open it up for abuse 🤔
Element: ThreadSafeArgContext can be quite a restriction and breaking change, no? Especially after we just opened up Element to custom types for v0.5. The trait ThreadSafeArgContext not having a blanket impl is a problem -- it means that we have yet another trait that has to be implemented manually (yes, #[derive] can do it, but I also anticipate moving to builders and proc-macro less APIs one day, and making them more boilerplaty isn't great 😉). There's some prior art: I made a blanket-impl with AsArg<Variant> for impl ToGodot<Pass=ByValue> -- it would mean removing many explicit impls though (and not sure if that works out for all).
I see that apply() takes exclusive refs &mut T, is there a distinction to allow shared-ref access &T, or should we treat both the same?
d37d968 to
d0a4657
Compare
Yeah, docs weren't being rebuilt due to conflicts with
This is actually an unsolved problem. Explicit conversion creates a gap in the thread safety guarantees. It's possible to smuggle in non-thread-safe values via thread locals or singletons and pass them to an API of a
Breaking change, yes; restricting, I'm not sure. All user-defined types should be The blanked impl for That's why I currently have the
I don't see much use for the apply function besides mutating the inner type. You can't return anything from the closure, so immutable access appears to be pointless so far. |
That's a reason why the trait should be sealed though, not private 🙂 It can have a private
It's possible to enforce such absence, but it will limit
Is What about other blanket impls, like the one I suggested? It's still semi-manual, but at least most users defining their own types can deal with one trait less that they might ultimately not care about.
You cannot return, but it's still possible to transport values outside through thread-safe means (e.g. |
d0a4657 to
cda3510
Compare
Fair point, I made it public and sealed now.
Piggybacking off
It's not Using We had the same issue when dealing with the signal arguments for the |
5ed11e8 to
6112df7
Compare
|
I have now added an additional associated type to Additionally, I have moved the runtime thread-safety validation into codegen so it is decoupled from |
36a8655 to
c8efece
Compare
Bromeon
left a comment
There was a problem hiding this comment.
Thanks for keeping this up-to-date! There's probably still quite a few things to be looked at holistically, but I commented again on some parts to get a more detailed understanding on the approach 🙂
| /// This might fail if the object is referenced by anything else or any of its internal references are shared with other objects. | ||
| /// Specific reasons for this conversion to fail: | ||
| /// | ||
| /// - Reference counter is > 1. | ||
| /// - Reference count of any property value is > 1. | ||
| /// - Any property value directly inherits from Object (manually managed). | ||
| /// - Any property value is of type Dictionary or any of the Array types. | ||
| /// - Any property is a custom callable. | ||
| /// - Any property fails these checks recursively. | ||
| /// | ||
| /// Since all checks are applied recursively to all objects which are referenced by the given value this conversion can potentially be quite expensive. | ||
| pub fn try_from_ref_counted(value: Gd<T>) -> Option<Self> |
There was a problem hiding this comment.
This is quite restrictive, is this method still useful in practice?
Gd<Node>etc isn't necessarily owning, pointing to nodes in the scene tree is harmless- No arrays/dictionaries excludes a ton of use cases
At the same time it's also not airtight:
- You don't list
Variantwhich can contain anything else - Any fields that aren't
#[var]are invisible Gdstrong refs can be trivially worked around withInstanceIdweak refs -- although this can be OK iffrom_instance_idis sound- The recursion hits a stack overflow when you have cyclic references
I'm not saying it needs to be perfect -- for threading we definitely have to make compromises, and best-effort is better than nothing. But we should probably state a clear goal here, and then depending on that decide how far we want to go.
It's also worth noting that such a recursive check can be extremely expensive at runtime.
There was a problem hiding this comment.
This is quite restrictive, is this method still useful in practice?
This is something I'm currently evaluating inside my own project. My theoretical use case here is that this should work well for engine built-in resources like materials, textures, animations, and so on.
You don't list Variant which can contain anything else
Variant types are being inspected during conversion.
Any fields that aren't #[var] are invisible
Yes, and that's why only engine ref-counted classes without a script are allowed. It's even more restrictive than you thought. 😁 I see that this detail is not yet covered by the description.
Gd strong refs can be trivially worked around with InstanceId weak refs -- although this can be OK if from_instance_id is sound
Yes, but I don't think engine classes store InstanceIDs; at least I haven't come across one yet. So it should be covered by the previous point. try_from_ref_counted is now also restricted to the main thread as well.
The recursion hits a stack overflow when you have cyclic references
Good point. I should probably guard against that. So far I'm just trusting that engine classes don't have cycling references.
There was a problem hiding this comment.
This now also tracks already visited InstanceIds to prevent cyclic references from causing stack overflows.
There was a problem hiding this comment.
What about this?
It's also worth noting that such a recursive check can be extremely expensive at runtime.
We should probably bench it to see how much overhead this check introduces for a realistic scene/nested setup. And consider moving it behind safeguards-strict gate. It's best-effort either way...
There was a problem hiding this comment.
We should probably bench it to see how much overhead this check introduces for a realistic scene/nested setup. And consider moving it behind safeguards-strict gate. It's best-effort either way...
We can benchmark it on some resources, but this check is completely voluntary. You have to manually call Unique::try_from_ref_counted(object) or the check never runs. You also can't run it on a node tree because they are manually managed, and we can't prove that we have unique access. It's really only meant for resources like textures, meshes, materials, and such.
There was a problem hiding this comment.
We could also make Unique::new_unchecked public, as an alternative, but I think that has big potential for abuse.
There was a problem hiding this comment.
I see. We currently have Gd::try_to_unique(), but it's just the object's refcount itself, not any nested parts. Do you think the 2 APIs should be aligned over time?
Also, maybe the docs should mention that the heuristic is deliberately conservative to err on the side of "not unique", even if that means many actually-unique objects won't pass it. Example:
Reference count of any property value is > 1.
An object with 2 object properties that are the same is unique, but won't pass this check. Of course, many things like scripts, Callables etc. are harmless as well but hard to prove statically.
We could also make
Unique::new_uncheckedpublic, as an alternative, but I think that has big potential for abuse.
Eventually we might need it, but for now I'm fine not exposing it.
There was a problem hiding this comment.
I see. We currently have Gd::try_to_unique(), but it's just the object's refcount itself, not any nested parts. Do you think the 2 APIs should be aligned over time?
Yes, I think they should be aligned. With the new Unique::try_from_ref_counted the existing Gd::try_to_unique might be confusing.
1b8978d to
64bf8d2
Compare
|
This is a drive by comment, but can you downcast a node stored in a unique? like, can you cast a |
At the moment that is not supported. Do you see a use case for downcasting here? |
1f3e4ae to
8d36cc3
Compare
8d36cc3 to
ab6575e
Compare
ab6575e to
58219e9
Compare
7175684 to
ed1bd66
Compare
cd52ba1 to
6392833
Compare
6392833 to
00bd0ac
Compare
All reference based Godot types can be created inside a Unique struct. The only way to interact with the godot class is with Send + Sync types. Unique::map and Unique::apply only allow Send and Sync closures and and trying to sneak in Gd<T>s via thread_locals will result in a runtime panic.
When experimental-threads is on we can only pass &Gd<T> to the engine on the main-thread. Other threads will panic.
Some tests to check if it works as intended. More real world use-cases would help.
00bd0ac to
61b59ce
Compare
|
I think this is finally ready for review. We probably should put this behind an opt-out feature and give people the ability to go back to the current |
The
Unique<T>type constrains the way non-thread-safe engine types can be used outside the main thread. With these constraints applied, we can safely send types that are wrapped byUnique<T>across threads.Values that are wrapped by
Uniquecan only be accessed via theUnique::applyorUnique::apply_gdfunctions. These functions accept aSend + Syncclosure to prevent any non-thread-safe values from getting passed into the wrapped types.Usage Example
Breaking Changes
This will very likely break code that relies on the
experimental-threadsfeature. The extent of the breakage needs to be assessed, and ideally a migration path can be offered for all safe use cases.ToGodothas a new associated typeThreadsthat has to be added to all manual implementations of the trait. Default values for associated types are still not stable.To Dos:
It's currently possible to pass array and dictionary types to the engine by reference. This breaks the uniqueness constraints when storing such types in a thread-local variable.