The goal of this guide is establishing a consistent way for the implementation of Horizon services on Ryujinx. It also aims to address some issues that the existing implementations have. It should not be considered complete, and will be edited as needed to include more information or update/correct existing ones.
The new IPC system addresses one large issue that the old implementation had, which is the need to "manually" deserialize the messages on the implementation function.
Now, a source generator takes care of this task, so one just need to define the correct function signature.
Let's look at a more practical example. This is how the implementation for RegisterService
used to look like:
[CommandHipc(2)]
// RegisterService(ServiceName name, u8 isLight, u32 maxHandles) -> handle<move, port>
public ResultCode RegisterServiceHipc(ServiceCtx context)
{
if (!_isInitialized)
{
return ResultCode.NotInitialized;
}
long namePosition = context.RequestData.BaseStream.Position;
string name = ReadName(context);
context.RequestData.BaseStream.Seek(namePosition + 8, SeekOrigin.Begin);
bool isLight = (context.RequestData.ReadInt32() & 1) != 0;
int maxSessions = context.RequestData.ReadInt32();
return RegisterService(context, name, isLight, maxSessions);
}
Now that's how it looks like:
[CmifCommand(2)]
public Result RegisterService([MoveHandle] out int handle, ServiceName name, int maxSessions, bool isLight)
{
if (!_initialized)
{
handle = 0;
return SmResult.InvalidClient;
}
return _serviceManager.RegisterService(out handle, _clientProcessId, name, maxSessions, isLight);
}
The above function is using 2 types of "command arguments", InArgument
and OutMoveHandle
. Each command argument has a set of types that are supported, which you can find on the table below.
Command argument | Valid C# types | Valid modifiers | Attribute |
---|---|---|---|
InArgument | Unmanaged | None / In / Ref | |
OutArgument | Unmanaged | Out | |
InBuffer | Unmanaged | None / In / Ref | Buffer |
InBuffer | ReadOnlySpan / Span | None | Buffer |
OutBuffer | Unmanaged | Out | Buffer |
OutBuffer | Span | None | Buffer |
InObject | IServiceObject* | None | |
OutObject | IServiceObject* | Out | |
InCopyHandle | int | None | CopyHandle |
OutCopyHandle | int | Out | CopyHandle |
InMoveHandle | int | None | MoveHandle |
OutMoveHandle | int | Out | MoveHandle |
ClientProcessId | ulong | None | ClientProcessId |
* Not only IServiceObject
is valid for object types, but also any type that implements the interface.
Anything with an attribute specified on the "Attribute" column on the table above needs to have an attribute added on the parameter for it to be considered of that type.
For example, the RegisterService
function above has the MoveHandle
attribute on the handle
output.
Without this attribute, it would be considered a regular argument and written to the raw data section of the response message.
With this attribute, it is considered a move handle and is written to the move handles section of the response.
The out
modifier indicates that it is an output and should be written to the response. Without this modifier, it would be considered an input and the handle would be retrieved from the request message.
The new IPC implementation will perform validation of the message before calling the service function. If the message does not match the function signature (like, if for example the function expects a handle but the message has none), it will return InvalidCmifRequest
error.
That means that incorrect function signatures may cause it to fail before even calling the function, so one must be careful with that.
Another thing to keep in mind is that arguments are sorted based on the type alignment and size on the message raw data section.
That means the order of the arguments on the function signature may not reflect the order they are read from (or written into) the message.
It also means that changes to the size or alignment of a type might affect the argument order of all functions using that type as argument type.
Because C# generally does not have a way to get the alignment of a type, the generator tries to do a guess. For primitive types, it will assume that the alignment matches the size. So a int
has an alignment of 4 bytes.
For structs, it will assume that the alignment is 1 unless explicitly overriden using the StructLayout
attribute Pack
property, in which case it uses the value specified there.
Note that the default alignment for structs is not 1, this is just an assumption made by the generator.
- DON'T use static fields/properties for service state.
One might be driven to do this because that's how it's done in the original service.
But on Horizon each service has it's own process, and the process memory is not shared.
This is not the case in a emulator, and is particularly problematic when you account for multiple emulator contexts, because services running on different contexts should not be aware of each other, but they are since they share the same state when you use static
(and will most likely not function correctly).
- DON'T access kernel objects directly.
Everything should be done using syscalls, rather than manipulating the kernel objects directly.
Eventually, the plan is moving the kernel to its own project, and they will be no longer accessible at all when that happens.
Services should also use the wrapper types instead of using syscalls directly. For example, instead of using svcSignalEvent
and svcClearEvent
directly, one should use InterProcessEvent.Signal
and InterProcessEvent.Clear
.
The IPC system will be tested and extended as services get implemented. We want to make it available as fast as possible to enable other developers to start using it and migrating services. When this change was first attempted, all services were also changed to use the new implementation. It caused regressions that were hard to track, and it was tricky to rebase later on since it changed so many files. To address this issue, we will now be migrating the services gradually to allow catching regressions easily. We can also use the opportunity to improve the existing implementations as much as possible and clean up the project.
- Change handle type to
Handle
.
Currently we use the int
type for handles. We should eventually start using a dedicated Handle
struct, because it cleaner and makes clear which APIs should take handles.
It also prevents someone using the code from passing some random integer value, for the most part.
- Support string buffers.
Currently any service that takes or returns strings must use spans, and then use some function to convert from/to string. We can instead support it on the generator and let it deal with the conversion.
Some common terms used on the project and Horizon that might not be immediately obvious.
- Horizon: The name of the Nintendo Switch operating system.
- HIPC: Horizon Inter-Process Communication.
- IPC: Inter-Process Communication.
- SF: Service Framework.
- CMIF: Common Message Interface Framework.
- SM: Service Manager.