Skip to content

Cleanup some stuff mainly related to script instance - #741

Merged
Bromeon merged 3 commits into
godot-rust:masterfrom
lilizoey:cleanup/script-instance-2
Jun 16, 2024
Merged

Bromeon merged 3 commits into
godot-rust:masterfrom
lilizoey:cleanup/script-instance-2

Conversation

@lilizoey

@lilizoey lilizoey commented Jun 2, 2024

Copy link
Copy Markdown
Member

Mainly:

  • Uses into/from_owned_sys functions instead of storing entire property lists and such in a hashmap
  • Moves functions that only operates on ScriptInstanceData to be methods
  • Removes several functions that are a simple wrappers around existing functions, or were only used in one place and which weren't very complicated
  • Reword/correct some safety docs
  • Adds a sys::conv module for common conversions
  • Adds a dedicated bool -> GDExtensionBool conversion function
  • Adds a true/false literal for GDExtensionBool
  • Adds a function to infallibly convert u32 -> usize (this fails at compile time instead of runtime and should have no overhead, since i do not believe we (will) support any targets where u32 <= usize)
  • Abstracts the logic for transferring a ptrlist to and from godot, while keeping track of length, into its own struct
  • Adds a #[deny(unsafe_op_in_unsafe_fn)] to all the ffi-functions in script instance so that we're sure all unsafe function calls are explicitly tracked and called out

Also @TitanNano since you made a lot of this originally

@lilizoey lilizoey added quality-of-life No new functionality, but improves ergonomics/internals c: ffi Low-level components and interaction with GDExtension API labels Jun 2, 2024
@GodotRust

Copy link
Copy Markdown

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

@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch 2 times, most recently from 21faba3 to ab284cf Compare June 2, 2024 17:58
@Bromeon

Bromeon commented Jun 2, 2024

Copy link
Copy Markdown
Member

Thanks a lot! I'd probably merge this after #736, as there are some possibly conflicting changes.

It's nice that even though there are a lot of changes, the diff is still visible in most places! If you plan further refactorings (like moving modules to files), it could make sense to do that in a separate PR 🙂

@lilizoey

lilizoey commented Jun 2, 2024

Copy link
Copy Markdown
Member Author

yeah i was considering moving modules to files and maybe moving around some functions to different places, but i figured it'd be best to avoid doing things that would confuse the diff to make it more easy to read

@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch from ab284cf to f5a4c0c Compare June 3, 2024 14:31

@Bromeon Bromeon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great idea with the constants and the conversion functions!

Comment thread godot-core/src/meta/mod.rs Outdated
Comment on lines +316 to +355
// SAFETY: `name` and `return_value` were created from the appropriate method calls, and have not been freed before this.
unsafe {
let _name = StringName::from_owned_string_sys(name);
PropertyInfo::free_owned_property_sys(return_value);
}

// SAFETY: These pointers were both created from a call to `as_mut_ptr` on a slice. Additionally these pointer will not be accessed
// again after this function call.
let (arguments_slice, default_arguments_slice) = unsafe {
(
std::slice::from_raw_parts_mut(arguments, u32_into_usize(argument_count)),
std::slice::from_raw_parts_mut(
default_arguments,
u32_into_usize(default_argument_count),
),
)
};

// SAFETY: We have exclusive ownership of these slices, and they were originally created from a call to `Box::leak`.
let (arguments, default_arguments) = unsafe {
(
Box::from_raw(arguments_slice),
Box::from_raw(default_arguments_slice),
)
};

for info in arguments.iter() {
// SAFETY: These infos were originally created from a call to `PropertyInfo::into_owned_property_sys`, and this method
// will not be called again on this pointer.
unsafe { PropertyInfo::free_owned_property_sys(*info) }
}

for variant in default_arguments.iter() {
// SAFETY: These pointers were originally created from a call to `Variant::into_owned_var_sys`, and this method will not be
// called again on this pointer.
let _variant = unsafe { Variant::from_owned_var_sys(*variant) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I find this a bit hard to read... On one hand the multi-line tuples, on the other all the separate unsafe blocks which cover the entire area of this code. It's only 10 lines of actual logic, but the initialization is so sophisticated that the data flow is not very obvious anymore. For example, if arguments_slice were used within a block, it would be immediately clear that it's only used once and not further down the function.

Some unsafe could possibly be collapsed (e.g. slice and box initialization). Imo it's fine if a SAFETY block explains more than one thing, or if multiple explanations are within an unsafe block (next to their respective operations).

I was also considering that instead of alternating a1 b1 a2 b2 statements, we could use a1 a2 b1 b2 -- i.e. do all the arguments stuff first, then all the default_arguments stuff. This keeps data dependencies more local, however it spreads out similar unsafe operations, so it might be worse. Not sure if it's needed at all after above changes.


All in all it's not a big deal, just my first impression 🙂

Comment thread godot-core/src/obj/script.rs Outdated
}

impl<T: ScriptInstance> ScriptInstanceData<T> {
/// Convert a `StringName` sys pointer to a reference with unbounded lifetime.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think "StringName" is wrong here.

Comment on lines +467 to +472
/// Helper struct to store the lengths of lists so they can be properly freed.
pub struct PtrlistContainer<T> {
list_lengths: Mutex<HashMap<*mut T, u32>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice abstraction!

Re name, "list" and "container" are somewhat redundant, or it's not really clear how they relate. Maybe we should somehow express that this stores the lengths? BoundedPtrList or something like that?

You could also mention that you use "list" and not "array" because these refer to "property/method lists" (in Godot terms).

Comment thread godot-core/src/obj/script.rs Outdated
unsafe fn transfer_ptr_list_from_godot<T>(ptr: *const T, list_length: usize) -> Vec<T> {
Vec::from_raw_parts(sys::force_mut_ptr(ptr), list_length, list_length)
}
(ptr as *const T, len)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sys::to_const_ptr...

or can you call as_ptr() instead of as_mut_ptr() directly? Does the hashmap need mut pointers?

@lilizoey lilizoey Jun 4, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we do need to call as_mut_ptr() because otherwise we get a pointer derived from a &[T] reference which we cannot later free again since it doesn't have write permissions.

The issue really is just that c++ const pointers can be freed, whereas in rust a pointer derived from a shared reference cannot. So there really isn't a great way to map c++ const pointers to rust in this case, but the functions in question do take const pointers so that's what we need to use.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, thanks for elaborating! Then you could use sys::to_const_ptr(ptr) in the return tuple, to avoid as.

Comment thread godot-ffi/src/conv.rs Outdated
Comment on lines +28 to +31
pub const fn bool_into_sys(value: bool) -> sys::GDExtensionBool {
value as sys::GDExtensionBool
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd use bool_to_sys. Even if it takes by value, being Copy means the original isn't consumed. And the shorter these functions are, the better...

Same for other similar ones.

@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch from f5a4c0c to c05b8f5 Compare June 7, 2024 17:35

@Bromeon Bromeon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! Nice discovery of cast_mut and cast_const 👍

I think a few safety statements could be slightly more concise, e.g.

// SAFETY: `arguments` was returned from a call to `as_mut_ptr` on a mutable slice, with length equal to `default_argument_count`. This
// length has not changed since it was created so `from_raw_parts_mut` is safe to call on it.

could be the following:

// SAFETY: 
// - `from_raw_parts_mut`: `arguments` comes from `as_mut_ptr()` on a mutable slice of length `default_argument_count`.
// - `Box::from_raw`: ...

(with line breaks if needed, I don't see it well in GitHub).

Comment thread godot-core/src/meta/mod.rs Outdated
Comment on lines +326 to +327
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so we `Box::from_raw`
// is safe to call on it.

@Bromeon Bromeon Jun 7, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so we `Box::from_raw`
// is safe to call on it.
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so `Box::from_raw`
// is safe to call on it.

(maybe obsolete with main comment)

Comment thread godot-core/src/meta/mod.rs Outdated
Comment on lines +343 to +344
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so we `Box::from_raw`
// is safe to call on it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so we `Box::from_raw`
// is safe to call on it.
// The slice was returned from a call to `Box::leak`, and we have ownership of the value behind this pointer, so `Box::from_raw`
// is safe to call on it.

@lilizoey

lilizoey commented Jun 9, 2024

Copy link
Copy Markdown
Member Author

Thanks! Nice discovery of cast_mut and cast_const 👍

I think a few safety statements could be slightly more concise, e.g.

// SAFETY: `arguments` was returned from a call to `as_mut_ptr` on a mutable slice, with length equal to `default_argument_count`. This
// length has not changed since it was created so `from_raw_parts_mut` is safe to call on it.

could be the following:

// SAFETY: 
// - `from_raw_parts_mut`: `arguments` comes from `as_mut_ptr()` on a mutable slice of length `default_argument_count`.
// - `Box::from_raw`: ...

(with line breaks if needed, I don't see it well in GitHub).

Sure though i dont entirely see the benefit of collapsing the two unsafe blocks into one if we're just gonna separately document each call anyway?

@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch 2 times, most recently from bdaee68 to 19fafc3 Compare June 9, 2024 17:34
@Bromeon

Bromeon commented Jun 9, 2024

Copy link
Copy Markdown
Member

Sure though i dont entirely see the benefit of collapsing the two unsafe blocks into one if we're just gonna separately document each call anyway?

Yeah, it's a bit unfortunate that unsafe { } is not just a marker but also introduces a scope, and this sometime requires weirder data flow.

In situations where that happens, another option would be:

// SAFETY: see inside.
unsafe {
    // SAFETY: ...
    let a = do_stuff();
    let b = do_other_stuff();

    // SAFETY: ...
    do_more(a, b);
}

but that might also be confusing. Every approach has some downsides... I didn't find a perfect one, so if you find one that works well enough, great 🙂

If the tuples are aggregated on a single line (using variables), that's also more readable than a multiline-tuple expression.

@lilizoey

lilizoey commented Jun 9, 2024

Copy link
Copy Markdown
Member Author

One way to at least be consistent is via the multiple_unsafe_ops_per_block lint which would at least force us to only put one unsafe operation in each block if enabled.

Cleanup script instance borrowing
@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch from 19fafc3 to 221030a Compare June 9, 2024 19:03
@Bromeon

Bromeon commented Jun 14, 2024

Copy link
Copy Markdown
Member

One way to at least be consistent is via the multiple_unsafe_ops_per_block lint which would at least force us to only put one unsafe operation in each block if enabled.

One problem is that our unsafe surface is huge due to the nature of our project being a binding, and there are different "danger levels". For example, we don't need to repeatedly justify calling a Godot C function, because we make sure it's mapped correctly with bindgen and the header file, and we check version compatibility at startup. With the preconditions checked, they might even be safe functions, but bindgen has no such option.

On the other hand, we do have to justify why reinterpreting pointers, assuming buffer lengths, etc. are sound, so unsafe is appropriate in these situations.

Making unsafe more annoying to use without addressing the above will likely lead to worse readability and higher maintenance effort.

@Bromeon

Bromeon commented Jun 14, 2024

Copy link
Copy Markdown
Member

I think the PR looks good, could you maybe group some of the commits? E.g. the two "add/use sys::conv" which are already mixed a bit. You can gladly leave those changing one logical things as individual commits.

@lilizoey

Copy link
Copy Markdown
Member Author

One way to at least be consistent is via the multiple_unsafe_ops_per_block lint which would at least force us to only put one unsafe operation in each block if enabled.

One problem is that our unsafe surface is huge due to the nature of our project being a binding, and there are different "danger levels". For example, we don't need to repeatedly justify calling a Godot C function, because we make sure it's mapped correctly with bindgen and the header file, and we check version compatibility at startup. With the preconditions checked, they might even be safe functions, but bindgen has no such option.

On the other hand, we do have to justify why reinterpreting pointers, assuming buffer lengths, etc. are sound, so unsafe is appropriate in these situations.

Making unsafe more annoying to use without addressing the above will likely lead to worse readability and higher maintenance effort.

Well there are ways to counteract some of these issues. For instance a token system can work, where we check the preconditions once, and return a token of some kind. Then this token allows the user to safely call functions since it can only be created when the preconditions have been checked. But i dont know if such a rewrite is worth it. Worth considering though in some cases maybe. Maybe we can make interface_fn! or similar safer.

lilizoey added 2 commits June 14, 2024 23:25
Add into/free owned_sys methods for MethodList

Remove helper functions that were just used in one place and which didnt really do much to improve readability
Add `#[deny(unsafe_op_in_unsafe_fn)]` to `script_instance_info`
Use `sys::conv` bools
Final touches
@lilizoey
lilizoey force-pushed the cleanup/script-instance-2 branch from 221030a to 686b59d Compare June 14, 2024 21:25
@Bromeon
Bromeon added this pull request to the merge queue Jun 16, 2024
Merged via the queue into godot-rust:master with commit cd31a83 Jun 16, 2024
@lilizoey
lilizoey deleted the cleanup/script-instance-2 branch June 17, 2024 18:35
@Bromeon Bromeon added the c: script-instance Script-instance APIs label Mar 29, 2025
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 c: script-instance Script-instance APIs quality-of-life No new functionality, but improves ergonomics/internals

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants