-
-
Notifications
You must be signed in to change notification settings - Fork 209
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
Deal with cross-thread access to user-defined GodotClass instances #18
Comments
Re-labeling this as |
212: Implement InstanceStorage optionally as sync for #18 r=Bromeon a=TitanNano This is a second implementation of `InstanceStorage` that is `Sync` and `Send` and should make it suitable for usage with rust threads. I hope it fits with the general plans for multithreading in #18. Feedback is more than welcome. Co-authored-by: Jovan Gerodetti <[email protected]>
212: Implement InstanceStorage optionally as sync for #18 r=Bromeon a=TitanNano This is a second implementation of `InstanceStorage` that is `Sync` and `Send` and should make it suitable for usage with rust threads. I hope it fits with the general plans for multithreading in #18. Feedback is more than welcome. Co-authored-by: Jovan Gerodetti <[email protected]> Co-authored-by: Jan Haller <[email protected]>
212: Implement InstanceStorage optionally as sync for #18 r=Bromeon a=TitanNano This is a second implementation of `InstanceStorage` that is `Sync` and `Send` and should make it suitable for usage with rust threads. I hope it fits with the general plans for multithreading in #18. Feedback is more than welcome. Co-authored-by: Jovan Gerodetti <[email protected]> Co-authored-by: Jan Haller <[email protected]>
# This is the 1st commit message: Parse gdextension_interface.h declarations using regex # This is the commit message #2: AsUninit trait to convert FFI pointers to their uninitialized versions # This is the commit message godot-rust#3: GodotFfi::from_sys_init() now uses uninitialized pointer types # This is the commit message godot-rust#4: Introduce GDExtensionUninitialized*Ptr, without changing semantics # This is the commit message godot-rust#5: Adjust init code to new get_proc_address mechanism # This is the commit message godot-rust#6: Make `trace` feature available in godot-ffi, fix interface access before initialization # This is the commit message godot-rust#7: Compatibility layer between Godot 4.0 and 4.1 (different GDExtension APIs) # This is the commit message godot-rust#8: Add GdextBuild to access build/runtime metadata # This is the commit message godot-rust#9: Detect 4.0 <-> 4.1 mismatches in both directions + missing `compatibility_minimum = 4.1` # This is the commit message godot-rust#10: Detect legacy/modern version of C header (also without `custom-godot` feature) # This is the commit message godot-rust#11: CI: add jobs that use patched 4.0.x versions # This is the commit message godot-rust#12: Remove several memory leaks by constructing into uninitialized pointers # This is the commit message godot-rust#13: CI: memcheck jobs for both 4.0.3 and nightly # This is the commit message godot-rust#14: Remove ToVariant, FromVariant, and VariantMetadata impls for pointers This commit splits SignatureTuple into two separate traits: PtrcallSignatureTuple and VarcallSignatureTuple. The latter is a child of the former. PtrcallSignatureTuple is used for ptrcall and only demands GodotFuncMarshall of its arguments. VarcallSignatureTuple is used for varcall and additionally demands ToVariant, FromVariant, and VariantMetadata of its arguments, so pointers cannot benefit from the optimizations provided by varcall over ptrcall. # This is the commit message godot-rust#15: Adds FromVariant and ToVariant proc macros # This is the commit message godot-rust#16: godot-core: builtin: reimplement Plane functions/methods # This is the commit message godot-rust#17: impl GodotFfi for Option<T> when T is pointer sized and nullable godot-rust#240 Additionally FromVariant and ToVariant are also implemented for Option<Gd<T>> to satisfy all the requirements for ffi and godot_api. # This is the commit message godot-rust#18: Fix UB in virtual method calls that take objects Fix incorrect incrementing of refcount when calling in to godot Fix refcount not being incremented when we receive a refcounted object in virtual methods # This is the commit message godot-rust#19: fix UB caused by preload weirdness # This is the commit message godot-rust#20: Implements swizzle and converts from/to tuples
To elaborate on my original plans a bit and keep the discussion public: As mentioned in #212 (comment), some operations are currently unsound, for example:
So individual unsound access points like The semantics here are actually more limiting than Rust's own:
This is very important, as So, what can we do? My idea is still more or less the same from the original post.
|
i still kinda feel like it'd be better to just have two separate mainly because i really think that making im addition, how do we document that? a separate type is easy to document, but documenting all the changes from this feature seems like it'd be very confusing and weird. i imagine there is a way to do it in the docs but i cant imagine it'd look nice. in addition, for the other core classes we can probably just make them anyway let's say we split We could also potentially use object metadata to store what thread affinity an object should have, if we want to allow
let foo = Gd::<Node>::from_instance_id(id);
let node: &Node = *foo;
thread::spawn(|| node.get_child()) maybe it is fine to allow this for classes that actually are Also there's a distinction i just thought of:
generally the first one of these will be true if the entire subtree of the class hierarchy is also send/sync. this would technically include user-classes but we can perform a check in user classes to make sure they're not created/shared where they shouldn't be. the second one may be true in some cases, for instance it's true for we may also have some cases where some individual methods are safe to call from other threads. i suspect this may be a bit too hard to model properly though, may be feasible through an There are also a couple other possibilities for dedicated smart pointers we could make if we wanted to. One I've thought of would be analogous to the |
Thanks a lot for the great feedback! Yep, it might be better to have its dedicated type Proliferation of public types with closely related meaning, such as Additionally, there are a lot of things we cannot check at compile time either way, by the nature how Godot operates. You mentioned a good one: if Alternatively, we could outlaw polymorphism on |
Just gonna summarize general thread safety issues for the various types. When we focus more on thread safety, we'll likely need to split all the thread unsafe types in two to create a thread-safe variant for each. But there are several types which likely dont need such a split, and some types may only require slight modifications to be made safe. Known thread safe
Likely thread safe
Likely thread unsafeString types can be easily cloned into a shared reference and then mutated afterwards, this is likely gonna cause concurrency issues.
Known thread unsafe
|
This is a great overview! 👍 If we want to provide ergonomic usage, we need to think if/how we can support common use cases such as:
Some observations:
As for |
One thing we could do is have a Its safety invariant is that it has unique ownership of the object. This would let us much more easily use it safely from different threads, it could be |
We could also start very conservatively, and limit the way how this
These operations are absent:
These operations could be marked unsafe (or also absent):
|
We do need to double check some of this, let mut foo = UniqueGd::<Node>::new_alloc();
let bar = Gd::<Node>::new_alloc();
foo.add_child(bar.clone());
send_to_main_thread(foo); // Also sends `bar` to the main thread?
bar.some_function(); // And we have a reference to `bar` on both threads? Maybe if there was some way to limit the internal APIs such that functions called through
Both of these could maybe just be a |
Good observation -- I checked
We could maybe reuse the trick from So mutable methods could not be implicitly called through unique.with_mut(move |this| {
this.add_child(other_unique);
}); |
So i experimented a bit with making a Which classes can be
|
Great observations!
I think we need to strike a balance between static type safety (and the messy APIs it can cause) and ergonomics. I'm fine with panicking in such cases, a descriptive message could tell the user immediately what's wrong -- often even more so than weird bound mismatches. This would also follow ideas here.
Hm, that indeed looks like a hard problem. Once you have Spontanous idea that I haven't tested would be to just provide a temporary proxy (bound to scope) that forwards method calls to the original object, or something that doesn't deref-mut to |
If we want uniqueness (or general multi-threading), we will likely need runtime information. In my comment above, I mentioned that let unique: UniqueGd<Node> = ...;
let id = unique.instance_id();
let secret_clone: Gd<Node> = Gd::from_instance_id(id); // regular Gd! Making And it doesn't solve a problem that doesn't even involve let one: Gd<Node> = ...;
let id = one.instance_id();
thread::spawn(|| {
let two: Gd<Node> = Gd::from_instance_id(id);
}); On the other hand, we could add meta-information ( static UNIQUE_OBJECTS: Global<HashSet<InstanceId>> In both cases, we need to trace all |
One thing to note is that these methods also exist separately on godots end, and we can't always guarantee that the godot versions of these aren't called by users since there's always |
Made a simple proof of concept for unique gd, and copied the example from https://docs.godotengine.org/en/stable/classes/class_arraymesh.html#class-arraymesh-method-surface-get-array-index-len to using it: // This can hopefully be done safely in one go somehow, like `ArrayMesh::new_unique()`.
let mut array_mesh = unsafe { UniqueGd::from_gd(ArrayMesh::new_gd()) };
array_mesh.with_mut(|array_mesh| {
let mut vertices = PackedVector3Array::new();
vertices.push(Vector3::new(0.0, 1.0, 0.0));
vertices.push(Vector3::new(1.0, 0.0, 0.0));
vertices.push(Vector3::new(0.0, 0.0, 1.0));
let mut arrays: VariantArray = Array::new();
arrays.resize(ArrayType::MAX.into_godot() as usize, &Variant::nil());
arrays.set(
ArrayType::VERTEX.into_godot() as usize,
vertices.to_variant(),
);
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, arrays);
});
let mut m = unsafe { UniqueGd::from_gd(MeshInstance3D::new_alloc()) };
m.with_mut(move |m| m.set_mesh(array_mesh)); this isn't a terrible API for |
Currently,
RefCell
andi32
are used to store user-definedGodotClass
instances and their reference counts:https://github.com/godot-rust/gdextension/blob/a64f22409e05d1353492fded4aa36a4b4e6b710d/godot-core/src/storage.rs#L15-L21
In a multi-threaded configuration of the engine, this may lead to unsoundness when the instances are accessed by GDScript or engine code. Noting that
GodotClass
does not have aSend
orSync
requirement, this can be as simple as a non-Send
field in the type.@Bromeon in Discord:
As prior art, the GDNative bindings deals with the problem with the user-data wrapper system.
The text was updated successfully, but these errors were encountered: