mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
[Json] Implement sce::Json::Value and String (construct / set / destroy) (#169)
* [Json] Implement sce::Json::Value and Json::String construct/set/destroy libSceJson previously only had the Initializer/MemAllocator setup path. The Value and String classes themselves were entirely absent, so a Prospero title that builds a JSON tree (Quake PPSA01880 does, to shape a web-API request) hit unresolved imports and faulted on the call. The imports it left unresolved right before its access violation are exactly these Value ctors/setters and String ctor/dtor. Model the Value/String payload host-side (JsonObjectHeap), keyed by the guest `this` pointer, following the handle-shadow pattern already used by Ngs2Exports. The guest object bytes are deliberately not written: these objects are usually stack-allocated with an unknown real layout, and writing a guessed layout risks smashing an adjacent stack canary (the same hazard the AudioOut2 context-param note in this tree records). Constructors and setters follow the Itanium ABI and return `this` in rax, which is correct whether the real setter returns void or Value&. Covered NIDs (complete-object C1/D1 variants, matching the observed imports): Value(default/bool/long/ulong/double/ValueType/char*/String), Value::~Value, Value::set(bool/long/ulong/double/ValueType/char*/String), Value::clear, String(char*/default/copy), String::~String. Only the payload the guest can reach through library methods is modelled; direct guest reads of the object bytes are out of scope and would need observed layout evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [Tests] Add SharpEmu.Libs.Tests covering the Json Value/String exports First test project for SharpEmu.Libs (xunit), the SharpEmu.Libs.Tests layout the maintainer already agreed to in issue #36. - A FakeCpuMemory (single contiguous region) drives the exports at the CpuContext level with no live guest. - Direct-call tests: ctor/setter round-trips for bool/int/uint/double (read from xmm0)/char*/String/ValueType, destructor cleanup, and the graceful-degradation paths (missing String shadow and a faulting char* pointer both fall back to the empty string instead of throwing). - Registration test: a real ModuleManager scans SharpEmu.Libs and the nine NIDs Quake left unresolved now resolve to the libSceJson exports and dispatch cleanly (returns `this` in rax). InternalsVisibleTo exposes JsonObjectHeap to the test assembly. The test project's packages.lock.json is committed for CI locked-mode restore; CI does not run tests yet, left as a maintainer decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [Json] Add Initializer::setGlobalNullAccessCallback Quake calls it during kexPSNWebAPI::Initialize and treats the not-found error as fatal for the whole Np Web API bring-up. Store the guest hook (never invoked by this HLE: shadows degrade to defaults instead of dereferencing missing members) and return success. Verified against the dump: the "setGlobalNullAccessCallback failed (0x80020002)" line is gone and kexPSNWebAPI::Initialize now logs "Np Web API Initialized"; the next blockers are sceNpAuthCreateRequest and sceUserServiceInitialize ordering, outside libSceJson. Also pins both Json test classes to one xunit collection: they share JsonObjectHeap statics and parallel class execution raced ResetForTests against a running test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
4c35831cb8
commit
df53ff59d9
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Json;
|
||||
|
||||
// These NIDs came back "unresolved" in the Quake (PPSA01880) import log right before its
|
||||
// access violation. This asserts they now resolve to the Json handlers and dispatch cleanly,
|
||||
// which is the plumbing the direct-call tests cannot cover.
|
||||
[Collection("JsonObjectHeap")]
|
||||
public sealed class JsonExportRegistrationTests
|
||||
{
|
||||
private static readonly (string Nid, string Name)[] ExpectedExports =
|
||||
{
|
||||
("qBMjqyBn3OM", "_ZN3sce4Json5ValueC1Ev"),
|
||||
("5yHuiWXo2gg", "_ZN3sce4Json5Value3setEb"),
|
||||
("QxVVYhP-mvg", "_ZN3sce4Json5Value3setEl"),
|
||||
("SIe1ZmW7e7s", "_ZN3sce4Json5Value3setEm"),
|
||||
("BSmWDIkV4w4", "_ZN3sce4Json5Value3setEd"),
|
||||
("IKQimvG9Wqs", "_ZN3sce4Json5Value3setENS0_9ValueTypeE"),
|
||||
("6l3Bv2gysNc", "_ZN3sce4Json5Value3setERKNS0_6StringE"),
|
||||
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
||||
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
||||
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
||||
};
|
||||
|
||||
private static ModuleManager CreateRegisteredManager()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
|
||||
return manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuakeUnresolvedJsonNids_ResolveToJsonExports()
|
||||
{
|
||||
var manager = CreateRegisteredManager();
|
||||
|
||||
foreach (var (nid, name) in ExpectedExports)
|
||||
{
|
||||
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||
Assert.Equal(name, export.Name);
|
||||
Assert.Equal("libSceJson", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetGlobalNullAccessCallback_StoresHookAndReturnsOk()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
var manager = CreateRegisteredManager();
|
||||
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = 0x1_0000_0000; // Initializer instance
|
||||
ctx[CpuRegister.Rsi] = 0x8_0012_3456; // guest callback
|
||||
ctx[CpuRegister.Rdx] = 0x1_0000_0800; // user context
|
||||
|
||||
Assert.True(manager.TryDispatch("+drDFyAS6u4", ctx, out var result));
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0x8_0012_3456UL, JsonObjectHeap.GlobalNullAccessCallback);
|
||||
Assert.Equal(0x1_0000_0800UL, JsonObjectHeap.GlobalNullAccessCallbackContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatchValueConstructor_RunsHandlerAndReturnsThis()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
var manager = CreateRegisteredManager();
|
||||
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = 0x1_0000_0000;
|
||||
|
||||
Assert.True(manager.TryDispatch("qBMjqyBn3OM", ctx, out var result));
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Json;
|
||||
|
||||
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
|
||||
// does not run them in parallel against it.
|
||||
[Collection("JsonObjectHeap")]
|
||||
public sealed class JsonValueExportsTests
|
||||
{
|
||||
private const ulong ThisAddress = 0x1_0000_0000;
|
||||
private const ulong StringAddress = 0x1_0000_1000;
|
||||
private const ulong TextAddress = 0x1_0000_2000;
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
|
||||
private readonly CpuContext _ctx;
|
||||
|
||||
public JsonValueExportsTests()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
|
||||
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||
|
||||
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0UL, false)]
|
||||
[InlineData(1UL, true)]
|
||||
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
|
||||
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = raw;
|
||||
|
||||
JsonValueExports.ValueSetBoolean(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Boolean, state.Kind);
|
||||
Assert.Equal(expected, state.Boolean);
|
||||
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetInteger_RoundTripsSignedValue()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
|
||||
|
||||
JsonValueExports.ValueSetInteger(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Integer, state.Kind);
|
||||
Assert.Equal(-42L, state.Integer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetUnsigned_RoundTripsFullWidth()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
|
||||
|
||||
JsonValueExports.ValueSetUnsigned(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.UInteger, state.Kind);
|
||||
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetReal_ReadsDoubleFromXmm0()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
|
||||
|
||||
JsonValueExports.ValueSetReal(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Real, state.Kind);
|
||||
Assert.Equal(3.14159, state.Real, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetCString_ReadsGuestString()
|
||||
{
|
||||
_memory.WriteCString(TextAddress, "hello json");
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||
|
||||
JsonValueExports.ValueSetCString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal("hello json", state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetType_KeepsRawGuestEnumValue()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = 7;
|
||||
|
||||
JsonValueExports.ValueSetType(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
|
||||
Assert.Equal(7u, state.ExplicitType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringConstructThenValueSetString_CopiesText()
|
||||
{
|
||||
_memory.WriteCString(TextAddress, "from string object");
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||
JsonValueExports.StringCStringConstructor(_ctx);
|
||||
|
||||
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
|
||||
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = StringAddress;
|
||||
JsonValueExports.ValueSetString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal("from string object", state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
|
||||
|
||||
JsonValueExports.ValueSetString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal(string.Empty, state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Destructors_RemoveShadowState()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
JsonValueExports.StringDefaultConstructor(_ctx);
|
||||
|
||||
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
JsonValueExports.ValueDestructor(_ctx);
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
JsonValueExports.StringDestructor(_ctx);
|
||||
|
||||
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
|
||||
|
||||
JsonValueExports.ValueSetCString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal(string.Empty, state.Text);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user