Skip to content

Instantly share code, notes, and snippets.

@gdkchan
Created December 28, 2022 21:22
Show Gist options
  • Save gdkchan/84ba88cd50efbe58d1babfaa7cd7c455 to your computer and use it in GitHub Desktop.
Save gdkchan/84ba88cd50efbe58d1babfaa7cd7c455 to your computer and use it in GitHub Desktop.
Ryujinx service implementation guidelines

Overview

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.

Service implementation

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.

DOs and DON'Ts

  • 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.

Test plan

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.

TO DO

  • 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.

Glossary

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment