How to Register HLE Modules in SharpEmu: The Complete ModuleManager Workflow

Registering HLE modules in SharpEmu involves calling ModuleManager.RegisterFromAssembly() to scan assemblies for methods marked with [SysAbiExport], creating delegates for each export, populating internal dispatch tables, and finally calling Freeze() to lock the registration and warm up the JIT compiler.

SharpEmu is a PlayStation 5 emulator that relies on High-Level Emulation (HLE) to expose host system functions to guest binaries. The process for registering HLE modules is handled by the ModuleManager class in src/SharpEmu.HLE/ModuleManager.cs, which discovers annotated methods, builds dispatch tables, and prepares the runtime for safe execution.

The HLE Registration Pipeline

The registration workflow follows a strict sequence to ensure thread safety and runtime stability. Each phase transforms static C# methods into callable HLE exports that guest code can invoke via NID (Numeric ID) lookups.

Scanning Assemblies with RegisterFromAssembly

Registration begins with RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null). This method first checks _scannedAssemblies to deduplicate the pair (assembly, generation), preventing double-scanning. It also verifies that _isFrozen is false, throwing an exception if registration occurs after the manager has been sealed.

Identifying Exports via SysAbiExportAttribute

For every type in the supplied assembly, RegisterFromAssembly inspects all methods (public, non-public, static, or instance) using reflection:

var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);

Only methods adorned with [SysAbiExport] are considered HLE exports. The attribute defines the library name (e.g., "sceAudio"), the NID (numeric identifier), and the export name.

Resolving Export Metadata

The private ResolveExportInfo method constructs an ExportInfo structure containing:

  • Library name (e.g., "sceAudio")
  • NID (hexadecimal identifier)
  • Export name
  • Target (linkage to the symbol catalog if supplied)

If resolution fails due to missing symbols, the method is skipped and registeredCount is not incremented.

Creating Handler Delegates

For valid exports, CreateHandler(type, method, instances) builds a Delegate capable of being invoked from the emulator core. This delegate is wrapped in a SysAbiFunction object that standardizes the calling convention for the dispatcher.

Populating Dispatch Tables

The manager populates three internal collections:

  • _dispatchTable – Maps NID strings to SysAbiFunction delegates for fast lookup during guest execution
  • _exportTable – Stores complete ExportInfo records for introspection
  • _exportNameTable – Indexes exports by human-readable names

Duplicate NIDs generate a warning and are ignored to prevent collision.

Finalizing with Freeze and WarmHleTypeInitializers

After all modules are registered, Freeze() sets _isFrozen = true and immediately calls WarmHleTypeInitializers(). This method walks every scanned assembly and forces static constructors to run via RuntimeHelpers.RunClassConstructor, then JIT-compiles each method using RuntimeHelpers.PrepareMethod. This eliminates first-time JIT stalls and protects the CLR from fail-fast crashes during guest execution.

Code Implementation Examples

Defining an HLE Export

Create a static class with methods annotated using SysAbiExportAttribute:

using SharpEmu.HLE;

public static class Audio
{
    [SysAbiExport(library: "sceAudio", nid: 0x12345678, name: "sceAudioOutput")]
    public static int Output(int channel, int volume, IntPtr buffer, uint size)
    {
        // Implementation that runs on the host
        return 0;
    }
}

Registering During Startup

Instantiate ModuleManager and scan your assembly before the guest begins execution:

using System.Reflection;
using SharpEmu.HLE;

var moduleMgr = new ModuleManager();
moduleMgr.RegisterFromAssembly(typeof(Audio).Assembly, Generation.Orbis2);
moduleMgr.Freeze();   // Locks registration and warms up JIT

Looking Up Functions by NID

Guest code can resolve and invoke HLE functions using their NID:

if (moduleMgr.TryGetFunction("0x12345678", out var func))
{
    var del = (Func<int, int, IntPtr, uint, int>)func;
    int result = del(0, 100, bufferPtr, 256);
}

Key Source Files and Architecture

The HLE infrastructure is distributed across these core files:

Summary

  • Discovery: RegisterFromAssembly scans assemblies for [SysAbiExport] methods and deduplicates by generation
  • Metadata: Each export is resolved into an ExportInfo containing library name, NID, and target symbol
  • Dispatch: Delegates are created and stored in _dispatchTable for NID-based lookups
  • Safety: Freeze() prevents further registration and triggers WarmHleTypeInitializers to pre-JIT all HLE methods
  • Integration: The process allows SharpEmu to expose host system calls to PlayStation 5 guest binaries through a type-safe, attributed API

Frequently Asked Questions

What is the purpose of the SysAbiExportAttribute in SharpEmu?

The SysAbiExportAttribute marks managed methods as exported HLE functions that guest code can call. It specifies the library namespace, the unique NID (Numeric ID), and the human-readable function name, allowing the ModuleManager to build the dispatch tables required for interop between the emulated guest and the host implementation.

Why does ModuleManager require a Generation parameter during registration?

The Generation parameter (e.g., Generation.Orbis2) indicates which PlayStation 5 system version the assembly targets. This allows the emulator to support multiple firmware generations simultaneously while preventing conflicts between APIs that may have changed across hardware revisions.

What happens if I try to register modules after calling Freeze()?

Calling RegisterFromAssembly after Freeze() has been invoked will result in an exception. The _isFrozen flag is set to true inside Freeze(), explicitly blocking any further registrations to ensure the dispatch tables remain immutable during guest execution and to prevent race conditions.

How does WarmHleTypeInitializers improve emulator performance?

WarmHleTypeInitializers executes static constructors and JIT-compiles every HLE method before the guest starts running. This eliminates "first-time" compilation stalls that would otherwise occur when the guest first invokes a system call, and it prevents the CLR from entering fail-fast mode due to concurrent type initialization during emulation.

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 →