Skip to content

fix: functions not releasing accesses correctly on error #315

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

Merged
merged 5 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -589,36 +589,39 @@ macro_rules! impl_script_function {

$( let $context = caller_context; )?
let world = caller_context.world()?;
world.begin_access_scope()?;
let mut current_arg = 0;

$(
current_arg += 1;
let $param = args.pop_front();
let $param = match $param {
Some($param) => $param,
None => {
if let Some(default) = <$param>::default_value() {
default
} else {
return Err(InteropError::argument_count_mismatch(expected_arg_count,received_args_len));
}
}
};
let $param = <$param>::from_script($param, world.clone())
.map_err(|e| InteropError::function_arg_conversion_error(current_arg.to_string(), e))?;
)*

let ret = {
let out = self( $( $context,)? $( $param.into(), )* );
$(
let $out = out?;
let out = $out;
)?
out.into_script(world.clone()).map_err(|e| InteropError::function_arg_conversion_error("return value".to_owned(), e))
// Safety: we're not holding any references to the world, the arguments which might have aliased will always be dropped
let ret: Result<ScriptValue, InteropError> = unsafe {
world.with_access_scope(||{
let mut current_arg = 0;

$(
current_arg += 1;
let $param = args.pop_front();
let $param = match $param {
Some($param) => $param,
None => {
if let Some(default) = <$param>::default_value() {
default
} else {
return Err(InteropError::argument_count_mismatch(expected_arg_count,received_args_len));
}
}
};
let $param = <$param>::from_script($param, world.clone())
.map_err(|e| InteropError::function_arg_conversion_error(current_arg.to_string(), e))?;
)*

let ret = {
let out = self( $( $context,)? $( $param.into(), )* );
$(
let $out = out?;
let out = $out;
)?
out.into_script(world.clone()).map_err(|e| InteropError::function_arg_conversion_error("return value".to_owned(), e))
};
ret
})?
};
// Safety: we're not holding any references to the world, the arguments which might have aliased have been dropped
unsafe { world.end_access_scope()? };
ret
})();
let script_value: ScriptValue = res.into();
Expand Down Expand Up @@ -695,6 +698,25 @@ mod test {
});
}

#[test]
fn test_interrupted_call_releases_access_scope() {
#[derive(bevy::prelude::Component, Reflect)]
struct Comp;

let fn_ = |_a: crate::bindings::function::from::Mut<Comp>| 0usize;
let script_function = fn_.into_dynamic_script_function().with_name("my_fn");

with_local_world(|| {
let out =
script_function.call(vec![ScriptValue::from(1)], FunctionCallContext::default());

assert!(out.is_err());
let world = FunctionCallContext::default().world().unwrap();
// assert no access is held
assert!(world.list_accesses().is_empty());
});
}

#[test]
fn test_overloaded_script_function() {
let mut registry = ScriptFunctionRegistry::default();
Expand Down
18 changes: 16 additions & 2 deletions crates/bevy_mod_scripting_core/src/bindings/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,22 @@ impl<'w> WorldAccessGuard<'w> {
self.0.cell.replace(None);
}

/// Runs a closure within an isolated access scope, releasing leftover accesses, should only be used in a single-threaded context.
///
/// Safety:
/// - The caller must ensure it's safe to release any potentially locked accesses.
pub(crate) unsafe fn with_access_scope<O, F: FnOnce() -> O>(
&self,
f: F,
) -> Result<O, InteropError> {
self.begin_access_scope()?;
let o = f();
unsafe { self.end_access_scope()? };
Ok(o)
}

/// Begins a new access scope. Currently this simply throws an erorr if there are any accesses held. Should only be used in a single-threaded context
pub(crate) fn begin_access_scope(&self) -> Result<(), InteropError> {
fn begin_access_scope(&self) -> Result<(), InteropError> {
if self.0.accesses.count_accesses() != 0 {
return Err(InteropError::invalid_access_count(self.0.accesses.count_accesses(), 0, "When beginning access scope, presumably for a function call, some accesses are still held".to_owned()));
}
Expand All @@ -133,7 +147,7 @@ impl<'w> WorldAccessGuard<'w> {
}

/// Ends the access scope, releasing all accesses. Should only be used in a single-threaded context
pub(crate) unsafe fn end_access_scope(&self) -> Result<(), InteropError> {
unsafe fn end_access_scope(&self) -> Result<(), InteropError> {
self.0.accesses.release_all_accesses();

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ impl Xtasks {
.clone()
.with_coverage()
// github actions has been throwing a lot of OOM SIGTERM's lately
.with_max_jobs(4),
.with_max_jobs(2),
subcmd: Xtasks::Test {
name: None,
package: None,
Expand Down
Loading