C
C#4d ago
ero

Versioned API client architecture

Unfortunately the IPC implementation I made a post about a while back was completely useless as there is no IL2CPP API to get a class' static field data. So now I'm falling back to manual memory interop. For this, I need to handle many different versions of both Mono and IL2CPP. I need to expose methods such as these;
nint GetMonoImage(string name);
nint GetMonoClass(nint pImage, uint typeToken);
nint GetMonoClass(nint pImage, string @namespace, string name);
nint GetMonoClassField(nint pClass, string name);
string GetFieldName(nint pField);
uint GetFieldOffset(nint pField);
nint GetFieldType(nint pField);
... (many more)
nint GetMonoImage(string name);
nint GetMonoClass(nint pImage, uint typeToken);
nint GetMonoClass(nint pImage, string @namespace, string name);
nint GetMonoClassField(nint pClass, string name);
string GetFieldName(nint pField);
uint GetFieldOffset(nint pField);
nint GetFieldType(nint pField);
... (many more)
The implementation of these methods and internal details (like linked lists or hashmaps) frequently changes between the (~8) versions. The crux of the problem is that working with this API requires some initialization, which is also different between all versions. I need a solution where I infer some basic information (Mono or IL2CPP), then run the code which fetches the initial data (some addresses, tokens) and finally return the correct class for the corresponding version. I need to be very clear that I'm using .NET Standard 2.0; I don't have access to static abstract. My initial naive idea was this:
interface IMonoApiInitializer; // like 2-3 methods
interface IIl2CppApiInitializer : IMonoApiInitializer; // maybe 1 or 2 more methods

class MonoInitializer10 // 1.0
: IMonoApiInitializer;

class MonoInitializer11 // 1.1
: MonoInitializer10;

class Il2CppInitializer180 // 18.0
: IIl2CppApiInitializer;

class Il2CppInitializer240 // 24.0
: Il2CppInitializer180;
interface IMonoApiInitializer; // like 2-3 methods
interface IIl2CppApiInitializer : IMonoApiInitializer; // maybe 1 or 2 more methods

class MonoInitializer10 // 1.0
: IMonoApiInitializer;

class MonoInitializer11 // 1.1
: MonoInitializer10;

class Il2CppInitializer180 // 18.0
: IIl2CppApiInitializer;

class Il2CppInitializer240 // 24.0
: Il2CppInitializer180;
interface IMonoInteroperator; // like 30-ish methods?

class MonoInterop10 // implements methods
: IMonoInteroperator;

class MonoInitializer11 // overrides maybe 2 methods
: MonoInitializer10;

class Il2CppInitializer180 // overrides something internal
: MonoInitializer11; // <- !!!

class Il2CppInitializer240 // ...
: Il2CppInitializer180;
interface IMonoInteroperator; // like 30-ish methods?

class MonoInterop10 // implements methods
: IMonoInteroperator;

class MonoInitializer11 // overrides maybe 2 methods
: MonoInitializer10;

class Il2CppInitializer180 // overrides something internal
: MonoInitializer11; // <- !!!

class Il2CppInitializer240 // ...
: Il2CppInitializer180;
7 Replies
ero
eroOP4d ago
cc @canton7 @goose @JohnStoober And then I need some factory class which can do something like this;
IMonoInteroperator Initialize(Process game)
{
if (game.TryGetModule("mono.dll", out var m1))
{
return GetVersion(m1) switch // no idea what GetVersion will look like lol
{
RuntimeVersion.Mono10 => new MonoInitializer10(m1).Initialize(), // again, can't use static abstract
RuntimeVersion.Mono11 => new MonoInitializer11(m1).Initialize(),
_ => throw null!
};
}
else if (game.TryGetModule("mono-2.0-bdwgc.dll", out var m2))
{
return GetVersion(m2) switch
{
RuntimeVersion.Mono20 => new MonoInitializer20(m2).Initialize(),
RuntimeVersion.Mono21 => new MonoInitializer21(m2).Initialize(),
_ => throw null!
};
}
else if (game.TryGetModule("GameAssembly.dll", out var ga)
|| game.TryGetModule("GameAssembly_plus.dll", out ga))
{
return GetVersion(ga) switch
{
RuntimeVersion.Il2Cpp180 => new Il2CppInitializer180(ga).Initialize(),
RuntimeVersion.Il2Cpp240 => new Il2CppInitializer240(ga).Initialize(),
_ => throw null!
};
}

throw null!;
}
IMonoInteroperator Initialize(Process game)
{
if (game.TryGetModule("mono.dll", out var m1))
{
return GetVersion(m1) switch // no idea what GetVersion will look like lol
{
RuntimeVersion.Mono10 => new MonoInitializer10(m1).Initialize(), // again, can't use static abstract
RuntimeVersion.Mono11 => new MonoInitializer11(m1).Initialize(),
_ => throw null!
};
}
else if (game.TryGetModule("mono-2.0-bdwgc.dll", out var m2))
{
return GetVersion(m2) switch
{
RuntimeVersion.Mono20 => new MonoInitializer20(m2).Initialize(),
RuntimeVersion.Mono21 => new MonoInitializer21(m2).Initialize(),
_ => throw null!
};
}
else if (game.TryGetModule("GameAssembly.dll", out var ga)
|| game.TryGetModule("GameAssembly_plus.dll", out ga))
{
return GetVersion(ga) switch
{
RuntimeVersion.Il2Cpp180 => new Il2CppInitializer180(ga).Initialize(),
RuntimeVersion.Il2Cpp240 => new Il2CppInitializer240(ga).Initialize(),
_ => throw null!
};
}

throw null!;
}
But, all of this might be redesigned completely as well, let me explain how this will normally be used If you look at the initial example methods I gave, you can see that you might be able to order them by what they belong to instead (MonoImage, MonoClass, MonoClassField, MonoType, ...). Eventually, I of course wanna have types that the user can have; my own copies of MonoImage, MonoClass, MonoClassField. I guess a first question here: Because ReadProcessMemory calls are somewhat expensive, I wanted to make the properties (like MonoClassField.Offset) lazy, calling the IMonoInteroperator.GetFieldName method and caching the result. Would you do this? Or would you make the types immutable and get all the info associated with it at request (can become very, very recursive!!!)? I'm thinking I could technically have a IMonoImageInterop or something for every type. But that would become very annoying with all the versions, I'm thinking?
canton7
canton74d ago
Maybe I'm missing something, but there doesn't look like there's anything too impossible here?
Because ReadProcessMemory calls are somewhat expensive, I wanted to make the properties (like MonoClassField.Offset) lazy, calling the IMonoInteroperator.GetFieldName method and caching the result. Would you do this? Or would you make the types immutable and get all the info associated with it at request (can become very, very recursive!!!)?
Why not do the caching in the IMonoInteroperator? Then your MonoClassField.Offset just calls IMonoInteroperator.GetClassFieldOffset(this....). That way if you have multiple MonoClassField instances for the same field, they all benefit from the cache (So your MonoClass etc classes are just very lightweight, version-independent wrappers around an IMonoInteroperator)
ero
eroOP4d ago
well a certain field can only ever be in one specific class like, two classes can't share a field just like a class can't be in 2 assemblies (images)
canton7
canton73d ago
I think you misunderstood
ero
eroOP3d ago
It was just in response to "if you have multiple MonoClassField instances for the same field" Which can't be a thing
canton7
canton73d ago
Can't you request info on the same field twice? Or does the IMonoInteroperator cache those instances?
ero
eroOP3d ago
well sure, but i was really just thinking of something like this
public int Offset => field ??= this._interop.GetMonoField(this.Address);
public int Offset => field ??= this._interop.GetMonoField(this.Address);

Did you find this page helpful?