// 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)) { } } /// /// A helper class that manages memory in the wrapper. /// 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 s_Allocations = new Dictionary(); private static Dictionary s_PinnedBuffers = new Dictionary(); private static Dictionary s_Callbacks = new Dictionary(); private static Dictionary s_StaticCallbacks = new Dictionary(); private static long s_LastClientDataId = 0; private static Dictionary s_ClientDatas = new Dictionary(); /// /// 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. /// /// The number of unmanaged allocations currently active within the wrapper. 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 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(ref TDisposable disposable) where TDisposable : IDisposable { if (disposable != null) { disposable.Dispose(); } } internal static void Dispose(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() { return default(T); } private static void GetAllocation(IntPtr source, out T target) { target = GetDefault(); 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(IntPtr source, out T? target) where T : struct { target = GetDefault(); 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(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 items = new List(); 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(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 items = new List(); 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(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(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[] 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 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; } } } } }