Files
sharpemu/src/SharpEmu.Libs/Json/JsonValueModel.cs
José Luis Caravaca Carretero df53ff59d9 [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>
2026-07-15 01:49:33 +03:00

111 lines
4.2 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Json;
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
// param sizing note in this project already ran into). The guest reaches the payload only through
// libSceJson methods, so shadowing by address is enough for the build path.
internal enum JsonValueKind : byte
{
Null = 0,
Boolean = 1,
Integer = 2,
UInteger = 3,
Real = 4,
String = 5,
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
// value verbatim rather than mapping it, because the canonical ValueType constants are not
// known from clean-room evidence and round-tripping the guest's own value is what matters.
ExplicitType = 6,
}
internal readonly struct JsonValueState
{
private JsonValueState(
JsonValueKind kind,
bool boolean = false,
long integer = 0,
ulong unsignedInteger = 0,
double real = 0,
string? text = null,
uint explicitType = 0)
{
Kind = kind;
Boolean = boolean;
Integer = integer;
UnsignedInteger = unsignedInteger;
Real = real;
Text = text;
ExplicitType = explicitType;
}
public JsonValueKind Kind { get; }
public bool Boolean { get; }
public long Integer { get; }
public ulong UnsignedInteger { get; }
public double Real { get; }
public string? Text { get; }
public uint ExplicitType { get; }
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
public static JsonValueState FromUnsignedInteger(ulong value) =>
new(JsonValueKind.UInteger, unsignedInteger: value);
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
public static JsonValueState FromExplicitType(uint value) =>
new(JsonValueKind.ExplicitType, explicitType: value);
}
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
internal static class JsonObjectHeap
{
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
// Guest function the library should call when a Value is read as the wrong type. This HLE
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
// fidelity but not invoked.
public static ulong GlobalNullAccessCallback;
public static ulong GlobalNullAccessCallbackContext;
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
public static void SetString(ulong address, string text) => Strings[address] = text;
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
// not track) degrades to the empty string rather than faulting.
public static string GetStringOrEmpty(ulong address) =>
Strings.TryGetValue(address, out var text) ? text : string.Empty;
internal static void ResetForTests()
{
Values.Clear();
Strings.Clear();
GlobalNullAccessCallback = 0;
GlobalNullAccessCallbackContext = 0;
}
}