SharpEmu HLE System Architecture: Implementing PlayStation 5 System Calls in C#

SharpEmu's High-Level Emulation (HLE) layer implements the PlayStation 5 system-call interface using attribute-driven registration, a dispatch table, and a CPU context abstraction to route guest NIDs to C# host implementations.

SharpEmu is an open-source PlayStation 5 emulator written in C# that relies on High-Level Emulation (HLE) to intercept and handle kernel functionality on the host side. The HLE system architecture centers on a metadata-driven approach where system calls are mapped to managed methods via numeric identifiers (NIDs), allowing the emulator to translate guest kernel requests into host execution without low-level virtualization overhead.

Core Components of the HLE Layer

The architecture isolates guest-side ABI concerns from host-side implementation through several tightly-focused types defined under src/SharpEmu.HLE.

SysAbiExportAttribute

The SysAbiExportAttribute class (defined in src/SharpEmu.HLE/SysAbiExportAttribute.cs) marks managed methods as HLE exports and supplies the metadata required for registration. Developers decorate kernel implementations with this attribute to specify the NID (numeric identifier), export name, and target firmware generation.

[SysAbiExport(Nid = "0x1234ABCD", ExportName = "sceKernelOpen", Target = Generation.Gen2)]
public static int KernelOpen(CpuContext ctx) { /* ... */ }

SysAbiFunction Delegate

All HLE methods must conform to the SysAbiFunction delegate signature defined in src/SharpEmu.HLE/SysAbiFunction.cs. This standardization allows the dispatcher to invoke every export uniformly without reflection at dispatch time.

public delegate int SysAbiFunction(CpuContext context);

CpuContext

The CpuContext class (located in src/SharpEmu.HLE/CpuContext.cs) represents the guest CPU state. It holds general-purpose registers (Rax, Rbx, etc.) and provides access to guest memory. HLE functions read arguments from and write return values to this context, mirroring the PlayStation 5 ABI directly.

ModuleManager

The ModuleManager class in src/SharpEmu.HLE/ModuleManager.cs serves as the central registry and dispatcher. It scans assemblies for methods bearing SysAbiExportAttribute, builds the dispatch table (mapping NIDs to delegates), and manages the lifecycle of HLE registration. The manager also performs "warming" operations—pre-JIT-compiling methods and triggering static constructors—to avoid fail-fast events when the guest thread invokes system calls.

Supporting Data Structures

How the Dispatch Pipeline Works

The SharpEmu HLE architecture operates through four distinct phases that bridge the guest kernel and host implementation.

Registration Phase

At emulator startup, ModuleManager.RegisterFromAssembly(Assembly assembly, Generation generation) reflects over the provided assembly, identifies methods decorated with SysAbiExportAttribute, and resolves export metadata. If a method specifies only an export name without a NID, the ResolveExportInfo routine consults an ISymbolCatalog (such as the pre-generated Aerolib) to map the name to the correct numeric identifier. Valid entries are stored in _dispatchTable as NID-to-delegate mappings.

Freezing Phase

After all modules load, calling ModuleManager.Freeze() locks the registration to prevent further mutations. This phase "warms" type initializers by triggering static constructors and JIT-compiles all exported methods. This preparation ensures that the Common Language Runtime (CLR) will not attempt compilation or initialization on the guest thread, which could trigger fail-fast events or deadlocks during emulation.

Dispatch Phase

When the emulated PlayStation 5 kernel invokes a system call, the guest CPU places the target NID in a register (typically Rax) and transfers control to the HLE layer. The emulator core calls ModuleManager.Dispatch(string nid, CpuContext context), which looks up the delegate in the frozen dispatch table, verifies that the target generation matches the current emulation context, and invokes the method. The HLE function writes its return value directly into context.Rax before control returns to the guest.

Symbol Resolution

For exports defined by name only, the ISymbolCatalog interface (implemented by Aerolib.cs) provides NID resolution without hardcoding values in the source. This separation allows the HLE layer to remain portable across firmware versions while maintaining accurate symbol mapping.

Complete Implementation Example

The following example demonstrates defining an HLE export, registering it with the module manager, and dispatching a system call:

// Defining an HLE export in src/SharpEmu.HLE/KernelFunctions.cs
[SysAbiExport(Nid = "0x2A3B4C5D", ExportName = "sceKernelPrint", Target = Generation.Gen2)]
public static int KernelPrint(CpuContext ctx)
{
    // Read a guest string pointer from Rdx
    var str = ctx.Memory.ReadString(ctx[CpuRegister.Rdx]);
    SharpEmu.Logging.ConsoleLogSink.WriteLine(str);
    return 0; // Success code written to guest Rax
}

// Registration during emulator initialization
var mgr = new ModuleManager();
int count = mgr.RegisterFromAssembly(typeof(KernelPrint).Assembly, Generation.Gen2);
mgr.Freeze(); // Lock and warm the module

// Dispatch from the CPU emulator core
// Assumes guest has placed NID 0x2A3B4C5D in Rax
string nid = ctx[CpuRegister.Rax].ToString("X8");
OrbisGen2Result result = mgr.Dispatch(nid, ctx);
// Return value already written to ctx.Rax by KernelPrint

Key Source Files Reference

File Role
src/SharpEmu.HLE/SysAbiExportAttribute.cs Attribute marking HLE functions and providing export metadata
src/SharpEmu.HLE/SysAbiFunction.cs Delegate type that all HLE functions must implement
src/SharpEmu.HLE/CpuContext.cs Guest CPU state representation (registers and memory access)
src/SharpEmu.HLE/ModuleManager.cs Central registration, dispatch, and warm-up logic
src/SharpEmu.HLE/ExportedFunction.cs Data holder for exported symbol metadata
src/SharpEmu.HLE/SysAbiSymbol.cs Symbol entry structure for catalog lookups
src/SharpEmu.HLE/Generation.cs Firmware generation enumeration
src/SharpEmu.HLE/Aerolib/Aerolib.cs Default symbol catalog providing NID mappings

Summary

  • SharpEmu's HLE system uses attribute-driven registration to map PlayStation 5 NIDs to C# methods without hardcoding dispatch logic.

  • The ModuleManager handles the full lifecycle: scanning assemblies via RegisterFromAssembly(), locking state with Freeze(), and routing calls through Dispatch().

  • CpuContext abstracts the guest CPU state, allowing HLE functions to read arguments and write return values using the PlayStation 5 ABI.

  • Pre-JIT compilation and static constructor warming during the freeze phase prevent runtime compilation on guest threads, ensuring stable emulation.

  • The ISymbolCatalog interface decouples NID resolution from implementation, supporting multiple firmware generations through the Generation enum.

Frequently Asked Questions

What is the role of CpuContext in SharpEmu's HLE system?

The CpuContext class encapsulates the guest CPU state, including general-purpose registers and guest memory access. HLE functions receive this context as their sole parameter, allowing them to read system call arguments from specific registers (such as Rdx or Rdi) and write return values back to Rax according to the PlayStation 5 ABI. This abstraction keeps the emulator core decoupled from specific HLE implementations.

How does ModuleManager prevent JIT compilation issues during guest execution?

Before entering the emulation loop, ModuleManager.Freeze() pre-JIT-compiles all registered HLE methods using PrepareMethod and triggers static constructors. This "warming" ensures that the CLR will not attempt to compile or initialize types when a guest thread invokes a system call, preventing potential deadlocks or fail-fast events that could occur if the runtime tried to acquire locks on the guest execution thread.

What is the purpose of the Generation enum in HLE exports?

The Generation enum (defined in src/SharpEmu.HLE/Generation.cs) specifies which PlayStation 5 firmware versions an HLE export supports, such as Gen1 or Gen2. During registration, ModuleManager filters exports based on the current target generation, allowing the emulator to maintain separate implementations for kernel functions that changed between system software updates without namespace collisions or runtime version checking.

How does SharpEmu resolve export names to NIDs without hardcoded values?

When a method specifies only an export name in SysAbiExportAttribute, the ModuleManager consults an implementation of ISymbolCatalog (typically the Aerolib class) to resolve the symbolic name to its numeric NID. This lookup occurs during the registration phase, allowing the dispatch table to store direct NID-to-delegate mappings while keeping the source code readable with symbolic names rather than hexadecimal constants.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →