Support marshalling Arrays as dynamic arrays in scripting API

This commit is contained in:
2025-04-27 17:20:06 +03:00
parent 51feaa0730
commit db47531a6d
4 changed files with 240 additions and 2 deletions

View File

@@ -658,6 +658,217 @@ namespace FlaxEngine.Interop
}
}
#if FLAX_EDITOR
[HideInEditor]
#endif
[CustomMarshaller(typeof(List<>), MarshalMode.ManagedToUnmanagedIn, typeof(ListMarshaller<,>.ManagedToNative))]
[CustomMarshaller(typeof(List<>), MarshalMode.UnmanagedToManagedOut, typeof(ListMarshaller<,>.ManagedToNative))]
[CustomMarshaller(typeof(List<>), MarshalMode.ManagedToUnmanagedOut, typeof(ListMarshaller<,>.NativeToManaged))]
[CustomMarshaller(typeof(List<>), MarshalMode.UnmanagedToManagedIn, typeof(ListMarshaller<,>.NativeToManaged))]
[CustomMarshaller(typeof(List<>), MarshalMode.ElementOut, typeof(ListMarshaller<,>.NativeToManaged))]
[CustomMarshaller(typeof(List<>), MarshalMode.ManagedToUnmanagedRef, typeof(ListMarshaller<,>.Bidirectional))]
[CustomMarshaller(typeof(List<>), MarshalMode.UnmanagedToManagedRef, typeof(ListMarshaller<,>.Bidirectional))]
[CustomMarshaller(typeof(List<>), MarshalMode.ElementRef, typeof(ListMarshaller<,>))]
[ContiguousCollectionMarshaller]
public static unsafe class ListMarshaller<T, TUnmanagedElement> where TUnmanagedElement : unmanaged
{
#if FLAX_EDITOR
[HideInEditor]
#endif
public static class NativeToManaged
{
public static List<T> AllocateContainerForManagedElements(TUnmanagedElement* unmanaged, int numElements)
{
if (unmanaged is null)
return null;
return new List<T>(numElements);
}
public static TUnmanagedElement* AllocateContainerForUnmanagedElements(List<T> managed, out int numElements)
{
if (managed is null)
{
numElements = 0;
return null;
}
numElements = managed.Count;
(ManagedHandle managedArrayHandle, _) = ManagedArray.AllocatePooledArray<TUnmanagedElement>(managed.Count);
return (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedArrayHandle);
}
public static ReadOnlySpan<T> GetManagedValuesSource(List<T> managed) => CollectionsMarshal.AsSpan(managed);
public static Span<TUnmanagedElement> GetUnmanagedValuesDestination(TUnmanagedElement* unmanaged)
{
if (unmanaged == null)
return Span<TUnmanagedElement>.Empty;
ManagedArray managedArray = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
return managedArray.ToSpan<TUnmanagedElement>();
}
public static Span<T> GetManagedValuesDestination(List<T> managed) => CollectionsMarshal.AsSpan(managed);
public static ReadOnlySpan<TUnmanagedElement> GetUnmanagedValuesSource(TUnmanagedElement* unmanaged, int numElements)
{
if (unmanaged == null)
return ReadOnlySpan<TUnmanagedElement>.Empty;
ManagedArray managedArray = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
return managedArray.ToSpan<TUnmanagedElement>();
}
public static void Free(TUnmanagedElement* unmanaged)
{
if (unmanaged == null)
return;
ManagedHandle handle = ManagedHandle.FromIntPtr(new IntPtr(unmanaged));
(Unsafe.As<ManagedArray>(handle.Target)).Free();
handle.Free();
}
public static Span<TUnmanagedElement> GetUnmanagedValuesDestination(TUnmanagedElement* unmanaged, int numElements)
{
if (unmanaged == null)
return Span<TUnmanagedElement>.Empty;
ManagedArray managedArray = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
return managedArray.ToSpan<TUnmanagedElement>();
}
}
#if FLAX_EDITOR
[HideInEditor]
#endif
public ref struct ManagedToNative
{
List<T> sourceArray;
ManagedArray managedArray;
ManagedHandle managedHandle;
public void FromManaged(List<T> managed)
{
if (managed is null)
return;
sourceArray = managed;
(managedHandle, managedArray) = ManagedArray.AllocatePooledArray<TUnmanagedElement>(managed.Count);
}
public ReadOnlySpan<T> GetManagedValuesSource() => CollectionsMarshal.AsSpan(sourceArray);
public Span<TUnmanagedElement> GetUnmanagedValuesDestination() => managedArray != null ? managedArray.ToSpan<TUnmanagedElement>() : Span<TUnmanagedElement>.Empty;
public TUnmanagedElement* ToUnmanaged() => (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedHandle);
public void Free() => managedArray?.FreePooled();
}
#if FLAX_EDITOR
[HideInEditor]
#endif
public struct Bidirectional
{
List<T> sourceArray;
ManagedArray managedArray;
ManagedHandle handle; // Valid only for pooled array
public void FromManaged(List<T> managed)
{
if (managed == null)
return;
sourceArray = managed;
(handle, managedArray) = ManagedArray.AllocatePooledArray<TUnmanagedElement>(managed.Count);
}
public ReadOnlySpan<T> GetManagedValuesSource() => CollectionsMarshal.AsSpan(sourceArray);
public Span<TUnmanagedElement> GetUnmanagedValuesDestination()
{
if (managedArray == null)
return Span<TUnmanagedElement>.Empty;
return managedArray.ToSpan<TUnmanagedElement>();
}
public TUnmanagedElement* ToUnmanaged() => (TUnmanagedElement*)ManagedHandle.ToIntPtr(handle);
public void FromUnmanaged(TUnmanagedElement* unmanaged)
{
ManagedArray arr = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
if (sourceArray == null || sourceArray.Count != arr.Length)
{
// Array was resized when returned from native code (as ref parameter)
managedArray.FreePooled();
if (sourceArray.Capacity < arr.Length)
sourceArray.Capacity = arr.Length;
for (int i = sourceArray.Count - 1; i > arr.Length; i--)
sourceArray.RemoveAt(i);
for (int i = sourceArray.Count; i < arr.Length; i++)
sourceArray.Add(default);
if (sourceArray.Count != arr.Length)
throw new Exception();
managedArray = arr;
handle = new ManagedHandle(); // Invalidate as it's not pooled array anymore
}
}
public ReadOnlySpan<TUnmanagedElement> GetUnmanagedValuesSource(int numElements)
{
if (managedArray == null)
return ReadOnlySpan<TUnmanagedElement>.Empty;
return managedArray.ToSpan<TUnmanagedElement>();
}
public Span<T> GetManagedValuesDestination(int numElements) => CollectionsMarshal.AsSpan(sourceArray);
public List<T> ToManaged() => sourceArray;
public void Free()
{
if (handle.IsAllocated)
managedArray.FreePooled();
}
}
public static TUnmanagedElement* AllocateContainerForUnmanagedElements(List<T> managed, out int numElements)
{
if (managed is null)
{
numElements = 0;
return null;
}
numElements = managed.Count;
(ManagedHandle managedArrayHandle, _) = ManagedArray.AllocatePooledArray<TUnmanagedElement>(managed.Count);
return (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedArrayHandle);
}
public static ReadOnlySpan<T> GetManagedValuesSource(List<T> managed) => CollectionsMarshal.AsSpan(managed);
public static Span<TUnmanagedElement> GetUnmanagedValuesDestination(TUnmanagedElement* unmanaged, int numElements)
{
if (unmanaged == null)
return Span<TUnmanagedElement>.Empty;
ManagedArray unmanagedArray = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
return unmanagedArray.ToSpan<TUnmanagedElement>();
}
public static List<T> AllocateContainerForManagedElements(TUnmanagedElement* unmanaged, int numElements) => unmanaged is null ? null : new List<T>(numElements);
public static Span<T> GetManagedValuesDestination(List<T> managed) => CollectionsMarshal.AsSpan(managed);
public static ReadOnlySpan<TUnmanagedElement> GetUnmanagedValuesSource(TUnmanagedElement* unmanaged, int numElements)
{
if (unmanaged == null)
return ReadOnlySpan<TUnmanagedElement>.Empty;
ManagedArray array = Unsafe.As<ManagedArray>(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
return array.ToSpan<TUnmanagedElement>();
}
public static void Free(TUnmanagedElement* unmanaged)
{
if (unmanaged == null)
return;
ManagedHandle handle = ManagedHandle.FromIntPtr(new IntPtr(unmanaged));
Unsafe.As<ManagedArray>(handle.Target).FreePooled();
}
}
#if FLAX_EDITOR
[HideInEditor]
#endif

View File

@@ -686,6 +686,8 @@ namespace Flax.Build.Bindings
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemArrayMarshaller))";
else if (nativeType == "object[]")
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemObjectArrayMarshaller))";
else if (parameterInfo.Type.Type == "Array" && parameterInfo.MarshalAsDynamicArray)
parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ListMarshaller<,>), CountElementName = \"__{parameterInfo.Name}Count\")";
else if (parameterInfo.Type.Type == "Array" && parameterInfo.Type.GenericArgs.Count > 0 && parameterInfo.Type.GenericArgs[0].Type == "bool")
parameterMarshalType = $"MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = {(!functionInfo.IsStatic ? 1 : 0) + functionInfo.Parameters.Count + (functionInfo.Glue.CustomParameters.FindIndex(x => x.Name == $"__{parameterInfo.Name}Count"))})";
else if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer" || nativeType == "Array")
@@ -716,6 +718,11 @@ namespace Flax.Build.Bindings
// Out parameters that need additional converting will be converted at the native side (eg. object reference)
if (parameterInfo.IsOut && !string.IsNullOrEmpty(GenerateCSharpManagedToNativeConverter(buildData, parameterInfo.Type, caller)))
nativeType = parameterInfo.Type.Type;
if (parameterInfo.Type.Type == "Array" && parameterInfo.MarshalAsDynamicArray)
{
var dynamicArrayType = TypeInfo.FromString($"List<{parameterInfo.Type.GenericArgs[0].Type}>");
nativeType = "System.Collections.Generic." + GenerateCSharpManagedToNativeType(buildData, dynamicArrayType, caller, true);
}
contents.Append(nativeType);
contents.Append(' ');
@@ -736,7 +743,9 @@ namespace Flax.Build.Bindings
if (parameterInfo.IsOut && parameterInfo.DefaultValue == "var __resultAsRef")
{
// TODO: make this code shared with MarshalUsing selection from the above
if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer")
if (parameterInfo.Type.Type == "Array" && parameterInfo.MarshalAsDynamicArray)
parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ListMarshaller<,>), CountElementName = \"{parameterInfo.Name}Count\")";
else if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer")
parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"{parameterInfo.Name}Count\")";
else if (parameterInfo.Type.Type == "Dictionary")
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)";
@@ -772,7 +781,12 @@ namespace Flax.Build.Bindings
if (parameterInfo.Type.IsArray || parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "BytesContainer" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BitArray")
{
if (!parameterInfo.IsOut)
contents.Append($"var __{parameterInfo.Name}Count = {(isSetter ? "value" : parameterInfo.Name)}?.Length ?? 0; ");
{
if (parameterInfo.Type.Type == "Array" && parameterInfo.MarshalAsDynamicArray)
contents.Append($"var __{parameterInfo.Name}Count = {(isSetter ? "value" : parameterInfo.Name)}?.Count ?? 0; ");
else
contents.Append($"var __{parameterInfo.Name}Count = {(isSetter ? "value" : parameterInfo.Name)}?.Length ?? 0; ");
}
}
}
#endif
@@ -1366,6 +1380,13 @@ namespace Flax.Build.Bindings
contents.Append('[').Append(parameterInfo.Attributes).Append(']').Append(' ');
var managedType = GenerateCSharpNativeToManaged(buildData, parameterInfo.Type, classInfo);
if (parameterInfo.Type.Type == "Array" && parameterInfo.MarshalAsDynamicArray)
{
var dynamicArrayType = TypeInfo.FromString($"List<{parameterInfo.Type.GenericArgs[0].Type}>");
managedType = GenerateCSharpNativeToManaged(buildData, dynamicArrayType, classInfo);
managedType = $"System.Collections.Generic.{managedType}";
}
if (parameterInfo.IsOut)
contents.Append("out ");
else if (parameterInfo.IsRef)

View File

@@ -378,6 +378,9 @@ namespace Flax.Build.Bindings
case "defaultvalue":
currentParam.DefaultValue = tag.Value;
break;
case "dynamicarray":
currentParam.MarshalAsDynamicArray = true;
break;
default:
bool valid = false;
ParseFunctionParameterTag?.Invoke(ref valid, tag, ref currentParam);

View File

@@ -16,6 +16,7 @@ namespace Flax.Build.Bindings
public TypeInfo Type;
public string DefaultValue;
public string Attributes;
public bool MarshalAsDynamicArray;
public bool IsRef;
public bool IsOut;
public bool IsThis;
@@ -35,6 +36,7 @@ namespace Flax.Build.Bindings
BindingsGenerator.Write(writer, DefaultValue);
BindingsGenerator.Write(writer, Attributes);
// TODO: convert into flags
writer.Write(MarshalAsDynamicArray);
writer.Write(IsRef);
writer.Write(IsOut);
writer.Write(IsThis);
@@ -48,6 +50,7 @@ namespace Flax.Build.Bindings
DefaultValue = BindingsGenerator.Read(reader, DefaultValue);
Attributes = BindingsGenerator.Read(reader, Attributes);
// TODO: convert into flags
MarshalAsDynamicArray = reader.ReadBoolean();
IsRef = reader.ReadBoolean();
IsOut = reader.ReadBoolean();
IsThis = reader.ReadBoolean();