Safe and idiomatic Rust wrapper around [`libctru`](https://github.com/devkitPro/libctru).
## Getting Started
Thoroughly read the [`ctru-rs` wiki](https://github.com/rust3ds/ctru-rs/wiki/Getting-Started) to meet the requirements
and to understand what it takes to develop homebrew software on the Nintendo 3DS family of consoles.
After that, you can simply add the crate as a dependency to your project and build your final binary by using [`cargo-3ds`](https://github.com/rust3ds/cargo-3ds)
or by manually compiling for the `armv6k-nintendo-3ds` target.
## Examples
Many examples to demonstrate the `ctru-rs` functionality are available in the [`examples`](./examples/) folder. Simply run them via
```bash
cargo 3ds run --example <example-name>
```
## License
This project is distributed under the Zlib license.
//! This is similar to the `hello-world` example, with the main difference of using 2 virtual `Console`s that can be alternated to print on both screens.
/// Guest Mii's won't be available regardless of their allowlist/blocklist state if the [`MiiSelector`] is run without setting [`Options::ENABLE_GUESTS`].
/// Look into [`MiiSelector::set_options()`] to see how to work with options.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// Guest Mii's won't be available regardless of their allowlist/blocklist state if the [`MiiSelector`] is run without setting [`Options::ENABLE_GUESTS`].
/// Look into [`MiiSelector::set_options()`] to see how to work with options.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// let mut mii_selector = MiiSelector::new();
///
/// // Blocklist the guest Mii at index 1 so that it cannot be selected.
/// Depending on the configuration, the Mii Selector window will appear either on the bottom screen (default behaviour) or the top screen (see [`Options::USE_TOP_SCREEN`]).
///
/// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::applets::mii_selector::{MiiSelector, Options};
///
/// let mut mii_selector = MiiSelector::new();
/// mii_selector.set_title("Select a Mii!");
///
/// let opts = Options::ENABLE_CANCEL & Options::ENABLE_GUESTS;
//! Applets are small integrated programs that the OS makes available to the developer to streamline commonly needed functionality.
//! Thanks to these integrations the developer can avoid wasting time re-implementing common features and instead use a more reliable base for their application.
//!
//! Unlike [services](crate::services), applets aren't accessed via a system subprocess (which would require obtaining a special handle at runtime).
//! Instead, the application builds a configuration storing the various parameters which is then used to "launch" the applet.
//!
//! Applets block execution of the thread that launches them as long as the user doesn't close the applet.
//! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text.
// TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. Improve `number of buttons` API when creating a new SoftwareKeyboard.
// TODO: Split the Parental PIN lock operations into a different type.
//! The [`Console`] works as a virtual shell that renders on screen all output of `stdout`. As such, it is useful as a basic interface to show info to the user,
//! such as in simple "Hello World" applications or more complex software that does not need much user interaction.
//!
//! Have a look at [`Soc::redirect_to_3dslink()`](crate::services::soc::Soc::redirect_to_3dslink) for a better alternative when debugging applications.
usestd::cell::RefMut;
usestd::default::Default;
@ -7,31 +14,129 @@ use crate::services::gfx::Screen;
@@ -7,31 +14,129 @@ use crate::services::gfx::Screen;
/// Error enum for generic errors within [`Console`].
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pubenumError{
/// The coordinate specified on the given axis exceeds the limits imposed by the [`Console`] window.
CoordinateOutOfBounds(Axis),
/// The size specified for the given dimension exceeds the limits imposed by the [`Console`] window.
DimensionOutOfBounds(Dimension),
}
/// 2D coordinate axes.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pubenumAxis{
X,
Y,
}
/// 2D dimensions.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pubenumDimension{
Width,
Height,
}
/// Virtual text console.
///
/// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen.
/// This means that any text written to `stdout` and `stderr` (e.g. using `println!`, `eprintln!` or `dbg!`) will become visible in the area taken by the console.
///
/// # Notes
///
/// The [`Console`] will take full possession of the screen handed to it as long as it stays alive. It also supports some ANSI codes, such as text color and cursor positioning.
/// The [`Console`]'s window size will be:
/// - 40x30 on the [`BottomScreen`](crate::services::gfx::BottomScreen).
/// - 50x30 on the normal [`TopScreen`](crate::services::gfx::TopScreen).
/// - 100x30 on the [`TopScreen`](crate::services::gfx::TopScreen) when wide mode is enabled.
///
/// # Alternatives
///
/// If you'd like to see live standard output while running the application but cannot or do not want to show the text on the 3DS itself,
/// you can try using [`Soc::redirect_to_3dslink`](crate::services::soc::Soc::redirect_to_3dslink) while activating the `--server` flag for `3dslink` (also supported by `cargo-3ds`).
/// More info in the [`cargo-3ds` docs](https://github.com/rust3ds/cargo-3ds#running-executables).
#[doc(alias = "PrintConsole")]
pubstructConsole<'screen>{
context: Box<PrintConsole>,
_screen: RefMut<'screen,dynScreen>,
screen: RefMut<'screen,dynScreen>,
}
impl<'screen>Console<'screen>{
/// Initialize a console on the chosen screen, overwriting whatever was on the screen
/// previously (including other consoles). The new console is automatically selected for
/// printing.
/// Initialize a console on the chosen screen.
///
/// # Notes
///
/// [Console] automatically takes care of flushing and swapping buffers for its screen when printing.
/// This operation overwrites whatever was on the screen before the initialization (including other [`Console`]s)
/// and changes the [`FramebufferFormat`](crate::services::gspgpu::FramebufferFormat) of the selected screen to better suit the [`Console`].
///
/// The new console is automatically selected for printing.
///
/// [`Console`] automatically takes care of flushing and swapping buffers for its screen when printing.
///
/// # Panics
///
/// If the [`Gfx`](crate::services::gfx::Gfx) service was initialised via [`Gfx::with_formats_vram()`](crate::services::gfx::Gfx::with_formats_vram)
/// this function will crash the program with an ARM exception.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::console::Console;
/// use ctru::services::gfx::Gfx;
///
/// // Initialize graphics (using framebuffers allocated on the HEAP).
/// let gfx = Gfx::new()?;
///
/// // Create a `Console` that takes control of the upper LCD screen.
/// let top_console = Console::new(gfx.top_screen.borrow_mut());
//! Among these features, [`ctru-rs`](crate) also automatically includes functionality to properly integrate the Rust `std` with the console's operating system,
//! which the developer would otherwise need to implement manually.
//!
//! # Usage
//!
//! Thoroughly read the official [`ctru-rs` wiki](https://github.com/rust3ds/ctru-rs/wiki) which guides you through the setup needed to install the required toolchain and helpful tools.
//! After following the guide and understanding the many quirks of the Nintendo 3DS homebrew development environment, you can create a new project by including this crate as a dependency
//! in your `Cargo.toml` manifest and build your binaries either manually (for the `armv6k-nintendo-3ds` target) or via [`cargo-3ds`](https://github.com/rust3ds/cargo-3ds).
#![crate_type = "rlib"]
#![crate_name = "ctru"]
#![feature(test)]
#![warn(missing_docs)]
#![feature(custom_test_frameworks)]
#![feature(try_trait_v2)]
#![feature(allocator_api)]
#![feature(nonnull_slice_from_raw_parts)]
#![test_runner(test_runner::run)]
#![test_runner(test_runner::run_gdb)]// TODO: does this make sense to have configurable?
/// Some values are not ordered *like* the Mii Editor UI. The mapped values can be seen [here](https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values).
///
/// This struct is returned by the [``MiiSelector``](crate::applets::mii_selector::MiiSelector)
/// This struct can be retrieved by [`MiiSelector::launch()`](crate::applets::mii_selector::MiiSelector::launch).
#[derive(Clone, Debug)]
pubstructMiiData{
puboptions: MiiDataOptions,
pubstructMii{
/// Mii options.
puboptions: Options,
/// Position taken by the Mii on the Mii Selector screen.
pubselector_position: SelectorPosition,
/// Console the Mii was created on.
pubconsole_identity: ConsoleIdentity,
/// Unique system ID, not dependant on the MAC address
pubsystem_id: [u8;8],
/// Console's MAC address.
pubmac_address: [u8;6],
/// General information about the Mii.
pubdetails: Details,
/// Mii name.
pubname: String,
/// Mii height.
pubheight: u8,
/// Mii width.
pubwidth: u8,
/// Face details.
pubface_details: FaceDetails,
/// Hair details.
pubhair_details: HairDetails,
/// Eyes details.
pubeye_details: EyeDetails,
/// Eyebrow details.
pubeyebrow_details: EyebrowDetails,
/// Nose details.
pubnose_details: NoseDetails,
/// Mouth details.
pubmouth_details: MouthDetails,
/// Mustache details.
pubmustache_details: MustacheDetails,
/// Beard details.
pubbeard_details: BeardDetails,
pubglass_details: GlassDetails,
/// Glasses details.
pubglass_details: GlassesDetails,
/// Mole details.
pubmole_details: MoleDetails,
/// Name of the Mii's original author.
pubauthor_name: String,
}
implFrom<ctru_sys::MiiData>forMiiData{
implFrom<ctru_sys::MiiData>forMii{
fnfrom(mii_data: ctru_sys::MiiData)-> Self{
letraw_mii_data=mii_data._bindgen_opaque_blob;
// Source for the representation and what each thing means: https://www.3dbrew.org/wiki/Mii
@ -272,7 +360,7 @@ impl From<ctru_sys::MiiData> for MiiData {
@@ -272,7 +360,7 @@ impl From<ctru_sys::MiiData> for MiiData {
/// Returns `true` if the application is running in the foreground as normal.
///
/// # Notes
///
/// This function is called as such since it automatically handles all checks for Home Menu switching, Sleep mode and other events that could take away control from the application.
/// For this reason, its main use is as the condition of a while loop that controls the main logic for your program.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// use std::error::Error;
/// use ctru::services::apt::Apt;
///
/// // In a simple `main` function, the structure should be the following.
/// fn main() -> Result<(), Box<dyn Error>> {
///
/// let apt = Apt::new()?;
///
/// while apt.main_loop() {
/// // Main program logic should be written here.
/// }
///
/// // Optional clean-ups after running the application should be written after the main loop.
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "aptMainLoop")]
pubfnmain_loop(&self)-> bool{
unsafe{ctru_sys::aptMainLoop()}
}
/// Set (in percentage) the amount of time to lend to the application thread spawned on the syscore (core #1).
///
/// # Notes
///
/// It is necessary to set a time limit before spawning threads on the syscore.
/// The percentage value must be withing 5% and 89%, though it is suggested to use lower values (around 30-45%) to avoid slowing down the OS processes.
/// The new `Gfx` instance will allocate the needed framebuffers in the CPU-GPU shared memory region (to ensure compatibiltiy with all possible uses of the `Gfx` service).
/// As such, it's the same as calling:
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # use ctru::services::gspgpu::FramebufferFormat;
//! System services used to handle system-specific functionalities.
//! OS services used to handle system-specific functionality.
//!
//! Most of the 3DS console's functionalities (when writing homebrew) are locked behind services,
//! Most of the 3DS console's functionalities (when writing user-land homebrew) are accessible via services,
//! which need to be initialized before accessing any particular feature.
//!
//! Some include: button input, audio playback, graphics rendering, built-in cameras, etc.
//! To ensure safety while using the underlying services, [`ctru-rs`](crate) leverages Rust's lifetime model.
//! After initializing the handle for a specific service (e.g. [`Apt`](apt::Apt)) the service will be accessible as long as there is at least one handle "alive".
//! As such, handles should be dropped *after* the use of a specific service. This is particularly important for services which are necessary for functionality
//! "outside" their associated methods, such as [`RomFS`](romfs::RomFS), which creates an accessible virtual filesystem, or [`Soc`](soc::Soc),
//! which enables all network communications via sockets.
//!
//! In [`ctru-rs`](crate) some services only allow a single handle to be created at a time, to ensure a safe and controlled environment.
//! The NDSP service is used to handle communications to the DSP processor present on the console's motherboard.
//! Thanks to the DSP processor the program can play sound effects and music on the console's built-in speakers or to any audio device
//! connected via the audio jack.
//!
//! To use NDSP audio, you will need to dump DSP firmware from a real 3DS using
//! something like [DSP1](https://www.gamebrew.org/wiki/DSP1_3DS).
//!
//! `libctru` expects to find it at `sdmc:/3ds/dspfirm.cdc` when initializing the NDSP service.
#![doc(alias = "audio")]
// As a result of requiring DSP firmware to initialize, all of the doctests in
// this module are `no_run`, since Citra doesn't provide a stub for the DSP firmware:
// https://github.com/citra-emu/citra/issues/6111
pubmodwave;
usewave::{Wave,WaveStatus};
usewave::{Status,Wave};
usecrate::error::ResultCode;
usecrate::services::ServiceReference;
@ -14,60 +28,101 @@ use std::sync::Mutex;
@@ -14,60 +28,101 @@ use std::sync::Mutex;
constNUMBER_OF_CHANNELS: u8=24;
/// Audio output mode.
#[doc(alias = "ndspOutputMode")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pubenumOutputMode{
/// Single-Channel.
Mono=ctru_sys::NDSP_OUTPUT_MONO,
/// Dual-Channel.
Stereo=ctru_sys::NDSP_OUTPUT_STEREO,
/// Surround.
Surround=ctru_sys::NDSP_OUTPUT_SURROUND,
}
/// PCM formats supported by the audio engine.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pubenumAudioFormat{
/// PCM 8bit single-channel.
PCM8Mono=ctru_sys::NDSP_FORMAT_MONO_PCM8,
/// PCM 16bit single-channel.
PCM16Mono=ctru_sys::NDSP_FORMAT_MONO_PCM16,
/// PCM 8bit interleaved dual-channel.
PCM8Stereo=ctru_sys::NDSP_FORMAT_STEREO_PCM8,
/// PCM 16bit interleaved dual-channel.
PCM16Stereo=ctru_sys::NDSP_FORMAT_STEREO_PCM16,
}
/// Representation of volume mix for a channel.
/// Representation of the volume mix for a channel.
#[derive(Copy, Clone, Debug, PartialEq)]
pubstructAudioMix{
raw: [f32;12],
}
/// Auxiliary Device index.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(usize)]
pubenumAuxDevice{
/// Aux device with index 0.
Zero=0,
/// Aux device with index 1.
One=1,
}
/// Interpolation used between audio frames.
#[doc(alias = "ndspInterpType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pubenumInterpolationType{
/// Polyphase interpolation.
Polyphase=ctru_sys::NDSP_INTERP_POLYPHASE,
/// Linear interpolation.
Linear=ctru_sys::NDSP_INTERP_LINEAR,
/// No interpolation.
None=ctru_sys::NDSP_INTERP_NONE,
}
/// Errors returned by [`ndsp`](self) functions.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pubenumNdspError{
/// Channel ID
pubenumError{
/// Channel with the specified ID does not exist.
InvalidChannel(u8),
/// Channel ID
/// Channel with the specified ID is already being used.
ChannelAlreadyInUse(u8),
/// Channel ID
/// The wave is already busy playing in the channel with the specified ID.
WaveBusy(u8),
/// Sample amount requested, Max sample amount
/// The sample amount requested was larger than the maximum.
SampleCountOutOfBounds(usize,usize),
}
/// NDSP Channel representation.
///
/// There are 24 individual channels in total and each can play a different audio [`Wave`] simultaneuosly.
///
/// # Default
///
/// NDSP initialises all channels with default values on initialization, but the developer is supposed to change these values to correctly work with the service.
///
/// In particular:
/// - Default audio format is set to [`AudioFormat::PCM16Mono`].
/// - Default sample rate is set to 1 Hz.
/// - Default interpolation type is set to [`InterpolationType::Polyphase`].
/// - Default mix is set to [`AudioMix::default()`]
///
/// The handle to a channel can be retrieved with [`Ndsp::channel()`]
pubstructChannel<'ndsp>{
id: u8,
_rf: RefMut<'ndsp,()>,// we don't need to hold any data
}
staticNDSP_ACTIVE: Mutex<usize>=Mutex::new(0);
staticNDSP_ACTIVE: Mutex<()>=Mutex::new(());
/// Handler of the DSP service and DSP processor.
/// Handle to the DSP service.
///
/// This is the main struct to handle audio playback using the 3DS' speakers and headphone jack.
/// Only one "instance" of this struct can exist at a time.
/// Only one handle for this service can exist at a time.
//! This service lets the application access a virtual mounted device created using a folder included within the application bundle.
//! After mounting the RomFS file system, the included files and folders will be accessible exactly like any other file, just by using the drive prefix `romfs:/<file-path>`.
//!
//! # Usage
//!
//! This module only gets compiled if the configured RomFS directory is found and the `romfs`
//! feature is enabled.
//!
//! Configure the path in Cargo.toml (the default path is "romfs"). Paths are relative to the
//! Configure the path in your project's `Cargo.toml` manifest (the default path is "romfs"). Paths are relative to the
//! `CARGO_MANIFEST_DIR` environment variable, which is the directory containing the manifest of
//! your package.
//!
@ -11,6 +16,16 @@
@@ -11,6 +16,16 @@
//! [package.metadata.cargo-3ds]
//! romfs_dir = "romfs"
//! ```
//!
//! Alternatively, you can include the RomFS archive manually when building with `3dsxtool`.
//!
//! # Notes
//!
//! `std::path` has problems when parsing file paths that include the `romfs:` prefix.
//! As such, it's suggested to use the paths directly or to do simple append operations to avoid unexpected behaviour.
//! Related [issue](https://github.com/rust-lang/rust/issues/52331).
#![doc(alias = "embed")]
#![doc(alias = "filesystem")]
usecrate::error::ResultCode;
usestd::ffi::CStr;
@ -18,17 +33,37 @@ use std::sync::Mutex;
@@ -18,17 +33,37 @@ use std::sync::Mutex;
usecrate::services::ServiceReference;
/// Handle to the RomFS service.
pubstructRomFS{
_service_handler: ServiceReference,
}
staticROMFS_ACTIVE: Mutex<usize>=Mutex::new(0);
staticROMFS_ACTIVE: Mutex<()>=Mutex::new(());
implRomFS{
/// Mount the bundled RomFS archive as a virtual drive.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::romfs::RomFS;
///
/// let romfs = RomFS::new()?;
///
/// // Remember to include the RomFS archive and to use your actual files!
/// let contents = std::fs::read_to_string("romfs:/test-file.txt");
//! By using this service the program enables the use of network sockets and utilities such as those found in `std::net`, which are completely inaccessible by default.
//! As such, remember to hold a handle to this service handle while using any network functionality, or else the `std::net` methods will return generic OS errors.
#![doc(alias = "socket")]
#![doc(alias = "network")]
uselibc::memalign;
usestd::net::Ipv4Addr;
@ -8,37 +13,66 @@ use crate::error::ResultCode;
@@ -8,37 +13,66 @@ use crate::error::ResultCode;
usecrate::services::ServiceReference;
usecrate::Error;
/// Network socket service
///
/// Initializing this service will enable the use of network sockets and utilities
/// such as those found in `std::net`. The service will close once this struct gets dropped.
/// Handle to the Network Socket service.
pubstructSoc{
_service_handler: ServiceReference,
sock_3dslink: libc::c_int,
}
staticSOC_ACTIVE: Mutex<usize>=Mutex::new(0);
staticSOC_ACTIVE: Mutex<()>=Mutex::new(());
implSoc{
/// Initialize the Soc service with a default buffer size of 0x100000 bytes
/// Initialize a new service handle using a socket buffer size of `0x100000` bytes.
///
/// # Errors
///
/// This function will return an error if the `Soc` service is already initialized
/// This function will return an error if the [`Soc`] service is already being used.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
///
/// let soc = Soc::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "socInit")]
pubfnnew()-> crate::Result<Self>{
Self::init_with_buffer_size(0x100000)
}
/// Initialize the Soc service with a custom buffer size in bytes. The size should be
/// 0x100000 bytes or greater.
/// Initialize a new service handle using a custom socket buffer size.
///
/// The size should be `0x100000` bytes or greater.
///
/// # Errors
///
/// This function will return an error if the `Soc` service is already initialized
/// This function will return an error if the [`Soc`] service is already being used.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
///
/// let soc = Soc::init_with_buffer_size(0x100000)?;