Skip to content

System Flaws

Meziu edited this page May 4, 2024 · 9 revisions

Missing Functionality

In this section are listed modules and general functionality completely unusable when working with Rust3DS's toolchain.

std::process

The std::process module is unsupported in its entirety. Due to how the system works, userland applications are completely incapable of spawning or managing processes. In Horizon OS (the 3DS' operating system), the concept of parent/child processes doesn’t exist since all processes (which mainly address system-specific functionality) are managed directly by the firmware. However, std::env is available for compatibility purposes even though, since environment variables are not passed from process to process, you will have to manually set them via std::env::set_var. Beware of external crates and libraries which require access to processes.

backtrace-rs

Runtime stack traces aren't (and probably will never be) supported. Consider using gdb instead. You can read a guide to setup rust-gdb in our guides.

Structural Differences

In this section are listed quirks of the 3DS systems which cause abnormal behaviour in standard use cases. Some of these may completely change the way you should look at development on this system, even though APIs may be left unchanged.

Cooperative threading and std::thread

Horizon OS uses a "cooperative" threading model (SCHED_FIFO in scheduler terms), unlike modern operating systems (Windows, MacOS, Linux etc.) which use the "preemptive" model (SCHED_OTHER). This means that you need to explicitly tell every thread spawned when to stop (called "yielding") and let other threads run (in the preemptive model, threads simply stop after some time and wait for other code to run). Simply put, cooperative threads work exactly how the async/await logic works, in which the developer has to explicitly tell the task to .await some data. For cooperative threads using std, this is done via std::thread::sleep, std::thread::yield_now, or via internal waits in functions like Mutex::lock.

Luckily, Rust's std is runtime-agnostic, which means that both cooperative and preemptive multi-threaded code can be written with it. However, because of the prevalent use of the preemptive model in modern operating systems, nearly every library (and developer!) expects threads to run "by themselves". On a cooperative runtime this can cause some dangerous behaviour.

For example, on modern systems these threads are handled concurrently:

use std::thread;

let child_thread = thread::spawn(move || {
    // some work here
});

// some other work here

let res = child_thread.join(); // We join the child thread with `main` after finishing the work

This looks nice and clean. If you have worked with threads before, you probably expect that child_thread and the main thread will run concurrently. On a Nintendo 3DS, sadly, that wouldn't be true. Since the developer never told child_thread when to stop/wait, this code would instead run in order (first all of main's work and then the work in child_thread, since main started first for the FIFO scheduling).

Even worse, a situation like this could happen:

let child_thread = thread::spawn(move || {
    loop {} // endless loop
});

// very important work

Without any sort of yielding, the loop inside child_thread will permanently lock the main thread! Beware of deadlocks.

Thread Priority and CPU Affinity

Horizon OS lets the developer give some additional information when spawning a thread to manage it in the best possible way. This info is given in the form of 2 parameters:

  • Priority: available threads with higher priority will run before threads with lower priority.
  • CPU Affinity: which CPU core to run the thread on. By default new threads will run on core #1 but, depending on the console's model, you can have up to 3 available cores to run threads on.

In general, you should keep in mind this great difference when building a multi-threaded application for the 3DS. We suggest you to learn how to use cooperative threads before writing a multi-threaded app. You can also have a look at this page to see some documentation on how things work under-the-hood.

Regardless, other options are available, such as the possibility to use an async runtime like Tokio instead of system threads.