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 Ruststd
library is not available. However, the ndless crate attempts to recreate as many of the features instd
. This means that any crates used will need to supportno_std
. This may involve configuring special features in yourCargo.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 ofnull
, 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
andcargo-generate
(install viacargo install cargo-make cargo-generate
)- The ndless toolchain installed
The following dependencies will be automatically installed when building a project:
- The
rust-src
component fromrustup
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 variableNDLESS_HOME
to the path to your installation of Ndless, i.e. the folder that yougit clone
d.For example, if you ran
git clone --recursive https://github.com/ndless-nspire/Ndless.git
in your home directory, you could addexport 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 yourPATH
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:
Type | Function |
---|---|
Title, message, OK Button | msg |
Title, message, 2 custom buttons | msg_2b |
Title, message, 3 custom buttons | msg_3b |
Title, message, string input with default | msg_input |
Title, message, numeric input with min/max | msg_numeric |
Title and two messages & numeric inputs with min/max | msg_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 Key
s 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
Description | Function |
---|---|
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
, bothtest.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.