Skip to content

Commit 50f90d5

Browse files
bors[bot]Waridley
andauthored
Merge #748
748: Fix `Dictionary::get` unsoundness and add a few convenience functions r=Bromeon a=Waridley For some strange reason `godot_dictionary_get` calls `Dictionary::operator[]` instead of `Dictionary::get`, which ends up inserting `Nil` if the key doesn't already exist in the dictionary. https://github.com/godotengine/godot/blame/3.3/modules/gdnative/gdnative/dictionary.cpp#L126 This means it is unsound to call `godot_dictionary_get` on a `Dictionary<Shared>`. This PR re-implements godot-rust's `Dictionary::get` in terms of `godot_dictionary_get_with_default` (the behavior originally intended and likely expected by most Rust users), and adds `Dictionary::get_or_insert_nil` to provide the original behavior if it is indeed desired, but only on `Dictionary<T: LocalThreadAccess>` and an `unsafe` counterpart for `Dictionary<Shared>`, like the other mutating functions on `Dictionary`. I also updated the documentation of `get_ref` and `get_mut_ref` since they call `operator[]`. Perhaps they should also be moved to non-shared access, but I figured that's less important since they're unsafe anyway? I also added `try_get` and `get_non_nil` while I was at it, to simplify checking if a key exists and checking that its corresponding value is not `Nil` respectively. Co-authored-by: Waridley <Waridley64@gmail.com>
2 parents 206f710 + eccbc56 commit 50f90d5

File tree

5 files changed

+62
-23
lines changed

5 files changed

+62
-23
lines changed

gdnative-core/src/core_types/dictionary.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,49 @@ impl<Access: ThreadAccess> Dictionary<Access> {
6969
unsafe { (get_api().godot_dictionary_has_all)(self.sys(), keys.sys()) }
7070
}
7171

72-
/// Returns a copy of the value corresponding to the key.
72+
/// Returns a copy of the value corresponding to the key if it exists.
7373
#[inline]
74-
pub fn get<K>(&self, key: K) -> Variant
74+
pub fn get<K>(&self, key: K) -> Option<Variant>
7575
where
7676
K: ToVariant + ToVariantEq,
77+
{
78+
let key = key.to_variant();
79+
if self.contains(&key) {
80+
// This should never return the default Nil, but there isn't a safe way to otherwise check
81+
// if the entry exists in a single API call.
82+
Some(self.get_or_nil(key))
83+
} else {
84+
None
85+
}
86+
}
87+
88+
/// Returns a copy of the value corresponding to the key, or `default` if it doesn't exist
89+
#[inline]
90+
pub fn get_or<K, D>(&self, key: K, default: D) -> Variant
91+
where
92+
K: ToVariant + ToVariantEq,
93+
D: ToVariant,
7794
{
7895
unsafe {
79-
Variant((get_api().godot_dictionary_get)(
96+
Variant((get_api().godot_dictionary_get_with_default)(
8097
self.sys(),
8198
key.to_variant().sys(),
99+
default.to_variant().sys(),
82100
))
83101
}
84102
}
85103

86-
/// Update an existing element corresponding ot the key.
104+
/// Returns a copy of the element corresponding to the key, or `Nil` if it doesn't exist.
105+
/// Shorthand for `self.get_or(key, Variant::new())`.
106+
#[inline]
107+
pub fn get_or_nil<K>(&self, key: K) -> Variant
108+
where
109+
K: ToVariant + ToVariantEq,
110+
{
111+
self.get_or(key, Variant::new())
112+
}
113+
114+
/// Update an existing element corresponding to the key.
87115
///
88116
/// # Panics
89117
///
@@ -106,12 +134,14 @@ impl<Access: ThreadAccess> Dictionary<Access> {
106134
}
107135
}
108136

109-
/// Returns a reference to the value corresponding to the key.
137+
/// Returns a reference to the value corresponding to the key, inserting a `Nil` value first if
138+
/// it does not exist.
110139
///
111140
/// # Safety
112141
///
113142
/// The returned reference is invalidated if the same container is mutated through another
114-
/// reference.
143+
/// reference, and other references may be invalidated if the entry does not already exist
144+
/// (which causes this function to insert `Nil` and thus possibly re-allocate).
115145
///
116146
/// `Variant` is reference-counted and thus cheaply cloned. Consider using `get` instead.
117147
#[inline]
@@ -125,13 +155,16 @@ impl<Access: ThreadAccess> Dictionary<Access> {
125155
))
126156
}
127157

128-
/// Returns a mutable reference to the value corresponding to the key.
158+
/// Returns a mutable reference to the value corresponding to the key, inserting a `Nil` value
159+
/// first if it does not exist.
129160
///
130161
/// # Safety
131162
///
132163
/// The returned reference is invalidated if the same container is mutated through another
133-
/// reference. It is possible to create two mutable references to the same memory location
134-
/// if the same `key` is provided, causing undefined behavior.
164+
/// reference, and other references may be invalidated if the `key` does not already exist
165+
/// (which causes this function to insert `Nil` and thus possibly re-allocate). It is also
166+
/// possible to create two mutable references to the same memory location if the same `key`
167+
/// is provided, causing undefined behavior.
135168
#[inline]
136169
#[allow(clippy::mut_from_ref)]
137170
pub unsafe fn get_mut_ref<K>(&self, key: K) -> &mut Variant
@@ -425,7 +458,7 @@ unsafe fn iter_next<Access: ThreadAccess>(
425458
None
426459
} else {
427460
let key = Variant::cast_ref(next_ptr).clone();
428-
let value = dic.get(&key);
461+
let value = dic.get_or_nil(&key);
429462
*last_key = Some(key.clone());
430463
Some((key, value))
431464
}
@@ -591,7 +624,7 @@ godot_test!(test_dictionary {
591624
let mut iter_keys = HashSet::new();
592625
let expected_keys = ["foo", "bar"].iter().map(|&s| s.to_string()).collect::<HashSet<_>>();
593626
for (key, value) in &dict {
594-
assert_eq!(value, dict.get(&key));
627+
assert_eq!(Some(value), dict.get(&key));
595628
if !iter_keys.insert(key.to_string()) {
596629
panic!("key is already contained in set: {:?}", key);
597630
}

gdnative-core/src/core_types/variant.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {
17531753

17541754
match key.as_str() {
17551755
"Ok" => {
1756-
let val = T::from_variant(&dict.get(key_variant)).map_err(|err| {
1756+
let val = T::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
17571757
FVE::InvalidEnumVariant {
17581758
variant: "Ok",
17591759
error: Box::new(err),
@@ -1762,7 +1762,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {
17621762
Ok(Ok(val))
17631763
}
17641764
"Err" => {
1765-
let err = E::from_variant(&dict.get(key_variant)).map_err(|err| {
1765+
let err = E::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
17661766
FVE::InvalidEnumVariant {
17671767
variant: "Err",
17681768
error: Box::new(err),
@@ -1912,11 +1912,11 @@ godot_test!(
19121912
test_variant_result {
19131913
let variant = Result::<i64, ()>::Ok(42_i64).to_variant();
19141914
let dict = variant.try_to_dictionary().expect("should be dic");
1915-
assert_eq!(Some(42), dict.get("Ok").try_to_i64());
1915+
assert_eq!(Some(42), dict.get("Ok").and_then(|v| v.try_to_i64()));
19161916

19171917
let variant = Result::<(), i64>::Err(54_i64).to_variant();
19181918
let dict = variant.try_to_dictionary().expect("should be dic");
1919-
assert_eq!(Some(54), dict.get("Err").try_to_i64());
1919+
assert_eq!(Some(54), dict.get("Err").and_then(|v| v.try_to_i64()));
19201920

19211921
let variant = Variant::from_bool(true);
19221922
assert_eq!(

gdnative-derive/src/variant/from.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result<TokenStream
8787
match __key.as_str() {
8888
#(
8989
#ref_var_ident_string_literals => {
90-
let #var_input_ident_iter = &__dict.get(&__keys.get(0));
90+
let #var_input_ident_iter = &__dict.get_or_nil(&__keys.get(0));
9191
(#var_from_variants).map_err(|err| FVE::InvalidEnumVariant {
9292
variant: #ref_var_ident_string_literals,
9393
error: Box::new(err),

gdnative-derive/src/variant/repr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ impl VariantRepr {
280280
let name_string_literals =
281281
name_strings.iter().map(|string| Literal::string(&string));
282282

283-
let expr_variant = &quote!(&__dict.get(&__key));
283+
let expr_variant = &quote!(&__dict.get_or_nil(&__key));
284284
let exprs = non_skipped_fields
285285
.iter()
286286
.map(|f| f.from_variant(expr_variant));

test/src/test_derive.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,25 @@ fn test_derive_to_variant() -> bool {
8383

8484
let variant = data.to_variant();
8585
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
86-
assert_eq!(Some(42), dictionary.get("foo").try_to_i64());
87-
assert_eq!(Some(54.0), dictionary.get("bar").try_to_f64());
86+
assert_eq!(Some(42), dictionary.get("foo").and_then(|v| v.try_to_i64()));
87+
assert_eq!(
88+
Some(54.0),
89+
dictionary.get("bar").and_then(|v| v.try_to_f64())
90+
);
8891
assert_eq!(
8992
Some("*mut ()".into()),
90-
dictionary.get("ptr").try_to_string()
93+
dictionary.get("ptr").and_then(|v| v.try_to_string())
9194
);
9295
assert!(!dictionary.contains("skipped"));
9396

9497
let enum_dict = dictionary
9598
.get("baz")
96-
.try_to_dictionary()
99+
.and_then(|v| v.try_to_dictionary())
97100
.expect("should be dictionary");
98-
assert_eq!(Some(true), enum_dict.get("Foo").try_to_bool());
101+
assert_eq!(
102+
Some(true),
103+
enum_dict.get("Foo").and_then(|v| v.try_to_bool())
104+
);
99105

100106
assert_eq!(
101107
Ok(ToVar::<f64, i128> {
@@ -146,7 +152,7 @@ fn test_derive_owned_to_variant() -> bool {
146152
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
147153
let array = dictionary
148154
.get("arr")
149-
.try_to_array()
155+
.and_then(|v| v.try_to_array())
150156
.expect("should be array");
151157
assert_eq!(3, array.len());
152158
assert_eq!(

0 commit comments

Comments
 (0)