You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
616 lines
14 KiB
616 lines
14 KiB
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace Epic.OnlineServices
|
|
{
|
|
internal class AllocationException : Exception
|
|
{
|
|
public AllocationException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal class ExternalAllocationException : AllocationException
|
|
{
|
|
public ExternalAllocationException(IntPtr address, Type type)
|
|
: base(string.Format("Attempting to allocate '{0}' over externally allocated memory at {1}", type, address.ToString("X")))
|
|
{
|
|
}
|
|
}
|
|
|
|
internal class CachedTypeAllocationException : AllocationException
|
|
{
|
|
public CachedTypeAllocationException(IntPtr address, Type foundType, Type expectedType)
|
|
: base(string.Format("Cached allocation is '{0}' but expected '{1}' at {2}", foundType, expectedType, address.ToString("X")))
|
|
{
|
|
}
|
|
}
|
|
|
|
internal class CachedArrayAllocationException : AllocationException
|
|
{
|
|
public CachedArrayAllocationException(IntPtr address, int foundLength, int expectedLength)
|
|
: base(string.Format("Cached array allocation has length {0} but expected {1} at {2}", foundLength, expectedLength, address.ToString("X")))
|
|
{
|
|
}
|
|
}
|
|
|
|
internal class DynamicBindingException : Exception
|
|
{
|
|
public DynamicBindingException(string bindingName)
|
|
: base(string.Format("Failed to hook dynamic binding for '{0}'", bindingName))
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A helper class that manages memory in the wrapper.
|
|
/// </summary>
|
|
public sealed partial class Helper
|
|
{
|
|
private struct Allocation
|
|
{
|
|
public int Size { get; private set; }
|
|
|
|
public object Cache { get; private set; }
|
|
|
|
public bool? IsArrayItemAllocated { get; private set; }
|
|
|
|
public Allocation(int size, object cache, bool? isArrayItemAllocated = null)
|
|
{
|
|
Size = size;
|
|
Cache = cache;
|
|
IsArrayItemAllocated = isArrayItemAllocated;
|
|
}
|
|
}
|
|
|
|
private class DelegateHolder
|
|
{
|
|
public Delegate Public { get; private set; }
|
|
public Delegate Private { get; private set; }
|
|
public Delegate[] StructDelegates { get; private set; }
|
|
public ulong? NotificationId { get; set; }
|
|
|
|
public DelegateHolder(Delegate publicDelegate, Delegate privateDelegate, params Delegate[] structDelegates)
|
|
{
|
|
Public = publicDelegate;
|
|
Private = privateDelegate;
|
|
StructDelegates = structDelegates;
|
|
}
|
|
}
|
|
|
|
private static Dictionary<IntPtr, Allocation> s_Allocations = new Dictionary<IntPtr, Allocation>();
|
|
private static Dictionary<IntPtr, GCHandle> s_PinnedBuffers = new Dictionary<IntPtr, GCHandle>();
|
|
private static Dictionary<IntPtr, DelegateHolder> s_Callbacks = new Dictionary<IntPtr, DelegateHolder>();
|
|
private static Dictionary<string, DelegateHolder> s_StaticCallbacks = new Dictionary<string, DelegateHolder>();
|
|
private static long s_LastClientDataId = 0;
|
|
private static Dictionary<IntPtr, object> s_ClientDatas = new Dictionary<IntPtr, object>();
|
|
|
|
/// <summary>
|
|
/// Gets the number of unmanaged allocations and other stored values in the wrapper. Use this to find leaks related to the usage of wrapper code.
|
|
/// </summary>
|
|
/// <returns>The number of unmanaged allocations currently active within the wrapper.</returns>
|
|
public static int GetAllocationCount()
|
|
{
|
|
return s_Allocations.Count + s_PinnedBuffers.Count + s_Callbacks.Count + s_ClientDatas.Count;
|
|
}
|
|
|
|
internal static void Copy(byte[] from, IntPtr to)
|
|
{
|
|
if (from != null && to != IntPtr.Zero)
|
|
{
|
|
Marshal.Copy(from, 0, to, from.Length);
|
|
}
|
|
}
|
|
|
|
internal static void Copy(ArraySegment<byte> from, IntPtr to)
|
|
{
|
|
if (from.Count != 0 && to != IntPtr.Zero)
|
|
{
|
|
Marshal.Copy(from.Array, from.Offset, to, from.Count);
|
|
}
|
|
}
|
|
|
|
internal static void Dispose(ref IntPtr value)
|
|
{
|
|
RemoveAllocation(ref value);
|
|
RemovePinnedBuffer(ref value);
|
|
}
|
|
|
|
internal static void Dispose<TDisposable>(ref TDisposable disposable)
|
|
where TDisposable : IDisposable
|
|
{
|
|
if (disposable != null)
|
|
{
|
|
disposable.Dispose();
|
|
}
|
|
}
|
|
|
|
internal static void Dispose<TEnum>(ref IntPtr value, TEnum currentEnum, TEnum expectedEnum)
|
|
{
|
|
if ((int)(object)currentEnum == (int)(object)expectedEnum)
|
|
{
|
|
Dispose(ref value);
|
|
}
|
|
}
|
|
|
|
private static int GetAnsiStringLength(byte[] bytes)
|
|
{
|
|
int length = 0;
|
|
foreach (byte currentByte in bytes)
|
|
{
|
|
if (currentByte == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
++length;
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
private static int GetAnsiStringLength(IntPtr address)
|
|
{
|
|
int length = 0;
|
|
while (Marshal.ReadByte(address, length) != 0)
|
|
{
|
|
++length;
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
internal static T GetDefault<T>()
|
|
{
|
|
return default(T);
|
|
}
|
|
|
|
private static void GetAllocation<T>(IntPtr source, out T target)
|
|
{
|
|
target = GetDefault<T>();
|
|
|
|
if (source == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
object allocationCache;
|
|
if (TryGetAllocationCache(source, out allocationCache))
|
|
{
|
|
if (allocationCache != null)
|
|
{
|
|
if (allocationCache.GetType() == typeof(T))
|
|
{
|
|
target = (T)allocationCache;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throw new CachedTypeAllocationException(source, allocationCache.GetType(), typeof(T));
|
|
}
|
|
}
|
|
}
|
|
|
|
target = (T)Marshal.PtrToStructure(source, typeof(T));
|
|
}
|
|
|
|
private static void GetAllocation<T>(IntPtr source, out T? target)
|
|
where T : struct
|
|
{
|
|
target = GetDefault<T?>();
|
|
|
|
if (source == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If this is an allocation containing cached data, we should be able to fetch it from the cache
|
|
object allocationCache;
|
|
if (TryGetAllocationCache(source, out allocationCache))
|
|
{
|
|
if (allocationCache != null)
|
|
{
|
|
if (allocationCache.GetType() == typeof(T))
|
|
{
|
|
target = (T?)allocationCache;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throw new CachedTypeAllocationException(source, allocationCache.GetType(), typeof(T));
|
|
}
|
|
}
|
|
}
|
|
|
|
target = (T?)Marshal.PtrToStructure(source, typeof(T));
|
|
}
|
|
|
|
private static void GetAllocation<THandle>(IntPtr source, out THandle[] target, int arrayLength)
|
|
where THandle : Handle, new()
|
|
{
|
|
target = null;
|
|
|
|
if (source == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If this is an allocation containing cached data, we should be able to fetch it from the cache
|
|
object allocationCache;
|
|
if (TryGetAllocationCache(source, out allocationCache))
|
|
{
|
|
if (allocationCache != null)
|
|
{
|
|
if (allocationCache.GetType() == typeof(THandle[]))
|
|
{
|
|
var cachedArray = (Array)allocationCache;
|
|
if (cachedArray.Length == arrayLength)
|
|
{
|
|
target = cachedArray as THandle[];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throw new CachedArrayAllocationException(source, cachedArray.Length, arrayLength);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new CachedTypeAllocationException(source, allocationCache.GetType(), typeof(THandle[]));
|
|
}
|
|
}
|
|
}
|
|
|
|
var itemSize = Marshal.SizeOf(typeof(IntPtr));
|
|
|
|
List<THandle> items = new List<THandle>();
|
|
for (int itemIndex = 0; itemIndex < arrayLength; ++itemIndex)
|
|
{
|
|
IntPtr itemAddress = new IntPtr(source.ToInt64() + itemIndex * itemSize);
|
|
itemAddress = Marshal.ReadIntPtr(itemAddress);
|
|
THandle item;
|
|
Convert(itemAddress, out item);
|
|
items.Add(item);
|
|
}
|
|
|
|
target = items.ToArray();
|
|
}
|
|
|
|
private static void GetAllocation<T>(IntPtr from, out T[] to, int arrayLength, bool isArrayItemAllocated)
|
|
{
|
|
to = null;
|
|
|
|
if (from == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If this is an allocation containing cached data, we should be able to fetch it from the cache
|
|
object allocationCache;
|
|
if (TryGetAllocationCache(from, out allocationCache))
|
|
{
|
|
if (allocationCache != null)
|
|
{
|
|
if (allocationCache.GetType() == typeof(T[]))
|
|
{
|
|
var cachedArray = (Array)allocationCache;
|
|
if (cachedArray.Length == arrayLength)
|
|
{
|
|
to = cachedArray as T[];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throw new CachedArrayAllocationException(from, cachedArray.Length, arrayLength);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new CachedTypeAllocationException(from, allocationCache.GetType(), typeof(T[]));
|
|
}
|
|
}
|
|
}
|
|
|
|
int itemSize;
|
|
if (isArrayItemAllocated)
|
|
{
|
|
itemSize = Marshal.SizeOf(typeof(IntPtr));
|
|
}
|
|
else
|
|
{
|
|
itemSize = Marshal.SizeOf(typeof(T));
|
|
}
|
|
|
|
List<T> items = new List<T>();
|
|
for (int itemIndex = 0; itemIndex < arrayLength; ++itemIndex)
|
|
{
|
|
IntPtr itemAddress = new IntPtr(from.ToInt64() + itemIndex * itemSize);
|
|
|
|
if (isArrayItemAllocated)
|
|
{
|
|
itemAddress = Marshal.ReadIntPtr(itemAddress);
|
|
}
|
|
|
|
T item;
|
|
GetAllocation(itemAddress, out item);
|
|
items.Add(item);
|
|
}
|
|
|
|
to = items.ToArray();
|
|
}
|
|
|
|
private static void GetAllocation(IntPtr source, out Utf8String target)
|
|
{
|
|
target = null;
|
|
|
|
if (source == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// C style strlen
|
|
int length = GetAnsiStringLength(source);
|
|
|
|
// +1 byte for the null terminator.
|
|
byte[] bytes = new byte[length + 1];
|
|
Marshal.Copy(source, bytes, 0, length + 1);
|
|
|
|
target = new Utf8String(bytes);
|
|
}
|
|
|
|
internal static IntPtr AddAllocation(int size)
|
|
{
|
|
if (size == 0)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
IntPtr address = Marshal.AllocHGlobal(size);
|
|
Marshal.WriteByte(address, 0, 0);
|
|
|
|
lock (s_Allocations)
|
|
{
|
|
s_Allocations.Add(address, new Allocation(size, null));
|
|
}
|
|
|
|
return address;
|
|
}
|
|
|
|
internal static IntPtr AddAllocation(uint size)
|
|
{
|
|
return AddAllocation((int)size);
|
|
}
|
|
|
|
private static IntPtr AddAllocation<T>(int size, T cache)
|
|
{
|
|
if (size == 0 || cache == null)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
IntPtr address = Marshal.AllocHGlobal(size);
|
|
Marshal.StructureToPtr(cache, address, false);
|
|
|
|
lock (s_Allocations)
|
|
{
|
|
s_Allocations.Add(address, new Allocation(size, cache));
|
|
}
|
|
|
|
return address;
|
|
}
|
|
|
|
private static IntPtr AddAllocation<T>(int size, T[] cache, bool? isArrayItemAllocated)
|
|
{
|
|
if (size == 0 || cache == null)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
IntPtr address = Marshal.AllocHGlobal(size);
|
|
Marshal.WriteByte(address, 0, 0);
|
|
|
|
lock (s_Allocations)
|
|
{
|
|
s_Allocations.Add(address, new Allocation(size, cache, isArrayItemAllocated));
|
|
}
|
|
|
|
return address;
|
|
}
|
|
|
|
private static IntPtr AddAllocation<T>(T[] array, bool isArrayItemAllocated)
|
|
{
|
|
if (array == null)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
int itemSize;
|
|
if (isArrayItemAllocated)
|
|
{
|
|
itemSize = Marshal.SizeOf(typeof(IntPtr));
|
|
}
|
|
else
|
|
{
|
|
itemSize = Marshal.SizeOf(typeof(T));
|
|
}
|
|
|
|
IntPtr newArrayAddress = AddAllocation(array.Length * itemSize, array, isArrayItemAllocated);
|
|
|
|
for (int itemIndex = 0; itemIndex < array.Length; ++itemIndex)
|
|
{
|
|
var item = (T)array.GetValue(itemIndex);
|
|
|
|
if (isArrayItemAllocated)
|
|
{
|
|
IntPtr newItemAddress;
|
|
if (typeof(T) == typeof(Utf8String))
|
|
{
|
|
newItemAddress = AddPinnedBuffer((Utf8String)(object)item);
|
|
}
|
|
else if (typeof(T).BaseType == typeof(Handle))
|
|
{
|
|
Convert((Handle)(object)item, out newItemAddress);
|
|
}
|
|
else
|
|
{
|
|
newItemAddress = AddAllocation(Marshal.SizeOf(typeof(T)), item);
|
|
}
|
|
|
|
// Copy the item's address into the array
|
|
IntPtr itemAddress = new IntPtr(newArrayAddress.ToInt64() + itemIndex * itemSize);
|
|
Marshal.StructureToPtr(newItemAddress, itemAddress, false);
|
|
}
|
|
else
|
|
{
|
|
// Copy the data straight into memory
|
|
IntPtr itemAddress = new IntPtr(newArrayAddress.ToInt64() + itemIndex * itemSize);
|
|
Marshal.StructureToPtr(item, itemAddress, false);
|
|
}
|
|
}
|
|
|
|
return newArrayAddress;
|
|
}
|
|
|
|
private static void RemoveAllocation(ref IntPtr address)
|
|
{
|
|
if (address == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Allocation allocation;
|
|
lock (s_Allocations)
|
|
{
|
|
if (!s_Allocations.TryGetValue(address, out allocation))
|
|
{
|
|
return;
|
|
}
|
|
|
|
s_Allocations.Remove(address);
|
|
}
|
|
|
|
// If the allocation is an array, dispose and release its items as needbe.
|
|
if (allocation.IsArrayItemAllocated.HasValue)
|
|
{
|
|
int itemSize;
|
|
if (allocation.IsArrayItemAllocated.Value)
|
|
{
|
|
itemSize = Marshal.SizeOf(typeof(IntPtr));
|
|
}
|
|
else
|
|
{
|
|
itemSize = Marshal.SizeOf(allocation.Cache.GetType().GetElementType());
|
|
}
|
|
|
|
var array = allocation.Cache as Array;
|
|
for (int itemIndex = 0; itemIndex < array.Length; ++itemIndex)
|
|
{
|
|
if (allocation.IsArrayItemAllocated.Value)
|
|
{
|
|
var itemAddress = new IntPtr(address.ToInt64() + itemIndex * itemSize);
|
|
itemAddress = Marshal.ReadIntPtr(itemAddress);
|
|
Dispose(ref itemAddress);
|
|
}
|
|
else
|
|
{
|
|
var item = array.GetValue(itemIndex);
|
|
if (item is IDisposable)
|
|
{
|
|
var disposable = item as IDisposable;
|
|
if (disposable != null)
|
|
{
|
|
disposable.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allocation.Cache is IDisposable)
|
|
{
|
|
var disposable = allocation.Cache as IDisposable;
|
|
if (disposable != null)
|
|
{
|
|
disposable.Dispose();
|
|
}
|
|
}
|
|
|
|
Marshal.FreeHGlobal(address);
|
|
address = IntPtr.Zero;
|
|
}
|
|
|
|
private static bool TryGetAllocationCache(IntPtr address, out object cache)
|
|
{
|
|
cache = null;
|
|
|
|
lock (s_Allocations)
|
|
{
|
|
Allocation allocation;
|
|
if (s_Allocations.TryGetValue(address, out allocation))
|
|
{
|
|
cache = allocation.Cache;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static IntPtr AddPinnedBuffer(Utf8String str)
|
|
{
|
|
if (str == null || str.Bytes == null)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
lock (s_PinnedBuffers)
|
|
{
|
|
GCHandle pinnedBuffer = GCHandle.Alloc(str.Bytes, GCHandleType.Pinned);
|
|
IntPtr address = Marshal.UnsafeAddrOfPinnedArrayElement(str.Bytes, 0);
|
|
s_PinnedBuffers.Add(address, pinnedBuffer);
|
|
return address;
|
|
}
|
|
}
|
|
|
|
internal static IntPtr AddPinnedBuffer(ArraySegment<byte> array)
|
|
{
|
|
if (array == null)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
lock (s_PinnedBuffers)
|
|
{
|
|
GCHandle pinnedBuffer = GCHandle.Alloc(array.Array, GCHandleType.Pinned);
|
|
IntPtr address = Marshal.UnsafeAddrOfPinnedArrayElement(array.Array, array.Offset);
|
|
s_PinnedBuffers.Add(address, pinnedBuffer);
|
|
return address;
|
|
}
|
|
}
|
|
|
|
private static void RemovePinnedBuffer(ref IntPtr address)
|
|
{
|
|
if (address == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (s_PinnedBuffers)
|
|
{
|
|
GCHandle pinnedBuffer;
|
|
if (s_PinnedBuffers.TryGetValue(address, out pinnedBuffer))
|
|
{
|
|
pinnedBuffer.Free();
|
|
s_PinnedBuffers.Remove(address);
|
|
address = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|