Browse Source

C#: Implement readonly-ness in Array and Dictionary

- Expose `IsReadOnly` and add `MakeReadOnly` method.
Raul Santos 2 years ago
parent
commit
1aa54141e6

+ 125 - 2
modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs

@@ -189,10 +189,15 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Resizes this <see cref="Array"/> to the given size.
         /// Resizes this <see cref="Array"/> to the given size.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="newSize">The new size of the array.</param>
         /// <param name="newSize">The new size of the array.</param>
         /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns>
         /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns>
         public Error Resize(int newSize)
         public Error Resize(int newSize)
         {
         {
+            ThrowIfReadOnly();
+
             var self = (godot_array)NativeValue;
             var self = (godot_array)NativeValue;
             return NativeFuncs.godotsharp_array_resize(ref self, newSize);
             return NativeFuncs.godotsharp_array_resize(ref self, newSize);
         }
         }
@@ -200,8 +205,13 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Shuffles the contents of this <see cref="Array"/> into a random order.
         /// Shuffles the contents of this <see cref="Array"/> into a random order.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         public void Shuffle()
         public void Shuffle()
         {
         {
+            ThrowIfReadOnly();
+
             var self = (godot_array)NativeValue;
             var self = (godot_array)NativeValue;
             NativeFuncs.godotsharp_array_shuffle(ref self);
             NativeFuncs.godotsharp_array_shuffle(ref self);
         }
         }
@@ -240,6 +250,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Returns the item at the given <paramref name="index"/>.
         /// Returns the item at the given <paramref name="index"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The property is assigned and the array is read-only.
+        /// </exception>
         /// <value>The <see cref="Variant"/> item at the given <paramref name="index"/>.</value>
         /// <value>The <see cref="Variant"/> item at the given <paramref name="index"/>.</value>
         public unsafe Variant this[int index]
         public unsafe Variant this[int index]
         {
         {
@@ -250,8 +263,11 @@ namespace Godot.Collections
             }
             }
             set
             set
             {
             {
+                ThrowIfReadOnly();
+
                 if (index < 0 || index >= Count)
                 if (index < 0 || index >= Count)
                     throw new ArgumentOutOfRangeException(nameof(index));
                     throw new ArgumentOutOfRangeException(nameof(index));
+
                 var self = (godot_array)NativeValue;
                 var self = (godot_array)NativeValue;
                 godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self);
                 godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self);
                 godot_variant* itemPtr = &ptrw[index];
                 godot_variant* itemPtr = &ptrw[index];
@@ -264,9 +280,14 @@ namespace Godot.Collections
         /// Adds an item to the end of this <see cref="Array"/>.
         /// Adds an item to the end of this <see cref="Array"/>.
         /// This is the same as <c>append</c> or <c>push_back</c> in GDScript.
         /// This is the same as <c>append</c> or <c>push_back</c> in GDScript.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="item">The <see cref="Variant"/> item to add.</param>
         /// <param name="item">The <see cref="Variant"/> item to add.</param>
         public void Add(Variant item)
         public void Add(Variant item)
         {
         {
+            ThrowIfReadOnly();
+
             godot_variant variantValue = (godot_variant)item.NativeVar;
             godot_variant variantValue = (godot_variant)item.NativeVar;
             var self = (godot_array)NativeValue;
             var self = (godot_array)NativeValue;
             _ = NativeFuncs.godotsharp_array_add(ref self, variantValue);
             _ = NativeFuncs.godotsharp_array_add(ref self, variantValue);
@@ -282,6 +303,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Erases all items from this <see cref="Array"/>.
         /// Erases all items from this <see cref="Array"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         public void Clear() => Resize(0);
         public void Clear() => Resize(0);
 
 
         /// <summary>
         /// <summary>
@@ -303,10 +327,15 @@ namespace Godot.Collections
         /// or the position at the end of the array.
         /// or the position at the end of the array.
         /// Existing items will be moved to the right.
         /// Existing items will be moved to the right.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="index">The index to insert at.</param>
         /// <param name="index">The index to insert at.</param>
         /// <param name="item">The <see cref="Variant"/> item to insert.</param>
         /// <param name="item">The <see cref="Variant"/> item to insert.</param>
         public void Insert(int index, Variant item)
         public void Insert(int index, Variant item)
         {
         {
+            ThrowIfReadOnly();
+
             if (index < 0 || index > Count)
             if (index < 0 || index > Count)
                 throw new ArgumentOutOfRangeException(nameof(index));
                 throw new ArgumentOutOfRangeException(nameof(index));
 
 
@@ -319,9 +348,14 @@ namespace Godot.Collections
         /// Removes the first occurrence of the specified <paramref name="item"/>
         /// Removes the first occurrence of the specified <paramref name="item"/>
         /// from this <see cref="Array"/>.
         /// from this <see cref="Array"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="item">The value to remove.</param>
         /// <param name="item">The value to remove.</param>
         public bool Remove(Variant item)
         public bool Remove(Variant item)
         {
         {
+            ThrowIfReadOnly();
+
             int index = IndexOf(item);
             int index = IndexOf(item);
             if (index >= 0)
             if (index >= 0)
             {
             {
@@ -335,9 +369,14 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Removes an element from this <see cref="Array"/> by index.
         /// Removes an element from this <see cref="Array"/> by index.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="index">The index of the element to remove.</param>
         /// <param name="index">The index of the element to remove.</param>
         public void RemoveAt(int index)
         public void RemoveAt(int index)
         {
         {
+            ThrowIfReadOnly();
+
             if (index < 0 || index > Count)
             if (index < 0 || index > Count)
                 throw new ArgumentOutOfRangeException(nameof(index));
                 throw new ArgumentOutOfRangeException(nameof(index));
 
 
@@ -358,7 +397,28 @@ namespace Godot.Collections
 
 
         object ICollection.SyncRoot => false;
         object ICollection.SyncRoot => false;
 
 
-        bool ICollection<Variant>.IsReadOnly => false;
+        /// <summary>
+        /// Returns <see langword="true"/> if the array is read-only.
+        /// See <see cref="MakeReadOnly"/>.
+        /// </summary>
+        public bool IsReadOnly => NativeValue.DangerousSelfRef.IsReadOnly;
+
+        /// <summary>
+        /// Makes the <see cref="Array"/> read-only, i.e. disabled modying of the
+        /// array's elements. Does not apply to nested content, e.g. content of
+        /// nested arrays.
+        /// </summary>
+        public void MakeReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                // Avoid interop call when the array is already read-only.
+                return;
+            }
+
+            var self = (godot_array)NativeValue;
+            NativeFuncs.godotsharp_array_make_read_only(ref self);
+        }
 
 
         /// <summary>
         /// <summary>
         /// Copies the elements of this <see cref="Array"/> to the given
         /// Copies the elements of this <see cref="Array"/> to the given
@@ -472,6 +532,14 @@ namespace Godot.Collections
         {
         {
             elem = NativeValue.DangerousSelfRef.Elements[index];
             elem = NativeValue.DangerousSelfRef.Elements[index];
         }
         }
+
+        private void ThrowIfReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                throw new InvalidOperationException("Array instance is read-only.");
+            }
+        }
     }
     }
 
 
     internal interface IGenericGodotArray
     internal interface IGenericGodotArray
@@ -592,6 +660,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Resizes this <see cref="Array{T}"/> to the given size.
         /// Resizes this <see cref="Array{T}"/> to the given size.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="newSize">The new size of the array.</param>
         /// <param name="newSize">The new size of the array.</param>
         /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns>
         /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns>
         public Error Resize(int newSize)
         public Error Resize(int newSize)
@@ -602,6 +673,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Shuffles the contents of this <see cref="Array{T}"/> into a random order.
         /// Shuffles the contents of this <see cref="Array{T}"/> into a random order.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         public void Shuffle()
         public void Shuffle()
         {
         {
             _underlyingArray.Shuffle();
             _underlyingArray.Shuffle();
@@ -634,6 +708,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Returns the value at the given <paramref name="index"/>.
         /// Returns the value at the given <paramref name="index"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The property is assigned and the array is read-only.
+        /// </exception>
         /// <value>The value at the given <paramref name="index"/>.</value>
         /// <value>The value at the given <paramref name="index"/>.</value>
         public unsafe T this[int index]
         public unsafe T this[int index]
         {
         {
@@ -644,8 +721,11 @@ namespace Godot.Collections
             }
             }
             set
             set
             {
             {
+                ThrowIfReadOnly();
+
                 if (index < 0 || index >= Count)
                 if (index < 0 || index >= Count)
                     throw new ArgumentOutOfRangeException(nameof(index));
                     throw new ArgumentOutOfRangeException(nameof(index));
+
                 var self = (godot_array)_underlyingArray.NativeValue;
                 var self = (godot_array)_underlyingArray.NativeValue;
                 godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self);
                 godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self);
                 godot_variant* itemPtr = &ptrw[index];
                 godot_variant* itemPtr = &ptrw[index];
@@ -673,10 +753,15 @@ namespace Godot.Collections
         /// or the position at the end of the array.
         /// or the position at the end of the array.
         /// Existing items will be moved to the right.
         /// Existing items will be moved to the right.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="index">The index to insert at.</param>
         /// <param name="index">The index to insert at.</param>
         /// <param name="item">The item to insert.</param>
         /// <param name="item">The item to insert.</param>
         public void Insert(int index, T item)
         public void Insert(int index, T item)
         {
         {
+            ThrowIfReadOnly();
+
             if (index < 0 || index > Count)
             if (index < 0 || index > Count)
                 throw new ArgumentOutOfRangeException(nameof(index));
                 throw new ArgumentOutOfRangeException(nameof(index));
 
 
@@ -688,6 +773,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Removes an element from this <see cref="Array{T}"/> by index.
         /// Removes an element from this <see cref="Array{T}"/> by index.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="index">The index of the element to remove.</param>
         /// <param name="index">The index of the element to remove.</param>
         public void RemoveAt(int index)
         public void RemoveAt(int index)
         {
         {
@@ -703,16 +791,35 @@ namespace Godot.Collections
         /// <returns>The number of elements.</returns>
         /// <returns>The number of elements.</returns>
         public int Count => _underlyingArray.Count;
         public int Count => _underlyingArray.Count;
 
 
-        bool ICollection<T>.IsReadOnly => false;
+        /// <summary>
+        /// Returns <see langword="true"/> if the array is read-only.
+        /// See <see cref="MakeReadOnly"/>.
+        /// </summary>
+        public bool IsReadOnly => _underlyingArray.IsReadOnly;
+
+        /// <summary>
+        /// Makes the <see cref="Array{T}"/> read-only, i.e. disabled modying of the
+        /// array's elements. Does not apply to nested content, e.g. content of
+        /// nested arrays.
+        /// </summary>
+        public void MakeReadOnly()
+        {
+            _underlyingArray.MakeReadOnly();
+        }
 
 
         /// <summary>
         /// <summary>
         /// Adds an item to the end of this <see cref="Array{T}"/>.
         /// Adds an item to the end of this <see cref="Array{T}"/>.
         /// This is the same as <c>append</c> or <c>push_back</c> in GDScript.
         /// This is the same as <c>append</c> or <c>push_back</c> in GDScript.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="item">The item to add.</param>
         /// <param name="item">The item to add.</param>
         /// <returns>The new size after adding the item.</returns>
         /// <returns>The new size after adding the item.</returns>
         public void Add(T item)
         public void Add(T item)
         {
         {
+            ThrowIfReadOnly();
+
             using var variantValue = VariantUtils.CreateFrom(item);
             using var variantValue = VariantUtils.CreateFrom(item);
             var self = (godot_array)_underlyingArray.NativeValue;
             var self = (godot_array)_underlyingArray.NativeValue;
             _ = NativeFuncs.godotsharp_array_add(ref self, variantValue);
             _ = NativeFuncs.godotsharp_array_add(ref self, variantValue);
@@ -721,6 +828,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Erases all items from this <see cref="Array{T}"/>.
         /// Erases all items from this <see cref="Array{T}"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         public void Clear()
         public void Clear()
         {
         {
             _underlyingArray.Clear();
             _underlyingArray.Clear();
@@ -769,10 +879,15 @@ namespace Godot.Collections
         /// Removes the first occurrence of the specified value
         /// Removes the first occurrence of the specified value
         /// from this <see cref="Array{T}"/>.
         /// from this <see cref="Array{T}"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The array is read-only.
+        /// </exception>
         /// <param name="item">The value to remove.</param>
         /// <param name="item">The value to remove.</param>
         /// <returns>A <see langword="bool"/> indicating success or failure.</returns>
         /// <returns>A <see langword="bool"/> indicating success or failure.</returns>
         public bool Remove(T item)
         public bool Remove(T item)
         {
         {
+            ThrowIfReadOnly();
+
             int index = IndexOf(item);
             int index = IndexOf(item);
             if (index >= 0)
             if (index >= 0)
             {
             {
@@ -812,5 +927,13 @@ namespace Godot.Collections
 
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static explicit operator Array<T>(Variant from) => from.AsGodotArray<T>();
         public static explicit operator Array<T>(Variant from) => from.AsGodotArray<T>();
+
+        private void ThrowIfReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                throw new InvalidOperationException("Array instance is read-only.");
+            }
+        }
     }
     }
 }
 }

+ 109 - 3
modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs

@@ -93,10 +93,15 @@ namespace Godot.Collections
         /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/>
         /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/>
         /// is <see langword="true"/>.
         /// is <see langword="true"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <param name="dictionary">Dictionary to copy entries from.</param>
         /// <param name="dictionary">Dictionary to copy entries from.</param>
         /// <param name="overwrite">If duplicate keys should be copied over as well.</param>
         /// <param name="overwrite">If duplicate keys should be copied over as well.</param>
         public void Merge(Dictionary dictionary, bool overwrite = false)
         public void Merge(Dictionary dictionary, bool overwrite = false)
         {
         {
+            ThrowIfReadOnly();
+
             var self = (godot_dictionary)NativeValue;
             var self = (godot_dictionary)NativeValue;
             var other = (godot_dictionary)dictionary.NativeValue;
             var other = (godot_dictionary)dictionary.NativeValue;
             NativeFuncs.godotsharp_dictionary_merge(ref self, in other, overwrite.ToGodotBool());
             NativeFuncs.godotsharp_dictionary_merge(ref self, in other, overwrite.ToGodotBool());
@@ -175,8 +180,12 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Returns the value at the given <paramref name="key"/>.
         /// Returns the value at the given <paramref name="key"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The property is assigned and the dictionary is read-only.
+        /// </exception>
         /// <exception cref="KeyNotFoundException">
         /// <exception cref="KeyNotFoundException">
-        /// An entry for <paramref name="key"/> does not exist in the dictionary.
+        /// The property is retrieved and an entry for <paramref name="key"/>
+        /// does not exist in the dictionary.
         /// </exception>
         /// </exception>
         /// <value>The value at the given <paramref name="key"/>.</value>
         /// <value>The value at the given <paramref name="key"/>.</value>
         public Variant this[Variant key]
         public Variant this[Variant key]
@@ -197,6 +206,8 @@ namespace Godot.Collections
             }
             }
             set
             set
             {
             {
+                ThrowIfReadOnly();
+
                 var self = (godot_dictionary)NativeValue;
                 var self = (godot_dictionary)NativeValue;
                 NativeFuncs.godotsharp_dictionary_set_value(ref self,
                 NativeFuncs.godotsharp_dictionary_set_value(ref self,
                     (godot_variant)key.NativeVar, (godot_variant)value.NativeVar);
                     (godot_variant)key.NativeVar, (godot_variant)value.NativeVar);
@@ -207,6 +218,9 @@ namespace Godot.Collections
         /// Adds an value <paramref name="value"/> at key <paramref name="key"/>
         /// Adds an value <paramref name="value"/> at key <paramref name="key"/>
         /// to this <see cref="Dictionary"/>.
         /// to this <see cref="Dictionary"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <exception cref="ArgumentException">
         /// <exception cref="ArgumentException">
         /// An entry for <paramref name="key"/> already exists in the dictionary.
         /// An entry for <paramref name="key"/> already exists in the dictionary.
         /// </exception>
         /// </exception>
@@ -214,6 +228,8 @@ namespace Godot.Collections
         /// <param name="value">The value to add.</param>
         /// <param name="value">The value to add.</param>
         public void Add(Variant key, Variant value)
         public void Add(Variant key, Variant value)
         {
         {
+            ThrowIfReadOnly();
+
             var variantKey = (godot_variant)key.NativeVar;
             var variantKey = (godot_variant)key.NativeVar;
             var self = (godot_dictionary)NativeValue;
             var self = (godot_dictionary)NativeValue;
 
 
@@ -230,8 +246,13 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Clears the dictionary, removing all entries from it.
         /// Clears the dictionary, removing all entries from it.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         public void Clear()
         public void Clear()
         {
         {
+            ThrowIfReadOnly();
+
             var self = (godot_dictionary)NativeValue;
             var self = (godot_dictionary)NativeValue;
             NativeFuncs.godotsharp_dictionary_clear(ref self);
             NativeFuncs.godotsharp_dictionary_clear(ref self);
         }
         }
@@ -267,15 +288,22 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Removes an element from this <see cref="Dictionary"/> by key.
         /// Removes an element from this <see cref="Dictionary"/> by key.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <param name="key">The key of the element to remove.</param>
         /// <param name="key">The key of the element to remove.</param>
         public bool Remove(Variant key)
         public bool Remove(Variant key)
         {
         {
+            ThrowIfReadOnly();
+
             var self = (godot_dictionary)NativeValue;
             var self = (godot_dictionary)NativeValue;
             return NativeFuncs.godotsharp_dictionary_remove_key(ref self, (godot_variant)key.NativeVar).ToBool();
             return NativeFuncs.godotsharp_dictionary_remove_key(ref self, (godot_variant)key.NativeVar).ToBool();
         }
         }
 
 
         bool ICollection<KeyValuePair<Variant, Variant>>.Remove(KeyValuePair<Variant, Variant> item)
         bool ICollection<KeyValuePair<Variant, Variant>>.Remove(KeyValuePair<Variant, Variant> item)
         {
         {
+            ThrowIfReadOnly();
+
             godot_variant variantKey = (godot_variant)item.Key.NativeVar;
             godot_variant variantKey = (godot_variant)item.Key.NativeVar;
             var self = (godot_dictionary)NativeValue;
             var self = (godot_dictionary)NativeValue;
             bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
             bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
@@ -311,7 +339,28 @@ namespace Godot.Collections
             }
             }
         }
         }
 
 
-        bool ICollection<KeyValuePair<Variant, Variant>>.IsReadOnly => false;
+        /// <summary>
+        /// Returns <see langword="true"/> if the dictionary is read-only.
+        /// See <see cref="MakeReadOnly"/>.
+        /// </summary>
+        public bool IsReadOnly => NativeValue.DangerousSelfRef.IsReadOnly;
+
+        /// <summary>
+        /// Makes the <see cref="Dictionary"/> read-only, i.e. disabled modying of the
+        /// dictionary's elements. Does not apply to nested content, e.g. content of
+        /// nested dictionaries.
+        /// </summary>
+        public void MakeReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                // Avoid interop call when the dictionary is already read-only.
+                return;
+            }
+
+            var self = (godot_dictionary)NativeValue;
+            NativeFuncs.godotsharp_dictionary_make_read_only(ref self);
+        }
 
 
         /// <summary>
         /// <summary>
         /// Gets the value for the given <paramref name="key"/> in the dictionary.
         /// Gets the value for the given <paramref name="key"/> in the dictionary.
@@ -397,6 +446,14 @@ namespace Godot.Collections
             using (str)
             using (str)
                 return Marshaling.ConvertStringToManaged(str);
                 return Marshaling.ConvertStringToManaged(str);
         }
         }
+
+        private void ThrowIfReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                throw new InvalidOperationException("Dictionary instance is read-only.");
+            }
+        }
     }
     }
 
 
     internal interface IGenericGodotDictionary
     internal interface IGenericGodotDictionary
@@ -508,6 +565,9 @@ namespace Godot.Collections
         /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/>
         /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/>
         /// is <see langword="true"/>.
         /// is <see langword="true"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <param name="dictionary">Dictionary to copy entries from.</param>
         /// <param name="dictionary">Dictionary to copy entries from.</param>
         /// <param name="overwrite">If duplicate keys should be copied over as well.</param>
         /// <param name="overwrite">If duplicate keys should be copied over as well.</param>
         public void Merge(Dictionary<TKey, TValue> dictionary, bool overwrite = false)
         public void Merge(Dictionary<TKey, TValue> dictionary, bool overwrite = false)
@@ -536,6 +596,13 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Returns the value at the given <paramref name="key"/>.
         /// Returns the value at the given <paramref name="key"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The property is assigned and the dictionary is read-only.
+        /// </exception>
+        /// <exception cref="KeyNotFoundException">
+        /// The property is retrieved and an entry for <paramref name="key"/>
+        /// does not exist in the dictionary.
+        /// </exception>
         /// <value>The value at the given <paramref name="key"/>.</value>
         /// <value>The value at the given <paramref name="key"/>.</value>
         public TValue this[TKey key]
         public TValue this[TKey key]
         {
         {
@@ -557,6 +624,8 @@ namespace Godot.Collections
             }
             }
             set
             set
             {
             {
+                ThrowIfReadOnly();
+
                 using var variantKey = VariantUtils.CreateFrom(key);
                 using var variantKey = VariantUtils.CreateFrom(key);
                 using var variantValue = VariantUtils.CreateFrom(value);
                 using var variantValue = VariantUtils.CreateFrom(value);
                 var self = (godot_dictionary)_underlyingDict.NativeValue;
                 var self = (godot_dictionary)_underlyingDict.NativeValue;
@@ -616,10 +685,15 @@ namespace Godot.Collections
         /// Adds an object <paramref name="value"/> at key <paramref name="key"/>
         /// Adds an object <paramref name="value"/> at key <paramref name="key"/>
         /// to this <see cref="Dictionary{TKey, TValue}"/>.
         /// to this <see cref="Dictionary{TKey, TValue}"/>.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <param name="key">The key at which to add the object.</param>
         /// <param name="key">The key at which to add the object.</param>
         /// <param name="value">The object to add.</param>
         /// <param name="value">The object to add.</param>
         public void Add(TKey key, TValue value)
         public void Add(TKey key, TValue value)
         {
         {
+            ThrowIfReadOnly();
+
             using var variantKey = VariantUtils.CreateFrom(key);
             using var variantKey = VariantUtils.CreateFrom(key);
             var self = (godot_dictionary)_underlyingDict.NativeValue;
             var self = (godot_dictionary)_underlyingDict.NativeValue;
 
 
@@ -645,9 +719,14 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Removes an element from this <see cref="Dictionary{TKey, TValue}"/> by key.
         /// Removes an element from this <see cref="Dictionary{TKey, TValue}"/> by key.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         /// <param name="key">The key of the element to remove.</param>
         /// <param name="key">The key of the element to remove.</param>
         public bool Remove(TKey key)
         public bool Remove(TKey key)
         {
         {
+            ThrowIfReadOnly();
+
             using var variantKey = VariantUtils.CreateFrom(key);
             using var variantKey = VariantUtils.CreateFrom(key);
             var self = (godot_dictionary)_underlyingDict.NativeValue;
             var self = (godot_dictionary)_underlyingDict.NativeValue;
             return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool();
             return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool();
@@ -683,7 +762,21 @@ namespace Godot.Collections
         /// <returns>The number of elements.</returns>
         /// <returns>The number of elements.</returns>
         public int Count => _underlyingDict.Count;
         public int Count => _underlyingDict.Count;
 
 
-        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly => false;
+        /// <summary>
+        /// Returns <see langword="true"/> if the dictionary is read-only.
+        /// See <see cref="MakeReadOnly"/>.
+        /// </summary>
+        public bool IsReadOnly => _underlyingDict.IsReadOnly;
+
+        /// <summary>
+        /// Makes the <see cref="Dictionary{TKey, TValue}"/> read-only, i.e. disabled
+        /// modying of the dictionary's elements. Does not apply to nested content,
+        /// e.g. content of nested dictionaries.
+        /// </summary>
+        public void MakeReadOnly()
+        {
+            _underlyingDict.MakeReadOnly();
+        }
 
 
         void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
         void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
             => Add(item.Key, item.Value);
             => Add(item.Key, item.Value);
@@ -691,6 +784,9 @@ namespace Godot.Collections
         /// <summary>
         /// <summary>
         /// Clears the dictionary, removing all entries from it.
         /// Clears the dictionary, removing all entries from it.
         /// </summary>
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The dictionary is read-only.
+        /// </exception>
         public void Clear() => _underlyingDict.Clear();
         public void Clear() => _underlyingDict.Clear();
 
 
         bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
         bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
@@ -740,6 +836,8 @@ namespace Godot.Collections
 
 
         bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
         bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
         {
         {
+            ThrowIfReadOnly();
+
             using var variantKey = VariantUtils.CreateFrom(item.Key);
             using var variantKey = VariantUtils.CreateFrom(item.Key);
             var self = (godot_dictionary)_underlyingDict.NativeValue;
             var self = (godot_dictionary)_underlyingDict.NativeValue;
             bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
             bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
@@ -789,5 +887,13 @@ namespace Godot.Collections
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static explicit operator Dictionary<TKey, TValue>(Variant from) =>
         public static explicit operator Dictionary<TKey, TValue>(Variant from) =>
             from.AsGodotDictionary<TKey, TValue>();
             from.AsGodotDictionary<TKey, TValue>();
+
+        private void ThrowIfReadOnly()
+        {
+            if (IsReadOnly)
+            {
+                throw new InvalidOperationException("Dictionary instance is read-only.");
+            }
+        }
     }
     }
 }
 }

+ 48 - 9
modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs

@@ -697,6 +697,9 @@ namespace Godot.NativeInterop
             private uint _safeRefCount;
             private uint _safeRefCount;
 
 
             public VariantVector _arrayVector;
             public VariantVector _arrayVector;
+
+            private unsafe godot_variant* _readOnly;
+
             // There are more fields here, but we don't care as we never store this in C#
             // There are more fields here, but we don't care as we never store this in C#
 
 
             public readonly int Size
             public readonly int Size
@@ -704,6 +707,12 @@ namespace Godot.NativeInterop
                 [MethodImpl(MethodImplOptions.AggressiveInlining)]
                 [MethodImpl(MethodImplOptions.AggressiveInlining)]
                 get => _arrayVector.Size;
                 get => _arrayVector.Size;
             }
             }
+
+            public readonly unsafe bool IsReadOnly
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get => _readOnly != null;
+            }
         }
         }
 
 
         [StructLayout(LayoutKind.Sequential)]
         [StructLayout(LayoutKind.Sequential)]
@@ -737,6 +746,12 @@ namespace Godot.NativeInterop
             get => _p != null ? _p->Size : 0;
             get => _p != null ? _p->Size : 0;
         }
         }
 
 
+        public readonly unsafe bool IsReadOnly
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => _p != null && _p->IsReadOnly;
+        }
+
         public unsafe void Dispose()
         public unsafe void Dispose()
         {
         {
             if (_p == null)
             if (_p == null)
@@ -766,35 +781,59 @@ namespace Godot.NativeInterop
     // A correctly constructed value needs to call the native default constructor to allocate `_p`.
     // A correctly constructed value needs to call the native default constructor to allocate `_p`.
     // Don't pass a C# default constructed `godot_dictionary` to native code, unless it's going to
     // Don't pass a C# default constructed `godot_dictionary` to native code, unless it's going to
     // be re-assigned a new value (the copy constructor checks if `_p` is null so that's fine).
     // be re-assigned a new value (the copy constructor checks if `_p` is null so that's fine).
-    [StructLayout(LayoutKind.Sequential)]
+    [StructLayout(LayoutKind.Explicit)]
     // ReSharper disable once InconsistentNaming
     // ReSharper disable once InconsistentNaming
     public ref struct godot_dictionary
     public ref struct godot_dictionary
     {
     {
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal readonly unsafe godot_dictionary* GetUnsafeAddress()
         internal readonly unsafe godot_dictionary* GetUnsafeAddress()
-            => (godot_dictionary*)Unsafe.AsPointer(ref Unsafe.AsRef(in _p));
+            => (godot_dictionary*)Unsafe.AsPointer(ref Unsafe.AsRef(in _getUnsafeAddressHelper));
 
 
-        private IntPtr _p;
+        [FieldOffset(0)] private byte _getUnsafeAddressHelper;
 
 
-        public readonly bool IsAllocated
+        [FieldOffset(0)] private unsafe DictionaryPrivate* _p;
+
+        [StructLayout(LayoutKind.Sequential)]
+        private struct DictionaryPrivate
+        {
+            private uint _safeRefCount;
+
+            private unsafe godot_variant* _readOnly;
+
+            // There are more fields here, but we don't care as we never store this in C#
+
+            public readonly unsafe bool IsReadOnly
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get => _readOnly != null;
+            }
+        }
+
+        public readonly unsafe bool IsAllocated
         {
         {
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get => _p != IntPtr.Zero;
+            get => _p != null;
         }
         }
 
 
-        public void Dispose()
+        public readonly unsafe bool IsReadOnly
         {
         {
-            if (_p == IntPtr.Zero)
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => _p != null && _p->IsReadOnly;
+        }
+
+        public unsafe void Dispose()
+        {
+            if (_p == null)
                 return;
                 return;
             NativeFuncs.godotsharp_dictionary_destroy(ref this);
             NativeFuncs.godotsharp_dictionary_destroy(ref this);
-            _p = IntPtr.Zero;
+            _p = null;
         }
         }
 
 
         [StructLayout(LayoutKind.Sequential)]
         [StructLayout(LayoutKind.Sequential)]
         // ReSharper disable once InconsistentNaming
         // ReSharper disable once InconsistentNaming
         internal struct movable
         internal struct movable
         {
         {
-            private IntPtr _p;
+            private unsafe DictionaryPrivate* _p;
 
 
             public static unsafe explicit operator movable(in godot_dictionary value)
             public static unsafe explicit operator movable(in godot_dictionary value)
                 => *(movable*)CustomUnsafe.AsPointer(ref CustomUnsafe.AsRef(value));
                 => *(movable*)CustomUnsafe.AsPointer(ref CustomUnsafe.AsRef(value));

+ 4 - 0
modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs

@@ -376,6 +376,8 @@ namespace Godot.NativeInterop
 
 
         public static partial Error godotsharp_array_resize(ref godot_array p_self, int p_new_size);
         public static partial Error godotsharp_array_resize(ref godot_array p_self, int p_new_size);
 
 
+        public static partial void godotsharp_array_make_read_only(ref godot_array p_self);
+
         public static partial void godotsharp_array_shuffle(ref godot_array p_self);
         public static partial void godotsharp_array_shuffle(ref godot_array p_self);
 
 
         public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str);
         public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str);
@@ -416,6 +418,8 @@ namespace Godot.NativeInterop
         public static partial godot_bool godotsharp_dictionary_remove_key(ref godot_dictionary p_self,
         public static partial godot_bool godotsharp_dictionary_remove_key(ref godot_dictionary p_self,
             in godot_variant p_key);
             in godot_variant p_key);
 
 
+        public static partial void godotsharp_dictionary_make_read_only(ref godot_dictionary p_self);
+
         public static partial void godotsharp_dictionary_to_string(ref godot_dictionary p_self, out godot_string r_str);
         public static partial void godotsharp_dictionary_to_string(ref godot_dictionary p_self, out godot_string r_str);
 
 
         // StringExtensions
         // StringExtensions

+ 10 - 0
modules/mono/glue/runtime_interop.cpp

@@ -1012,6 +1012,10 @@ int32_t godotsharp_array_resize(Array *p_self, int32_t p_new_size) {
 	return (int32_t)p_self->resize(p_new_size);
 	return (int32_t)p_self->resize(p_new_size);
 }
 }
 
 
+void godotsharp_array_make_read_only(Array *p_self) {
+	p_self->make_read_only();
+}
+
 void godotsharp_array_shuffle(Array *p_self) {
 void godotsharp_array_shuffle(Array *p_self) {
 	p_self->shuffle();
 	p_self->shuffle();
 }
 }
@@ -1081,6 +1085,10 @@ bool godotsharp_dictionary_remove_key(Dictionary *p_self, const Variant *p_key)
 	return p_self->erase(*p_key);
 	return p_self->erase(*p_key);
 }
 }
 
 
+void godotsharp_dictionary_make_read_only(Dictionary *p_self) {
+	p_self->make_read_only();
+}
+
 void godotsharp_dictionary_to_string(const Dictionary *p_self, String *r_str) {
 void godotsharp_dictionary_to_string(const Dictionary *p_self, String *r_str) {
 	*r_str = Variant(*p_self).operator String();
 	*r_str = Variant(*p_self).operator String();
 }
 }
@@ -1439,6 +1447,7 @@ static const void *unmanaged_callbacks[]{
 	(void *)godotsharp_array_insert,
 	(void *)godotsharp_array_insert,
 	(void *)godotsharp_array_remove_at,
 	(void *)godotsharp_array_remove_at,
 	(void *)godotsharp_array_resize,
 	(void *)godotsharp_array_resize,
+	(void *)godotsharp_array_make_read_only,
 	(void *)godotsharp_array_shuffle,
 	(void *)godotsharp_array_shuffle,
 	(void *)godotsharp_array_to_string,
 	(void *)godotsharp_array_to_string,
 	(void *)godotsharp_dictionary_try_get_value,
 	(void *)godotsharp_dictionary_try_get_value,
@@ -1454,6 +1463,7 @@ static const void *unmanaged_callbacks[]{
 	(void *)godotsharp_dictionary_merge,
 	(void *)godotsharp_dictionary_merge,
 	(void *)godotsharp_dictionary_recursive_equal,
 	(void *)godotsharp_dictionary_recursive_equal,
 	(void *)godotsharp_dictionary_remove_key,
 	(void *)godotsharp_dictionary_remove_key,
+	(void *)godotsharp_dictionary_make_read_only,
 	(void *)godotsharp_dictionary_to_string,
 	(void *)godotsharp_dictionary_to_string,
 	(void *)godotsharp_string_simplify_path,
 	(void *)godotsharp_string_simplify_path,
 	(void *)godotsharp_string_to_camel_case,
 	(void *)godotsharp_string_to_camel_case,