using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32.Foundation;
namespace Dalamud;
///
/// Class facilitating safe memory access.
///
///
/// Attention! The performance of these methods is severely worse than regular calls.
/// Please consider using those instead in performance-critical code.
///
public static class SafeMemory
{
private static readonly HANDLE Handle;
static SafeMemory()
{
Handle = Windows.Win32.PInvoke.GetCurrentProcess();
}
///
/// Read a byte array from the current process.
///
/// The address to read from.
/// The amount of bytes to read.
/// The result buffer.
/// Whether the read succeeded.
public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer)
{
if (Handle.IsNull)
{
buffer = [];
return false;
}
buffer = new byte[count <= 0 ? 0 : count];
fixed (byte* p = buffer)
{
UIntPtr bytesRead;
if (!Windows.Win32.PInvoke.ReadProcessMemory(
Handle,
address.ToPointer(),
p,
new UIntPtr((uint)count),
&bytesRead))
{
return false;
}
}
return true;
}
///
/// Write a byte array to the current process.
///
/// The address to write to.
/// The buffer to write.
/// Whether the write succeeded.
public static unsafe bool WriteBytes(IntPtr address, byte[] buffer)
{
if (Handle.IsNull)
return false;
if (buffer.Length == 0)
return true;
UIntPtr bytesWritten;
fixed (byte* p = buffer)
{
if (!Windows.Win32.PInvoke.WriteProcessMemory(
Handle,
address.ToPointer(),
p,
new UIntPtr((uint)buffer.Length),
&bytesWritten))
{
return false;
}
}
return true;
}
///
/// Read an object of the specified struct from the current process.
///
/// The type of the struct.
/// The address to read from.
/// The resulting object.
/// Whether the read succeeded.
public static bool Read(IntPtr address, out T result) where T : struct
{
if (!ReadBytes(address, SizeCache.Size, out var buffer))
{
result = default;
return false;
}
using var mem = new LocalMemory(buffer.Length);
mem.Write(buffer);
result = mem.Read();
return true;
}
///
/// Read an array of objects of the specified struct from the current process.
///
/// The type of the struct.
/// The address to read from.
/// The length of the array.
/// An array of the read objects, or null if any entry of the array failed to read.
public static T[]? Read(IntPtr address, int count) where T : struct
{
var size = SizeOf();
if (!ReadBytes(address, count * size, out var buffer))
return null;
var result = new T[count];
using var mem = new LocalMemory(buffer.Length);
mem.Write(buffer);
for (var i = 0; i < result.Length; i++)
result[i] = mem.Read(i * size);
return result;
}
///
/// Write a struct to the current process.
///
/// The type of the struct.
/// The address to write to.
/// The object to write.
/// Whether the write succeeded.
public static bool Write(IntPtr address, T obj) where T : struct
{
using var mem = new LocalMemory(SizeCache.Size);
mem.Write(obj);
return WriteBytes(address, mem.Read());
}
///
/// Write an array of structs to the current process.
///
/// The type of the structs.
/// The address to write to.
/// The array to write.
/// Whether the write succeeded.
public static bool Write(IntPtr address, T[] objArray) where T : struct
{
if (objArray == null || objArray.Length == 0)
return true;
var size = SizeCache.Size;
using var mem = new LocalMemory(objArray.Length * size);
for (var i = 0; i < objArray.Length; i++)
mem.Write(objArray[i], i * size);
return WriteBytes(address, mem.Read());
}
///
/// Read a string from the current process(UTF-8).
///
///
/// Attention! This will use the .NET Encoding.UTF8 class to decode the text.
/// If you read a FFXIV string, please use ReadBytes and parse the string with the applicable class,
/// since Encoding.UTF8 destroys the FFXIV payload structure.
///
/// The address to read from.
/// The maximum length of the string.
/// The read string, or null in case the read was not successful.
public static string? ReadString(IntPtr address, int maxLength = 256)
{
return ReadString(address, Encoding.UTF8, maxLength);
}
///
/// Read a string from the current process(UTF-8).
///
///
/// Attention! This will use the .NET Encoding class to decode the text.
/// If you read a FFXIV string, please use ReadBytes and parse the string with the applicable class,
/// since Encoding may destroy the FFXIV payload structure.
///
/// The address to read from.
/// The encoding to use to decode the string.
/// The maximum length of the string.
/// The read string, or null in case the read was not successful.
public static string? ReadString(IntPtr address, Encoding encoding, int maxLength = 256)
{
if (!ReadBytes(address, maxLength, out var buffer))
return null;
var data = encoding.GetString(buffer);
var eosPos = data.IndexOf('\0');
return eosPos == -1 ? data : data[..eosPos];
}
///
/// Write a string to the current process.
///
///
/// Attention! This will use the .NET Encoding class to encode the text.
/// If you read a FFXIV string, please use WriteBytes with the applicable encoded SeString,
/// since Encoding may destroy the FFXIV payload structure.
///
/// The address to write to.
/// The string to write.
/// Whether the write succeeded.
public static bool WriteString(IntPtr address, string str)
{
return WriteString(address, str, Encoding.UTF8);
}
///
/// Write a string to the current process.
///
///
/// Attention! This will use the .NET Encoding class to encode the text.
/// If you read a FFXIV string, please use WriteBytes with the applicable encoded SeString,
/// since Encoding may destroy the FFXIV payload structure.
///
/// The address to write to.
/// The string to write.
/// The encoding to use.
/// Whether the write succeeded.
public static bool WriteString(IntPtr address, string str, Encoding encoding)
{
if (string.IsNullOrEmpty(str))
return true;
return WriteBytes(address, encoding.GetBytes(str + "\0"));
}
///
/// Marshals data from an unmanaged block of memory to a managed object.
///
/// The type to create.
/// The address to read from.
/// The read object, or null, if it could not be read.
public static T? PtrToStructure(IntPtr addr) where T : struct => (T?)PtrToStructure(addr, typeof(T));
///
/// Marshals data from an unmanaged block of memory to a managed object.
///
/// The address to read from.
/// The type to create.
/// The read object, or null, if it could not be read.
public static object? PtrToStructure(IntPtr addr, Type type)
{
var size = Marshal.SizeOf(type);
if (!ReadBytes(addr, size, out var buffer))
return null;
var mem = new LocalMemory(size);
mem.Write(buffer);
return mem.Read(type);
}
///
/// Get the size of the passed type.
///
/// The type to inspect.
/// The size of the passed type.
public static int SizeOf() where T : struct
{
return SizeCache.Size;
}
private static class SizeCache
{
// ReSharper disable once StaticMemberInGenericType
public static readonly int Size;
static SizeCache()
{
var type = typeof(T);
if (type.IsEnum)
type = type.GetEnumUnderlyingType();
Size = Type.GetTypeCode(type) == TypeCode.Boolean ? 1 : Marshal.SizeOf(type);
}
}
private sealed class LocalMemory : IDisposable
{
private readonly int size;
private IntPtr hGlobal;
public LocalMemory(int size)
{
this.size = size;
this.hGlobal = Marshal.AllocHGlobal(this.size);
}
~LocalMemory() => this.Dispose();
public byte[] Read()
{
var bytes = new byte[this.size];
Marshal.Copy(this.hGlobal, bytes, 0, this.size);
return bytes;
}
public T Read(int offset = 0) => Marshal.PtrToStructure(this.hGlobal + offset);
public object? Read(Type type, int offset = 0) => Marshal.PtrToStructure(this.hGlobal + offset, type);
public void Write(byte[] data, int index = 0) => Marshal.Copy(data, index, this.hGlobal, this.size);
public void Write(T data, int offset = 0) => Marshal.StructureToPtr(data, this.hGlobal + offset, false);
public void Dispose()
{
Marshal.FreeHGlobal(this.hGlobal);
this.hGlobal = IntPtr.Zero;
GC.SuppressFinalize(this);
}
}
}