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 toSysAbiFunctiondelegates for fast lookup during guest execution_exportTable– Stores completeExportInforecords 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:
src/SharpEmu.HLE/ModuleManager.cs– Core registration logic, dispatch table management, and warm-up routinessrc/SharpEmu.HLE/SysAbiExportAttribute.cs– Attribute definition that marks methods as HLE exportssrc/SharpEmu.HLE/IModuleManager.cs– Interface exposingRegisterFromAssembly,Freeze, and lookup operationssrc/SharpEmu.HLE/ExportedFunction.cs– Data transfer object representing export metadata (library, NID, name)src/SharpEmu.HLE/SysAbiFunction.cs– Wrapper that encapsulates the delegate and invocation safety
Summary
- Discovery:
RegisterFromAssemblyscans assemblies for[SysAbiExport]methods and deduplicates by generation - Metadata: Each export is resolved into an
ExportInfocontaining library name, NID, and target symbol - Dispatch: Delegates are created and stored in
_dispatchTablefor NID-based lookups - Safety:
Freeze()prevents further registration and triggersWarmHleTypeInitializersto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →