diff --git a/src/SharpEmu.Libs/Json/JsonExports.cs b/src/SharpEmu.Libs/Json/JsonExports.cs index 8d02202..caacd37 100644 --- a/src/SharpEmu.Libs/Json/JsonExports.cs +++ b/src/SharpEmu.Libs/Json/JsonExports.cs @@ -29,6 +29,18 @@ public static class JsonExports private static readonly ConcurrentDictionary _strings = new(); private static readonly JsonElement _nullElement = CreateNullElement(); + // sce::Json::Array is reached through Value::getArray and walked with an 8-byte iterator. The + // elements are laid out once as consecutive Value objects in guest memory; the Array object + // holds their base and count, and an iterator holds the address of its element, so stepping + // adds ValueObjectSize and dereferencing returns the element itself. + private sealed record JsonArrayState(ulong Elements, int Count); + + private const int ArrayObjectSize = 0x10; + private static readonly ConcurrentDictionary _arrays = new(); + + // The Array built for a parsed value, reused while that value is unchanged. + private static readonly ConcurrentDictionary _valueArrays = new(); + [SysAbiExport( Nid = "-hJRce8wn1U", ExportName = "_ZN3sce4Json12MemAllocatorC2Ev", @@ -598,6 +610,289 @@ public static class JsonExports return 0; } + [SysAbiExport( + Nid = "ONT8As5R1ug", + ExportName = "_ZNK3sce4Json5Value8getArrayEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ValueGetArray(CpuContext ctx) + { + var valueAddress = ctx[CpuRegister.Rdi]; + JsonValueState? source = null; + var hasParsed = !JsonObjectHeap.Values.ContainsKey(valueAddress) && + _values.TryGetValue(valueAddress, out source); + if (hasParsed && + _valueArrays.TryGetValue(valueAddress, out var cached) && + ReferenceEquals(cached.Source, source)) + { + ctx[CpuRegister.Rax] = cached.Array; + return 0; + } + + // A value that is not an array reads as an empty one. + var element = GetValue(valueAddress); + var count = element.ValueKind == System.Text.Json.JsonValueKind.Array ? element.GetArrayLength() : 0; + if (!TryAllocateGuestObject(ctx, ArrayObjectSize + (count * ValueObjectSize), out var arrayAddress)) + { + ctx[CpuRegister.Rax] = 0; + return 0; + } + + var elements = arrayAddress + ArrayObjectSize; + for (var index = 0; index < count; index++) + { + StoreValue(ctx, elements + (ulong)(index * ValueObjectSize), element[index]); + } + + _arrays[arrayAddress] = new JsonArrayState(elements, count); + ctx.TryWriteUInt64(arrayAddress, elements); + ctx.TryWriteUInt64(arrayAddress + 8, (ulong)count); + if (hasParsed) + { + _valueArrays[valueAddress] = (source!, arrayAddress); + } + + ctx[CpuRegister.Rax] = arrayAddress; + TraceJson("Value.getArray", valueAddress, (ulong)count); + return 0; + } + + [SysAbiExport( + Nid = "JP-PtKMiI1E", + ExportName = "_ZN3sce4Json5ArrayC1Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayConstructor(CpuContext ctx) => ConstructArray(ctx); + + [SysAbiExport( + Nid = "t6AHhkWu4mY", + ExportName = "_ZN3sce4Json5ArrayC2Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayBaseConstructor(CpuContext ctx) => ConstructArray(ctx); + + [SysAbiExport( + Nid = "HJ8GpRT1aiw", + ExportName = "_ZN3sce4Json5ArrayD1Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayDestructor(CpuContext ctx) => DestroyArray(ctx); + + [SysAbiExport( + Nid = "MIyMPw7YTGM", + ExportName = "_ZN3sce4Json5ArrayD2Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayBaseDestructor(CpuContext ctx) => DestroyArray(ctx); + + [SysAbiExport( + Nid = "rQGJeNjOuUk", + ExportName = "_ZNK3sce4Json5Array4sizeEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArraySize(CpuContext ctx) + { + ctx[CpuRegister.Rax] = (ulong)GetArray(ctx[CpuRegister.Rdi]).Count; + return 0; + } + + [SysAbiExport( + Nid = "9uP25i6ipno", + ExportName = "_ZNK3sce4Json5Array5emptyEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayEmpty(CpuContext ctx) + { + ctx[CpuRegister.Rax] = GetArray(ctx[CpuRegister.Rdi]).Count == 0 ? 1UL : 0UL; + return 0; + } + + // begin() and end() return the iterator through the hidden result pointer in RDI; the Array + // is in RSI. + [SysAbiExport( + Nid = "bcH5EnFE2xY", + ExportName = "_ZNK3sce4Json5Array5beginEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayBegin(CpuContext ctx) => + ReturnIterator(ctx, GetArray(ctx[CpuRegister.Rsi]).Elements); + + [SysAbiExport( + Nid = "WXF2ihRF+B8", + ExportName = "_ZNK3sce4Json5Array3endEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int ArrayEnd(CpuContext ctx) + { + var array = GetArray(ctx[CpuRegister.Rsi]); + return ReturnIterator(ctx, array.Elements + (ulong)(array.Count * ValueObjectSize)); + } + + [SysAbiExport( + Nid = "TAPTWYaYf8Q", + ExportName = "_ZN3sce4Json5Array8iteratorC1Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorConstructor(CpuContext ctx) => SetIterator(ctx, ctx[CpuRegister.Rdi], 0); + + [SysAbiExport( + Nid = "JSg1MSo7NKE", + ExportName = "_ZN3sce4Json5Array8iteratorC2Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorBaseConstructor(CpuContext ctx) => SetIterator(ctx, ctx[CpuRegister.Rdi], 0); + + [SysAbiExport( + Nid = "IaIvX3kgyb0", + ExportName = "_ZN3sce4Json5Array8iteratorC1ERKS2_", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorCopyConstructor(CpuContext ctx) => CopyIterator(ctx); + + [SysAbiExport( + Nid = "Hdc3DSIsG2I", + ExportName = "_ZN3sce4Json5Array8iteratorC2ERKS2_", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorBaseCopyConstructor(CpuContext ctx) => CopyIterator(ctx); + + [SysAbiExport( + Nid = "T3WReElX4BY", + ExportName = "_ZN3sce4Json5Array8iteratoraSERKS2_", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorAssignment(CpuContext ctx) => CopyIterator(ctx); + + [SysAbiExport( + Nid = "9yLjn46Ypfs", + ExportName = "_ZN3sce4Json5Array8iteratorD1Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorDestructor(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return 0; + } + + [SysAbiExport( + Nid = "9niNrXsF3Bc", + ExportName = "_ZN3sce4Json5Array8iteratorD2Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorBaseDestructor(CpuContext ctx) => IteratorDestructor(ctx); + + [SysAbiExport( + Nid = "5AZPp99ogrc", + ExportName = "_ZNK3sce4Json5Array8iteratorneERKS2_", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorNotEqual(CpuContext ctx) + { + ctx[CpuRegister.Rax] = + ReadIterator(ctx, ctx[CpuRegister.Rdi]) != ReadIterator(ctx, ctx[CpuRegister.Rsi]) ? 1UL : 0UL; + return 0; + } + + [SysAbiExport( + Nid = "wcgr5mte7T8", + ExportName = "_ZNK3sce4Json5Array8iteratordeEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorDereference(CpuContext ctx) + { + ctx[CpuRegister.Rax] = ReadIterator(ctx, ctx[CpuRegister.Rdi]); + return 0; + } + + [SysAbiExport( + Nid = "iAIYn4oAWvI", + ExportName = "_ZNK3sce4Json5Array8iteratorptEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorArrow(CpuContext ctx) => IteratorDereference(ctx); + + [SysAbiExport( + Nid = "w5+VCznos5E", + ExportName = "_ZN3sce4Json5Array8iteratorppEv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorPreIncrement(CpuContext ctx) + { + var iterator = ctx[CpuRegister.Rdi]; + return SetIterator(ctx, iterator, ReadIterator(ctx, iterator) + ValueObjectSize); + } + + // Postfix ++ returns the old position through the hidden result pointer in RDI; the iterator + // is in RSI. + [SysAbiExport( + Nid = "GEylwSsiGG8", + ExportName = "_ZN3sce4Json5Array8iteratorppEi", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorPostIncrement(CpuContext ctx) + { + var iterator = ctx[CpuRegister.Rsi]; + var position = ReadIterator(ctx, iterator); + ctx.TryWriteUInt64(iterator, position + ValueObjectSize); + return ReturnIterator(ctx, position); + } + + [SysAbiExport( + Nid = "43MESLndGvQ", + ExportName = "_ZN3sce4Json5Array8iterator7advanceEm", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson")] + public static int IteratorAdvance(CpuContext ctx) + { + var iterator = ctx[CpuRegister.Rdi]; + var step = ctx[CpuRegister.Rsi] * ValueObjectSize; + return SetIterator(ctx, iterator, ReadIterator(ctx, iterator) + step); + } + + private static int ConstructArray(CpuContext ctx) + { + var thisAddress = ctx[CpuRegister.Rdi]; + if (thisAddress != 0) + { + _arrays[thisAddress] = new JsonArrayState(0, 0); + ctx.TryWriteUInt64(thisAddress, 0); + ctx.TryWriteUInt64(thisAddress + 8, 0); + } + + ctx[CpuRegister.Rax] = thisAddress; + return 0; + } + + private static int DestroyArray(CpuContext ctx) + { + _arrays.TryRemove(ctx[CpuRegister.Rdi], out _); + ctx[CpuRegister.Rax] = 0; + return 0; + } + + private static JsonArrayState GetArray(ulong address) => + _arrays.TryGetValue(address, out var array) ? array : new JsonArrayState(0, 0); + + private static ulong ReadIterator(CpuContext ctx, ulong iterator) => + iterator != 0 && ctx.TryReadUInt64(iterator, out var position) ? position : 0; + + private static int SetIterator(CpuContext ctx, ulong iterator, ulong position) + { + if (iterator != 0) + { + ctx.TryWriteUInt64(iterator, position); + } + + ctx[CpuRegister.Rax] = iterator; + return 0; + } + + private static int CopyIterator(CpuContext ctx) => + SetIterator(ctx, ctx[CpuRegister.Rdi], ReadIterator(ctx, ctx[CpuRegister.Rsi])); + + private static int ReturnIterator(CpuContext ctx, ulong position) => + SetIterator(ctx, ctx[CpuRegister.Rdi], position); + private static int ConstructValue(CpuContext ctx) { var thisAddress = ctx[CpuRegister.Rdi]; @@ -642,6 +937,7 @@ public static class JsonExports { var thisAddress = ctx[CpuRegister.Rdi]; _values.TryRemove(thisAddress, out _); + _valueArrays.TryRemove(thisAddress, out _); JsonObjectHeap.RemoveValue(thisAddress); if (thisAddress != 0) { diff --git a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs index ce5227d..3942dfc 100644 --- a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs +++ b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs @@ -165,4 +165,167 @@ public sealed class JsonExportRegistrationTests Assert.Equal(userDataAddress, BitConverter.ToUInt64(parameter, 8)); Assert.All(parameter[0x10..], value => Assert.Equal(0, value)); } + + private const ulong ArrayMemoryBase = 0x1_0000_0000; + private const ulong ArrayRoot = ArrayMemoryBase; + private const ulong ArrayText = ArrayMemoryBase + 0x100; + private const ulong ArrayKey = ArrayMemoryBase + 0x300; + private const ulong ArrayBeginIterator = ArrayMemoryBase + 0x400; + private const ulong ArrayEndIterator = ArrayMemoryBase + 0x408; + + private static (CpuContext Context, FakeCpuMemory Memory) Parse(string json) + { + JsonObjectHeap.ResetForTests(); + var memory = new FakeCpuMemory(ArrayMemoryBase, 0x10000); + var ctx = new CpuContext(memory, Generation.Gen5); + ctx[CpuRegister.Rdi] = ArrayRoot; + JsonExports.ValueConstructor(ctx); + memory.WriteCString(ArrayText, json); + ctx[CpuRegister.Rdi] = ArrayRoot; + ctx[CpuRegister.Rsi] = ArrayText; + ctx[CpuRegister.Rdx] = (ulong)json.Length; + Assert.Equal(0, JsonExports.ParserParseBuffer(ctx)); + return (ctx, memory); + } + + private static ulong Member(CpuContext ctx, FakeCpuMemory memory, string name) + { + memory.WriteCString(ArrayKey, name); + ctx[CpuRegister.Rdi] = ArrayRoot; + ctx[CpuRegister.Rsi] = ArrayKey; + JsonExports.ValueIndexCString(ctx); + return ctx[CpuRegister.Rax]; + } + + private static ulong GetArray(CpuContext ctx, ulong value) + { + ctx[CpuRegister.Rdi] = value; + JsonExports.ValueGetArray(ctx); + return ctx[CpuRegister.Rax]; + } + + private static void Range(CpuContext ctx, ulong array) + { + ctx[CpuRegister.Rdi] = ArrayBeginIterator; + ctx[CpuRegister.Rsi] = array; + JsonExports.ArrayBegin(ctx); + Assert.Equal(ArrayBeginIterator, ctx[CpuRegister.Rax]); + ctx[CpuRegister.Rdi] = ArrayEndIterator; + ctx[CpuRegister.Rsi] = array; + JsonExports.ArrayEnd(ctx); + Assert.Equal(ArrayEndIterator, ctx[CpuRegister.Rax]); + } + + private static bool NotAtEnd(CpuContext ctx) + { + ctx[CpuRegister.Rdi] = ArrayBeginIterator; + ctx[CpuRegister.Rsi] = ArrayEndIterator; + JsonExports.IteratorNotEqual(ctx); + return ctx[CpuRegister.Rax] != 0; + } + + [Fact] + public void AnArrayOfRealsIsWalkedFromBeginToEnd() + { + var (ctx, memory) = Parse("{\"scale\":[1.5,-2.25,3.0]}"); + var array = GetArray(ctx, Member(ctx, memory, "scale")); + Range(ctx, array); + + var reals = new List(); + while (NotAtEnd(ctx)) + { + ctx[CpuRegister.Rdi] = ArrayBeginIterator; + JsonExports.IteratorDereference(ctx); + var element = ctx[CpuRegister.Rax]; + + ctx[CpuRegister.Rdi] = element; + JsonExports.ValueGetType(ctx); + Assert.Equal(4UL, ctx[CpuRegister.Rax]); + + ctx[CpuRegister.Rdi] = element; + JsonExports.ValueGetReal(ctx); + Assert.True(ctx.TryReadUInt64(ctx[CpuRegister.Rax], out var bits)); + reals.Add(BitConverter.Int64BitsToDouble(unchecked((long)bits))); + + ctx[CpuRegister.Rdi] = ArrayBeginIterator; + JsonExports.IteratorPreIncrement(ctx); + Assert.Equal(ArrayBeginIterator, ctx[CpuRegister.Rax]); + } + + Assert.Equal([1.5, -2.25, 3.0], reals); + ctx[CpuRegister.Rdi] = array; + JsonExports.ArraySize(ctx); + Assert.Equal(3UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void ReadingTheSameValueAgainReusesItsArray() + { + var (ctx, memory) = Parse("{\"list\":[1,2]}"); + var member = Member(ctx, memory, "list"); + + Assert.Equal(GetArray(ctx, member), GetArray(ctx, member)); + } + + [Fact] + public void AValueThatIsNotAnArrayReadsAsEmpty() + { + var (ctx, memory) = Parse("{\"number\":7}"); + var array = GetArray(ctx, Member(ctx, memory, "number")); + Assert.NotEqual(0UL, array); + Range(ctx, array); + + Assert.False(NotAtEnd(ctx)); + ctx[CpuRegister.Rdi] = array; + JsonExports.ArrayEmpty(ctx); + Assert.Equal(1UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void PostfixIncrementAndCopiesKeepTheirOwnPosition() + { + const ulong old = ArrayMemoryBase + 0x410; + const ulong copy = ArrayMemoryBase + 0x418; + var (ctx, memory) = Parse("{\"list\":[10,20,30]}"); + Range(ctx, GetArray(ctx, Member(ctx, memory, "list"))); + Assert.True(ctx.TryReadUInt64(ArrayBeginIterator, out var first)); + + ctx[CpuRegister.Rdi] = copy; + ctx[CpuRegister.Rsi] = ArrayBeginIterator; + JsonExports.IteratorCopyConstructor(ctx); + ctx[CpuRegister.Rdi] = old; + ctx[CpuRegister.Rsi] = ArrayBeginIterator; + ctx[CpuRegister.Rdx] = 0; + JsonExports.IteratorPostIncrement(ctx); + + Assert.Equal(old, ctx[CpuRegister.Rax]); + Assert.True(ctx.TryReadUInt64(old, out var oldPosition)); + Assert.True(ctx.TryReadUInt64(copy, out var copyPosition)); + Assert.True(ctx.TryReadUInt64(ArrayBeginIterator, out var advanced)); + Assert.Equal(first, oldPosition); + Assert.Equal(first, copyPosition); + Assert.Equal(first + 0x20, advanced); + + ctx[CpuRegister.Rdi] = ArrayBeginIterator; + ctx[CpuRegister.Rsi] = 2; + JsonExports.IteratorAdvance(ctx); + Assert.False(NotAtEnd(ctx)); + } + + [Theory] + [InlineData("ONT8As5R1ug", "_ZNK3sce4Json5Value8getArrayEv")] + [InlineData("bcH5EnFE2xY", "_ZNK3sce4Json5Array5beginEv")] + [InlineData("WXF2ihRF+B8", "_ZNK3sce4Json5Array3endEv")] + [InlineData("5AZPp99ogrc", "_ZNK3sce4Json5Array8iteratorneERKS2_")] + [InlineData("wcgr5mte7T8", "_ZNK3sce4Json5Array8iteratordeEv")] + [InlineData("w5+VCznos5E", "_ZN3sce4Json5Array8iteratorppEv")] + [InlineData("9yLjn46Ypfs", "_ZN3sce4Json5Array8iteratorD1Ev")] + public void ArrayExportsAreRegistered(string nid, string name) + { + var manager = CreateRegisteredManager(); + + Assert.True(manager.TryGetExport(nid, out var export)); + Assert.Equal(name, export.Name); + Assert.Equal("libSceJson", export.LibraryName); + } }