This commit is contained in:
2025-12-09 21:49:37 +02:00
parent 6586a98f8d
commit 968de34cae
19 changed files with 920 additions and 190 deletions

View File

@@ -74,7 +74,7 @@ namespace FlaxEngine.Interop
internal static IntPtr MarshalReturnValueString(ref string returnValue)
{
return returnValue != null ? ManagedString.ToNativeWeak(returnValue) : IntPtr.Zero;
return returnValue != null ? ManagedString.ToNative/*Weak*/(returnValue) : IntPtr.Zero;
}
internal static IntPtr MarshalReturnValueManagedHandle(ref ManagedHandle returnValue)
@@ -157,7 +157,7 @@ namespace FlaxEngine.Interop
if (returnObject == null)
return IntPtr.Zero;
if (returnType == typeof(string))
return ManagedString.ToNativeWeak(Unsafe.As<string>(returnObject));
return ManagedString.ToNative/*Weak*/(Unsafe.As<string>(returnObject));
if (returnType == typeof(ManagedHandle))
return ManagedHandle.ToIntPtr((ManagedHandle)(object)returnObject);
if (returnType == typeof(bool))
@@ -168,7 +168,7 @@ namespace FlaxEngine.Interop
return ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(Unsafe.As<Array>(returnObject)), GCHandleType.Weak);
if (returnType.IsArray)
return ManagedHandle.ToIntPtr(ManagedArrayToGCHandleWrappedArray(Unsafe.As<Array>(returnObject)), GCHandleType.Weak);
return ManagedHandle.ToIntPtr(returnObject, GCHandleType.Weak);
return ManagedHandle.ToIntPtr(returnObject/*, GCHandleType.Weak*/);
}
internal static IntPtr MarshalReturnValueThunk<TRet>(ref TRet returnValue)
@@ -181,7 +181,7 @@ namespace FlaxEngine.Interop
if (returnObject == null)
return IntPtr.Zero;
if (returnType == typeof(string))
return ManagedString.ToNativeWeak(Unsafe.As<string>(returnObject));
return ManagedString.ToNative/*Weak*/(Unsafe.As<string>(returnObject));
if (returnType == typeof(IntPtr))
return (IntPtr)(object)returnObject;
if (returnType == typeof(ManagedHandle))
@@ -210,7 +210,7 @@ namespace FlaxEngine.Interop
return (IntPtr)new UIntPtr((ulong)(System.UInt32)(object)returnObject);
if (returnType == typeof(System.UInt64))
return (IntPtr)new UIntPtr((ulong)(System.UInt64)(object)returnObject);
return ManagedHandle.ToIntPtr(returnObject, GCHandleType.Weak);
return ManagedHandle.ToIntPtr(returnObject/*, GCHandleType.Weak*/);
}
#if !USE_AOT

View File

@@ -1,5 +1,8 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#define USE_CONCURRENT_DICT
#define USE_GCHANDLE
#if USE_NETCORE
using System;
using System.Collections.Generic;
@@ -8,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Threading;
using FlaxEngine.Assertions;
using System.Collections.Concurrent;
#pragma warning disable 1591
@@ -356,18 +360,77 @@ namespace FlaxEngine.Interop
#endif
public struct ManagedHandle
{
#if USE_GCHANDLE
private GCHandle handle;
private static HashSet<IntPtr> _weakHandles = new HashSet<nint>();
private static ConcurrentDictionary<IntPtr, string> _handles = new();
private static ConcurrentDictionary<IntPtr, string> _handles2 = new();
private ManagedHandle(IntPtr handle) => this.handle = GCHandle.FromIntPtr(handle);
private ManagedHandle(object value, GCHandleType type) //=> handle = GCHandle.Alloc(value, type);
{
if (type == GCHandleType.Weak || type == GCHandleType.WeakTrackResurrection)
{
type = GCHandleType.Normal;
}
handle = GCHandle.Alloc(value, type);
if (type == GCHandleType.Weak || type == GCHandleType.WeakTrackResurrection)
_weakHandles.Add((IntPtr)handle);
else if (_handles.Count < 14000)
_handles.TryAdd((IntPtr)handle, value?.GetType().FullName ?? "");
else
{
if (_handles2.Count > 12)
type = type;
if (value?.GetType() == typeof(string))
type = type;
_handles2.TryAdd((IntPtr)handle, value?.GetType().FullName ?? "");
}
}
#else
private IntPtr handle;
private ManagedHandle(IntPtr handle) => this.handle = handle;
private ManagedHandle(object value, GCHandleType type) => handle = ManagedHandlePool.AllocateHandle(value, type);
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ManagedHandle Alloc(object value) => new ManagedHandle(value, GCHandleType.Normal);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ManagedHandle Alloc(object value, GCHandleType type) => new ManagedHandle(value, type);
#if USE_GCHANDLE
public void Free()// => handle.Free();
{
if ((IntPtr)handle == IntPtr.Zero)
return;
if (_weakHandles.Remove((IntPtr)handle))
{
if (!handle.IsAllocated)
handle = handle;
handle = handle;
}
else if (_handles.Remove((IntPtr)handle, out _))
{
handle = handle;
}
else if (_handles2.Remove((IntPtr)handle, out _))
{
handle = handle;
}
handle.Free();
}
public object Target => handle.Target;
public bool IsAllocated => handle.IsAllocated;
#else
public void Free()
{
if (handle == IntPtr.Zero)
@@ -383,6 +446,7 @@ namespace FlaxEngine.Interop
}
public bool IsAllocated => handle != IntPtr.Zero;
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator ManagedHandle(IntPtr value) => FromIntPtr(value);
@@ -393,6 +457,16 @@ namespace FlaxEngine.Interop
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator IntPtr(ManagedHandle value) => ToIntPtr(value);
#if USE_GCHANDLE
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr ToIntPtr(object value) => (IntPtr)Alloc(value, GCHandleType.Normal).handle;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr ToIntPtr(object value, GCHandleType type) => (IntPtr)Alloc(value, type).handle;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr ToIntPtr(ManagedHandle value) => (IntPtr)value.handle;
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr ToIntPtr(object value) => ManagedHandlePool.AllocateHandle(value, GCHandleType.Normal);
@@ -401,6 +475,7 @@ namespace FlaxEngine.Interop
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr ToIntPtr(ManagedHandle value) => value.handle;
#endif
public override int GetHashCode() => handle.GetHashCode();
@@ -412,6 +487,15 @@ namespace FlaxEngine.Interop
public static bool operator !=(ManagedHandle a, ManagedHandle b) => a.handle != b.handle;
#if USE_GCHANDLE
internal static class ManagedHandlePool
{
internal static void TryCollectWeakHandles(bool force = false)
{
}
}
#else
internal static class ManagedHandlePool
{
private const int WeakPoolCollectionSizeThreshold = 10000000;
@@ -425,17 +509,31 @@ namespace FlaxEngine.Interop
// Dictionaries for storing the valid handles.
// Note: Using locks seems to be generally the fastest when adding or fetching from the dictionary.
// Concurrent dictionaries could also be considered, but they perform much slower when adding to the dictionary.
#if USE_CONCURRENT_DICT
private static ConcurrentDictionary<IntPtr, object> persistentPool = new();
private static ConcurrentDictionary<IntPtr, GCHandle> pinnedPool = new();
#else
private static Dictionary<IntPtr, object> persistentPool = new();
private static Dictionary<IntPtr, GCHandle> pinnedPool = new();
private static Lock persistentPoolLock = new();
private static Lock pinnedPoolLock = new();
#endif
// TODO: Performance of pinned handles are poor at the moment due to GCHandle wrapping.
// TODO: .NET8: Experiment with pinned arrays for faster pinning: https://github.com/dotnet/runtime/pull/89293
// Manage double-buffered pool for weak handles in order to avoid collecting in-flight handles.
// Periodically when the pools are being accessed and conditions are met, the other pool is cleared and swapped.
private static Dictionary<IntPtr, object> weakPool = new();
private static Dictionary<IntPtr, object> weakPoolOther = new();
private static object weakPoolLock = new object();
private static int weakPoolSize = 1;
private static int weakPoolOtherSize = 1;
#if USE_CONCURRENT_DICT
private static ConcurrentDictionary<IntPtr, object> weakPool = new(-1, weakPoolSize);
private static ConcurrentDictionary<IntPtr, object> weakPoolOther = new(-1, weakPoolOtherSize);
#else
private static Dictionary<IntPtr, object> weakPool = new(weakPoolSize);
private static Dictionary<IntPtr, object> weakPoolOther = new(weakPoolOtherSize);
#endif
private static Lock weakPoolLock = new();
private static ulong nextWeakPoolCollection;
private static int nextWeakPoolGCCollection;
private static long lastWeakPoolCollectionTime;
@@ -466,6 +564,19 @@ namespace FlaxEngine.Interop
// Swap the pools and release the oldest pool for GC
(weakPool, weakPoolOther) = (weakPoolOther, weakPool);
(weakPoolSize, weakPoolOtherSize) = (weakPoolOtherSize, weakPoolSize);
#if USE_CONCURRENT_DICT
if (weakPool.Count > weakPoolSize)
{
var newMax = Math.Max(Math.Max(weakPoolSize, weakPoolOtherSize), weakPool.Count);
//FlaxEditor.Editor.Log($"growth from {weakPoolSize} to {weakPool.Count}, max {newMax}");
weakPoolSize = newMax;
weakPool = new(-1, weakPoolSize);
}
//else if (weakPool.Count > 0)
// weakPool.Clear();
//else
#endif
weakPool.Clear();
}
@@ -485,14 +596,43 @@ namespace FlaxEngine.Interop
internal static IntPtr AllocateHandle(object value, GCHandleType type)
{
IntPtr handle = NewHandle(type);
#if USE_CONCURRENT_DICT
switch (type)
{
case GCHandleType.Normal:
lock (persistentPool)
persistentPool.Add(handle, value);
//lock (persistentPoolLock)
persistentPool.TryAdd(handle, value);
//if (value?.GetType().Name.Contains("RenderContext") ?? false)
// value = value;
break;
case GCHandleType.Pinned:
lock (pinnedPool)
//lock (pinnedPoolLock)
pinnedPool.TryAdd(handle, GCHandle.Alloc(value, GCHandleType.Pinned));
break;
case GCHandleType.Weak:
case GCHandleType.WeakTrackResurrection:
lock (weakPoolLock)
{
TryCollectWeakHandles();
//weakPool.TryAdd(handle, value);
weakPool[handle] = value;
//if (value?.GetType().Name.Contains("RenderContext") ?? false)
// value = value;
}
break;
}
#else
switch (type)
{
case GCHandleType.Normal:
lock (persistentPoolLock)
persistentPool.Add(handle, value);
//if (value?.GetType().Name.Contains("RenderContext") ?? false)
// value = value;
break;
case GCHandleType.Pinned:
lock (pinnedPoolLock)
pinnedPool.Add(handle, GCHandle.Alloc(value, GCHandleType.Pinned));
break;
case GCHandleType.Weak:
@@ -501,25 +641,36 @@ namespace FlaxEngine.Interop
{
TryCollectWeakHandles();
weakPool.Add(handle, value);
//weakPool[handle] = value;
//if (value?.GetType().Name.Contains("RenderContext") ?? false)
// value = value;
}
break;
}
#endif
return handle;
}
internal static object GetObject(IntPtr handle)
{
switch (GetHandleType(handle))
GCHandleType type = GetHandleType(handle);
switch (type)
{
case GCHandleType.Normal:
lock (persistentPool)
#if !USE_CONCURRENT_DICT
lock (persistentPoolLock)
#endif
{
if (persistentPool.TryGetValue(handle, out object value))
return value;
}
break;
case GCHandleType.Pinned:
lock (pinnedPool)
#if !USE_CONCURRENT_DICT
lock (pinnedPoolLock)
#endif
{
if (pinnedPool.TryGetValue(handle, out GCHandle gcHandle))
return gcHandle.Target;
@@ -537,15 +688,98 @@ namespace FlaxEngine.Interop
}
break;
}
throw new NativeInteropException("Invalid ManagedHandle");
throw new NativeInteropException($"Invalid ManagedHandle of type '{type}'");
}
internal static void SetObject(IntPtr handle, object value)
{
switch (GetHandleType(handle))
GCHandleType type = GetHandleType(handle);
#if USE_CONCURRENT_DICT
switch (type)
{
case GCHandleType.Normal:
lock (persistentPool)
//lock (persistentPoolLock)
{
//ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(persistentPool, handle);
if (persistentPool.TryGetValue(handle, out var oldValue))
{
if (persistentPool.TryUpdate(handle, value, oldValue))
//if (!Unsafe.IsNullRef(ref obj))
{
//obj = value;
return;
}
}
}
break;
case GCHandleType.Pinned:
//lock (pinnedPoolLock)
{
/*ref GCHandle gcHandle = ref CollectionsMarshal.GetValueRefOrNullRef(pinnedPool, handle);
if (!Unsafe.IsNullRef(ref gcHandle))
{
gcHandle.Target = value;
return;
}*/
if (pinnedPool.TryGetValue(handle, out var gcHandle))
{
gcHandle.Target = value;
//if (pinnedPool.TryUpdate(handle, value, oldValue))
//if (!Unsafe.IsNullRef(ref obj))
//{
//obj = value;
return;
}
//}
}
break;
case GCHandleType.Weak:
case GCHandleType.WeakTrackResurrection:
lock (weakPoolLock)
{
TryCollectWeakHandles();
if (weakPool.TryGetValue(handle, out var oldValue))
{
if (weakPool.TryUpdate(handle, value, oldValue))
//if (!Unsafe.IsNullRef(ref obj))
{
//obj = value;
return;
}
}
if (weakPoolOther.TryGetValue(handle, out oldValue))
{
if (weakPoolOther.TryUpdate(handle, value, oldValue))
//if (!Unsafe.IsNullRef(ref obj))
{
//obj = value;
return;
}
}
/*{
ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(weakPool, handle);
if (!Unsafe.IsNullRef(ref obj))
{
obj = value;
return;
}
}
{
ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(weakPoolOther, handle);
if (!Unsafe.IsNullRef(ref obj))
{
obj = value;
return;
}
}*/
}
break;
}
#else
switch (type)
{
case GCHandleType.Normal:
lock (persistentPoolLock)
{
ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(persistentPool, handle);
if (!Unsafe.IsNullRef(ref obj))
@@ -556,7 +790,7 @@ namespace FlaxEngine.Interop
}
break;
case GCHandleType.Pinned:
lock (pinnedPool)
lock (pinnedPoolLock)
{
ref GCHandle gcHandle = ref CollectionsMarshal.GetValueRefOrNullRef(pinnedPool, handle);
if (!Unsafe.IsNullRef(ref gcHandle))
@@ -590,22 +824,32 @@ namespace FlaxEngine.Interop
}
break;
}
throw new NativeInteropException("Invalid ManagedHandle");
#endif
throw new NativeInteropException($"Invalid ManagedHandle of type '{type}'");
}
internal static void FreeHandle(IntPtr handle)
{
switch (GetHandleType(handle))
GCHandleType type = GetHandleType(handle);
switch (type)
{
case GCHandleType.Normal:
lock (persistentPool)
#if !USE_CONCURRENT_DICT
lock (persistentPoolLock)
#endif
{
if (persistentPool.Remove(handle))
if (persistentPool.Remove(handle, out _))
{
//if (value?.GetType().Name.Contains("RenderContext") ?? false)
// value = value;
return;
}
}
break;
case GCHandleType.Pinned:
lock (pinnedPool)
#if !USE_CONCURRENT_DICT
lock (pinnedPoolLock)
#endif
{
if (pinnedPool.Remove(handle, out GCHandle gcHandle))
{
@@ -617,12 +861,19 @@ namespace FlaxEngine.Interop
case GCHandleType.Weak:
case GCHandleType.WeakTrackResurrection:
lock (weakPoolLock)
{
TryCollectWeakHandles();
return;
if (weakPool.Remove(handle, out _))
return;
else if (weakPoolOther.Remove(handle, out _))
return;
return;
}
}
throw new NativeInteropException("Invalid ManagedHandle");
throw new NativeInteropException($"Invalid ManagedHandle of type '{type}'");
}
}
#endif
}
}

View File

@@ -59,8 +59,9 @@ namespace FlaxEngine.Interop
#endif
public struct ManagedToNativeState
{
ManagedArray managedArray;
IntPtr handle;
ManagedArray _pooledManagedArray;
ManagedArray _managedArray;
ManagedHandle handle;
public void FromManaged(object managed)
{
@@ -73,29 +74,30 @@ namespace FlaxEngine.Interop
if (NativeInterop.ArrayFactory.GetMarshalledType(elementType) == elementType)
{
// Use pooled managed array wrapper to be passed around as handle to it
(ManagedHandle tmp, managedArray) = ManagedArray.WrapPooledArray(arr);
handle = ManagedHandle.ToIntPtr(tmp);
(handle, _pooledManagedArray) = ManagedArray.WrapPooledArray(arr);
}
else
{
// Convert array contents to be properly accessed by the native code (as GCHandles array)
managedArray = NativeInterop.ManagedArrayToGCHandleWrappedArray(arr);
handle = ManagedHandle.ToIntPtr(ManagedHandle.Alloc(managedArray));
managedArray = null; // It's not pooled
_managedArray = NativeInterop.ManagedArrayToGCHandleWrappedArray(arr);
handle = ManagedHandle.Alloc(_pooledManagedArray);
}
}
else
handle = ManagedHandle.ToIntPtr(managed, GCHandleType.Weak);
handle = ManagedHandle.Alloc(managed/*, GCHandleType.Weak*/);
}
public IntPtr ToUnmanaged()
{
return handle;
}
public IntPtr ToUnmanaged() => ManagedHandle.ToIntPtr(handle);
public void Free()
{
managedArray?.FreePooled();
if (_pooledManagedArray != null)
_pooledManagedArray.FreePooled();
else
{
handle.Free();
_managedArray?.Free();
}
}
}
@@ -109,6 +111,7 @@ namespace FlaxEngine.Interop
public static void Free(IntPtr unmanaged)
{
ManagedHandle.FromIntPtr(unmanaged).Free();
}
}
@@ -409,7 +412,7 @@ namespace FlaxEngine.Interop
public static void Free(IntPtr unmanaged)
{
//DictionaryMarshaller<T, U>.Free(unmanaged); // No need to free weak handles
DictionaryMarshaller<T, U>.Free(unmanaged); // No need to free weak handles
}
}
@@ -678,7 +681,7 @@ namespace FlaxEngine.Interop
public static class NativeToManaged
{
public static string ConvertToManaged(IntPtr unmanaged) => ManagedString.ToManaged(unmanaged);
public static unsafe IntPtr ConvertToUnmanaged(string managed) => managed == null ? IntPtr.Zero : ManagedHandle.ToIntPtr(managed, GCHandleType.Weak);
public static unsafe IntPtr ConvertToUnmanaged(string managed) => managed == null ? IntPtr.Zero : ManagedHandle.ToIntPtr(managed/*, GCHandleType.Weak*/);
public static void Free(IntPtr unmanaged) => ManagedString.Free(unmanaged);
}
@@ -688,11 +691,11 @@ namespace FlaxEngine.Interop
public static class ManagedToNative
{
public static string ConvertToManaged(IntPtr unmanaged) => ManagedString.ToManaged(unmanaged);
public static unsafe IntPtr ConvertToUnmanaged(string managed) => managed == null ? IntPtr.Zero : ManagedHandle.ToIntPtr(managed, GCHandleType.Weak);
public static unsafe IntPtr ConvertToUnmanaged(string managed) => managed == null ? IntPtr.Zero : ManagedHandle.ToIntPtr(managed/*, GCHandleType.Weak*/);
public static void Free(IntPtr unmanaged)
{
//ManagedString.Free(unmanaged); // No need to free weak handles
ManagedString.Free(unmanaged); // No need to free weak handles
}
}

View File

@@ -9,6 +9,8 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Threading;
using FlaxEngine.Assertions;
using FlaxEngine.Utilities;
#pragma warning disable 1591
@@ -602,13 +604,13 @@ namespace FlaxEngine.Interop
[UnmanagedCallersOnly]
internal static IntPtr NewStringUTF16(char* text, int length)
{
return ManagedString.ToNativeWeak(new string(new ReadOnlySpan<char>(text, length)));
return ManagedString.ToNative/*Weak*/(new string(new ReadOnlySpan<char>(text, length)));
}
[UnmanagedCallersOnly]
internal static IntPtr NewStringUTF8(sbyte* text, int length)
{
return ManagedString.ToNativeWeak(new string(text, 0, length, System.Text.Encoding.UTF8));
return ManagedString.ToNative/*Weak*/(new string(text, 0, length, System.Text.Encoding.UTF8));
}
[UnmanagedCallersOnly]
@@ -661,7 +663,7 @@ namespace FlaxEngine.Interop
{
Type type = Unsafe.As<TypeHolder>(typeHandle.Target);
object value = MarshalToManaged(valuePtr, type);
return ManagedHandle.Alloc(value, GCHandleType.Weak);
return ManagedHandle.Alloc(value/*, GCHandleType.Weak*/);
}
/// <summary>
@@ -714,7 +716,7 @@ namespace FlaxEngine.Interop
internal static IntPtr InvokeMethod(ManagedHandle instanceHandle, ManagedHandle methodHandle, IntPtr paramPtr, IntPtr exceptionPtr)
{
MethodHolder methodHolder = Unsafe.As<MethodHolder>(methodHandle.Target);
#if !USE_AOT
#if false//#if !USE_AOT
if (methodHolder.TryGetDelegate(out var methodDelegate, out var methodDelegateContext))
{
// Fast path, invoke the method with minimal allocations
@@ -737,7 +739,7 @@ namespace FlaxEngine.Interop
// Slow path, method parameters needs to be stored in heap
object returnObject;
int numParams = methodHolder.parameterTypes.Length;
object[] methodParameters = new object[numParams];
object[] methodParameters = new object[numParams];//ObjectArrayPool.Rent(numParams);//new object[numParams];
for (int i = 0; i < numParams; i++)
{
@@ -748,6 +750,17 @@ namespace FlaxEngine.Interop
try
{
returnObject = methodHolder.method.Invoke(instanceHandle.IsAllocated ? instanceHandle.Target : null, methodParameters);
// 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(methodParameters[i], nativePtr, parameterType.GetElementType());
}
}
}
catch (Exception exception)
{
@@ -762,19 +775,11 @@ namespace FlaxEngine.Interop
throw realException;
return IntPtr.Zero;
}
// Marshal reference parameters back to original unmanaged references
for (int i = 0; i < numParams; i++)
finally
{
Type parameterType = methodHolder.parameterTypes[i];
if (parameterType.IsByRef)
{
IntPtr nativePtr = Unsafe.Read<IntPtr>((IntPtr.Add(paramPtr, sizeof(IntPtr) * i)).ToPointer());
MarshalToNative(methodParameters[i], nativePtr, parameterType.GetElementType());
}
//ObjectArrayPool.Return(methodParameters);
}
// Return value
return Invoker.MarshalReturnValueGeneric(methodHolder.returnType, returnObject);
}
}
@@ -908,7 +913,7 @@ namespace FlaxEngine.Interop
if (File.Exists(pdbPath))
{
// Load including debug symbols
using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open);
using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open, FileAccess.Read);
assembly = scriptingAssemblyLoadContext.LoadFromStream(stream, pdbStream);
}
else
@@ -998,6 +1003,8 @@ namespace FlaxEngine.Interop
Debug.Logger.LogHandler.LogWrite(LogType.Warning, "Scripting AssemblyLoadContext was not unloaded.");
weakRef.Free();
Assert.IsFalse(AssemblyLoadContext.All.Any(x => x.Name == "Flax"));
static bool IsHandleAlive(GCHandle weakRef)
{
// Checking the target in scope somehow holds a reference to it...?
@@ -1026,6 +1033,7 @@ namespace FlaxEngine.Interop
// Clear all caches which might hold references to assemblies in collectible ALC
cachedDelegatesCollectible.Clear();
cachedDelegatesCollectible = new();
foreach (var pair in managedTypesCollectible)
pair.Value.handle.Free();
managedTypesCollectible.Clear();
@@ -1115,6 +1123,9 @@ namespace FlaxEngine.Interop
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
// Unload the ALC
scriptingAssemblyLoadContext.Unload();
scriptingAssemblyLoadContext.Resolving -= OnScriptingAssemblyLoadContextResolving;

View File

@@ -18,6 +18,7 @@ using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading;
using System.Buffers;
namespace FlaxEngine.Interop
{
@@ -1244,7 +1245,7 @@ namespace FlaxEngine.Interop
internal static void ToNativeString(ref string managedValue, IntPtr nativePtr)
{
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), ManagedString.ToNativeWeak(managedValue));
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), ManagedString.ToNative/*Weak*/(managedValue));
}
internal static void ToNativeType(ref Type managedValue, IntPtr nativePtr)
@@ -1291,14 +1292,14 @@ namespace FlaxEngine.Interop
}
else
managedArray = ManagedArrayToGCHandleWrappedArray(arr);
managedPtr = ManagedHandle.ToIntPtr(managedArray, GCHandleType.Weak);
managedPtr = ManagedHandle.ToIntPtr(managedArray/*, GCHandleType.Weak*/);
}
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), managedPtr);
}
internal static void ToNative(ref T managedValue, IntPtr nativePtr)
{
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), managedValue != null ? ManagedHandle.ToIntPtr(managedValue, GCHandleType.Weak) : IntPtr.Zero);
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), managedValue != null ? ManagedHandle.ToIntPtr(managedValue/*, GCHandleType.Weak*/) : IntPtr.Zero);
}
}
@@ -1529,11 +1530,12 @@ namespace FlaxEngine.Interop
private static GCHandle[] pinnedBoxedValues = new GCHandle[256];
private static uint pinnedBoxedValuesPointer = 0;
private static (IntPtr ptr, int size)[] pinnedAllocations = new (IntPtr ptr, int size)[256];
private static Action[] pinnedNativeFreeDelegates = new Action[256];
private static uint pinnedAllocationsPointer = 0;
private delegate IntPtr UnboxerDelegate(object value, object converter);
private delegate IntPtr UnboxerDelegate(object value, object toNativeConverter, object nativeFree);
private static ConcurrentDictionary<Type, (UnboxerDelegate deleg, object toNativeDeleg)> unboxers = new(1, 3);
private static ConcurrentDictionary<Type, (UnboxerDelegate deleg, object/*Delegate*/ toNativeConverter, object nativeFree)> unboxers = new(1, 3);
private static MethodInfo unboxerMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointer), BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo unboxerToNativeMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointerWithConverter), BindingFlags.Static | BindingFlags.NonPublic);
@@ -1546,8 +1548,12 @@ namespace FlaxEngine.Interop
var toNativeMethod = attr?.NativeType.GetMethod("ToNative", BindingFlags.Static | BindingFlags.NonPublic);
if (toNativeMethod != null)
{
tuple.deleg = unboxerToNativeMethod.MakeGenericMethod(type, toNativeMethod.ReturnType).CreateDelegate<UnboxerDelegate>();
tuple.toNativeDeleg = toNativeMethod.CreateDelegate(typeof(ToNativeDelegate<,>).MakeGenericType(type, toNativeMethod.ReturnType));
Type internalType = toNativeMethod.ReturnType;
tuple.deleg = unboxerToNativeMethod.MakeGenericMethod(type, internalType).CreateDelegate<UnboxerDelegate>();
tuple.toNativeConverter = toNativeMethod.CreateDelegate(typeof(ToNativeDelegate<,>).MakeGenericType(type, internalType));
MethodInfo freeNativeMethod = attr.NativeType.GetMethod("Free", BindingFlags.Static | BindingFlags.NonPublic);
tuple.nativeFree = freeNativeMethod?.CreateDelegate(typeof(Action<>).MakeGenericType(internalType));
}
else
{
@@ -1555,7 +1561,7 @@ namespace FlaxEngine.Interop
}
tuple = unboxers.GetOrAdd(type, tuple);
}
return tuple.deleg(value, tuple.toNativeDeleg);
return tuple.deleg(value, tuple.toNativeConverter, tuple.nativeFree);
}
private static void PinValue(object value)
@@ -1569,12 +1575,15 @@ namespace FlaxEngine.Interop
handle = GCHandle.Alloc(value, GCHandleType.Pinned);
}
private static IntPtr PinValue<T>(T value) where T : struct
private static IntPtr PinValue<T>(T value, out uint index) where T : struct
{
// Store the converted value in unmanaged memory so it will not be relocated by the garbage collector.
int size = TypeHelpers<T>.MarshalSize;
uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length;
index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length;
ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index];
Action freeNativeDelegate = pinnedNativeFreeDelegates[index];
if (freeNativeDelegate != null)
freeNativeDelegate();
if (alloc.size < size)
{
if (alloc.ptr != IntPtr.Zero)
@@ -1586,7 +1595,7 @@ namespace FlaxEngine.Interop
return alloc.ptr;
}
private static IntPtr UnboxPointer<T>(object value, object converter) where T : struct
private static IntPtr UnboxPointer<T>(object value, object toNativeConverter, object nativeFreeDelegate) where T : struct
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>()) // Cannot pin structure with references
return IntPtr.Zero;
@@ -1594,11 +1603,16 @@ namespace FlaxEngine.Interop
return new IntPtr(Unsafe.AsPointer(ref Unsafe.Unbox<T>(value)));
}
private static IntPtr UnboxPointerWithConverter<T, TInternal>(object value, object converter) where T : struct
where TInternal : struct
private static IntPtr UnboxPointerWithConverter<T, TInternal>(
object value, object nativeConverterDelegate, object nativeFreeDelegate) where T : struct
where TInternal : struct
{
ToNativeDelegate<T, TInternal> toNative = Unsafe.As<ToNativeDelegate<T, TInternal>>(converter);
return PinValue<TInternal>(toNative(Unsafe.Unbox<T>(value)));
ToNativeDelegate<T, TInternal> toNative = Unsafe.As<ToNativeDelegate<T, TInternal>>(nativeConverterDelegate);
TInternal nativeValue = toNative(Unsafe.Unbox<T>(value));
IntPtr pinnedPtr = PinValue<TInternal>(nativeValue, out uint index);
Action<TInternal> freeNative = Unsafe.As<Action<TInternal>>(nativeFreeDelegate);
//pinnedNativeFreeDelegates[index] = () => freeNative(nativeValue);
return pinnedPtr;
}
}
@@ -1743,25 +1757,37 @@ namespace FlaxEngine.Interop
internal static void InitMethods()
{
return;
MakeNewCustomDelegateFunc =
typeof(Expression).Assembly.GetType("System.Linq.Expressions.Compiler.DelegateHelpers")
.GetMethod("MakeNewCustomDelegate", BindingFlags.NonPublic | BindingFlags.Static).CreateDelegate<Func<Type[], Type>>();
MakeNewCustomDelegateFunc(new[] { typeof(void) });
#if FLAX_EDITOR
// Load System.Linq.Expressions assembly to collectible ALC.
// The dynamic assembly where delegates are stored is cached in the DelegateHelpers class, so we should
// use the DelegateHelpers in collectible ALC to make sure the delegates are also stored in the same ALC.
Assembly assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(typeof(Expression).Assembly.Location);
//using var _ = scriptingAssemblyLoadContext.EnterContextualReflection();
//Debug.Logger.LogHandler.LogWrite(LogType.Warning, "InitMethods.");
var asdfa = AssemblyLoadContext.All.ToArray();
var asma = typeof(Expression).Assembly;
var loc = asma.Location;
using FileStream stream = new FileStream(loc, FileMode.Open, FileAccess.Read);
Assembly assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(loc);
MakeNewCustomDelegateFuncCollectible =
assembly.GetType("System.Linq.Expressions.Compiler.DelegateHelpers")
.GetMethod("MakeNewCustomDelegate", BindingFlags.NonPublic | BindingFlags.Static).CreateDelegate<Func<Type[], Type>>();
// Create dummy delegates to force the dynamic Snippets assembly to be loaded in correcet ALCs
MakeNewCustomDelegateFunc(new[] { typeof(void) });
Debug.Logger.LogHandler.LogWrite(LogType.Warning, "InitMethods ok .");
{
// Ensure the new delegate is placed in the collectible ALC
// Ensure any future delegates is placed in the collectible ALC
using var ctx = scriptingAssemblyLoadContext.EnterContextualReflection();
MakeNewCustomDelegateFuncCollectible(new[] { typeof(void) });
var ret = MakeNewCustomDelegateFuncCollectible(new[] { typeof(void) });
Assert.IsTrue(ret.IsCollectible);
}
#endif
}
@@ -1785,6 +1811,102 @@ namespace FlaxEngine.Interop
#endif
}
internal static class ObjectArrayPool
{
[ThreadStatic]
private static List<(bool InUse, object[] Array)> _pool;
[ThreadStatic]
private static List<(bool InUse, object[] Array)> _pool2;
/// <summary>
/// Rents an array from the pool.
/// </summary>
/// <param name="size">Exact size of the array.</param>
internal static object[] Rent(int size)
{
if (size == 0)
return Array.Empty<object>();
if (_pool == null)
_pool = new(16);
foreach (ref var tuple in CollectionsMarshal.AsSpan(_pool))
{
if (tuple.InUse)
continue;
if (tuple.Array.Length != size)
continue;
tuple.InUse = true;
return tuple.Array;
}
var newTuple = (InUse: true, Array: new object[size]);
_pool.Add(newTuple);
return newTuple.Array;
}
internal static object[] Rent2()
{
if (_pool2 == null)
_pool2 = new(16);
foreach (ref var tuple in CollectionsMarshal.AsSpan(_pool2))
{
if (tuple.InUse)
continue;
tuple.InUse = true;
return tuple.Array;
}
var newTuple = (InUse: true, Array: new object[8]);
_pool2.Add(newTuple);
return newTuple.Array;
}
/// <summary>
/// Returns the rented object array back to the pool.
/// </summary>
/// <param name="array">The array rented from the pool.</param>
internal static void Return(object[] array)
{
if (array.Length == 0)
return;
foreach (ref var tuple in CollectionsMarshal.AsSpan(_pool))
{
if (tuple.Array != array)
continue;
tuple.InUse = false;
for (int i = 0; i < array.Length; i++)
array[i] = null;
return;
}
throw new NativeInteropException("Tried to free non-pooled object array as pooled ManagedArray");
}
internal static void Return2(object[] array)
{
foreach (ref var tuple in CollectionsMarshal.AsSpan(_pool2))
{
if (tuple.Array != array)
continue;
tuple.InUse = false;
for (int i = 0; i < array.Length; i++)
array[i] = null;
return;
}
throw new NativeInteropException("Tried to free non-pooled object array as pooled ManagedArray");
}
}
#if !USE_AOT
/// <summary>
/// Wrapper class for invoking function pointers from unmanaged code.
@@ -1792,13 +1914,18 @@ namespace FlaxEngine.Interop
internal class ThunkContext
{
internal MethodInfo method;
internal MethodInvoker invoker;
internal Type[] parameterTypes;
internal Invoker.InvokeThunkDelegate methodDelegate;
internal object methodDelegateContext;
internal static object[] objectPool = new object[128];
internal static int objectPoolIndex = 0;
internal ThunkContext(MethodInfo method)
{
this.method = method;
invoker = MethodInvoker.Create(method);
parameterTypes = method.GetParameterTypes();
// Thunk delegates don't support IsByRef parameters (use generic invocation that handles 'out' and 'ref' prams)
@@ -1826,6 +1953,7 @@ namespace FlaxEngine.Interop
genericParamTypes.Add(type);
}
#if false
string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(method.ReturnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}";
Type invokerType = Type.GetType(invokerTypeName);
if (invokerType != null)
@@ -1838,11 +1966,13 @@ namespace FlaxEngine.Interop
if (methodDelegate != null)
Assert.IsTrue(methodDelegateContext != null);
#endif
}
public IntPtr InvokeThunk(ManagedHandle instanceHandle, IntPtr param1, IntPtr param2, IntPtr param3, IntPtr param4, IntPtr param5, IntPtr param6, IntPtr param7)
{
IntPtr* nativePtrs = stackalloc IntPtr[] { param1, param2, param3, param4, param5, param6, param7 };
#if false
if (methodDelegate != null)
{
IntPtr returnValue;
@@ -1861,36 +1991,69 @@ namespace FlaxEngine.Interop
return returnValue;
}
else
#endif
{
// The parameters are wrapped (boxed) in GCHandles
object returnObject;
int numParams = parameterTypes.Length;
object[] methodParameters = new object[numParams];
//object[] methodParameters = ObjectArrayPool.Rent(numParams);//new object[numParams]; // TODO: object array pool
if (objectPoolIndex + numParams > 128)
objectPoolIndex = 0;
var paramSpan = objectPool.AsSpan(objectPoolIndex, numParams);
objectPoolIndex += numParams;
for (int i = 0; i < numParams; i++)
{
IntPtr nativePtr = nativePtrs[i];
object managed = null;
if (nativePtr != IntPtr.Zero)
{
object managed = null;
Type type = parameterTypes[i];
Type elementType = type.GetElementType();
if (type.IsByRef)
{
// References use indirection to support value returning
nativePtr = Unsafe.Read<IntPtr>(nativePtr.ToPointer());
type = elementType;
type = type.GetElementType();
}
if (type.IsArray)
managed = MarshalToManaged(nativePtr, type); // Array might be in internal format of custom structs so unbox if need to
else if (type == typeof(Type))
managed = Unsafe.As<TypeHolder>(ManagedHandle.FromIntPtr(nativePtr).Target).type;
else
managed = ManagedHandle.FromIntPtr(nativePtr).Target;
paramSpan[i] = managed;
}
methodParameters[i] = managed;
}
try
{
returnObject = method.Invoke(instanceHandle.IsAllocated ? instanceHandle.Target : null, methodParameters);
returnObject = invoker.Invoke(instanceHandle.IsAllocated ? instanceHandle.Target : null, paramSpan);
//returnObject = method.Invoke(instanceHandle.IsAllocated ? instanceHandle.Target : null, methodParameters);
// Marshal reference parameters back to original unmanaged references
for (int i = 0; i < numParams; i++)
{
IntPtr nativePtr = nativePtrs[i];
Type type = parameterTypes[i];
if (nativePtr != IntPtr.Zero && type.IsByRef)
{
object managed = paramSpan[i];
type = type.GetElementType();
if (managed == null)
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), IntPtr.Zero);
else if (type.IsArray)
MarshalToNative(managed, nativePtr, type);
else
{
var ptr = nativePtr.ToPointer();
if (type == typeof(Type))
managed = GetTypeHolder(Unsafe.As<Type>(managed));
var handle = ManagedHandle.Alloc(managed/*, GCHandleType.Weak*/);
var handleptr = ManagedHandle.ToIntPtr(handle);
Unsafe.Write<IntPtr>(ptr, handleptr);
//Unsafe.Write<IntPtr>(nativePtr.ToPointer(), ManagedHandle.ToIntPtr(ManagedHandle.Alloc(managed, GCHandleType.Weak)));
}
}
}
}
catch (Exception exception)
{
@@ -1900,32 +2063,19 @@ namespace FlaxEngine.Interop
Unsafe.Write<IntPtr>(exceptionPtr.ToPointer(), ManagedHandle.ToIntPtr(exception, GCHandleType.Weak));
return IntPtr.Zero;
}
// Marshal reference parameters back to original unmanaged references
for (int i = 0; i < numParams; i++)
finally
{
IntPtr nativePtr = nativePtrs[i];
Type type = parameterTypes[i];
object managed = methodParameters[i];
if (nativePtr != IntPtr.Zero && type.IsByRef)
{
type = type.GetElementType();
if (managed == null)
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), IntPtr.Zero);
else if (type.IsArray)
MarshalToNative(managed, nativePtr, type);
else
Unsafe.Write<IntPtr>(nativePtr.ToPointer(), ManagedHandle.ToIntPtr(ManagedHandle.Alloc(managed, GCHandleType.Weak)));
}
//ObjectArrayPool.Return(methodParameters);
for (int i = 0; i < numParams; i++)
paramSpan[i] = null;
}
// Return value
return Invoker.MarshalReturnValueThunkGeneric(method.ReturnType, returnObject);
}
}
}
#endif
}
}
internal class NativeInteropException : Exception
{