Dalamud/Dalamud/Memory/MemoryHelper.cs
2025-09-11 00:39:23 -07:00

1068 lines
47 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory.Exceptions;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using Microsoft.Extensions.ObjectPool;
using Windows.Win32.Foundation;
using Windows.Win32.System.Memory;
// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
namespace Dalamud.Memory;
/// <summary>
/// A simple class that provides read/write access to arbitrary memory.
/// </summary>
public static unsafe class MemoryHelper
{
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
ObjectPool.Create(new StringBuilderPooledObjectPolicy());
private static readonly HANDLE ThisProcessPseudoHandle = new(unchecked((nint)0xFFFFFFFF));
#region Cast
/// <summary>Casts the given memory address as the reference to the live object.</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The reference to the live object.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T Cast<T>(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress;
/// <summary>Casts the given memory address as the span of the live object(s).</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <param name="length">The number of items.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The span containing reference to the live object(s).</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> Cast<T>(nint memoryAddress, int length) where T : unmanaged =>
new((void*)memoryAddress, length);
/// <summary>Casts the given memory address as the span of the live object(s), until it encounters a zero.</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <param name="maxLength">The maximum number of items.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The span containing reference to the live object(s).</returns>
/// <remarks>If <typeparamref name="T"/> is <c>byte</c> or <c>char</c> and <paramref name="maxLength"/> is not
/// specified, consider using <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(byte*)"/> or
/// <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(char*)"/>.</remarks>
public static Span<T> CastNullTerminated<T>(nint memoryAddress, int maxLength = int.MaxValue)
where T : unmanaged, IEquatable<T>
{
var typedPointer = (T*)memoryAddress;
var length = 0;
while (length < maxLength && !default(T).Equals(*typedPointer++))
length++;
return new((void*)memoryAddress, length);
}
#endregion
#region Read
/// <summary>
/// Reads a generic type from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in struct.</returns>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Read<T>(nint memoryAddress) where T : unmanaged
=> Read<T>(memoryAddress, false);
/// <summary>
/// Reads a generic type from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <returns>The read in struct.</returns>
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Read<T>(nint memoryAddress, bool marshal) =>
marshal
? Marshal.PtrToStructure<T>(memoryAddress)
: Unsafe.Read<T>((void*)memoryAddress);
/// <summary>
/// Reads a byte array from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <returns>The read in byte array.</returns>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadRaw(nint memoryAddress, int length) => Cast<byte>(memoryAddress, length).ToArray();
/// <summary>
/// Reads a generic type array from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <returns>The read in struct array.</returns>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] Read<T>(nint memoryAddress, int arrayLength) where T : unmanaged
=> Cast<T>(memoryAddress, arrayLength).ToArray();
/// <summary>
/// Reads a generic type array from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <returns>The read in struct array.</returns>
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
public static T[] Read<T>(nint memoryAddress, int arrayLength, bool marshal)
{
var structSize = SizeOf<T>(marshal);
var value = new T[arrayLength];
for (var i = 0; i < arrayLength; i++)
{
Read(memoryAddress, out T result, marshal);
value[i] = result;
memoryAddress += structSize;
}
return value;
}
/// <summary>
/// Reads a null-terminated byte array from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in byte array.</returns>
/// <remarks>If you do not need to make a copy, use <see cref="CastNullTerminated{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadRawNullTerminated(nint memoryAddress) =>
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray();
#endregion
#region Read(out)
/// <summary>
/// Reads a generic type from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">Local variable to receive the read in struct.</param>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, out T value) where T : unmanaged
=> value = Read<T>(memoryAddress);
/// <summary>
/// Reads a generic type from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">Local variable to receive the read in struct.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, out T value, bool marshal)
=> value = Read<T>(memoryAddress, marshal);
/// <summary>
/// Reads raw data from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <param name="value">Local variable to receive the read in bytes.</param>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadRaw(nint memoryAddress, int length, out byte[] value)
=> value = ReadRaw(memoryAddress, length);
/// <summary>
/// Reads a generic type array from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="value">The read in struct array.</param>
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged
=> value = Read<T>(memoryAddress, arrayLength);
/// <summary>
/// Reads a generic type array from a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <param name="value">The read in struct array.</param>
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, int arrayLength, bool marshal, out T[] value)
=> value = Read<T>(memoryAddress, arrayLength, marshal);
#endregion
#region ReadString
/// <summary>
/// Compares if the given char span equals to the null-terminated string at <paramref name="memoryAddress"/>.
/// </summary>
/// <param name="charSpan">The character span.</param>
/// <param name="memoryAddress">The address of null-terminated string.</param>
/// <param name="encoding">The encoding of the null-terminated string.</param>
/// <param name="maxLength">The maximum length of the null-terminated string.</param>
/// <returns>Whether they are equal.</returns>
public static bool EqualsZeroTerminatedString(
ReadOnlySpan<char> charSpan,
nint memoryAddress,
Encoding? encoding = null,
int maxLength = int.MaxValue)
{
encoding ??= Encoding.UTF8;
maxLength = Math.Min(maxLength, charSpan.Length + 4);
var pmem = ((byte*)memoryAddress)!;
var length = 0;
while (length < maxLength && pmem[length] != 0)
length++;
var mem = new Span<byte>(pmem, length);
var memCharCount = encoding.GetCharCount(mem);
if (memCharCount != charSpan.Length)
return false;
if (memCharCount < 1024)
{
Span<char> chars = stackalloc char[memCharCount];
encoding.GetChars(mem, chars);
return charSpan.SequenceEqual(chars);
}
else
{
var rented = ArrayPool<char>.Shared.Rent(memCharCount);
var chars = rented.AsSpan(0, memCharCount);
encoding.GetChars(mem, chars);
var equals = charSpan.SequenceEqual(chars);
ArrayPool<char>.Shared.Return(rented);
return equals;
}
}
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ReadStringNullTerminated(nint memoryAddress)
=> Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
/// <summary>
/// Read a string with the given encoding from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <returns>The read in string.</returns>
public static string ReadStringNullTerminated(nint memoryAddress, Encoding encoding)
{
switch (encoding)
{
case UTF8Encoding:
case var _ when encoding.IsSingleByte:
return encoding.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
case UnicodeEncoding:
// Note that it may be in little or big endian, so using `new string(...)` is not always correct.
return encoding.GetString(
MemoryMarshal.Cast<char, byte>(
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress)));
case UTF32Encoding:
return encoding.GetString(MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress)));
default:
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
// non-null character, then this branch can be merged with UTF8Encoding one.
return encoding.GetString(ReadRawNullTerminated(memoryAddress));
}
}
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum number of bytes to read.
/// Note that this is NOT the maximum length of the returned string.</param>
/// <returns>The read in string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ReadString(nint memoryAddress, int maxLength)
=> Encoding.UTF8.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
/// <summary>
/// Read a string with the given encoding from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="maxLength">The maximum number of bytes to read.
/// Note that this is NOT the maximum length of the returned string.</param>
/// <returns>The read in string.</returns>
public static string ReadString(nint memoryAddress, Encoding encoding, int maxLength)
{
if (maxLength <= 0)
return string.Empty;
switch (encoding)
{
case UTF8Encoding:
case var _ when encoding.IsSingleByte:
return encoding.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
case UnicodeEncoding:
return encoding.GetString(
MemoryMarshal.Cast<char, byte>(CastNullTerminated<char>(memoryAddress, maxLength / 2)));
case UTF32Encoding:
return encoding.GetString(
MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress, maxLength / 4)));
default:
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
// non-null character, then this branch can be merged with UTF8Encoding one.
var data = encoding.GetString(Cast<byte>(memoryAddress, maxLength));
var eosPos = data.IndexOf('\0');
return eosPos >= 0 ? data[..eosPos] : data;
}
}
/// <summary>
/// Read a null-terminated SeString from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SeString ReadSeStringNullTerminated(nint memoryAddress) =>
SeString.Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
/// <summary>
/// Read an SeString from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <returns>The read in string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SeString ReadSeString(nint memoryAddress, int maxLength) =>
// Note that a valid SeString never contains a null character, other than for the sequence terminator purpose.
SeString.Parse(CastNullTerminated<byte>(memoryAddress, maxLength));
/// <summary>
/// Read an SeString from a specified Utf8String structure.
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <returns>The read in string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("CS types in Dalamud are deprecated.")]
[Api14ToDo(Api14ToDoAttribute.Remove)]
public static SeString ReadSeString(Utf8String* utf8String) =>
utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan());
/// <summary>
/// Reads an SeString from a specified memory address, and extracts the outermost string.<br />
/// If the SeString is malformed, behavior is undefined.
/// </summary>
/// <param name="containsNonRepresentedPayload">Whether the SeString contained a non-represented payload.</param>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="stopOnFirstNonRepresentedPayload">Stop reading on encountering the first non-represented payload.
/// What payloads are represented via this function may change.</param>
/// <param name="nonRepresentedPayloadReplacement">Replacement for non-represented payloads.</param>
/// <returns>The read in string.</returns>
public static string ReadSeStringAsString(
out bool containsNonRepresentedPayload,
nint memoryAddress,
int maxLength = int.MaxValue,
bool stopOnFirstNonRepresentedPayload = false,
string nonRepresentedPayloadReplacement = "*")
{
var sb = StringBuilderPool.Get();
sb.EnsureCapacity(maxLength = CastNullTerminated<byte>(memoryAddress, maxLength).Length);
// 1 utf-8 codepoint can spill up to 2 characters.
Span<char> tmp = stackalloc char[2];
var pin = (byte*)memoryAddress;
containsNonRepresentedPayload = false;
while (*pin != 0 && maxLength > 0)
{
if (*pin != ReadOnlySeString.Stx)
{
var len = *pin switch
{
< 0x80 => 1,
>= 0b11000000 and <= 0b11011111 => 2,
>= 0b11100000 and <= 0b11101111 => 3,
>= 0b11110000 and <= 0b11110111 => 4,
_ => 0,
};
if (len == 0 || len > maxLength)
break;
var numChars = Encoding.UTF8.GetChars(new(pin, len), tmp);
sb.Append(tmp[..numChars]);
pin += len;
maxLength -= len;
continue;
}
// Start byte
++pin;
--maxLength;
// Payload type
var payloadType = (MacroCode)(*pin++);
// Payload length
if (!ReadIntExpression(ref pin, ref maxLength, out var expressionLength))
break;
if (expressionLength > maxLength)
break;
pin += expressionLength;
maxLength -= unchecked((int)expressionLength);
// End byte
if (*pin++ != ReadOnlySeString.Etx)
break;
--maxLength;
switch (payloadType)
{
case MacroCode.NewLine:
sb.AppendLine();
break;
case MacroCode.Hyphen:
sb.Append('');
break;
case MacroCode.SoftHyphen:
sb.Append('\u00AD');
break;
default:
sb.Append(nonRepresentedPayloadReplacement);
containsNonRepresentedPayload = true;
if (stopOnFirstNonRepresentedPayload)
maxLength = 0;
break;
}
}
var res = sb.ToString();
StringBuilderPool.Return(sb);
return res;
static bool ReadIntExpression(ref byte* p, ref int maxLength, out uint value)
{
if (maxLength <= 0)
{
value = 0;
return false;
}
var typeByte = *p++;
--maxLength;
switch (typeByte)
{
case > 0 and < 0xD0:
value = (uint)typeByte - 1;
return true;
case >= 0xF0 and <= 0xFE:
++typeByte;
value = 0u;
if ((typeByte & 8) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 24;
}
if ((typeByte & 4) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 16;
}
if ((typeByte & 2) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 8;
}
if ((typeByte & 1) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= *p++;
}
return true;
default:
value = 0;
return false;
}
}
}
#endregion
#region ReadString(out)
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadStringNullTerminated(nint memoryAddress, out string value)
=> value = ReadStringNullTerminated(memoryAddress);
/// <summary>
/// Read a string with the given encoding from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="value">The read in string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadStringNullTerminated(nint memoryAddress, Encoding encoding, out string value)
=> value = ReadStringNullTerminated(memoryAddress, encoding);
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
/// <param name="maxLength">The maximum length of the string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadString(nint memoryAddress, out string value, int maxLength)
=> value = ReadString(memoryAddress, maxLength);
/// <summary>
/// Read a string with the given encoding from a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="value">The read in string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadString(nint memoryAddress, Encoding encoding, int maxLength, out string value)
=> value = ReadString(memoryAddress, encoding, maxLength);
/// <summary>
/// Read a null-terminated SeString from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in SeString.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value)
=> value = ReadSeStringNullTerminated(memoryAddress);
/// <summary>
/// Read an SeString from a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="value">The read in SeString.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadSeString(nint memoryAddress, int maxLength, out SeString value)
=> value = ReadSeString(memoryAddress, maxLength);
/// <summary>
/// Read an SeString from a specified Utf8String structure.
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("CS types in Dalamud are deprecated.")]
[Api14ToDo(Api14ToDoAttribute.Remove)]
public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
=> value = ReadSeString(utf8String);
#endregion
#region Write
/// <summary>
/// Writes a generic type to a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="item">The item to write to the address.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T item) where T : unmanaged
=> Write(memoryAddress, item, false);
/// <summary>
/// Writes a generic type to a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="item">The item to write to the address.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
public static void Write<T>(nint memoryAddress, T item, bool marshal)
{
if (marshal)
Marshal.StructureToPtr(item, memoryAddress, false);
else
Unsafe.Write((void*)memoryAddress, item);
}
/// <summary>
/// Writes raw data to a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="data">The bytes to write to memoryAddress.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteRaw(nint memoryAddress, byte[] data) => Marshal.Copy(data, 0, memoryAddress, data.Length);
/// <summary>
/// Writes a generic type array to a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="items">The array of items to write to the address.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T[] items) where T : unmanaged
=> Write(memoryAddress, items, false);
/// <summary>
/// Writes a generic type array to a specified memory address.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="items">The array of items to write to the address.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T[] items, bool marshal)
{
var structSize = SizeOf<T>(marshal);
for (var i = 0; i < items.Length; i++)
{
var address = memoryAddress + (structSize * i);
Write(address, items[i], marshal);
}
}
#endregion
#region WriteString
/// <summary>
/// Write a UTF-8 encoded string to a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The string to write.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteString(nint memoryAddress, string? value)
=> WriteString(memoryAddress, value, Encoding.UTF8);
/// <summary>
/// Write a string with the given encoding to a specified memory address.
/// </summary>
/// <remarks>
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The string to write.</param>
/// <param name="encoding">The encoding to use.</param>
public static void WriteString(nint memoryAddress, string? value, Encoding encoding)
{
var ptr = 0;
if (value is not null)
ptr = encoding.GetBytes(value, Cast<byte>(memoryAddress, encoding.GetMaxByteCount(value.Length)));
encoding.GetBytes("\0", Cast<byte>(memoryAddress + ptr, 4));
}
/// <summary>
/// Write an SeString to a specified memory address.
/// </summary>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The SeString to write.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteSeString(nint memoryAddress, SeString? value)
{
if (value is null)
return;
WriteRaw(memoryAddress, value.Encode());
}
#endregion
#region ApiWrappers
/// <summary>
/// Allocates fixed size of memory inside the target memory source via Windows API calls.
/// Returns the address of newly allocated memory.
/// </summary>
/// <param name="length">Amount of bytes to be allocated.</param>
/// <returns>Address to the newly allocated memory.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Allocate(int length)
{
var address = Windows.Win32.PInvoke.VirtualAlloc(
null,
(nuint)length,
VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT | VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE,
PAGE_PROTECTION_FLAGS.PAGE_EXECUTE_READWRITE);
if (address == null)
throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
return new IntPtr(address);
}
/// <summary>
/// Allocates fixed size of memory inside the target memory source via Windows API calls.
/// Returns the address of newly allocated memory.
/// </summary>
/// <param name="length">Amount of bytes to be allocated.</param>
/// <param name="memoryAddress">Address to the newly allocated memory.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Allocate(int length, out nint memoryAddress)
=> memoryAddress = Allocate(length);
/// <summary>
/// Frees memory previously allocated with Allocate via Windows API calls.
/// </summary>
/// <param name="memoryAddress">The address of the memory to free.</param>
/// <returns>True if the operation is successful.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Free(nint memoryAddress)
{
return Windows.Win32.PInvoke.VirtualFree(memoryAddress.ToPointer(), nuint.Zero, VIRTUAL_FREE_TYPE.MEM_RELEASE);
}
/// <summary>
/// Changes the page permissions for a specified combination of address and length via Windows API calls.
/// </summary>
/// <param name="memoryAddress">The memory address for which to change page permissions for.</param>
/// <param name="length">The region size for which to change permissions for.</param>
/// <param name="newPermissions">The new permissions to set.</param>
/// <returns>The old page permissions.</returns>
public static MemoryProtection ChangePermission(nint memoryAddress, int length, MemoryProtection newPermissions)
{
var result = Windows.Win32.PInvoke.VirtualProtect(
memoryAddress.ToPointer(),
(nuint)length,
(PAGE_PROTECTION_FLAGS)newPermissions,
out var oldPermissions);
if (!result)
throw new MemoryPermissionException($"Unable to change permissions at {Util.DescribeAddress(memoryAddress)} of length {length} and permission {newPermissions} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryPermissionException($"Unable to change permissions at {Util.DescribeAddress(memoryAddress)} of length {length} and permission {newPermissions} (error={last})");
return (MemoryProtection)oldPermissions;
}
/// <summary>
/// Changes the page permissions for a specified combination of address and length via Windows API calls.
/// </summary>
/// <param name="memoryAddress">The memory address for which to change page permissions for.</param>
/// <param name="length">The region size for which to change permissions for.</param>
/// <param name="newPermissions">The new permissions to set.</param>
/// <param name="oldPermissions">The old page permissions.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ChangePermission(
nint memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
=> oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
/// <summary>
/// Changes the page permissions for a specified combination of address and element from which to deduce size via Windows API calls.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address for which to change page permissions for.</param>
/// <param name="baseElement">The struct element from which the region size to change permissions for will be calculated.</param>
/// <param name="newPermissions">The new permissions to set.</param>
/// <param name="marshal">Set to true to calculate the size of the struct after marshalling instead of before.</param>
/// <returns>The old page permissions.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryProtection ChangePermission<T>(
nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
=> ChangePermission(memoryAddress, SizeOf<T>(marshal), newPermissions);
/// <summary>
/// Reads raw data from a specified memory address via Windows API calls.
/// This is noticably slower than Unsafe or Marshal.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <returns>The read in bytes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadProcessMemory(nint memoryAddress, int length)
{
var value = new byte[length];
ReadProcessMemory(memoryAddress, ref value);
return value;
}
/// <summary>
/// Reads raw data from a specified memory address via Windows API calls.
/// This is noticably slower than Unsafe or Marshal.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <param name="value">The read in bytes.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadProcessMemory(nint memoryAddress, int length, out byte[] value)
=> value = ReadProcessMemory(memoryAddress, length);
/// <summary>
/// Reads raw data from a specified memory address via Windows API calls.
/// This is noticably slower than Unsafe or Marshal.
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in bytes.</param>
public static void ReadProcessMemory(nint memoryAddress, ref byte[] value)
{
unchecked
{
var length = value.Length;
fixed (byte* pVal = value)
{
var result = Windows.Win32.PInvoke.ReadProcessMemory(
ThisProcessPseudoHandle,
memoryAddress.ToPointer(),
pVal,
(nuint)length,
null);
if (!result)
throw new MemoryReadException($"Unable to read memory at {Util.DescribeAddress(memoryAddress)} of length {length} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryReadException($"Unable to read memory at {Util.DescribeAddress(memoryAddress)} of length {length} (error={last})");
}
}
}
/// <summary>
/// Writes raw data to a specified memory address via Windows API calls.
/// This is noticably slower than Unsafe or Marshal.
/// </summary>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="data">The bytes to write to memoryAddress.</param>
public static void WriteProcessMemory(nint memoryAddress, byte[] data)
{
unchecked
{
var length = data.Length;
fixed (byte* pData = data)
{
var result = Windows.Win32.PInvoke.WriteProcessMemory(
ThisProcessPseudoHandle,
memoryAddress.ToPointer(),
pData,
(nuint)length,
null);
if (!result)
throw new MemoryWriteException($"Unable to write memory at {Util.DescribeAddress(memoryAddress)} of length {length} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryWriteException($"Unable to write memory at {Util.DescribeAddress(memoryAddress)} of length {length} (error={last})");
}
}
}
#endregion
#region Sizing
/// <summary>
/// Returns the size of a specific primitive or struct type.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <returns>The size of the primitive or struct.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>()
=> SizeOf<T>(false);
/// <summary>
/// Returns the size of a specific primitive or struct type.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
/// <returns>The size of the primitive or struct.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(bool marshal)
=> marshal ? Marshal.SizeOf<T>() : Unsafe.SizeOf<T>();
/// <summary>
/// Returns the size of a specific primitive or struct type.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="elementCount">The number of array elements present.</param>
/// <returns>The size of the primitive or struct array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(int elementCount) where T : unmanaged
=> SizeOf<T>() * elementCount;
/// <summary>
/// Returns the size of a specific primitive or struct type.
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="elementCount">The number of array elements present.</param>
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
/// <returns>The size of the primitive or struct array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(int elementCount, bool marshal)
=> SizeOf<T>(marshal) * elementCount;
#endregion
#region Game
/// <summary>
/// Allocate memory in the game's UI memory space.
/// </summary>
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateUi(ulong size, ulong alignment = 0)
{
return new nint(IMemorySpace.GetUISpace()->Malloc(size, alignment));
}
/// <summary>
/// Allocate memory in the game's default memory space.
/// </summary>
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateDefault(ulong size, ulong alignment = 0)
{
return new nint(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
}
/// <summary>
/// Allocate memory in the game's animation memory space.
/// </summary>
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateAnimation(ulong size, ulong alignment = 0)
{
return new nint(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
}
/// <summary>
/// Allocate memory in the game's apricot memory space.
/// </summary>
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateApricot(ulong size, ulong alignment = 0)
{
return new nint(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
}
/// <summary>
/// Allocate memory in the game's sound memory space.
/// </summary>
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateSound(ulong size, ulong alignment = 0)
{
return new nint(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
}
/// <summary>
/// Free memory in the game's memory space.
/// </summary>
/// <remarks>The memory you are freeing must be allocated with game allocators.</remarks>
/// <param name="ptr">Position at which the memory to be freed is located.</param>
/// <param name="size">Amount of bytes to free.</param>
public static void GameFree(ref nint ptr, ulong size)
{
if (ptr == nint.Zero)
{
return;
}
IMemorySpace.Free((void*)ptr, size);
ptr = nint.Zero;
}
#endregion
#region Utility
/// <summary>
/// Null-terminate a byte array.
/// </summary>
/// <param name="bytes">The byte array to terminate.</param>
/// <returns>The terminated byte array.</returns>
public static byte[] NullTerminate(this byte[] bytes)
{
if (bytes.Length == 0 || bytes[^1] != 0)
{
var newBytes = new byte[bytes.Length + 1];
Array.Copy(bytes, newBytes, bytes.Length);
newBytes[^1] = 0;
return newBytes;
}
return bytes;
}
#endregion
}