We recommend that if you've never used Rust before, you should check out the Getting started guide in addition to the Rust book. There are also many other great tools available at the Rust learning page. You should also check out the Rust website for up-to-date information.

Introduction

The TI-Nspire has many options for writing programs. The calculator officially supports Lua, and Ndless programs are typically written in C or C++. This project aims to add Rust to that list of supported languages.

What is Ndless?

Ndless is a popular program written for the TI-Nspire, which allows running arbitrary code, bypassing TI's restrictions. It has a complete toolchain for compiling, linking, and packaging TI-Nspire programs.

What is Rust?

Rust is a modern language that's main focus is safety. For example, if you never type the word unsafe in your code, your program will never segfault. It has no runtime or overhead (such as in Python, Go, Java, JS, or C#)—just straight machine code. Its zero-cost abstractions allow you to feel like you're writing in a high level language, but compiles down to exactly what you would write in C or assembly. Rust was the most loved language in the 2016, 2017, 2018, and 2019 StackOverflow Developer Survey. In benchmarks, Rust is usually in between C and C++ in terms of speed, but while maintaining an easy-to-use syntax with extremely helpful error messages, that often provide the fix in the message. Rust uses RAII, meaning that no garbage collector is needed, but there is no need to manually allocate and free memory. Rust combines speed of procedural languages with the ease-of-use of functional languages in one, creating one powerful language.

Note

As ndless does not feature a full OS, only #![no_std] features are available. These include the core, alloc, and ndless libraries. The Rust std library is not available. However, the ndless crate attempts to recreate as many of the features in std. This means that any crates used will need to support no_std. This may involve configuring special features in your Cargo.toml.

Why Rust?

  • Rust is fast: Rust is beats C++ in many cases, and C in some.
  • Rust is safe: Rust guarantees safety. What does that mean? Rust guarantees that you will never read past the end of a pointer, free something twice (or not at all), or leak memory unless you explicitly tell it to. Rust eliminates the concept of null, and instead replaces it with a common language item: Option. The best part: Rust has a concept of zero-cost abstractions. This means that safety doesn't have any performance penalties!
  • Rust has very good error messages: C++ has long and indecipherable error messages for simple problems. On the other hand, Rust error messages will explain exactly why your program didn't compile, give you a suggestion to fix it, and link you to examples of similar problems.
  • Package manager: Rust has a very good package manager, Cargo, which has many packages ready to be installed—just copy and paste a line from a package's page, and it will be automatically installed.
  • Documentation: One of the best reasons why Rust is useful is documentation. Web API docs are automatically generated for every package on Cargo, which provides examples, explanations, and types. Here's the documentation for the ndless crate.

Sample code

Here's a comparison of common tasks in C vs Rust.

Formatting text

C

(Yes, you could use printf directly. This is just to show how one would store an intermediate buffer, for purposes such as passing to another function.)

const char * msg = "message";
const int num = 5;
// First, get the size needed to store the string
// Don't forget the + 1! Otherwise, you will have a buffer overflow.
size_t needed = snprintf(NULL, 0, "%s: %d", msg, num) + 1;
// Then, make the buffer
char *buffer = malloc(needed);
// Finally, actually store it
sprintf(buffer, "%s: %d", msg, num);
// Print it
printf("%s\n", buffer);
// Later...
free(buffer);

Rust

fn main() {
let msg = "message";
let num = 5;
// All in one step
let buffer = format!("{}: {}", msg, num);
// Print it
println!("{}", buffer);
// the buffer will automatically be freed
}

Installing

This project has several requirements. Specifically, it requires:

  • An up-to-date nightly Rust build (nightly-2020-05-31 works). It's recommended to install via rustup.
  • cargo-ndless and cargo-generate (install via cargo install cargo-make cargo-generate)
  • The ndless toolchain installed

The following dependencies will be automatically installed when building a project:

  • The rust-src component from rustup

See the next page on how to do so on Unix-like platforms.

Installing on Linux & Mac

Install Rust

You need:

  • Ndless toolchain installed and added to path
  • Rustup installed
  • Latest Rust Nightly installed (nightly-2020-05-07 works)
  • Unix-like (tested on Linux, most likely Mac and Cygwin will work as well)

Complete install script:

curl https://sh.rustup.rs -sSf | sh # skip if rustup already installed
rustup install nightly # skip if nightly already installed
cargo install cargo-ndless

Install ndless

Follow the steps described in the Ndless wiki.

Pro tip

Although you can permanently add the ndless tools to your PATH as suggested in the wiki, you may not want to as that will mess with other projects that use arm-none-eabi-gcc and similar commands. Instead, you may set the environment variable NDLESS_HOME to the path to your installation of Ndless, i.e. the folder that you git cloned.

For example, if you ran git clone --recursive https://github.com/ndless-nspire/Ndless.git in your home directory, you could add export NDLESS_HOME="$HOME/Ndless" to the end of your ~/.bash_profile. If you're using cargo-ndless as suggested, the correct directories will automatically be added to your PATH variable.

Creating a Project

Run cargo generate --git https://github.com/lights0123/nspire-rust-template.git. It will prompt you to name it. After naming your project, it will create a new folder with a Rust template.

Get started by running

cargo +nightly ndless build

to start development. Your .tns file will be available in target/armv5te-nspire-eabi/debug/{{project-name}}.tns.

When you're ready to release your application, don't forget to compile in release mode with

cargo +nightly ndless build -- --release

Your .tns file will be available in target/armv5te-nspire-eabi/release/{{project-name}}.tns.

If you have the Firebird emulator installed, you can also send the compiled binary straight to it. Just run:

cargo +nightly ndless run
cargo +nightly ndless run -- --release

You may skip +nightly if you set nightly as your default compiler (rustup default nightly), or set a directory override.

Sample projects

There's a few examples of ndless programs written in Rust. See the example-nspire repository for some generic examples. Additionally, the n-89 and n-flashcards projects are written in Rust. Note that these may contain older methods of writing code. This book and the API documents of the various ndless-rs crates are the most up-to-date sources of information.

Interacting with the System

The Nspire has many different parts, many of which can be interacted with in Rust.

Message Boxes

There are various types of message boxes with ndless. The following message boxes are supported:

TypeFunction
Title, message, OK Buttonmsg
Title, message, 2 custom buttonsmsg_2b
Title, message, 3 custom buttonsmsg_3b
Title, message, string input with defaultmsg_input
Title, message, numeric input with min/maxmsg_numeric
Title and two messages & numeric inputs with min/maxmsg_2numeric

Use it like this:

fn main() {
	ndless::msg::msg("Title", "Message");
}

Message boxes with more than one button return a variant of Button. Use it like:

fn main() {
	let button_pressed = msg_3b("Hello", "Hello, World!", "1", "2", "3");
	let message = match button_pressed {
		Button::ONE => "one",
		Button::TWO => "two",
		Button::THREE => "three",
	};
}

Pressing escape will return the first button.

Getting Input

Currently, the ndless crate supports input from the keyboard and touchpad. The input module contains these functions.

Use get_keys to get a Vec of Keys that are currently being pressed. An empty Vec will be returned if no keys are currently pressed. See the following example:

use ndless::input::{get_keys, Key};

let keys = get_keys();
if keys.len() == 0 { /* No keys currently pressed */ }

However, a more efficient way is to use iter_keys, which does not allocate. However, it must be used immediately, as each iteration of the loop checks if the key is being pressed at that time. For example:

use ndless::prelude::*;
use ndless::input::iter_keys;
for key in iter_keys() {
    println!("Key {:?} is being pressed.", key);
}

Additionally, it may be used like any other Iterator in Rust:

// Print all keys except escape
use ndless::prelude::*;
use ndless::input::{iter_keys, Key};
iter_keys()
    .filter(|key| key != Key::Esc)
    .for_each(|key| println!("Key {:?} is being pressed.", key));

Simple (boolean) functions

DescriptionFunction
Returns true if the specific key is pressed. Note that you may pass either an owned Key or a borrowed &Key.is_key_pressed
Returns true if any buttons are currently pressed, including pushing the touchpad.any_key_pressed
Returns true if the "On" key is currently pressed.key_on_pressed
Suspends the program until any_key_pressed returns true.wait_key_pressed
Suspends the program until any_key_pressed returns false.wait_no_key_pressed

File I/O

The ndless crate has the ability to interact with the nspire's file system. It does so by maintaining an API similar to that of the Rust standard library. Check out the documentation for up-to-date information.

Note

If an absolute path is not specified, all paths are relative to the executable. For example, /documents/test.tns will always reference that file. However, if the executable is at /documents/ndless/app.tns, both test.tns and ./test.tns will refer to /documents/ndless/test.tns. This is different from the default behavior of the C version.

Remember that all paths on the TI-Nspire start with /documents, and all visible files have the extension .tns.

Many examples here use several advanced Rust features. If you are new to the language, we recommend checking out the question mark and returning from main features.

Creating a new file

use ndless::prelude::*;
use ndless::fs::File;
use ndless::io::prelude::*;

fn main() -> ndless::io::Result<()> {
    let mut file = File::create("/documents/test.txt.tns")?;
    file.write_all(b"This replaces the contents of test.txt.tns, or creates the file")?;
    Ok(())
}

Another option to do the same thing:

use ndless::prelude::*;
use ndless::fs::OpenOptions;
use ndless::io::prelude::*;

fn main() -> ndless::io::Result<()> {
    let mut file = OpenOptions::new().write(true).open("/documents/test.txt.tns")?;
    file.write_all(b"This replaces the contents of test.txt.tns, or creates the file")?;
    Ok(())
}

Reading a file

use ndless::prelude::*;
use ndless::fs::File;
use ndless::io::prelude::*;

fn main() -> ndless::io::Result<()> {
    let mut file = File::open("/documents/test.txt.tns")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    assert_eq!(contents, "This replaces the contents of test.txt.tns, or creates the file!");
    Ok(())
}

Another option to do the same thing:

use ndless::prelude::*;
use ndless::fs::OpenOptions;
use ndless::io::prelude::*;

fn main() -> ndless::io::Result<()> {
    let mut file = OpenOptions::new().read(true).open("/documents/test.txt.tns")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    assert_eq!(contents, "This replaces the contents of test.txt.tns, or creates the file!");
    Ok(())
}

Viewing the contents of a directory

Be sure to check out the docs on this, as there are great examples available.

use ndless::prelude::*;
use ndless::fs::PathBuf;

fn main() -> ndless::io::Result<()> {
    let path = PathBuf::from("/documents");
    let files = path.read_dir()?;
    for file in files {
        let file = file?;
        println!("Path: {:?}", file.path());
    }
    Ok(())
}

Another option to do the same thing:

use ndless::prelude::*;
use ndless::fs;

fn main() -> ndless::io::Result<()> {
    let files = fs::read_dir("/documents");
    for file in files {
        let file = file?;
        println!("Path: {:?}", file.path());
    }
    Ok(())
}

Currently, graphics is done with SDL. Use the ndless-sdl crate to do so. You'll first need to initialize the screen at the beginning of your main function:

use ndless_sdl::{SurfaceFlag, VideoFlag};
ndless_sdl::init(&[ndless_sdl::InitFlag::Video]);
let screen = ndless_sdl::video::set_video_mode(
    320,
    240,
    16,
    &[SurfaceFlag::SWSurface],
    &[VideoFlag::NoFrame],
)
.expect("failed to set video mode");

Then, at the end, stop it:

ndless_sdl::quit();

Now, you can draw on the screen with the screen variable, which is a Surface. Typically, you'll want to use fill_rect to draw rectangles, draw_str to draw low-res text, and blit_rect to draw images loaded by the image module. When you're ready to display to the screen, call screen.flip(). For a basic example, see hello-world-sdl. For a more advanced example loading a real font, see hello-world-text. For a real program using fonts and images, see n-flashcards.

Rust has many features that make it great to use. Check out some of them listed here, or as listed on the Rust book.

The question mark operator, ?

When returning a Result from a function, it can get tedious to have to continuously check for errors from each function. Use the question mark operator to automatically return the error if one occurs. Check out the docs for more on this topic.

Note that this can also be used with Option as well, where using the ? operator will return None if the value is None.

Returning from main

In some code, especially related to File I/O, error handling is important. If you would prefer to allow your application to exit with an error while still freeing memory and closing files without creating your own error handler, this feature is perfect for you. Simply return a Result from your function. If an error occurs, then a message box will be displayed that describes the error. This is different from using .unwrap or .expect, as resources will be properly released, and a nicer error is shown.

You can allow this to work on arbitrary types. Follow the documentation of the Termination trait.

About

This project was created by Ben Schattinger (GitHub) as an easier way to write Ndless apps for the TI-Nspire. I was frustrated with the odd parts of C and C++ and the need to remember to manually free memory.

Feel free to improve this book by submitting a Pull Request. Feel free to any questions by opening an issue on the ndless-rs repository.