_net8 wip
This commit is contained in:
@@ -766,10 +766,21 @@ namespace FlaxEngine.Interop
|
|||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
// Slow path, method parameters needs to be stored in heap
|
// Slow path, method parameters needs to be stored in heap
|
||||||
|
// TODO: Benchmark again against the fast path
|
||||||
object returnObject;
|
object returnObject;
|
||||||
|
object instance = instanceHandle.IsAllocated ? instanceHandle.Target : null;
|
||||||
int numParams = methodHolder.parameterTypes.Length;
|
int numParams = methodHolder.parameterTypes.Length;
|
||||||
object[] methodParameters = new object[numParams];
|
|
||||||
|
|
||||||
|
//if (numParams > maxnumpars)
|
||||||
|
// maxnumpars = numParams;
|
||||||
|
|
||||||
|
#if false
|
||||||
|
if (numParams > 4)
|
||||||
|
#else
|
||||||
|
if (true)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
object[] methodParameters = new object[numParams];
|
||||||
for (int i = 0; i < numParams; i++)
|
for (int i = 0; i < numParams; i++)
|
||||||
{
|
{
|
||||||
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
||||||
@@ -778,7 +789,7 @@ namespace FlaxEngine.Interop
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
returnObject = methodHolder.method.Invoke(instanceHandle.IsAllocated ? instanceHandle.Target : null, methodParameters);
|
returnObject = methodHolder.method.Invoke(instance, methodParameters);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -804,6 +815,186 @@ namespace FlaxEngine.Interop
|
|||||||
MarshalToNative(methodParameters[i], nativePtr, parameterType.GetElementType());
|
MarshalToNative(methodParameters[i], nativePtr, parameterType.GetElementType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (numParams == 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnObject = methodHolder.methodInvoker.Invoke(instance);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// The internal exception thrown in MethodInfo.Invoke is caught here
|
||||||
|
Exception realException = exception;
|
||||||
|
if (exception.InnerException != null && exception.TargetSite.ReflectedType.Name == "MethodInvoker")
|
||||||
|
realException = exception.InnerException;
|
||||||
|
|
||||||
|
if (exceptionPtr != IntPtr.Zero)
|
||||||
|
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(realException, GCHandleType.Weak));
|
||||||
|
else
|
||||||
|
throw realException;
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (numParams == 1)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr1 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 0)).ToPointer());
|
||||||
|
Span<object?> paramSpan = [
|
||||||
|
MarshalToManaged(nativePtr1, methodHolder.parameterTypes[0])
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnObject = methodHolder.methodInvoker.Invoke(instance, paramSpan);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// The internal exception thrown in MethodInfo.Invoke is caught here
|
||||||
|
Exception realException = exception;
|
||||||
|
if (exception.InnerException != null && exception.TargetSite.ReflectedType.Name == "MethodInvoker")
|
||||||
|
realException = exception.InnerException;
|
||||||
|
|
||||||
|
if (exceptionPtr != IntPtr.Zero)
|
||||||
|
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(realException, GCHandleType.Weak));
|
||||||
|
else
|
||||||
|
throw realException;
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal reference parameters back to original unmanaged references
|
||||||
|
for (int i = 0; i < numParams; i++)
|
||||||
|
{
|
||||||
|
Type parameterType = methodHolder.parameterTypes[i];
|
||||||
|
if (parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
||||||
|
MarshalToNative(paramSpan[i], nativePtr, parameterType.GetElementType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (numParams == 2)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr1 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 0)).ToPointer());
|
||||||
|
IntPtr nativePtr2 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 1)).ToPointer());
|
||||||
|
Span<object?> paramSpan = [
|
||||||
|
MarshalToManaged(nativePtr1, methodHolder.parameterTypes[0]),
|
||||||
|
MarshalToManaged(nativePtr2, methodHolder.parameterTypes[1])
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnObject = methodHolder.methodInvoker.Invoke(instance, paramSpan);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// The internal exception thrown in MethodInfo.Invoke is caught here
|
||||||
|
Exception realException = exception;
|
||||||
|
if (exception.InnerException != null && exception.TargetSite.ReflectedType.Name == "MethodInvoker")
|
||||||
|
realException = exception.InnerException;
|
||||||
|
|
||||||
|
if (exceptionPtr != IntPtr.Zero)
|
||||||
|
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(realException, GCHandleType.Weak));
|
||||||
|
else
|
||||||
|
throw realException;
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal reference parameters back to original unmanaged references
|
||||||
|
for (int i = 0; i < numParams; i++)
|
||||||
|
{
|
||||||
|
Type parameterType = methodHolder.parameterTypes[i];
|
||||||
|
if (parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
||||||
|
MarshalToNative(paramSpan[i], nativePtr, parameterType.GetElementType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (numParams == 3)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr1 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 0)).ToPointer());
|
||||||
|
IntPtr nativePtr2 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 1)).ToPointer());
|
||||||
|
IntPtr nativePtr3 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 2)).ToPointer());
|
||||||
|
Span<object?> paramSpan = [
|
||||||
|
MarshalToManaged(nativePtr1, methodHolder.parameterTypes[0]),
|
||||||
|
MarshalToManaged(nativePtr2, methodHolder.parameterTypes[1]),
|
||||||
|
MarshalToManaged(nativePtr3, methodHolder.parameterTypes[2])
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnObject = methodHolder.methodInvoker.Invoke(instance, paramSpan);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// The internal exception thrown in MethodInfo.Invoke is caught here
|
||||||
|
Exception realException = exception;
|
||||||
|
if (exception.InnerException != null && exception.TargetSite.ReflectedType.Name == "MethodInvoker")
|
||||||
|
realException = exception.InnerException;
|
||||||
|
|
||||||
|
if (exceptionPtr != IntPtr.Zero)
|
||||||
|
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(realException, GCHandleType.Weak));
|
||||||
|
else
|
||||||
|
throw realException;
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal reference parameters back to original unmanaged references
|
||||||
|
for (int i = 0; i < numParams; i++)
|
||||||
|
{
|
||||||
|
Type parameterType = methodHolder.parameterTypes[i];
|
||||||
|
if (parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
||||||
|
MarshalToNative(paramSpan[i], nativePtr, parameterType.GetElementType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else //if (numParams == 4)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr1 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 0)).ToPointer());
|
||||||
|
IntPtr nativePtr2 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 1)).ToPointer());
|
||||||
|
IntPtr nativePtr3 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 2)).ToPointer());
|
||||||
|
IntPtr nativePtr4 = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * 3)).ToPointer());
|
||||||
|
Span<object?> paramSpan = [
|
||||||
|
MarshalToManaged(nativePtr1, methodHolder.parameterTypes[0]),
|
||||||
|
MarshalToManaged(nativePtr2, methodHolder.parameterTypes[1]),
|
||||||
|
MarshalToManaged(nativePtr3, methodHolder.parameterTypes[2]),
|
||||||
|
MarshalToManaged(nativePtr4, methodHolder.parameterTypes[3])
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnObject = methodHolder.methodInvoker.Invoke(instance, paramSpan);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// The internal exception thrown in MethodInfo.Invoke is caught here
|
||||||
|
Exception realException = exception;
|
||||||
|
if (exception.InnerException != null && exception.TargetSite.ReflectedType.Name == "MethodInvoker")
|
||||||
|
realException = exception.InnerException;
|
||||||
|
|
||||||
|
if (exceptionPtr != IntPtr.Zero)
|
||||||
|
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(realException, GCHandleType.Weak));
|
||||||
|
else
|
||||||
|
throw realException;
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal reference parameters back to original unmanaged references
|
||||||
|
for (int i = 0; i < numParams; i++)
|
||||||
|
{
|
||||||
|
Type parameterType = methodHolder.parameterTypes[i];
|
||||||
|
if (parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
|
||||||
|
MarshalToNative(paramSpan[i], nativePtr, parameterType.GetElementType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return value
|
// Return value
|
||||||
return Invoker.MarshalReturnValueGeneric(methodHolder.returnType, returnObject);
|
return Invoker.MarshalReturnValueGeneric(methodHolder.returnType, returnObject);
|
||||||
|
|||||||
@@ -116,16 +116,39 @@ namespace FlaxEngine.Interop
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !USE_AOT
|
#if !USE_AOT
|
||||||
|
#if !NET8_0_OR_GREATER
|
||||||
// Cache offsets to frequently accessed fields of FlaxEngine.Object
|
// Cache offsets to frequently accessed fields of FlaxEngine.Object
|
||||||
private static int unmanagedPtrFieldOffset = IntPtr.Size + (Unsafe.Read<int>((typeof(FlaxEngine.Object).GetField("__unmanagedPtr", BindingFlags.Instance | BindingFlags.NonPublic).FieldHandle.Value + 4 + IntPtr.Size).ToPointer()) & 0xFFFFFF);
|
private static int unmanagedPtrFieldOffset = IntPtr.Size + (Unsafe.Read<int>((typeof(FlaxEngine.Object).GetField("__unmanagedPtr", BindingFlags.Instance | BindingFlags.NonPublic).FieldHandle.Value + 4 + IntPtr.Size).ToPointer()) & 0xFFFFFF);
|
||||||
private static int internalIdFieldOffset = IntPtr.Size + (Unsafe.Read<int>((typeof(FlaxEngine.Object).GetField("__internalId", BindingFlags.Instance | BindingFlags.NonPublic).FieldHandle.Value + 4 + IntPtr.Size).ToPointer()) & 0xFFFFFF);
|
private static int internalIdFieldOffset = IntPtr.Size + (Unsafe.Read<int>((typeof(FlaxEngine.Object).GetField("__internalId", BindingFlags.Instance | BindingFlags.NonPublic).FieldHandle.Value + 4 + IntPtr.Size).ToPointer()) & 0xFFFFFF);
|
||||||
|
#endif
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
[UnmanagedCallersOnly]
|
||||||
internal static void ScriptingObjectSetInternalValues(ManagedHandle objectHandle, IntPtr unmanagedPtr, IntPtr idPtr)
|
internal static void ScriptingObjectSetInternalValues(ManagedHandle objectHandle, IntPtr unmanagedPtr, IntPtr idPtr)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
//Object obj2 = objectHandle.Target as Object;
|
||||||
object obj = objectHandle.Target;
|
object obj = objectHandle.Target;
|
||||||
if (obj is not Object)
|
if (obj is not Object obj2)
|
||||||
return;
|
return;
|
||||||
|
{
|
||||||
|
ref IntPtr fieldRef = ref SetObjectUnmanagedPtr(obj2);
|
||||||
|
fieldRef = unmanagedPtr;
|
||||||
|
}
|
||||||
|
if (idPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
ref Guid nativeId = ref Unsafe.AsRef<Guid>(idPtr.ToPointer());
|
||||||
|
ref Guid fieldRef = ref SetObjectInternalId(obj2);
|
||||||
|
fieldRef = nativeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "__unmanagedPtr")]
|
||||||
|
extern static ref IntPtr SetObjectUnmanagedPtr(Object obj);
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "__internalId")]
|
||||||
|
extern static ref Guid SetObjectInternalId(Object obj);
|
||||||
|
#else
|
||||||
|
object obj = objectHandle.Target;
|
||||||
{
|
{
|
||||||
ref IntPtr fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<IntPtr>(unmanagedPtrFieldOffset, ref obj);
|
ref IntPtr fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<IntPtr>(unmanagedPtrFieldOffset, ref obj);
|
||||||
fieldRef = unmanagedPtr;
|
fieldRef = unmanagedPtr;
|
||||||
@@ -136,6 +159,7 @@ namespace FlaxEngine.Interop
|
|||||||
ref Guid fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<Guid>(internalIdFieldOffset, ref obj);
|
ref Guid fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<Guid>(internalIdFieldOffset, ref obj);
|
||||||
fieldRef = nativeId;
|
fieldRef = nativeId;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
[UnmanagedCallersOnly]
|
||||||
@@ -1272,6 +1296,9 @@ namespace FlaxEngine.Interop
|
|||||||
internal class MethodHolder
|
internal class MethodHolder
|
||||||
{
|
{
|
||||||
internal Type[] parameterTypes;
|
internal Type[] parameterTypes;
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
internal MethodInvoker methodInvoker;
|
||||||
|
#endif
|
||||||
internal MethodInfo method;
|
internal MethodInfo method;
|
||||||
internal Type returnType;
|
internal Type returnType;
|
||||||
#if !USE_AOT
|
#if !USE_AOT
|
||||||
@@ -1284,6 +1311,10 @@ namespace FlaxEngine.Interop
|
|||||||
this.method = method;
|
this.method = method;
|
||||||
returnType = method.ReturnType;
|
returnType = method.ReturnType;
|
||||||
parameterTypes = method.GetParameterTypes();
|
parameterTypes = method.GetParameterTypes();
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
methodInvoker = MethodInvoker.Create(method);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !USE_AOT
|
#if !USE_AOT
|
||||||
@@ -1388,7 +1419,11 @@ namespace FlaxEngine.Interop
|
|||||||
{
|
{
|
||||||
internal Type type;
|
internal Type type;
|
||||||
internal Type wrappedType;
|
internal Type wrappedType;
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
internal MethodInvoker ctorInvoker;
|
||||||
|
#else
|
||||||
internal ConstructorInfo ctor;
|
internal ConstructorInfo ctor;
|
||||||
|
#endif
|
||||||
internal IntPtr managedClassPointer; // MClass*
|
internal IntPtr managedClassPointer; // MClass*
|
||||||
|
|
||||||
internal TypeHolder(Type type)
|
internal TypeHolder(Type type)
|
||||||
@@ -1404,7 +1439,13 @@ namespace FlaxEngine.Interop
|
|||||||
wrappedType = abstractWrapper;
|
wrappedType = abstractWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ConstructorInfo ctor = wrappedType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
|
||||||
|
if (ctor != null)
|
||||||
|
ctorInvoker = MethodInvoker.Create(ctor);
|
||||||
|
#else
|
||||||
ctor = wrappedType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
|
ctor = wrappedType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object CreateObject()
|
internal object CreateObject()
|
||||||
@@ -1416,9 +1457,29 @@ namespace FlaxEngine.Interop
|
|||||||
internal object CreateScriptingObject(IntPtr unmanagedPtr, IntPtr idPtr)
|
internal object CreateScriptingObject(IntPtr unmanagedPtr, IntPtr idPtr)
|
||||||
{
|
{
|
||||||
object obj = RuntimeHelpers.GetUninitializedObject(wrappedType);
|
object obj = RuntimeHelpers.GetUninitializedObject(wrappedType);
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
if (obj is Object obj2)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
ref IntPtr fieldRef = ref SetObjectUnmanagedPtr(obj2);
|
||||||
|
fieldRef = unmanagedPtr;
|
||||||
|
}
|
||||||
|
if (idPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
ref Guid nativeId = ref Unsafe.AsRef<Guid>(idPtr.ToPointer());
|
||||||
|
ref Guid fieldRef = ref SetObjectInternalId(obj2);
|
||||||
|
fieldRef = nativeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "__unmanagedPtr")]
|
||||||
|
extern static ref IntPtr SetObjectUnmanagedPtr(Object obj);
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "__internalId")]
|
||||||
|
extern static ref Guid SetObjectInternalId(Object obj);
|
||||||
|
}
|
||||||
|
#else
|
||||||
if (obj is Object)
|
if (obj is Object)
|
||||||
{
|
{
|
||||||
// TODO: use UnsafeAccessorAttribute on .NET 8 and use this path on all platforms (including non-Desktop, see MCore::ScriptingObject::CreateScriptingObject)
|
|
||||||
{
|
{
|
||||||
ref IntPtr fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<IntPtr>(unmanagedPtrFieldOffset, ref obj);
|
ref IntPtr fieldRef = ref FieldHelper.GetReferenceTypeFieldReference<IntPtr>(unmanagedPtrFieldOffset, ref obj);
|
||||||
fieldRef = unmanagedPtr;
|
fieldRef = unmanagedPtr;
|
||||||
@@ -1430,8 +1491,14 @@ namespace FlaxEngine.Interop
|
|||||||
fieldRef = nativeId;
|
fieldRef = nativeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
if (ctorInvoker != null)
|
||||||
|
ctorInvoker.Invoke(obj);
|
||||||
|
#else
|
||||||
if (ctor != null)
|
if (ctor != null)
|
||||||
ctor.Invoke(obj, null);
|
ctor.Invoke(obj, null);
|
||||||
|
#endif
|
||||||
else
|
else
|
||||||
throw new NativeInteropException($"Missing empty constructor in type '{wrappedType}'.");
|
throw new NativeInteropException($"Missing empty constructor in type '{wrappedType}'.");
|
||||||
return obj;
|
return obj;
|
||||||
|
|||||||
Reference in New Issue
Block a user