Rust on Dreamcast: Difference between revisions
Line 184: | Line 184: | ||
For example, this can be added to a standard <code>Makefile</code>-based KallistiOS project by editing the <code>Makefile</code>: | For example, this can be added to a standard <code>Makefile</code>-based KallistiOS project by editing the <code>Makefile</code>: | ||
<syntaxhighlight lang="make"> | <syntaxhighlight lang="make"> | ||
$(TARGET): $(OBJS) | $(TARGET): $(OBJS) | ||
kos-cc -o $(TARGET) $(OBJS) -L/opt/toolchains/dc/rust/examples/cargo-addlib/target/sh-elf/debug -laddlib | kos-cc -o $(TARGET) $(OBJS) -L/opt/toolchains/dc/rust/examples/cargo-addlib/target/sh-elf/debug -laddlib | ||
</syntaxhighlight> | |||
Then, we can use the code in our C source: | |||
<syntaxhighlight lang="c"> | |||
/* Declare the external function from the Rust library */ | |||
int add_integers(int a, int b); | |||
/* Use the function */ | |||
printf("Five plus six is %d\n", add_integers(5, 6)); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Revision as of 17:30, 18 February 2024
WIP: This article is currently under construction. The repos linked to below are not yet live.
Rust is a systems programming language rising in popularity which emphasizes memory safety and performance. Due to its operating at a low level, it is an ideal candidate for running on the Dreamcast. Doing so presents a bit of a challenge, however, as the official Rust compiler is based on the LLVM toolchain infrastructure, which does not support the Dreamcast CPU's SuperH architecture. Dreamcast programming is instead typically done with GCC, the GNU Compiler Collection. There are currently two viable solutions to this challenge:
- rustc_codegen_gcc: A libgccjit-based codegen backend for rustc (preferred method)
- gccrs: a Rust frontend for GCC
Neither solution is complete at this time, and both are under active development. Using either of them to target the Dreamcast should be considered experimental. rustc_codegen_gcc is quite further along, however, and is quite usable with some patience with its current limitations and rapid change. On the other hand, while gccrs can compile for Dreamcast, it is in a very early stage, with much of the language unimplemented and no libcore support. Below we will focus on using rustc_codegen_gcc. For more information on using gccrs, see the gccrs page.
Using rustc_codegen_gcc to develop on Dreamcast
With rustc_codegen_gcc, we can interface the standard rustc compiler frontend with libgccjit, a GCC code-generation API. With the help of the Rust-for-Dreamcast repo and the kos-rs crate containing KallistiOS bindings, we can set up rustc_codegen_gcc to compile Rust programs with core and alloc support (but not the entirety of std). Rust-for-Dreamcast includes wrapper scripts to invoke the rustc and cargo tools in a familiar way. The familiar borrow checker still works, and one can import and use no_std
crates. Despite this support, rustc_codegen_gcc is still in active development, so if using such a setup, expect that things may change rapidly over time. We will need to use some patches and workarounds to make this solution work. See the rustc_codegen_gcc progress reports for more information on the project's progress.
What Works
- libcore -- the core components of the language for running on bare metal (basics like integers, floats, enums, bools, chars, tuples, arrays, slices, closures, iterators, etc.)
- liballoc -- the core components of the language that require a heap, including collections (Vec, String, Box, etc.)
- linking to KallistiOS -- KallistiOS and kos-ports can be used if one manually manages interoperating with C via unsafe
- including
no_std
crates with thecargo
build system
Future Goals
- libc support -- Adding KallistiOS support to Rust's libc crate
- libstd support -- built-in language support for I/O, networking, threads, time and date, HashMap/HashSet, unwinding on panic, etc.
- KallistiOS bindings -- properly idiomatic Rust support for KallistiOS
- Inclusion as a tier 3 target officially
- Expansion of
cargo-dc
to support more dcdev-specific functionallity like generating Dreamcast disc images using metadata specified inCargo.toml
Prerequisites
We will build rustc_codegen_gcc support for the Dreamcast in the instructions below. Before we begin, though:
- You must already have a KallistiOS development environment set up. This means you have installed the typical dependencies, you have created a cross-compiling toolchain for SH4, you have set up your KallistiOS
environ.sh
file, and you have built KallistiOS with it. Ideally, you will already have at least some familiarity with KallistiOS dev already. See Getting Started with Dreamcast development for more information, as well as the KallistiOS Doxygen.- For the purposes of this guide, we will assume you are using the standard paths for Dreamcast development tools; i.e. your environment is set up in
/opt/toolchains/dc
. Some included scripts and examples may assume this. - Your KallistiOS installation will need its
KOS_SH4_PRECISION
setting set to-m4-single
. At this time, rustc_codegen_gcc support will not compile with KallistiOS's default-m4-single-only
setting. This setting can be changed in KallistiOS'senviron.sh
, but changing the setting may require you to rebuild your toolchain if you have not built it withm4-single
support (which is off by default, but can be enabled in theconfig.mk
file). Once you modify the setting in yourenviron.sh
and re-source theenviron.sh
, you'll need to rebuild KallistiOS with amake clean
andmake
for the changes to take effect. kos-ports being used will also need rebuilding with-m4-single
. Keep in mind, however, that because KallistiOS doesn't officially support-m4-single
yet, some things may be broken, especially libraries in kos-ports that haven't been heavily tested with this setting.
- For the purposes of this guide, we will assume you are using the standard paths for Dreamcast development tools; i.e. your environment is set up in
- You must already have a relatively up-to-date Rust installation, either using your operating system's package manager or rustup. Ideally, you will already have some familiarity with Rust's tools.
If you run into any errors or other challenges while following this tutorial, or simply need clarification on any of the steps, feel free to ask for assistance on the message board and we would be happy to aid you and update the guide for the benefit of future readers and others in the community.
Building a cross-compiling libgccjit.so for rustc_codegen_gcc
Before we can use rustc_codegen_gcc, we must compile libgccjit.so
, the libgccjit library, for your system. This entails building a unique copy of the SH4 toolchain in its own directory under /opt/toolchains/dc/rust
, using a forked version of GCC with enhancements made to libgccjit. The forked version is based on the latest GCC 14.0.1 development branch.
- NOTE: This forked version of GCC 14.0.1 with libgccjit changes is actively developed alongside rustc_codegen_gcc itself, so if you update your rustc_codegen_gcc installation, you may also need to rebuild libgccjit to pull down changes rustc_codegen_gcc depends upon.
We will first clone the rust-for-dreamcast
repository, which contains various supporting files needed to create Rust support for Dreamcast. Using git
, clone the rust-for-dreamcast
repository to /opt/toolchains/dc/rust
:
git clone https://github.com/darcagn/rust-for-dreamcast /opt/toolchains/dc/rust
Enter your KallistiOS installation's dc-chain
directory:
cd /opt/toolchains/dc/kos/utils/dc-chain
Clear out any existing build files:
make clean-keep-archives
Copy the necessary toolchain patches to your dc-chain
setup:
cp /opt/toolchains/dc/rust/toolchain/*.diff patches/
Copy the rustc_codegen_gcc configuration file into place:
cp /opt/toolchains/dc/rust/toolchain/config.mk.rustc.sample config.mk
Make any desired changes to this config.mk
configuration file (e.g., change makejobs=-j2
to the number of CPU threads you'd like to use during compilation), and then compile the SH4 toolchain:
make build-sh4
When this command is completed successfully, a new SH4 cross-compiler toolchain will exist at /opt/toolchains/dc/rust/sh-elf
and your libgccjit.so
will be installed to /opt/toolchains/dc/rust/sh-elf/lib/libgccjit.so
.
Building rustc_codegen_gcc
Clone the rustc_codegen_gcc to your rust directory:
git clone https://github.com/rust-lang/rustc_codegen_gcc.git /opt/toolchains/dc/rust/rustc_codegen_gcc
rustc_codegen_gcc needs a config.toml
file that specifies the location of libgccjit.so
. Let's write the the gcc-path
to the location of our libgccjit.so
library file in this file:
echo 'gcc-path = "/opt/toolchains/dc/rust/sh-elf/lib"' > /opt/toolchains/dc/rust/rustc_codegen_gcc/config.toml
The rust-for-dreamcast
repository contains scripts and wrappers to assist you in building rustc_codegen_gcc and using it in conjunction with cargo
and rustc
. We'll need to add the path to those scripts to our PATH
environment variable:
export PATH="/opt/toolchains/dc/rust/bin:$PATH"
You may also want to add the above lines to your shell's startup file or else you'll need to run them every time you open a new shell.
Now we can use the included scripts to set up rustc_codegen_gcc. Various patches need to be applied to rustc_codegen_gcc for it to compile properly for our target platform. Let's apply them:
rcg-dc patch
Now we can prepare and build rustc_codegen_gcc!
rcg-dc prepare rcg-dc build
Using Rust for Dreamcast
If all went well, rustc_codegen_gcc will have built successfully.
You can now use the scripts included in the Rust for Dreamcast repo:
rcg-dc
script can be used to rebuild the rustc_codegen_gcc code after updating or editing itrustc-dc
script can be used to compile Rust modulescargo-dc
script can be used to build Rust crates
Examples are included with the Rust for Dreamcast repo to help you get started:
cargo-hello
demonstrates how to create a simple "Hello, world!" application with KallistiOS usingcargo
cargo-cube
demonstrates a Rust project using KallistiOS with GLdccargo-addlib
demonstrates how to create a Rust library that can be included with a KallistiOS projectrustc-hello
demonstrates how to compile and include a Rust module into a standard KallistiOSMakefile
-based project
These examples rely on the kos-rs crate being present on your computer locally. This is in a separate repo, so let's pull it down now:
git clone https://github.com/darcagn/kos-rs /opt/toolchains/dc/rust/kos-rs
Creating a new Rust project with Cargo
We'll demonstrate creating a new "Hello, world!" project with cargo-dc
. This will follow the cargo-hello
example included in the rust-for-dreamcast
repo.
In a directory of your choosing, let's invoke cargo-dc
to create a new project and then enter the directory:
cargo-dc new hellow cd hellow
Let's add our kos-rs crate to gain access to current KallistiOS bindings. Open Cargo.toml
in your text editor and add:
[dependencies]
kos = { package = "kos-rs", path = "/opt/toolchains/dc/rust/kos-rs" }
Next, we'll need to let Cargo know about our custom link wrapper script. Create a .cargo
directory, and within it, a new config
file:
mkdir .cargo touch .cargo/config
Open this new .cargo/config
file in your text editor, add the following entry:
[target.sh-elf]
linker = "sh-link-wrapper"
Now we can open up src/main.rs
and write our "Hello, world!" example code:
#![no_std]
#![no_main]
extern crate alloc;
use kos::println;
#[no_mangle]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
println!("Hello, world!");
return 0;
}
#![no_std]
and#![no_main]
tell Rust that our project does not use the standard library and we will not have Rust use amain
function as an entry point -- this will be handled by KallistiOS.extern crate alloc;
tells Rust to use the alloc crate to gain access to heap-allocated types (in our case,String
).use kos::println!;
tells Rust to use theprintln!
macro defined in the kos-rs crate. With this, we can print output to ourdc-tool
console.#[no_mangle]
tells Rust to disable name mangling so that themain
function can be used by KallistiOS.- Finally, we have a
fn main
with the function signature of a typical Cmain
function, containing a basic "Hello, world!" exclamation.
Now we can use cargo-dc build
to build our project. If all goes well, there will be a target/sh-elf/debug/hellow.elf
file that can be sent to the Dreamcast with dc-tool
.
If you have KOS_LOADER
set in your KallistiOS environment, you can invoke it directly with cargo-dc run
.
Creating a Rust project using kos-ports libraries
The cargo-cube
example included in the rust-for-dreamcast
repo demonstrates creating a rotating 3D cube using Rust as the primary language, while calling C functions provided by the GLdc library available in kos-ports. This project's initial setup is done the same as the above cargo-hello
example.
NOTE: There is a currently a bug in GLdc when using -m4-single
, which is required when using Rust. A merge request has been submitted upstream for the issue. In the mean time, the cube will not appear correctly unless you apply the small changes shown in the merge request and rebuild the GLdc kos-port. To do this, change directory to $KOS_PORTS/libGL
, run make uninstall clean
, then run make fetch
to retrieve fresh sources. Open dist/libGL-1.0.0/GL/matrix.c
in your text editor and make these changes, then run make install
to compile and install the fixed GLdc.
We'll be using the GLdc graphics and libm math libraries, so we need to tell cargo-dc
to link them in. To do this, we'll edit our build.rs
file to add:
println!("cargo:rustc-link-lib=GL");
println!("cargo:rustc-link-lib=m");
We don't need to add the paths to these libraries, because this is already done for us in the kos-rs crate. However, if in your project you need to include a separate unique library path, you can do that like so:
println!("cargo:rustc-link-search=native={}", lib_path);
While not shown here, our build.rs
also demonstrates how to use a build script to convert JPG images to VQ-compressed textures with the vqenc
tool included with KallistiOS. These texture files are then included in our project using the include_bytes!
macro.
The workings of this example's source code are too great to detail here line-by-line, but the example demonstrates declaring and binding external C functions, constants, and structures and then using them in Rust code. Since the entirety of the example is C interop, the main()
source is wrapped in unsafe {}
. In the future, this would be much less necessary as higher level safe Rust bindings to KallistiOS and other libraries become mature.
Creating a Rust library
Next, we'll demonstrate creating a Rust library with cargo-dc
that can be included in other Dreamcast code. This will follow the cargo-addlib
example included in the rust-for-dreamcast
repo. Once again, this project's initial setup is done the same as the above cargo-hello
example, but you'll create the new project using cargo-dc new --lib addlib
to specify that we're creating a library. You'll also need to add the following text to this project's Cargo.toml
file:
[lib]
crate-type = ["staticlib"]
This tells Rust to build a static .a
library archive file from our code, which is located in src/lib.rs
:
#![no_std]
extern crate alloc;
use kos::print;
#[no_mangle]
pub extern "C" fn print_added(a: isize, b: isize) {
print!("{}", a + b);
}
#[no_mangle]
pub extern "C" fn add_integers(a: isize, b: isize) -> isize {
a + b
}
The source code here starts similarly to the "Hello, world!" example, except we don't need to specify #![no_main]
as this is a library which wouldn't have a main()
function anyway.
Two simple functions are provided: one for adding two integers and returning the result, and another for adding two integers and printing the result as text. Because these functions use #[no_mangle]
and are declared extern "C"
, they can be called by name in C code that links this library.
When built using cargo-dc build
, a target/sh-elf/debug/libaddlib.a
file will be generated. This can be linked into other projects to gain the use of these functions.
For example, this can be added to a standard Makefile
-based KallistiOS project by editing the Makefile
:
$(TARGET): $(OBJS)
kos-cc -o $(TARGET) $(OBJS) -L/opt/toolchains/dc/rust/examples/cargo-addlib/target/sh-elf/debug -laddlib
Then, we can use the code in our C source:
/* Declare the external function from the Rust library */
int add_integers(int a, int b);
/* Use the function */
printf("Five plus six is %d\n", add_integers(5, 6));
Compiling individual modules into object files with rustc
- This will follow the
rustc-hello
example
To incorporate Rust source files into a standard KallistiOS Makefile
-based project, you can use the rustc-dc
wrapper. If we assume the Rust module file is named example.rs
, you'll need to add example.o
as an object file in your Makefile
's OBJS =
declaration. Additionally, you'll need to add the following lines so that make
knows how to compile Rust modules into .o
object files:
%.o: %.rs
rustc-dc $< -o $@
Alternatively, you can add those lines to your KallistiOS Makefile.rules
file to avoid having to place it in every project's Makefile
.
An example "Hello, world!" program built in this style which also demonstrates basic C interoperation is included with the Rust-for-Dreamcast repository, located at examples/rustc-hello
.