Skip to content

Commit c6d68f4

Browse files
authored
Merge pull request #798 from godot-rust/qol/call-errors-limit
Prevent global `CallError` tracker from growing indefinitely
2 parents f0fc6f6 + 44e53ca commit c6d68f4

File tree

1 file changed

+91
-8
lines changed

1 file changed

+91
-8
lines changed

godot-core/src/private.rs

+91-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::global::godot_error;
1919
use crate::meta::error::CallError;
2020
use crate::meta::CallContext;
2121
use crate::sys;
22-
use std::collections::HashMap;
2322
use std::sync::{atomic, Arc, Mutex};
2423
use sys::Global;
2524

@@ -41,24 +40,57 @@ sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);
4140

4241
// Note: if this leads to many allocated IDs that are not removed, we could limit to 1 per thread-ID.
4342
// Would need to check if re-entrant calls with multiple errors per thread are possible.
44-
#[derive(Default)]
4543
struct CallErrors {
46-
map: HashMap<i32, CallError>,
47-
next_id: i32,
44+
ring_buffer: Vec<Option<CallError>>,
45+
next_id: u8,
46+
generation: u16,
47+
}
48+
49+
impl Default for CallErrors {
50+
fn default() -> Self {
51+
Self {
52+
// [None; N] requires Clone.
53+
ring_buffer: std::iter::repeat_with(|| None)
54+
.take(Self::MAX_ENTRIES as usize)
55+
.collect(),
56+
next_id: 0,
57+
generation: 0,
58+
}
59+
}
4860
}
4961

5062
impl CallErrors {
63+
const MAX_ENTRIES: u8 = 32;
64+
5165
fn insert(&mut self, err: CallError) -> i32 {
5266
let id = self.next_id;
53-
self.next_id = self.next_id.wrapping_add(1);
5467

55-
self.map.insert(id, err);
56-
id
68+
self.next_id = self.next_id.wrapping_add(1) % Self::MAX_ENTRIES;
69+
if self.next_id == 0 {
70+
self.generation = self.generation.wrapping_add(1);
71+
}
72+
73+
self.ring_buffer[id as usize] = Some(err);
74+
75+
(self.generation as i32) << 16 | id as i32
5776
}
5877

5978
// Returns success or failure.
6079
fn remove(&mut self, id: i32) -> Option<CallError> {
61-
self.map.remove(&id)
80+
let generation = (id >> 16) as u16;
81+
let id = id as u8;
82+
83+
// If id < next_id, the generation must be the current one -- otherwise the one before.
84+
if id < self.next_id {
85+
if generation != self.generation {
86+
return None;
87+
}
88+
} else if generation != self.generation.wrapping_sub(1) {
89+
return None;
90+
}
91+
92+
// Returns Some if there's still an entry, None if it was already removed.
93+
self.ring_buffer[id as usize].take()
6294
}
6395
}
6496

@@ -348,3 +380,54 @@ where
348380
}
349381
}
350382
}
383+
384+
// ----------------------------------------------------------------------------------------------------------------------------------------------
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::{CallError, CallErrors};
389+
use crate::meta::CallContext;
390+
391+
fn make(index: usize) -> CallError {
392+
let method_name = format!("method_{index}");
393+
let ctx = CallContext::func("Class", &method_name);
394+
CallError::failed_by_user_panic(&ctx, "some panic reason".to_string())
395+
}
396+
397+
#[test]
398+
fn test_call_errors() {
399+
let mut store = CallErrors::default();
400+
401+
let mut id07 = 0;
402+
let mut id13 = 0;
403+
let mut id20 = 0;
404+
for i in 0..24 {
405+
let id = store.insert(make(i));
406+
match i {
407+
7 => id07 = id,
408+
13 => id13 = id,
409+
20 => id20 = id,
410+
_ => {}
411+
}
412+
}
413+
414+
let e = store.remove(id20).expect("must be present");
415+
assert_eq!(e.method_name(), "method_20");
416+
417+
let e = store.remove(id20);
418+
assert!(e.is_none());
419+
420+
for i in 24..CallErrors::MAX_ENTRIES as usize {
421+
store.insert(make(i));
422+
}
423+
for i in 0..10 {
424+
store.insert(make(i));
425+
}
426+
427+
let e = store.remove(id07);
428+
assert!(e.is_none(), "generation overwritten");
429+
430+
let e = store.remove(id13).expect("generation not yet overwritten");
431+
assert_eq!(e.method_name(), "method_13");
432+
}
433+
}

0 commit comments

Comments
 (0)