diff --git a/Source/Engine/Scripting/ScriptingObject.cpp b/Source/Engine/Scripting/ScriptingObject.cpp index dee97a849..dd530c50f 100644 --- a/Source/Engine/Scripting/ScriptingObject.cpp +++ b/Source/Engine/Scripting/ScriptingObject.cpp @@ -717,6 +717,12 @@ DEFINE_INTERNAL_CALL(MString*) ObjectInternal_GetTypeName(ScriptingObject* obj) return MUtils::ToString(obj->GetType().Fullname); } +FORCE_INLINE bool ObjectInternal_MatchesType(ScriptingObject* obj, MClass* klass) +{ + return !klass || + (klass->IsInterface() ? obj->GetClass()->HasInterface(klass) : obj->Is(klass)); +} + DEFINE_INTERNAL_CALL(MObject*) ObjectInternal_FindObject(Guid* id, MTypeObject* type, bool skipLog = false) { if (!id->IsValid()) @@ -732,7 +738,7 @@ DEFINE_INTERNAL_CALL(MObject*) ObjectInternal_FindObject(Guid* id, MTypeObject* } if (obj) { - if (klass && !obj->Is(klass)) + if (!ObjectInternal_MatchesType(obj, klass)) { if (!skipLog) { @@ -762,7 +768,7 @@ DEFINE_INTERNAL_CALL(MObject*) ObjectInternal_FindObject(Guid* id, MTypeObject* DEFINE_INTERNAL_CALL(MObject*) ObjectInternal_TryFindObject(Guid* id, MTypeObject* type) { ScriptingObject* obj = Scripting::TryFindObject(*id); - if (obj && !obj->Is(MUtils::GetClass(type))) + if (obj && !ObjectInternal_MatchesType(obj, MUtils::GetClass(type))) obj = nullptr; return obj ? obj->GetOrCreateManagedInstance() : nullptr; } diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs index b8e07e448..7c6bac2a3 100644 --- a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs @@ -12,6 +12,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers internal class ExtendedDefaultContractResolver : DefaultContractResolver { private readonly Type _flaxType = typeof(Object); + private static readonly JsonConverter InterfaceObjectReferenceConverterInstance = new InterfaceObjectReferenceConverter(); private readonly Type[] AttributesIgnoreList = { @@ -33,6 +34,88 @@ namespace FlaxEngine.Json.JsonCustomSerializers _attributesIgnoreList = isManagedOnly ? AttributesIgnoreListManaged : AttributesIgnoreList; } + private static bool HasObjectInterfaceReferenceAttribute(IEnumerable attributes) + { + return attributes.Any(x => x is ScriptingObjectInterfaceReferenceAttribute || x is SoftObjectInterfaceReferenceAttribute); + } + + private static Type GetCollectionItemType(Type type) + { + if (type.IsArray) + return type.GetElementType(); + if (!type.IsGenericType || type == typeof(string)) + return null; + + var types = type.GetInterfaces().Concat(new[] { type }); + var dictionaryType = types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + if (dictionaryType != null) + return dictionaryType.GetGenericArguments()[1]; + var enumerableType = types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + return enumerableType?.GetGenericArguments()[0]; + } + + private static void SetupInterfaceObjectReferenceItems(JsonContainerContract contract, Type itemType) + { + if (itemType?.IsInterface == true) + { + contract.ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; + contract.ItemConverter = InterfaceObjectReferenceConverterInstance; + } + } + + private void SetupObjectReferenceProperty(JsonProperty jsonProperty, Type type, IEnumerable attributes) + { + var hasObjectInterfaceReferenceAttribute = HasObjectInterfaceReferenceAttribute(attributes); + if (_flaxType.IsAssignableFrom(type) || (type.IsInterface && hasObjectInterfaceReferenceAttribute)) + { + jsonProperty.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; + jsonProperty.Converter = JsonSerializer.ObjectConverter; + } + if (hasObjectInterfaceReferenceAttribute && GetCollectionItemType(type)?.IsInterface == true) + { + jsonProperty.ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; + jsonProperty.ItemConverter = JsonSerializer.ObjectConverter; + } + } + + private sealed class InterfaceObjectReferenceConverter : JsonConverter + { + public override unsafe void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + if (value is Object obj) + { + var id = obj.ID; + writer.WriteValue(JsonSerializer.GetStringID(&id)); + } + else if (value == null) + { + writer.WriteNull(); + } + else + { + serializer.Serialize(writer, value, value.GetType()); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String && JsonSerializer.TryParseID((string)reader.Value, out var id)) + { + return Object.Find(ref id, objectType, true); + } + if (reader.TokenType == JsonToken.Null) + return null; + // objectType is the same interface item type that selected this converter. Passing it back to + // Newtonsoft can cause this converter to be chosen again and recurse until the stack overflows. + return Newtonsoft.Json.Linq.JToken.Load(reader).ToObject(serializer); + } + + public override bool CanConvert(Type objectType) + { + return objectType.IsInterface; + } + } + /// protected override JsonContract CreateContract(Type objectType) { @@ -47,11 +130,23 @@ namespace FlaxEngine.Json.JsonCustomSerializers return contract; } + /// + protected override JsonArrayContract CreateArrayContract(Type objectType) + { + var contract = base.CreateArrayContract(objectType); + + SetupInterfaceObjectReferenceItems(contract, contract.CollectionItemType); + + return contract; + } + /// protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { var contract = base.CreateDictionaryContract(objectType); + SetupInterfaceObjectReferenceItems(contract, contract.DictionaryValueType); + // Override contract to save enums keys as integer if (contract.DictionaryKeyType?.IsEnum ?? false) { @@ -108,11 +203,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers jsonProperty.Writable = true; jsonProperty.Readable = true; - if (_flaxType.IsAssignableFrom(f.FieldType)) - { - jsonProperty.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; - jsonProperty.Converter = JsonSerializer.ObjectConverter; - } + SetupObjectReferenceProperty(jsonProperty, f.FieldType, attributes); result.Add(jsonProperty); } @@ -151,11 +242,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers jsonProperty.Writable = true; jsonProperty.Readable = !isObsolete; - if (_flaxType.IsAssignableFrom(p.PropertyType)) - { - jsonProperty.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; - jsonProperty.Converter = JsonSerializer.ObjectConverter; - } + SetupObjectReferenceProperty(jsonProperty, p.PropertyType, attributes); result.Add(jsonProperty); } diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index c8d00567a..758ebf1ff 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -618,6 +618,31 @@ namespace FlaxEngine.Json return id; } + /// + /// Tries to parse the given object identifier represented in the internal serialization format. + /// + /// The ID string. + /// The identifier. + /// True if parsing succeeded, otherwise false. + public static unsafe bool TryParseID(string str, out Guid id) + { + id = Guid.Empty; + if (str == null || str.Length != 32) + return false; + + GuidInterop g; + if (!TryParseHex(str, 0, 8, out g.A) || + !TryParseHex(str, 8, 8, out g.B) || + !TryParseHex(str, 16, 8, out g.C) || + !TryParseHex(str, 24, 8, out g.D)) + { + return false; + } + + id = *(Guid*)&g; + return true; + } + /// /// Parses the given object identifier represented in the internal serialization format. /// @@ -625,76 +650,40 @@ namespace FlaxEngine.Json /// The identifier. public static unsafe void ParseID(string str, out Guid id) { - GuidInterop g; - - // Broken after VS 15.5 - /*fixed (char* a = str) - { - char* b = a + 8; - char* c = b + 8; - char* d = c + 8; - - ParseHex(a, 8, out g.A); - ParseHex(b, 8, out g.B); - ParseHex(c, 8, out g.C); - ParseHex(d, 8, out g.D); - }*/ - - // Temporary fix (not using raw char* pointer) - ParseHex(str, 0, 8, out g.A); - ParseHex(str, 8, 8, out g.B); - ParseHex(str, 16, 8, out g.C); - ParseHex(str, 24, 8, out g.D); - - id = *(Guid*)&g; + TryParseID(str, out id); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static unsafe void ParseHex(char* str, int length, out uint result) { - uint sum = 0; - char* p = str; - char* end = str + length; - - if (*p == '0' && *(p + 1) == 'x') - p += 2; - - while (p < end && *p != 0) - { - int c = *p - '0'; - - if (c < 0 || c > 9) - { - c = char.ToLower(*p) - 'a' + 10; - if (c < 10 || c > 15) - { - result = 0; - return; - } - } - - sum = 16 * sum + (uint)c; - - p++; - } - - result = sum; + TryParseHex(new ReadOnlySpan(str, length), out result); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void ParseHex(string str, int start, int length, out uint result) { - uint sum = 0; - int p = start; - int end = start + length; + TryParseHex(str, start, length, out result); + } - if (str.Length < end) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryParseHex(string str, int start, int length, out uint result) + { + if (str.Length < start + length) { result = 0; - return; + return false; } + return TryParseHex(str.AsSpan(start, length), out result); + } - if (str[p] == '0' && str[p + 1] == 'x') + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryParseHex(ReadOnlySpan str, out uint result) + { + uint sum = 0; + int p = 0; + int end = str.Length; + + if (p + 1 < end && str[p] == '0' && str[p + 1] == 'x') p += 2; while (p < end && str[p] != 0) @@ -707,7 +696,7 @@ namespace FlaxEngine.Json if (c < 10 || c > 15) { result = 0; - return; + return false; } } @@ -717,6 +706,7 @@ namespace FlaxEngine.Json } result = sum; + return p == end; } } } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 604d501ab..81fb9d873 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -315,6 +315,83 @@ namespace Flax.Build.Bindings return value; } + private static bool IsInterfaceRefArrayLike(TypeInfo typeInfo) + { + return typeInfo != null && + (typeInfo.Type == "Array" || typeInfo.Type == "Span" || typeInfo.Type == "DataContainer") && + typeInfo.GenericArgs != null && + typeInfo.GenericArgs.Count != 0 && + typeInfo.GenericArgs[0].IsInterfaceRef; + } + + private static bool IsInterfaceRefDictionary(TypeInfo typeInfo) + { + return typeInfo != null && + typeInfo.Type == "Dictionary" && + typeInfo.GenericArgs != null && + typeInfo.GenericArgs.Count == 2 && + (typeInfo.GenericArgs[0].IsInterfaceRef || typeInfo.GenericArgs[1].IsInterfaceRef); + } + + private static bool IsInterfaceRefContainer(TypeInfo typeInfo) + { + return IsInterfaceRefArrayLike(typeInfo) || IsInterfaceRefDictionary(typeInfo); + } + + private static TypeInfo GetInterfaceRefElementType(TypeInfo typeInfo) + { + if (typeInfo == null) + return null; + if (typeInfo.IsInterfaceRef) + return typeInfo; + if (IsInterfaceRefArrayLike(typeInfo)) + return typeInfo.GenericArgs[0]; + if (IsInterfaceRefDictionary(typeInfo)) + return typeInfo.GenericArgs[1].IsInterfaceRef ? typeInfo.GenericArgs[1] : typeInfo.GenericArgs[0]; + return null; + } + + private static string GenerateInterfaceRefToNative(BuildData buildData, TypeInfo interfaceRefType, ApiTypeInfo caller, string value) + { + return $"FlaxEngine.Object.GetUnmanagedInterface({value}, typeof({GenerateCSharpNativeToManaged(buildData, interfaceRefType.GenericArgs[0], caller)}))"; + } + + private static string GenerateInterfaceRefToManaged(BuildData buildData, TypeInfo interfaceRefType, ApiTypeInfo caller, string value, bool fromHandle) + { + var managedType = GenerateCSharpNativeToManaged(buildData, interfaceRefType.GenericArgs[0], caller); + return fromHandle + ? $"{value} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr({value}).Target) : null" + : $"{value} != null ? Unsafe.As<{managedType}>({value}) : null"; + } + + private static string GenerateInterfaceRefContainerToNative(TypeInfo typeInfo) + { + if (IsInterfaceRefArrayLike(typeInfo)) + return "{0} != null ? FlaxEngine.Interop.NativeInterop.ManagedArrayToGCHandleArray({0}) : null"; + if (IsInterfaceRefDictionary(typeInfo)) + { + var keyConverter = typeInfo.GenericArgs[0].IsInterfaceRef ? "(object)x.Key" : "x.Key"; + var valueConverter = typeInfo.GenericArgs[1].IsInterfaceRef ? "(object)x.Value" : "x.Value"; + return $"{{0}} != null ? System.Linq.Enumerable.ToDictionary({{0}}, x => {keyConverter}, x => {valueConverter}) : null"; + } + return string.Empty; + } + + private static string GenerateInterfaceRefContainerToManaged(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, string value) + { + if (IsInterfaceRefArrayLike(typeInfo)) + return $"{value}?.ConvertArray(x => {GenerateInterfaceRefToManaged(buildData, typeInfo.GenericArgs[0], caller, "x", true)})"; + if (IsInterfaceRefDictionary(typeInfo)) + { + var keyTypeInfo = typeInfo.GenericArgs[0]; + var valueTypeInfo = typeInfo.GenericArgs[1]; + var keyConverter = keyTypeInfo.IsInterfaceRef ? GenerateInterfaceRefToManaged(buildData, keyTypeInfo, caller, "x.Key", false) : "x.Key"; + var valueConverter = valueTypeInfo.IsInterfaceRef ? GenerateInterfaceRefToManaged(buildData, valueTypeInfo, caller, "x.Value", false) : "x.Value"; + return $"{value} != null ? System.Linq.Enumerable.ToDictionary({value}, x => {keyConverter}, x => {valueConverter}) : null"; + } + return value; + } + private static string GenerateCSharpNativeToManaged(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, bool marshalling = false) { string result; @@ -554,29 +631,25 @@ namespace Flax.Build.Bindings case "Array": case "Span": case "DataContainer": + if (IsInterfaceRefArrayLike(typeInfo)) + return GenerateInterfaceRefContainerToNative(typeInfo); if (typeInfo.GenericArgs != null) { // Convert array that uses different type for marshalling var arrayTypeInfo = typeInfo.GenericArgs[0]; - if (arrayTypeInfo.IsInterfaceRef) - return "{0} != null ? FlaxEngine.Interop.NativeInterop.ManagedArrayToGCHandleArray({0}) : null"; var arrayApiType = FindApiTypeInfo(buildData, arrayTypeInfo, caller); if (arrayApiType != null && arrayApiType.MarshalAs != null) return $"{{0}}.ConvertArray(x => ({GenerateCSharpNativeToManaged(buildData, arrayApiType.MarshalAs, caller)})x)"; } return string.Empty; case "Dictionary": - if (typeInfo.GenericArgs != null && typeInfo.GenericArgs.Count == 2 && (typeInfo.GenericArgs[0].IsInterfaceRef || typeInfo.GenericArgs[1].IsInterfaceRef)) - { - var keyConverter = typeInfo.GenericArgs[0].IsInterfaceRef ? "(object)x.Key" : "x.Key"; - var valueConverter = typeInfo.GenericArgs[1].IsInterfaceRef ? "(object)x.Value" : "x.Value"; - return $"{{0}} != null ? System.Linq.Enumerable.ToDictionary({{0}}, x => {keyConverter}, x => {valueConverter}) : null"; - } + if (IsInterfaceRefDictionary(typeInfo)) + return GenerateInterfaceRefContainerToNative(typeInfo); return string.Empty; default: // Interface reference property if (typeInfo.IsInterfaceRef) - return string.Format("FlaxEngine.Object.GetUnmanagedInterface({{0}}, typeof({0}))", GenerateCSharpNativeToManaged(buildData, typeInfo.GenericArgs[0], caller)); + return GenerateInterfaceRefToNative(buildData, typeInfo, caller, "{0}"); var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) @@ -801,20 +874,10 @@ namespace Flax.Build.Bindings } #endif const string interfaceResultName = "__interfaceResult"; - const string interfaceArrayResultName = "__interfaceArrayResult"; - const string interfaceDictionaryResultName = "__interfaceDictionaryResult"; + const string interfaceContainerResultName = "__interfaceContainerResult"; var returnInterfaceRef = !functionInfo.Glue.UseReferenceForResult && functionInfo.ReturnType.IsInterfaceRef; - var returnInterfaceRefArray = !functionInfo.Glue.UseReferenceForResult && - (functionInfo.ReturnType.Type == "Array" || functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "DataContainer") && - functionInfo.ReturnType.GenericArgs != null && - functionInfo.ReturnType.GenericArgs.Count != 0 && - functionInfo.ReturnType.GenericArgs[0].IsInterfaceRef; - var returnInterfaceRefDictionary = !functionInfo.Glue.UseReferenceForResult && - functionInfo.ReturnType.Type == "Dictionary" && - functionInfo.ReturnType.GenericArgs != null && - functionInfo.ReturnType.GenericArgs.Count == 2 && - (functionInfo.ReturnType.GenericArgs[0].IsInterfaceRef || functionInfo.ReturnType.GenericArgs[1].IsInterfaceRef); + var returnInterfaceRefContainer = !functionInfo.Glue.UseReferenceForResult && IsInterfaceRefContainer(functionInfo.ReturnType); if (functionInfo.Glue.UseReferenceForResult) { @@ -823,13 +886,9 @@ namespace Flax.Build.Bindings { contents.Append("var ").Append(interfaceResultName).Append(" = "); } - else if (returnInterfaceRefArray) + else if (returnInterfaceRefContainer) { - contents.Append("var ").Append(interfaceArrayResultName).Append(" = "); - } - else if (returnInterfaceRefDictionary) - { - contents.Append("var ").Append(interfaceDictionaryResultName).Append(" = "); + contents.Append("var ").Append(interfaceContainerResultName).Append(" = "); } else if (!functionInfo.ReturnType.IsVoid) { @@ -903,21 +962,11 @@ namespace Flax.Build.Bindings contents.Append(')'); if (returnInterfaceRef) { - var managedType = GenerateCSharpNativeToManaged(buildData, functionInfo.ReturnType.GenericArgs[0], caller); - contents.Append("; return ").Append(interfaceResultName).Append(" != IntPtr.Zero ? Unsafe.As<").Append(managedType).Append(">(ManagedHandle.FromIntPtr(").Append(interfaceResultName).Append(").Target) : null"); + contents.Append("; return ").Append(GenerateInterfaceRefToManaged(buildData, functionInfo.ReturnType, caller, interfaceResultName, true)); } - else if (returnInterfaceRefArray) + else if (returnInterfaceRefContainer) { - var managedType = GenerateCSharpNativeToManaged(buildData, functionInfo.ReturnType.GenericArgs[0].GenericArgs[0], caller); - contents.Append("; return ").Append(interfaceArrayResultName).Append("?.ConvertArray(x => x != IntPtr.Zero ? Unsafe.As<").Append(managedType).Append(">(ManagedHandle.FromIntPtr(x).Target) : null)"); - } - else if (returnInterfaceRefDictionary) - { - var keyTypeInfo = functionInfo.ReturnType.GenericArgs[0]; - var valueTypeInfo = functionInfo.ReturnType.GenericArgs[1]; - var keyConverter = keyTypeInfo.IsInterfaceRef ? $"x.Key != null ? Unsafe.As<{GenerateCSharpNativeToManaged(buildData, keyTypeInfo.GenericArgs[0], caller)}>(x.Key) : null" : "x.Key"; - var valueConverter = valueTypeInfo.IsInterfaceRef ? $"x.Value != null ? Unsafe.As<{GenerateCSharpNativeToManaged(buildData, valueTypeInfo.GenericArgs[0], caller)}>(x.Value) : null" : "x.Value"; - contents.Append("; return ").Append(interfaceDictionaryResultName).Append(" != null ? System.Linq.Enumerable.ToDictionary(").Append(interfaceDictionaryResultName).Append(", x => ").Append(keyConverter).Append(", x => ").Append(valueConverter).Append(") : null"); + contents.Append("; return ").Append(GenerateInterfaceRefContainerToManaged(buildData, functionInfo.ReturnType, caller, interfaceContainerResultName)); } else if ((functionInfo.ReturnType.Type == "Array" || functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "DataContainer") && functionInfo.ReturnType.GenericArgs != null) { @@ -1056,22 +1105,8 @@ namespace Flax.Build.Bindings { GenerateCSharpAttributes(buildData, contents, indent, apiTypeInfo, memberInfo.Attributes, memberInfo.Comment, true, useUnmanaged, defaultValue, memberInfo.DeprecatedMessage, defaultValueType); var memberType = (memberInfo as FieldInfo)?.Type ?? (memberInfo as PropertyInfo)?.Type; - var interfaceRefType = memberType; - if ((memberType?.Type == "Array" || memberType?.Type == "Span" || memberType?.Type == "DataContainer") && - memberType.GenericArgs != null && - memberType.GenericArgs.Count != 0 && - memberType.GenericArgs[0].IsInterfaceRef) - { - interfaceRefType = memberType.GenericArgs[0]; - } - else if (memberType?.Type == "Dictionary" && - memberType.GenericArgs != null && - memberType.GenericArgs.Count == 2 && - (memberType.GenericArgs[0].IsInterfaceRef || memberType.GenericArgs[1].IsInterfaceRef)) - { - interfaceRefType = memberType.GenericArgs[1].IsInterfaceRef ? memberType.GenericArgs[1] : memberType.GenericArgs[0]; - } - if (interfaceRefType != null && interfaceRefType.IsInterfaceRef) + var interfaceRefType = GetInterfaceRefElementType(memberType); + if (interfaceRefType != null) { var attribute = interfaceRefType.Type == "SoftObjectInterfaceReference" ? "SoftObjectInterfaceReference" : "ScriptingObjectInterfaceReference"; contents.Append(indent).Append("[FlaxEngine.").Append(attribute).AppendLine("]");