Skip to content

Commit 7a251c0

Browse files
authored
fix: functions not releasing accesses correctly on error (#315)
1 parent de204bc commit 7a251c0

File tree

3 files changed

+68
-32
lines changed

3 files changed

+68
-32
lines changed

crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs

+51-29
Original file line numberDiff line numberDiff line change
@@ -589,36 +589,39 @@ macro_rules! impl_script_function {
589589

590590
$( let $context = caller_context; )?
591591
let world = caller_context.world()?;
592-
world.begin_access_scope()?;
593-
let mut current_arg = 0;
594-
595-
$(
596-
current_arg += 1;
597-
let $param = args.pop_front();
598-
let $param = match $param {
599-
Some($param) => $param,
600-
None => {
601-
if let Some(default) = <$param>::default_value() {
602-
default
603-
} else {
604-
return Err(InteropError::argument_count_mismatch(expected_arg_count,received_args_len));
605-
}
606-
}
607-
};
608-
let $param = <$param>::from_script($param, world.clone())
609-
.map_err(|e| InteropError::function_arg_conversion_error(current_arg.to_string(), e))?;
610-
)*
611-
612-
let ret = {
613-
let out = self( $( $context,)? $( $param.into(), )* );
614-
$(
615-
let $out = out?;
616-
let out = $out;
617-
)?
618-
out.into_script(world.clone()).map_err(|e| InteropError::function_arg_conversion_error("return value".to_owned(), e))
592+
// Safety: we're not holding any references to the world, the arguments which might have aliased will always be dropped
593+
let ret: Result<ScriptValue, InteropError> = unsafe {
594+
world.with_access_scope(||{
595+
let mut current_arg = 0;
596+
597+
$(
598+
current_arg += 1;
599+
let $param = args.pop_front();
600+
let $param = match $param {
601+
Some($param) => $param,
602+
None => {
603+
if let Some(default) = <$param>::default_value() {
604+
default
605+
} else {
606+
return Err(InteropError::argument_count_mismatch(expected_arg_count,received_args_len));
607+
}
608+
}
609+
};
610+
let $param = <$param>::from_script($param, world.clone())
611+
.map_err(|e| InteropError::function_arg_conversion_error(current_arg.to_string(), e))?;
612+
)*
613+
614+
let ret = {
615+
let out = self( $( $context,)? $( $param.into(), )* );
616+
$(
617+
let $out = out?;
618+
let out = $out;
619+
)?
620+
out.into_script(world.clone()).map_err(|e| InteropError::function_arg_conversion_error("return value".to_owned(), e))
621+
};
622+
ret
623+
})?
619624
};
620-
// Safety: we're not holding any references to the world, the arguments which might have aliased have been dropped
621-
unsafe { world.end_access_scope()? };
622625
ret
623626
})();
624627
let script_value: ScriptValue = res.into();
@@ -695,6 +698,25 @@ mod test {
695698
});
696699
}
697700

701+
#[test]
702+
fn test_interrupted_call_releases_access_scope() {
703+
#[derive(bevy::prelude::Component, Reflect)]
704+
struct Comp;
705+
706+
let fn_ = |_a: crate::bindings::function::from::Mut<Comp>| 0usize;
707+
let script_function = fn_.into_dynamic_script_function().with_name("my_fn");
708+
709+
with_local_world(|| {
710+
let out =
711+
script_function.call(vec![ScriptValue::from(1)], FunctionCallContext::default());
712+
713+
assert!(out.is_err());
714+
let world = FunctionCallContext::default().world().unwrap();
715+
// assert no access is held
716+
assert!(world.list_accesses().is_empty());
717+
});
718+
}
719+
698720
#[test]
699721
fn test_overloaded_script_function() {
700722
let mut registry = ScriptFunctionRegistry::default();

crates/bevy_mod_scripting_core/src/bindings/world.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,22 @@ impl<'w> WorldAccessGuard<'w> {
123123
self.0.cell.replace(None);
124124
}
125125

126+
/// Runs a closure within an isolated access scope, releasing leftover accesses, should only be used in a single-threaded context.
127+
///
128+
/// Safety:
129+
/// - The caller must ensure it's safe to release any potentially locked accesses.
130+
pub(crate) unsafe fn with_access_scope<O, F: FnOnce() -> O>(
131+
&self,
132+
f: F,
133+
) -> Result<O, InteropError> {
134+
self.begin_access_scope()?;
135+
let o = f();
136+
unsafe { self.end_access_scope()? };
137+
Ok(o)
138+
}
139+
126140
/// 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
127-
pub(crate) fn begin_access_scope(&self) -> Result<(), InteropError> {
141+
fn begin_access_scope(&self) -> Result<(), InteropError> {
128142
if self.0.accesses.count_accesses() != 0 {
129143
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()));
130144
}
@@ -133,7 +147,7 @@ impl<'w> WorldAccessGuard<'w> {
133147
}
134148

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

139153
Ok(())

crates/xtask/src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1372,7 +1372,7 @@ impl Xtasks {
13721372
.clone()
13731373
.with_coverage()
13741374
// github actions has been throwing a lot of OOM SIGTERM's lately
1375-
.with_max_jobs(4),
1375+
.with_max_jobs(2),
13761376
subcmd: Xtasks::Test {
13771377
name: None,
13781378
package: None,

0 commit comments

Comments
 (0)