-
Notifications
You must be signed in to change notification settings - Fork 212
Construct object from custom native class #181
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
Conversation
Force-push(es):
|
Thanks a lot for this PR! This is quite some code to review and I already started, but I will probably need some time to go through all of it. |
Updated with implementation of customizable user-data as per discussion in #182. |
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.
Wew tons of code! 😅 Really good job so far, I like it!
gdnative-core/src/class.rs
Outdated
} | ||
|
||
/// Calls a function with a NativeClass instance and its owner, and returns its return | ||
/// value. Can be used for multiple times via aliasing. For reference-counted types |
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.
Is this notice still true when considering that this allows multiple mutable aliases?
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.
You can't make multiple mutable aliases of the NativeClass
because the reference is only valid for the closure. If you call map_mut
again inside, the user data wrapper should prevent it.
The _aliased
part refers to the Godot object, and for Reference types from_sys(to_sys())
is indeed (currently) equivalent to a clone.
gdnative-core/src/class.rs
Outdated
} | ||
) => ( | ||
godot_class! { | ||
class $name ($crate::user_data::MutexData<$name>) : $owner { |
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 is just personal preference, but I would probably prefer to have the default be the single-threaded version (so RefCell
based). Most scripts will not be used in multiple threads so this might be more costly than it is useful in the general case.
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.
(After finishing reviewing I realized single-threaded versions don't exist, so feel free to ignore this)
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 is true that _physics_process
and _process
happen on the same thread, but signals connections seem to get called immediately unless they are CONNECT_DEFERRED
. That's quite some chance to get exposed to threads, imo.
parking_lot
mutexes need only one atomic operation for uncontended locks, so it may not be as scary as it sounds, but a feature flag should be useful, as said in replies to other comments on this topic.
//! to make sure that the Rust rules for references are always held for the `self` argument, and | ||
//! no UB can occur because we freed `owner` or put another script on it. | ||
//! | ||
//! ## Which wrapper to use? |
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 is a really nice summary, nice job!
We should probably add single-threaded versions of this later on, that way you don't have any overhead due to locks or atomics when using a NO_THREADS
build of Godot or know in advance that no access via threads will be done.
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 think that is a good idea, combined with the default wrapper type alias. Though wrapper types that are not thread safe should be put behind a no_threads
feature flag, imo, as it isn't unsafe
to just slap an unsafe wrapper type on something that could be accessed by threads.
Actually, maybe under a sub module called something like hazmat
, and re-export at user_data
level only with a feature flag. So users could still reach for them if they really wanted to in a non-no_threads
build, but can be informed of the unsafeness during the process through the type path.
The return type of `NativeScript::new` is incorrectly reported as `Object` in api.json, while it should have been `Variant`. A manual fix is done while a fix in Godot itself is pending. See godotengine/godot#31606
When creating an game, it's often desired to return new nodes with custom Rust behaviors attached. However, it's not previously possible to obtain a handle to the current `GDNativeLibrary`, in order to create `NativeScript` instances, through exposed APIs. Fortunately, a pointer to the GDNativeLibrary being initialized is passed through `godot_gdnative_init_options`. This commit saves this pointer in a static, to use it later to instance scripted objects. For more safety, the trait `Instanciable` is introduced as a generic bound. `GDNativeLibrary::current_library` is added to the bindings as a type safe way for users to get the object for their own uses. The old, unused `NativeRef` type is replaced.
This adds the `UserData` trait, and a set of user-data wrappers that handle casts to and from raw pointers internally, exposing a safe interface. The `user_data` attribute is added to the derive macro for `NativeClass`. The default is `MutexData` which is safe and works for most types. Fixes #182.
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 is looking pretty good now! I only suggested fixing a typo in a doc comment and that's it. Great work :)
It's a lot to ask, but I feel like I would be more comfortable merging this after having some benchmarks that show how the default behavior changes with this PR applied. I could try to set up a benchmark if you don't want to put even more work on this, but I think this would be really useful to see what impact this has. |
I think I'll try setting one up. This should mainly impact frequent calls from Godot, so maybe just a tight loop calling methods? |
Yes, the entry and exit of method calls are the most interesting part in that regard, so having a loop with calls seems like a good idea. To get a more real example maybe we can make one version with very simple function and another where the functions do some sort of work to make it more realistic. |
Got some results using the following benchmark functions:
Results for
Results for
... I think the takeaway here is that GDScript is so slow it doesn't really matter a lot. We could try setting up a benchmark in C++, if you think that would make a difference. Benchmark code
On Rust side: use std::cell::RefCell;
#[derive(Clone, Debug)]
struct PtrUserData(*const RefCell<CallBenchmark>);
unsafe impl user_data::UserData for PtrUserData {
type Target = CallBenchmark;
fn new(val: Self::Target) -> Self {
// don't care about leaks in a benchmark
PtrUserData(Box::into_raw(Box::new(RefCell::new(val))))
}
unsafe fn into_user_data(self) -> *const libc::c_void {
self.0 as *const libc::c_void
}
unsafe fn consume_user_data_unchecked(ptr: *const libc::c_void) -> Self {
PtrUserData(ptr as *const RefCell<CallBenchmark>)
}
unsafe fn clone_from_user_data_unchecked(ptr: *const libc::c_void) -> Self {
PtrUserData(ptr as *const RefCell<CallBenchmark>)
}
}
impl user_data::Map for PtrUserData {
type Err = ();
fn map<F, U>(&self, op: F) -> Result<U, Self::Err>
where
F: FnOnce(&Self::Target) -> U,
{
unsafe {
Ok(op(&*(&*self.0).borrow()))
}
}
}
impl user_data::MapMut for PtrUserData {
type Err = ();
fn map_mut<F, U>(&self, op: F) -> Result<U, Self::Err>
where
F: FnOnce(&mut Self::Target) -> U,
{
unsafe {
Ok(op(&mut *(&*self.0).borrow_mut()))
}
}
}
#[derive(Debug)]
struct CallBenchmark(i32);
impl NativeClass for CallBenchmark {
type Base = Reference;
//type UserData = user_data::MutexData<CallBenchmark>;
type UserData = PtrUserData;
fn class_name() -> &'static str {
"CallBenchmark"
}
fn init(_owner: Reference) -> CallBenchmark {
CallBenchmark(1)
}
fn register_properties(_builder: &init::ClassBuilder<Self>) {
}
}
#[methods]
impl CallBenchmark {
#[export]
fn set_target(&mut self, _owner: Reference, target: i32) {
self.0 = target;
}
#[export]
fn echo_add(&mut self, _owner: Reference) -> i32 {
debug_assert!(false, "build this with optimizations!");
let num = self.0;
self.0 += 1;
num
}
#[export]
fn naive_factor(&self, _owner: Reference) -> i32 {
debug_assert!(false, "build this with optimizations!");
if self.0 < 2 {
godot_error!("call set_target with a value larger than 1");
return -1
}
let max = f64::ceil(f64::sqrt(self.0 as f64)) as i32;
for i in 2..=max {
if self.0 % i == 0 {
return i;
}
}
-1
}
} GDScript: extends Node
func _ready():
print(" -- Rust gdnative test suite:")
var gdn = GDNative.new()
var status = false;
gdn.library = load("res://gdnative.gdnlib")
if gdn.initialize():
status = gdn.call_native("standard_varcall", "run_tests", [])
#gdn.terminate()
else:
print(" -- Could not load the gdnative library.")
if status:
print(" -- Test run completed successfully.")
else:
print(" -- Test run completed with errors.")
OS.exit_code = 1
if status:
print(" -- Rust gdnative call benchmarks:")
_bench_echo_add(gdn.library, 1000000)
_bench_naive_factor(gdn.library, 500000)
print(" -- exiting.")
get_tree().quit()
func _bench_echo_add(library, times):
var script = NativeScript.new()
script.set_library(library)
script.set_class_name("CallBenchmark")
var bench = Reference.new()
bench.set_script(script)
bench.set_target(757)
var start = OS.get_system_time_msecs()
for i in range(0, times):
assert((757 + i) == bench.echo_add())
var end = OS.get_system_time_msecs()
var elapsed = end - start
var ops = float(times) / float(elapsed) * 1000
print("_bench_echo_add: ", times, " times in ", elapsed, " msecs, ", ops, " ops/sec")
func _bench_naive_factor(library, times):
var script = NativeScript.new()
script.set_library(library)
script.set_class_name("CallBenchmark")
var bench = Reference.new()
bench.set_script(script)
bench.set_target(773 * 991)
var start = OS.get_system_time_msecs()
for _i in range(0, times):
assert(773 == bench.naive_factor())
var end = OS.get_system_time_msecs()
var elapsed = end - start
var ops = float(times) / float(elapsed)
print("_bench_naive_factor: ", times, " times in ", elapsed, " msecs, ", ops, " ops/sec") |
Seems fine, yes GDScript is not the fastest and since inside of Rust you generally don't call methods through the scripting interface it should be fine :) |
This patch allows new objects with
NativeClass
script instances attached to be constructed and returned from Rust.Depends on #187. Implements #154. Fixes #182.
This is a fairly large patch touching many parts of the package. Explanations for each commit are available in commit messages. Each commit should individually compile and pass tests when applied sequentially.
Motivation
When creating an game, it's often desired to return new objects with custom Rust behaviors attached. For example:
yield
-able objects likeGDScriptFunctionState
, to avoid blocking the main thread for computationally intensive operations.This is not previously possible in
godot-rust
, because as it stands,ToVariant
can't be implemented forNativeClass
:NativeClass
instances no longer carry the native class header, and can be constructed freely without an underlying Godot object. There is nothing to put in the resultingVariant
.from_variant
is required forToVariant
, and there is no sensible way to implement it forNativeClass
instances, becauseuser_data
is just aBox<RefCell<T>>
with no run-time type information.This patch changes the API in a way so (1) and (2) can be resolved, and (3) can be worked around.
Explanation
This patch does 3 major API changes that are required for the feature to work:
NativeRef
type withInstance
, that wraps around Godot objects with Rust script instances.Instanciable: GodotObject
as a generic bound for type safety.Instance API
This is a safer wrapper (compared to a plain tuple or the original
NativeRef
) around a Godot object and a RustNativeClass
implementation. It can be used in the following ways:Instanciable
This trait is implemented by the bindings generator for all instanciable classes, and used on
Instance::new
as a generic bound onT::Base
, making sure that onlyNativeClass
instances that extend instanciable base classes can be used.The trait has one function,
construct
, that takes no arguments and return a fresh object. It issafe
, unlike its super traitGodotObject
, because it's always valid to create a new instance.Instance::new
As an implementation detail, the function works by:
gdnative_init
, save a pointer to theGDNativeLibrary
being initialized passed throughgodot_gdnative_init_options
, as a static ingdnative-core
.Instance::new
is called, construct aNativeScript
instance using saidGDNativeLibrary
andNativeClass::class_name
.T::Base
withInstanciable::construct
.Object::set_script
on the base object, with theNativeScript
instance as argument.T::Base
withgodot_nativescript_get_userdata
, and clone into aNativeInstanceWrapper<T>
.T::Base
and theNativeInstanceWrapper<T>
in anInstance
struct.The function is implemented using direct
GodotApi
calls, to keep it all ingdnative-core
and avoid depending ongdnative-bindings
.Customizable user data storage
See conversation in #182.
Drawbacks
T: NativeClass
and anInstance<T>
. The former is just an inert Rust value, and only the latter actually works in the engine. Users need to be taught about this.gdnative-core
depending on NativeScript classes. Not aware of a better way to do this.NativeClass
functions from Godot hasRc
overhead, but is necessary to uphold safe invariants.Rationale and alternatives
I consider this design better than the alternatives below because:
gdnative-core
.Instance
calls.Putting Instance::new outside gdnative-core
It's possible to put the function into a new crate, e.g.
gdnative-utils
, which will depend on bothgdnative-core
andgdnative-bindings
. However, this has multiple problems:new
to be available under the type it constructs.Re-introducing manually managed headers
Having only GDNativeLibrary::this_library
With only
GDNativeLibrary::this_library
, construction is possible. However, without the APIInstance<T>
provides, the code to construct and use custom instances would be very clumsy, involveunsafe
, and have little type safety. It would look something like:Versus:
Not doing this at all
Unresolved questions
Better way to put a "one-off" function into bindings?
Currently
GDNativeLibrary::this_library
is "generated" by just writing a string into the generated file. This feels rather contrived. Is there a better way to do this (or not do this at all)?Creating an manually managed Instance and not passing it to the engine / freeing it leaks memory.
There is probably no way to handle them properly without some form of ownership model, as non-Reference GodotObjects drop by forgetting. Refer to #70.
Should we implement
Send
(or evenSync
) for Godot types?Raw pointers are neither
Send
orSync
in Rust. This means that for allNativeClass
that holds some reference to a Godot object,Send
orSync
need to be manually implemented for any of the safe wrappers to be used.Godot's C++ code contain plenty of locks, but there is no comprehensive information available about the thread safety for all the types.
I think
Sync
is certainly being too confident. Is it reasonably safe to just assume that everything is at leastSend
, since there're nothing we can do about it anyway?Future possibilities
Should we implement
FromVariant
forInstance
, and how?Currently, it's not possible to implement
FromVariant
because the user data is just aBox<RefCell<T>>
with no type information. This is acceptable for methods, where we trust NativeScript to call the correct functions, but unacceptable forFromVariant
.Further complicating the problem, is the situation that the current implementation of
godot_nativescript_get_userdata
in Godot only checks that the script is any NativeScript: that is, not necessarily from this library or even language. Thus, trying to read a 8-byteTypeId
, or rather, anything at all from it may lead to a swift segfault.It may be feasible to track all user data pointers and their
TypeId
s in a static HashMap, but that may incur extra cost for types that don't need to beFromVariant
. Maybe we could introduce a separate marker trait users can explicitly opt into, and implementFromVariant
for those types through a wrapper newtype?This also prevents safe downcasting from
T::Base
toInstance<T>
.Can we add a more ergonomic constructor to the NativeScript API itself?
The current
Instance::new
implementation is ugly, and Godot is open source. Maybe this could be changed somehow in the future.