Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
namespace CheatEngine.Client.Memory;

/// <summary>Provides the bounded raw-memory operations available to an application read codec.</summary>
/// <remarks>
/// The Client invalidates this context immediately when the codec invocation returns or throws. Codecs must not
/// retain the context; a later member access throws
/// <see cref="CheatEngine.Client.Results.CheatEngineActivationExpiredException" />.
/// </remarks>
public interface IMemoryReadContext
{
/// <summary>Gets the selected target's pointer size in bytes.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
namespace CheatEngine.Client.Memory;

/// <summary>Provides the bounded raw-memory operations available to an application write codec.</summary>
/// <remarks>
/// The Client invalidates this context immediately when the codec invocation returns or throws. Codecs must not
/// retain the context; a later member access throws
/// <see cref="CheatEngine.Client.Results.CheatEngineActivationExpiredException" />.
/// </remarks>
public interface IMemoryWriteContext
{
/// <summary>Gets the selected target's pointer size in bytes.</summary>
Expand Down
61 changes: 61 additions & 0 deletions libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using CheatEngine.Client.Core.Infrastructure;
using CheatEngine.SDK.Engine.Memory;
using CheatEngine.SDK.Engine.Values;

namespace CheatEngine.Client.Core.Domains;

/// <summary>Provides the SDK-backed operations available to a scoped application memory codec.</summary>
/// <remarks>
/// The port is internal so Core tests can prove that an expired context never reaches SDK statics. It is not a
/// replacement public memory abstraction.
/// </remarks>
internal interface IMemoryCodecContextPort
{
public bool IsTarget64Bit();

public bool TryReadBytes(Address address, Span<byte> destination, out string? failure);

public bool TryWriteBytes(Address address, ReadOnlySpan<byte> source, out string? failure);
}

/// <summary>Calls the SDK memory primitives after the owning context has admitted the operation.</summary>
internal sealed class SdkMemoryCodecContextPort : IMemoryCodecContextPort
{
internal static SdkMemoryCodecContextPort Instance
{
get;
} = new();

private SdkMemoryCodecContextPort()
{
}

public bool IsTarget64Bit()
{
return ClientLuaGlobals.TargetIs64Bit();
}

public bool TryReadBytes(Address address, Span<byte> destination, out string? failure)
{
if (TargetMemory.TryReadBytes(address, destination, out MemoryAccessFailure sdkFailure))
{
failure = null;
return true;
}

failure = sdkFailure.ToString();
return false;
}

public bool TryWriteBytes(Address address, ReadOnlySpan<byte> source, out string? failure)
{
if (TargetMemory.TryWriteBytes(address, source, out MemoryAccessFailure sdkFailure))
{
failure = null;
return true;
}

failure = sdkFailure.ToString();
return false;
}
}
129 changes: 104 additions & 25 deletions libs/CheatEngine.Client.Core/Domains/MemoryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,28 @@

namespace CheatEngine.Client.Core.Domains;

internal sealed class MemoryClient(ICheatEngineDispatcher dispatcher) : IMemoryClient
internal sealed class MemoryClient : IMemoryClient
{
private readonly ICheatEngineDispatcher _dispatcher =
dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
private readonly ICheatEngineDispatcher _dispatcher;

private readonly CoreLifetime _lifetime;

private readonly IMemoryCodecContextPort _codecContextPort;

internal MemoryClient(ICheatEngineDispatcher dispatcher, CoreLifetime lifetime)
: this(dispatcher, lifetime, SdkMemoryCodecContextPort.Instance)
{
}

internal MemoryClient(
ICheatEngineDispatcher dispatcher,
CoreLifetime lifetime,
IMemoryCodecContextPort codecContextPort)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
_codecContextPort = codecContextPort ?? throw new ArgumentNullException(nameof(codecContextPort));
}

public bool TryReadPrimitive<T>(Address address, [MaybeNullWhen(false)] out T value,
out CheatEngineFailure failure, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -440,32 +458,46 @@ private bool TryInvoke<TState, TResult>(TState state, Func<TState, TResult> call
return _dispatcher.TryInvoke(() => callback(state), out result, out failure, cancellationToken);
}

private static bool TryReadCore<T>(MemoryReadRequest<T> request, [MaybeNullWhen(false)] out T value,
private bool TryReadCore<T>(MemoryReadRequest<T> request, [MaybeNullWhen(false)] out T value,
out string? failure)
{
failure = null;
TargetMemoryCodecContext context = TargetMemoryCodecContext.Create();
if (request.Codec.TryRead(context, request.Address, out value))
TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort);
try
{
return true;
}
if (request.Codec.TryRead(context, request.Address, out value))
{
return true;
}

failure = context.Failure ?? $"The codec for '{typeof(T).Name}' rejected the target-memory read.";
return false;
failure = context.Failure ?? $"The codec for '{typeof(T).Name}' rejected the target-memory read.";
return false;
}
finally
{
context.Expire();
}
}

private static bool TryWriteCore<T>(MemoryWriteRequest<T> request, out string? failure)
private bool TryWriteCore<T>(MemoryWriteRequest<T> request, out string? failure)
{
failure = null;
TargetMemoryCodecContext context = TargetMemoryCodecContext.Create();
T value = request.Value;
if (request.Codec.TryWrite(context, request.Address, in value))
TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort);
try
{
return true;
}
T value = request.Value;
if (request.Codec.TryWrite(context, request.Address, in value))
{
return true;
}

failure = context.Failure ?? $"The codec for '{typeof(T).Name}' rejected the target-memory write.";
return false;
failure = context.Failure ?? $"The codec for '{typeof(T).Name}' rejected the target-memory write.";
return false;
}
finally
{
context.Expire();
}
}

private static void ValidateBatch<T>(ImmutableArray<T> values, string parameterName)
Expand Down Expand Up @@ -706,8 +738,28 @@ private static PrimitiveWriteOutcome Write<TValue>(Writer<TValue> writer, Addres

private sealed class TargetMemoryCodecContext : IMemoryReadContext, IMemoryWriteContext
{
private const string _operation = "Memory.CodecContext";

private readonly long _activationEpoch;
private readonly ICheatEngineDispatcher _dispatcher;
private readonly CoreLifetime _lifetime;
private readonly IMemoryCodecContextPort _port;
private readonly int _threadId;
private int _expired;
private int _pointerSize;

private TargetMemoryCodecContext(
CoreLifetime lifetime,
ICheatEngineDispatcher dispatcher,
IMemoryCodecContextPort port)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_port = port ?? throw new ArgumentNullException(nameof(port));
_activationEpoch = lifetime.Epoch;
_threadId = Environment.CurrentManagedThreadId;
}

internal string? Failure
{
get;
Expand All @@ -718,44 +770,71 @@ public int PointerSize
{
get
{
ThrowIfUsable();
if (_pointerSize != 0)
{
return _pointerSize;
}

int pointerSize = ClientLuaGlobals.TargetIs64Bit() ? sizeof(ulong) : sizeof(uint);
int pointerSize = _port.IsTarget64Bit() ? sizeof(ulong) : sizeof(uint);
_pointerSize = pointerSize;
return pointerSize;
}
}

public bool TryReadBytes(Address address, Span<byte> destination)
{
if (TargetMemory.TryReadBytes(address, destination, out MemoryAccessFailure sdkFailure))
ThrowIfUsable();
if (_port.TryReadBytes(address, destination, out string? failure))
{
Failure = null;
return true;
}

Failure = sdkFailure.ToString();
Failure = failure;
return false;
}

public bool TryWriteBytes(Address address, ReadOnlySpan<byte> source)
{
if (TargetMemory.TryWriteBytes(address, source, out MemoryAccessFailure sdkFailure))
ThrowIfUsable();
if (_port.TryWriteBytes(address, source, out string? failure))
{
Failure = null;
return true;
}

Failure = sdkFailure.ToString();
Failure = failure;
return false;
}

internal static TargetMemoryCodecContext Create()
internal static TargetMemoryCodecContext Create(
CoreLifetime lifetime,
ICheatEngineDispatcher dispatcher,
IMemoryCodecContextPort port)
{
return new TargetMemoryCodecContext(lifetime, dispatcher, port);
}

internal void Expire()
{
Volatile.Write(ref _expired, 1);
}

private void ThrowIfUsable()
{
return new TargetMemoryCodecContext();
if (Volatile.Read(ref _expired) != 0 ||
_activationEpoch != _lifetime.Epoch ||
!_lifetime.IsActivationCurrent ||
Environment.CurrentManagedThreadId != _threadId ||
!_dispatcher.IsMainThread)
{
throw new CheatEngineActivationExpiredException(
_operation,
"The memory codec context is no longer valid for the current Cheat Engine invocation.");
}

_lifetime.ThrowIfInactive(_operation);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ private static void AddCoreServices(IServiceCollection services)
serviceProvider.GetRequiredService<ProcessClient>());

services.TryAddSingleton<MemoryClient>(static serviceProvider =>
new MemoryClient(serviceProvider.GetRequiredService<SdkMainThreadDispatcher>()));
new MemoryClient(
serviceProvider.GetRequiredService<SdkMainThreadDispatcher>(),
serviceProvider.GetRequiredService<CoreLifetime>()));
services.TryAddSingleton<IMemoryClient>(static serviceProvider =>
serviceProvider.GetRequiredService<MemoryClient>());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;

using CheatEngine.Client.Core.Domains;
using CheatEngine.Client.Core.Tests.TestSupport;
using CheatEngine.Client.Dispatching;
using CheatEngine.Client.Memory;
using CheatEngine.Client.Results;
Expand All @@ -18,7 +19,7 @@ public void TypedReadForwardsCancellationAndReturnsTheCodecValue()
CancellationToken cancellationToken = TestContext.Current.CancellationToken;
RecordingDispatcher dispatcher = new();
ProbeCodec codec = new() { ReadValue = 1337 };
MemoryClient client = new(dispatcher);
MemoryClient client = new(dispatcher, InertCoreLifetime.Create());

bool succeeded = client.TryRead(new MemoryReadRequest<int>(_address, codec), out int value,
out CheatEngineFailure failure, cancellationToken);
Expand All @@ -39,7 +40,7 @@ public void TypedWriteForwardsCancellationAndPreservesTheValueForTheCodec()
CancellationToken cancellationToken = TestContext.Current.CancellationToken;
RecordingDispatcher dispatcher = new();
ProbeCodec codec = new();
MemoryClient client = new(dispatcher);
MemoryClient client = new(dispatcher, InertCoreLifetime.Create());

bool succeeded = client.TryWrite(new MemoryWriteRequest<int>(_address, 42, codec),
out CheatEngineFailure failure, cancellationToken);
Expand All @@ -58,7 +59,7 @@ public void TypedWriteForwardsCancellationAndPreservesTheValueForTheCodec()
public void FailedCodecReadUsesTheFallbackFailureAndTheConvenienceMethodThrowsIt()
{
ProbeCodec codec = new() { ReadSucceeds = false };
MemoryClient client = new(new RecordingDispatcher());
MemoryClient client = new(new RecordingDispatcher(), InertCoreLifetime.Create());
MemoryReadRequest<int> request = new(_address, codec);

bool succeeded = client.TryRead(request, out int value, out CheatEngineFailure failure,
Expand All @@ -79,7 +80,7 @@ public void FailedCodecReadUsesTheFallbackFailureAndTheConvenienceMethodThrowsIt
public void FailedCodecWriteUsesTheFallbackFailureAndTheConvenienceMethodThrowsIt()
{
ProbeCodec codec = new() { WriteSucceeds = false };
MemoryClient client = new(new RecordingDispatcher());
MemoryClient client = new(new RecordingDispatcher(), InertCoreLifetime.Create());
MemoryWriteRequest<int> request = new(_address, 77, codec);

bool succeeded = client.TryWrite(request, out CheatEngineFailure failure,
Expand All @@ -104,7 +105,7 @@ public void CancelledDispatchPreventsCodecExecutionAndPreservesTheCancellationFa
CancellationToken cancellationToken = cancellation.Token;
CancellationAwareDispatcher dispatcher = new();
ProbeCodec codec = new();
MemoryClient client = new(dispatcher);
MemoryClient client = new(dispatcher, InertCoreLifetime.Create());
MemoryReadRequest<int> readRequest = new(_address, codec);
MemoryWriteRequest<int> writeRequest = new(_address, 9, codec);

Expand All @@ -128,7 +129,8 @@ public void CancelledDispatchPreventsCodecExecutionAndPreservesTheCancellationFa
[Fact]
public void ConstructorRejectsANullDispatcher()
{
Assert.Throws<ArgumentNullException>(() => new MemoryClient(null!));
Assert.Throws<ArgumentNullException>(() => new MemoryClient(null!, InertCoreLifetime.Create()));
Assert.Throws<ArgumentNullException>(() => new MemoryClient(new RecordingDispatcher(), null!));
}

private sealed class ProbeCodec : IMemoryCodec<int>
Expand Down
Loading