Skip to content

Commit

Permalink
Add Switch.Service class (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate authored Dec 6, 2024
1 parent 2daafbd commit e5bd2d5
Show file tree
Hide file tree
Showing 11 changed files with 603 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-rice-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nx.js/runtime": patch
---

Add `Switch.Service` class
5 changes: 5 additions & 0 deletions .changeset/proud-dogs-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nx.js/constants": minor
---

Add `SfBufferAttr` and `SfOutHandleAttr` enums
193 changes: 193 additions & 0 deletions docs/content/runtime/concepts/services.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: Services
description: Interacting with system service modules via IPC
---

`libnx`, the core C library used by nx.js to interact with the Switch, contains a
vast number of functions for interacting with various system services in a myriad
of different ways. Many of these functions are higher level wrappers on top of
the Switch's IPC communication mechanism, also known as ["service modules"](https://switchbrew.org/wiki/Services_API#Service_List).

It would not be reasonable for nx.js to attempt to expose all of these functions
directly, so instead nx.js provides a low-level API for interacting with these
system modules via IPC, which allows functionality that is not directly exposed by
nx.js to be implemented in userland.

> [!WARNING]
This is a low-level API which is not recommended to be used directly in most cases.
You should look for an npm package which provides a higher-level API for the service
you are trying to interact with.

## Porting libnx functions to nx.js

### Example with output value

Let's say you want your application to retrieve the current system region code. In `libnx`,
this can be done using the `setGetRegionCode()` function, which is exposed by the
[`set`](https://switchbrew.org/wiki/Settings_services#set) service module.

However, nx.js does not directly expose this function, so your app can use the
`Switch.Service` class to interact with the `set` service module.

To do this correctly, you may need to reference the [libnx source code](https://github.com/switchbrew/libnx)
to understand which command ID and parameters are required for the command
you are attempting to port.

For example, the [`setGetRegionCode()` function](https://github.com/switchbrew/libnx/blob/e79dd7ac52cc7fdc41134c9b55e6f55ec8b8799f/nx/source/services/set.c#L216-L221) is implemented as follows:

```c title="libnx/source/services/set.c"
Result setGetRegionCode(SetRegion *out) {
s32 code=0;
Result rc = _setCmdNoInOutU32(&g_setSrv, (u32*)&code, 4);
if (R_SUCCEEDED(rc) && out) *out = code;
return rc;
}
```
Based on this implementation, we can see that:
* [`set`](https://switchbrew.org/wiki/Settings_services#set) is the named service module
* The command ID is `4`
* There is no input data
* The output data is a [region code](https://switchbrew.org/wiki/Settings_services#RegionCode), which is a `u32` value
Porting this function to nx.js would look like this:
```typescript title="src/main.ts"
const setSrv = new Switch.Service('set');
function getRegionCode() {
const code = new Uint32Array(1);
setSrv.dispatchOut(4, code.buffer);
return code[0];
}
console.log(getRegionCode());
```

### Example with input value

Let's say you want to interact with a function which takes an input value, such as `setsysSetRegionCode()`.

The [`setsysSetRegionCode()` function](https://github.com/switchbrew/libnx/blob/e79dd7ac52cc7fdc41134c9b55e6f55ec8b8799f/nx/source/services/set.c#L546-L548) is implemented as follows:

```c title="libnx/source/services/set.c"
Result setsysSetRegionCode(SetRegion region) {
return _setCmdInU32NoOut(&g_setsysSrv, region, 57);
}
```
* [`set:sys`](https://switchbrew.org/wiki/Settings_services#set:sys) is the named service module
* The command ID is `57`
* The input data is a [region code](https://switchbrew.org/wiki/Settings_services#RegionCode), which is a `u32` value
* There is no output data
Porting this function to nx.js would look like this:
```typescript title="src/main.ts"
const setSysSrv = new Switch.Service('set:sys');
function setRegionCode(region: number) {
setSysSrv.dispatchIn(57, new Uint32Array([region]).buffer);
}
const regionCode = 1; // SetRegion_USA
setRegionCode(regionCode);
```

### Example with buffers

Some commands require use of input and/or output buffers, such as `setGetAvailableLanguageCodes()`. Let's take a look at how to port this function to nx.js.

The [`setGetAvailableLanguageCodes()` function](https://github.com/switchbrew/libnx/blob/e79dd7ac52cc7fdc41134c9b55e6f55ec8b8799f/nx/source/services/set.c#L187-L204) is implemented as follows (some parts are omitted for brevity):

```c title="libnx/source/services/set.c"
Result setGetAvailableLanguageCodes(s32 *total_entries, u64 *LanguageCodes, size_t max_entries) {
return serviceDispatchOut(&g_setSrv, 5, *total_entries,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { LanguageCodes, max_entries*sizeof(u64) } },
);
}
```
* [`set`](https://switchbrew.org/wiki/Settings_services#set) is the named service module
* The command ID is `5`
* There is no input data
* The output data is a `s32` number representing the number of entries that were returned
* The output buffer is an array of `u64` values, which represent the available language codes
> [!IMPORTANT]
> Additionally, looking at the [Switchbrew wiki for `LanguageCode`](https://switchbrew.org/wiki/Settings_services#LanguageCode),
> we can see that the language codes are represented as a `u64` value, which is _actually_ a NUL-terminated string.
Porting this function to nx.js would look like this:
```typescript title="src/main.ts"
import { SfBufferAttr } from '@nx.js/constants';
const setSrv = new Switch.Service('set');
function getAvailableLanguageCodes() {
const totalEntriesArr = new Int32Array(1);
const languageCodesBuf = new ArrayBuffer(20 * 8);
setSrv.dispatchOut(5, totalEntriesArr.buffer, {
bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.Out],
buffers: [languageCodesBuf],
});
const decoder = new TextDecoder();
const languageCodes: string[] = [];
// Process the output buffer into the list of language codes as strings
const totalEntries = totalEntriesArr[0];
for (let i = 0; i < totalEntries; i++) {
const data = languageCodesBuf.slice(i * 8, i * 8 + 8);
const nul = new Uint8Array(data).indexOf(0);
languageCodes.push(decoder.decode(data.slice(0, nul)));
}
return languageCodes;
}
console.log(getAvailableLanguageCodes());
```

### Example with output service object

In some cases, the result of a command is itself a new `Switch.Service` instance.

One such example is the [`clkrstOpenSession()` function](https://github.com/switchbrew/libnx/blob/e79dd7ac52cc7fdc41134c9b55e6f55ec8b8799f/nx/source/services/clkrst.c#L25-L34):

```c title="libnx/source/services/clkrst.c"
Result clkrstOpenSession(ClkrstSession* session_out, PcvModuleId module_id, u32 unk) {
const struct {
u32 module_id;
u32 unk;
} in = { module_id, unk };
return serviceDispatchIn(&g_clkrstSrv, 0, in,
.out_num_objects = 1,
.out_objects = &session_out->s,
);
}
```
In this case, you can create an "uninitialized" `Switch.Service` instance (by
omitting the `name` from the constructor), and then pass the instance to the
`outObjects` option in the dispatch function:
```typescript title="src/main.ts"
const clkrstSrv = new Switch.Service('clkrst');
function openSession(moduleId: number) {
const sessionSrv = new Switch.Service();
const inArr = new Uint32Array([moduleId, 3]);
clkrstSrv.dispatchIn(0, inArr.buffer, {
outObjects: [sessionSrv],
});
return sessionSrv;
}
const moduleId = 0x40000001; // PcvModuleId_CpuBus
const session = openSession(moduleId);
// Dispatch additional commands to the session service instance
```
1 change: 1 addition & 0 deletions packages/constants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './errno';
export * from './fs';
export * from './gamepad';
export * from './hid';
export * from './service';
export * from './swkbd';
16 changes: 16 additions & 0 deletions packages/constants/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export enum SfBufferAttr {
In = 1 << 0,
Out = 1 << 1,
HipcMapAlias = 1 << 2,
HipcPointer = 1 << 3,
FixedSize = 1 << 4,
HipcAutoSelect = 1 << 5,
HipcMapTransferAllowsNonSecure = 1 << 6,
HipcMapTransferAllowsNonDevice = 1 << 7,
}

export enum SfOutHandleAttr {
None = 0,
HipcCopy = 1,
HipcMove = 2,
}
5 changes: 5 additions & 0 deletions packages/runtime/src/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ProfileUid,
SaveData,
SaveDataCreationInfo,
Service,
Stats,
Versions,
} from './switch';
Expand Down Expand Up @@ -223,6 +224,10 @@ export interface Init {
nsAppNew(id: string | bigint | ArrayBuffer | null): Application;
nsAppNext(index: number): bigint | null;

// service.c
serviceInit(c: ClassOf<Service>): () => void;
serviceNew(name?: string): Service;

// software-keyboard.c
swkbdCreate(fns: {
onCancel: (this: VirtualKeyboard) => void;
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/switch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './irsensor';
export * from './profile';
export * from './album';
export * from './file-system';
export * from './service';
export { Socket, Server };

export type PathLike = string | URL;
Expand Down
80 changes: 80 additions & 0 deletions packages/runtime/src/switch/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { $ } from '../$';
import { proto, stub } from '../utils';

export interface ServiceDispatchParams {
//Handle target_session;
targetSession?: number;

//u32 context;
context?: number;

//SfBufferAttrs buffer_attrs;
//SfBuffer buffers[8];
bufferAttrs?: number[];
buffers?: ArrayBuffer[];

//bool in_send_pid;
inSendPid?: boolean;

//u32 in_num_objects;
//const Service* in_objects[8];
inObjects?: Service[];

//u32 in_num_handles;
//Handle in_handles[8];
inHandles?: number[];

//u32 out_num_objects;
//Service* out_objects;
// XXX: This seems to always be 1 (hence why its not an array?)
outObjects?: Service[];

//SfOutHandleAttrs out_handle_attrs;
//Handle* out_handles;
outHandleAttrs?: number[];
outHandles?: number[];
}

export class Service {
constructor(name?: string) {
return proto($.serviceNew(name), Service);
}

isActive() {
stub();
}

isDomain() {
stub();
}

isOverride() {
stub();
}

dispatch(rid: number, params?: ServiceDispatchParams) {
this.dispatchInOut(rid, undefined, undefined, params);
}

dispatchIn(rid: number, inData: ArrayBuffer, parmas?: ServiceDispatchParams) {
this.dispatchInOut(rid, inData, undefined, parmas);
}

dispatchOut(
rid: number,
outData: ArrayBuffer,
params?: ServiceDispatchParams,
) {
this.dispatchInOut(rid, undefined, outData, params);
}

dispatchInOut(
rid: number,
inData?: ArrayBuffer,
outData?: ArrayBuffer,
params?: ServiceDispatchParams,
) {
stub();
}
}
$.serviceInit(Service);
2 changes: 2 additions & 0 deletions source/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "nifm.h"
#include "ns.h"
#include "poll.h"
#include "service.h"
#include "software-keyboard.h"
#include "tcp.h"
#include "tls.h"
Expand Down Expand Up @@ -629,6 +630,7 @@ int main(int argc, char *argv[]) {
nx_init_irs(ctx, nx_ctx->init_obj);
nx_init_nifm(ctx, nx_ctx->init_obj);
nx_init_ns(ctx, nx_ctx->init_obj);
nx_init_service(ctx, nx_ctx->init_obj);
nx_init_tcp(ctx, nx_ctx->init_obj);
nx_init_tls(ctx, nx_ctx->init_obj);
nx_init_url(ctx, nx_ctx->init_obj);
Expand Down
Loading

0 comments on commit e5bd2d5

Please sign in to comment.