C#: Add Variant conversion callbacks for generic Godot collections

This allows using generic Godot collections as type arguments for other
generic Godot collections. This also allows generic Godot collections
as parameter or return type in dynamic Callable invocations.
This commit is contained in:
Ignacio Roldán Etcheverry 2022-10-28 22:59:15 +02:00
parent 282bd37e5c
commit f66a352c0f
3 changed files with 106 additions and 37 deletions

View File

@ -489,25 +489,37 @@ namespace Godot.Collections
ICollection<T>,
IEnumerable<T>
{
private static godot_variant ToVariantFunc(in Array<T> godotArray) =>
VariantUtils.CreateFromArray(godotArray);
private static Array<T> FromVariantFunc(in godot_variant variant) =>
VariantUtils.ConvertToArrayObject<T>(variant);
// ReSharper disable StaticMemberInGenericType
// Warning is about unique static fields being created for each generic type combination:
// https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html
// In our case this is exactly what we want.
private static unsafe delegate* managed<in T, godot_variant> _convertToVariantCallback;
private static unsafe delegate* managed<in godot_variant, T> _convertToManagedCallback;
private static readonly unsafe delegate* managed<in T, godot_variant> ConvertToVariantCallback;
private static readonly unsafe delegate* managed<in godot_variant, T> ConvertToManagedCallback;
// ReSharper restore StaticMemberInGenericType
static unsafe Array()
{
_convertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<T>();
_convertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<T>();
VariantConversionCallbacks.GenericConversionCallbacks[typeof(Array<T>)] =
(
(IntPtr)(delegate* managed<in Array<T>, godot_variant>)&ToVariantFunc,
(IntPtr)(delegate* managed<in godot_variant, Array<T>>)&FromVariantFunc
);
ConvertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<T>();
ConvertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<T>();
}
private static unsafe void ValidateVariantConversionCallbacks()
{
if (_convertToVariantCallback == null || _convertToManagedCallback == null)
if (ConvertToVariantCallback == null || ConvertToManagedCallback == null)
{
throw new InvalidOperationException(
$"The array element type is not supported for conversion to Variant: '{typeof(T).FullName}'.");
@ -653,7 +665,7 @@ namespace Godot.Collections
get
{
_underlyingArray.GetVariantBorrowElementAt(index, out godot_variant borrowElem);
return _convertToManagedCallback(borrowElem);
return ConvertToManagedCallback(borrowElem);
}
set
{
@ -663,7 +675,7 @@ namespace Godot.Collections
godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self);
godot_variant* itemPtr = &ptrw[index];
(*itemPtr).Dispose();
*itemPtr = _convertToVariantCallback(value);
*itemPtr = ConvertToVariantCallback(value);
}
}
@ -675,7 +687,7 @@ namespace Godot.Collections
/// <returns>The index of the item, or -1 if not found.</returns>
public unsafe int IndexOf(T item)
{
using var variantValue = _convertToVariantCallback(item);
using var variantValue = ConvertToVariantCallback(item);
var self = (godot_array)_underlyingArray.NativeValue;
return NativeFuncs.godotsharp_array_index_of(ref self, variantValue);
}
@ -693,7 +705,7 @@ namespace Godot.Collections
if (index < 0 || index > Count)
throw new ArgumentOutOfRangeException(nameof(index));
using var variantValue = _convertToVariantCallback(item);
using var variantValue = ConvertToVariantCallback(item);
var self = (godot_array)_underlyingArray.NativeValue;
NativeFuncs.godotsharp_array_insert(ref self, index, variantValue);
}
@ -726,7 +738,7 @@ namespace Godot.Collections
/// <returns>The new size after adding the item.</returns>
public unsafe void Add(T item)
{
using var variantValue = _convertToVariantCallback(item);
using var variantValue = ConvertToVariantCallback(item);
var self = (godot_array)_underlyingArray.NativeValue;
_ = NativeFuncs.godotsharp_array_add(ref self, variantValue);
}

View File

@ -356,35 +356,47 @@ namespace Godot.Collections
IDictionary<TKey, TValue>,
IReadOnlyDictionary<TKey, TValue>
{
private static godot_variant ToVariantFunc(in Dictionary<TKey, TValue> godotDictionary) =>
VariantUtils.CreateFromDictionary(godotDictionary);
private static Dictionary<TKey, TValue> FromVariantFunc(in godot_variant variant) =>
VariantUtils.ConvertToDictionaryObject<TKey, TValue>(variant);
// ReSharper disable StaticMemberInGenericType
// Warning is about unique static fields being created for each generic type combination:
// https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html
// In our case this is exactly what we want.
private static unsafe delegate* managed<in TKey, godot_variant> _convertKeyToVariantCallback;
private static unsafe delegate* managed<in godot_variant, TKey> _convertKeyToManagedCallback;
private static unsafe delegate* managed<in TValue, godot_variant> _convertValueToVariantCallback;
private static unsafe delegate* managed<in godot_variant, TValue> _convertValueToManagedCallback;
private static readonly unsafe delegate* managed<in TKey, godot_variant> ConvertKeyToVariantCallback;
private static readonly unsafe delegate* managed<in godot_variant, TKey> ConvertKeyToManagedCallback;
private static readonly unsafe delegate* managed<in TValue, godot_variant> ConvertValueToVariantCallback;
private static readonly unsafe delegate* managed<in godot_variant, TValue> ConvertValueToManagedCallback;
// ReSharper restore StaticMemberInGenericType
static unsafe Dictionary()
{
_convertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TKey>();
_convertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TKey>();
_convertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TValue>();
_convertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TValue>();
VariantConversionCallbacks.GenericConversionCallbacks[typeof(Dictionary<TKey, TValue>)] =
(
(IntPtr)(delegate* managed<in Dictionary<TKey, TValue>, godot_variant>)&ToVariantFunc,
(IntPtr)(delegate* managed<in godot_variant, Dictionary<TKey, TValue>>)&FromVariantFunc
);
ConvertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TKey>();
ConvertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TKey>();
ConvertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TValue>();
ConvertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TValue>();
}
private static unsafe void ValidateVariantConversionCallbacks()
{
if (_convertKeyToVariantCallback == null || _convertKeyToManagedCallback == null)
if (ConvertKeyToVariantCallback == null || ConvertKeyToManagedCallback == null)
{
throw new InvalidOperationException(
$"The dictionary key type is not supported for conversion to Variant: '{typeof(TKey).FullName}'.");
}
if (_convertValueToVariantCallback == null || _convertValueToManagedCallback == null)
if (ConvertValueToVariantCallback == null || ConvertValueToManagedCallback == null)
{
throw new InvalidOperationException(
$"The dictionary value type is not supported for conversion to Variant: '{typeof(TValue).FullName}'.");
@ -473,14 +485,14 @@ namespace Godot.Collections
{
get
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantKey = ConvertKeyToVariantCallback(key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
if (NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
variantKey, out godot_variant value).ToBool())
{
using (value)
return _convertValueToManagedCallback(value);
return ConvertValueToManagedCallback(value);
}
else
{
@ -489,8 +501,8 @@ namespace Godot.Collections
}
set
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantValue = _convertValueToVariantCallback(value);
using var variantKey = ConvertKeyToVariantCallback(key);
using var variantValue = ConvertValueToVariantCallback(value);
var self = (godot_dictionary)_underlyingDict.NativeValue;
NativeFuncs.godotsharp_dictionary_set_value(ref self,
variantKey, variantValue);
@ -539,8 +551,8 @@ namespace Godot.Collections
using (value)
{
return new KeyValuePair<TKey, TValue>(
_convertKeyToManagedCallback(key),
_convertValueToManagedCallback(value));
ConvertKeyToManagedCallback(key),
ConvertValueToManagedCallback(value));
}
}
@ -552,13 +564,13 @@ namespace Godot.Collections
/// <param name="value">The object to add.</param>
public unsafe void Add(TKey key, TValue value)
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantKey = ConvertKeyToVariantCallback(key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
if (NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool())
throw new ArgumentException("An element with the same key already exists.", nameof(key));
using var variantValue = _convertValueToVariantCallback(value);
using var variantValue = ConvertValueToVariantCallback(value);
NativeFuncs.godotsharp_dictionary_add(ref self, variantKey, variantValue);
}
@ -569,7 +581,7 @@ namespace Godot.Collections
/// <returns>Whether or not this dictionary contains the given key.</returns>
public unsafe bool ContainsKey(TKey key)
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantKey = ConvertKeyToVariantCallback(key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
return NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool();
}
@ -580,7 +592,7 @@ namespace Godot.Collections
/// <param name="key">The key of the element to remove.</param>
public unsafe bool Remove(TKey key)
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantKey = ConvertKeyToVariantCallback(key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool();
}
@ -593,13 +605,13 @@ namespace Godot.Collections
/// <returns>If an object was found for the given <paramref name="key"/>.</returns>
public unsafe bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
using var variantKey = _convertKeyToVariantCallback(key);
using var variantKey = ConvertKeyToVariantCallback(key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
variantKey, out godot_variant retValue).ToBool();
using (retValue)
value = found ? _convertValueToManagedCallback(retValue) : default;
value = found ? ConvertValueToManagedCallback(retValue) : default;
return found;
}
@ -625,7 +637,7 @@ namespace Godot.Collections
unsafe bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
using var variantKey = _convertKeyToVariantCallback(item.Key);
using var variantKey = ConvertKeyToVariantCallback(item.Key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
variantKey, out godot_variant retValue).ToBool();
@ -635,7 +647,7 @@ namespace Godot.Collections
if (!found)
return false;
using var variantValue = _convertValueToVariantCallback(item.Value);
using var variantValue = ConvertValueToVariantCallback(item.Value);
return NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool();
}
}
@ -670,7 +682,7 @@ namespace Godot.Collections
unsafe bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
using var variantKey = _convertKeyToVariantCallback(item.Key);
using var variantKey = ConvertKeyToVariantCallback(item.Key);
var self = (godot_dictionary)_underlyingDict.NativeValue;
bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self,
variantKey, out godot_variant retValue).ToBool();
@ -680,7 +692,7 @@ namespace Godot.Collections
if (!found)
return false;
using var variantValue = _convertValueToVariantCallback(item.Value);
using var variantValue = ConvertValueToVariantCallback(item.Value);
if (NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool())
{
return NativeFuncs.godotsharp_dictionary_remove_key(
@ -717,6 +729,7 @@ namespace Godot.Collections
public static implicit operator Variant(Dictionary<TKey, TValue> from) => Variant.CreateFrom(from);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator Dictionary<TKey, TValue>(Variant from) => from.AsGodotDictionary<TKey, TValue>();
public static explicit operator Dictionary<TKey, TValue>(Variant from) =>
from.AsGodotDictionary<TKey, TValue>();
}
}

View File

@ -1,11 +1,15 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Godot.NativeInterop;
// TODO: Change VariantConversionCallbacks<T>. Store the callback in a static field for quick repeated access, instead of checking every time.
internal static unsafe class VariantConversionCallbacks
{
internal static System.Collections.Generic.Dictionary<Type, (IntPtr ToVariant, IntPtr FromVariant)>
GenericConversionCallbacks = new();
[SuppressMessage("ReSharper", "RedundantNameQualifier")]
internal static delegate*<in T, godot_variant> GetToVariantCallback<T>()
{
@ -503,6 +507,26 @@ internal static unsafe class VariantConversionCallbacks
&FromVariant;
}
// TODO:
// IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode.
// We could make the Godot collections implement an interface and use IsAssignableFrom instead.
// Or we could just skip the check and always look for a conversion callback for the type.
if (typeOfT.IsGenericType)
{
var genericTypeDef = typeOfT.GetGenericTypeDefinition();
if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) ||
genericTypeDef == typeof(Godot.Collections.Array<>))
{
RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle);
if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion))
{
return (delegate*<in T, godot_variant>)genericConversion.ToVariant;
}
}
}
return null;
}
@ -1006,6 +1030,26 @@ internal static unsafe class VariantConversionCallbacks
&ToVariant;
}
// TODO:
// IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode.
// We could make the Godot collections implement an interface and use IsAssignableFrom instead.
// Or we could just skip the check and always look for a conversion callback for the type.
if (typeOfT.IsGenericType)
{
var genericTypeDef = typeOfT.GetGenericTypeDefinition();
if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) ||
genericTypeDef == typeof(Godot.Collections.Array<>))
{
RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle);
if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion))
{
return (delegate*<in godot_variant, T>)genericConversion.FromVariant;
}
}
}
// ReSharper restore RedundantCast
return null;