How HLE Exports Are Registered and Dispatched in SharpEmu's ModuleManager

SharpEmu uses the ModuleManager class to collect, store, and invoke High-Level Emulation (HLE) exports through an attribute-driven registration system that pre-warms the CLR before runtime dispatch.

The ModuleManager in SharpEmu serves as the central registry for bridging guest system calls to host implementations. This article examines how the emulator registers HLE exports using source generators, freezes the registration state, and dispatches calls based on NID lookups according to the par274/sharpemu source code.

Understanding the Export Structure

Each HLE function is represented by an ExportedFunction object defined in src/SharpEmu.HLE/ExportedFunction.cs. This POCO encapsulates the metadata required to map a guest system call to its host implementation:

  • Library name: The target PlayStation library (e.g., libKernel)
  • NID: A unique identifier string (e.g., "0x12345678")
  • Export name: A friendly identifier (e.g., sceKernelGetProcessId)
  • Target generation: The PlayStation generation this export supports
  • Delegate: A SysAbiFunction containing the actual host implementation

The SysAbiFunction delegate type defines the standard signature for all HLE methods, accepting a CpuContext and returning an integer result.

Attribute-Driven Export Generation

Rather than manually wiring exports, SharpEmu uses the [SysAbiExport] attribute found in src/SharpEmu.HLE/SysAbiExportAttribute.cs to mark host methods for automatic discovery.

Source generators scan the codebase for methods decorated with this attribute and emit registration code at compile time. The attribute properties specify:

  • Nid: The hexadecimal identifier string
  • ExportName: The human-readable function name
  • LibraryName: The target system library
  • Target: The supported Generation enum value (e.g., Generation.Gen2)

This approach eliminates manual registration errors and ensures type safety between the attribute metadata and the actual method signatures.

Registering Exports in ModuleManager

When the emulator boots, generated code calls ModuleManager.RegisterExports(IReadOnlyList<ExportedFunction>) to populate the dispatch tables. This method, implemented in src/SharpEmu.HLE/ModuleManager.cs, performs several critical operations:

  1. Thread-safety: Acquires the _registrationGate lock to ensure no race conditions during the registration window.
  2. Dispatch table population: Adds each export's delegate to _dispatchTable using the NID as the key. Duplicate NIDs trigger a warning and are ignored.
  3. Metadata storage: Stores the full ExportedFunction in _exportTable (by NID) and _exportNameTable (by export name) for lookups.
  4. Assembly tracking: Records the defining assembly in _warmupAssemblies for later JIT compilation.

The method returns the count of successfully registered exports, allowing the bootstrap code to verify that all expected modules loaded correctly.

Freezing and Pre-Warming the CLR

After all modules register their exports, the emulator calls ModuleManager.Freeze() to seal the registration state. This method sets _isFrozen = true and immediately invokes WarmHleTypeInitializers() to prevent CLR fail-fast crashes during guest execution.

The warming process resolves all assemblies that contributed exports plus any guest-reachable interop assemblies (such as Silk.NET). It then:

  • Executes static constructors using RuntimeHelpers.RunClassConstructor for every type
  • Forces JIT compilation via RuntimeHelpers.PrepareMethod for every method

This pre-warming ensures that when a guest thread first triggers an HLE export, the host implementation is already compiled and initialized, avoiding JIT-related delays or threading issues.

Runtime Dispatch Mechanism

When guest code invokes a system call, the emulator routes the request through ModuleManager.TryDispatch(string nid, CpuContext context, out OrbisGen2Result result). The dispatch flow follows these steps:

  1. Lookup: Searches _dispatchTable for the delegate and _exportTable for the corresponding ExportedFunction metadata. If missing, returns a "not found" error.
  2. Generation validation: Verifies that the export's Target generation matches the current guest generation. Mismatches return a "not implemented" error.
  3. Invocation: Clears any pre-existing RAX-write flag, invokes the SysAbiFunction delegate with the provided CpuContext, and captures the returned integer.
  4. Result handling: Casts the returned integer to OrbisGen2Result and stores it in the CPU context's RAX register unless the implementation already wrote to it.

This NID-based lookup allows the emulator to dynamically route system calls without hardcoded switch statements, maintaining clean separation between the HLE layer and the core emulation logic.

Practical Implementation Examples

Defining an HLE export requires applying the attribute to a static method with the correct signature:

[SysAbiExport(
    Nid = "0x12345678", 
    ExportName = "sceKernelGetProcessId", 
    LibraryName = "libKernel", 
    Target = Generation.Gen2)]
public static int GetProcessId(CpuContext ctx)
{
    ctx[CpuRegister.Rax] = (ulong)CurrentProcessId;
    return (int)OrbisGen2Result.ORBIS_GEN2_SUCCESS;
}

The source generator produces registration code similar to this simplified version:

var exports = new List<ExportedFunction>
{
    new ExportedFunction(
        libraryName: "libKernel",
        nid: "0x12345678",
        name: "sceKernelGetProcessId",
        target: Generation.Gen2,
        function: (SysAbiFunction)GetProcessId)
};
moduleManager.RegisterExports(exports);

During emulation, the core invokes the export through the dispatch method:

var result = moduleManager.TryDispatch("0x12345678", cpuContext, out var orbisResult);
// orbisResult contains the execution status from the host implementation

Summary

  • ExportedFunction objects encapsulate HLE metadata and delegates in src/SharpEmu.HLE/ExportedFunction.cs.
  • [SysAbiExport] attributes drive compile-time code generation that eliminates manual registration.
  • RegisterExports populates thread-safe lookup tables (_dispatchTable, _exportTable) while tracking assemblies for warming.
  • Freeze() seals the registration state and executes WarmHleTypeInitializers() to JIT-compile all HLE methods before guest execution begins.
  • TryDispatch performs NID-based lookups with generation validation and manages CPU context state during invocation.

Frequently Asked Questions

What is the purpose of the NID in HLE exports?

The NID (Numeric ID) serves as a unique string key that identifies a specific system call within a PlayStation library. SharpEmu uses this string to look up the corresponding host implementation in _dispatchTable and _exportTable, allowing the emulator to decouple guest code addresses from host function pointers.

Why does ModuleManager require a freeze mechanism before running games?

The freeze mechanism prevents dynamic registration after the emulator begins executing guest code. By calling Freeze(), SharpEmu sets _isFrozen = true and immediately runs WarmHleTypeInitializers(), which JIT-compiles all HLE methods and runs their static constructors. This prevents CLR threading issues and fail-fast crashes that could occur if a guest thread triggered JIT compilation during emulation.

How does SharpEmu handle conflicts when two exports share the same NID?

During registration in RegisterExports, the method checks for existing NIDs in _dispatchTable. If a duplicate is detected, the registration logs a warning and ignores the conflicting export, keeping the first registered implementation. This ensures deterministic behavior when multiple modules expose the same system call.

What happens if guest code calls an HLE export from the wrong generation?

The TryDispatch method validates the export's Target generation against the current guest generation before invocation. If they do not match, the method returns an error indicating the function is not implemented for that generation, preventing incompatible system calls from executing on incorrect hardware configurations.

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 →