Rust and OpenGL from scratch - Window
Welcome back!
Previously, we got some motivation to use Rust and configured our development environment.
In this lesson, we will create a window! If that does not sound too exciting, we will also learn about Rust libraries. Very exciting.
A window
To create a window that works across multiple platforms, as well as provides such niceties as OpenGL context or multi-platform input, we will use SDL2.
Crates
Rust libraries are called crates
, and the Rust crate manager is cargo
command-line tool.
Crates can be found by searching central Rust crate repository at crates.io.
A new Rust library can be created either manually or using cargo new library-name
command.
The directory structure for a new library would look quite similar to our first hello-world project:
library-name
src
lib.rs
Cargo.toml
The difference from executable project, is that instead of main.rs
there is lib.rs
in src
directory.
This is configuration by convention, and it could be changed if needed.
Cargo.toml
file describes the crate. It contains crate identifier, list of dependencies, links to
documentation, and many other things explained in cargo documentation book.
SDL2 crate
As you may know, libsdl is, in fact, a C project.
The sdl2
crate, however, is a safe Rust wrapper around
SDL2 C API.
Let’s go to crates.io and search for sdl2
.
On sdl2 crate page, you can find links to documentation, github repository.
A note about SDL choice
For better or worse, I have picked SDL for this tutorial. However, you should be aware that there are Rust alternatives. For example winit crate has an API that is very similar to Rust-SDL.
So if the SDL2 does not work, or you want to go with more Rusty solution,
try winit
! The next lessons won’t use anything else but window events from
SDL2 anyways.
Documentation
Library authors can provide link to crate documentation in their Cargo.toml
file. This is that link.
It contains API reference.
Github repository
This links to github rust-sdl2
project. This page may also contain documentation, in the root
README.md
file rendered by github. In case of sdl2
crate, it contains information of how to set-up
SDL2.
Dependencies
Let’s create a new project named game
.
> cargo new --bin game
Add [dependencies]
section to Cargo.toml
file, with sdl2
crate as dependency:
(Cargo.toml, incomplete)
[dependencies]
sdl2 = "0.31.0"
At the time of writing this, the latest sdl2
crate version was 0.31.0
. You can input the exact same
version to make sure everything compiles.
Specifying a dependency allows the crate to be downloaded.
To use it, we then need to reference it.
Crate reference
At the top of main.rs
file, reference sdl2
by writing
extern crate sdl2;
This “mounts” sdl2 crate at our crate root module. Sdl functions can then be referenced
like sdl2::init()
.
Initializing SDL
Replace println!("Hello, world!");
with the code to initialize sdl:
(full main.rs is provided)
extern crate sdl2;
fn main() {
let _sdl = sdl2::init().unwrap();
}
We will go over every detail in the code soon. But first, run it!
…
It is unlikely that worked, unless you already had SDL2 installed and was not using Windows!
I got this error:
error: linking with `link.exe` failed: exit code: 1181
|
= note: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\...
= note: LINK : fatal error LNK1181: cannot open input file 'SDL2.lib'
error: aborting due to previous error
error: Could not compile `lesson-01-window`.
This is easily fixed on OSX or Linux by installing libsdl2-dev package (sdl2 on homebrew). Everything is explained in detail in rust-sdl2/README.md.
For windows, we may learn what build scripts are, create a rust “script” that runs
at compile time, tells cargo
how to link lib files as well as copies dll files to executable
directory. Details are in the beforementioned README.
Or, we may use quite recent “Bundled” feature, which makes sdl2
do all this work itself.
From the README:
Since 0.31, this crate supports a feature named “bundled” which downloads SDL2 from source, compiles it and links it automatically. While this should work for any architecture, you will need a C compiler (like gcc, clang, or MS’s own compiler) to use this feature properly.
Crate features
Crates have a powerful conditional compilation mechanism available, that allows them to conditionally include/exclude functionality based on architecture, OS, and you guessed it, features.
This can be even used to include additional platform-dependant dependencies, invoke compilers or even crazy things like downloading sdl sources and compiling them.
Linking SDL
In our case, we just want to tell sdl2
to use this “bundled” feature.
For that, sdl2
dependency string can be replaced with map {}
to contain additional information:
(Cargo.toml, incomplete)
[dependencies]
sdl2 = { version = "0.31.0", features = ["bundled", "static-link"] }
We also throw-in “static-link” feature to avoid copying dll
files around. Without “static-link”, we would
need to fish out the dll out of build directory target/debug/build/sdl2-sys-???/out/bin
and copy
it to target/debug
location manually (where our main executable is placed after it is compiled).
However, if that’s the case, it may be better to fall back to build-script method of setting up sdl2.
There is an alternative way to specify sdl2 dependency:
(Cargo.toml, incomplete)
[dependencies.sdl2]
version = "0.31.0"
features = ["bundled", "static-link"]
This way, we can use multiple lines to list sdl2 properties. Make sure previous sdl2 = "0.31.0"
entry is
removed.
Run the project!
Cargo will now fetch a gazilion of other dependencies (don’t worry, they are build-time dependencies),
compile SDL2 with CMake (using MSVC compiler), make sure it is linked into sdl2-sys
crate, which in turn
will be linked into sdl2
crate.
The executable should run and return 0:
...
Finished dev [unoptimized + debuginfo] target(s) in 136.27 secs
Running `target\debug\lesson-01-window.exe`
Process finished with exit code 0
SDL initialization and shutdown
Let’s go back to the code.
fn main() {
let _sdl = sdl2::init().unwrap();
}
Let’s unpack it bit by bit.
sdl2::init()
initializes SDL. It is educational to go to sdl2
documentation,
scroll down and find init
function, click on it, read it.
It is should be quite clear how it works. What may be not clear, is Rust-specific stuff.
The init
function signature is pub fn init() -> Result<Sdl, String>
, it returns
Result<Sdl, String>
. If initialization was successful, the Result
contains Sdl
struct as an Ok value,
otherwise it contains the String
as Err value.
Rust standard libary reference contains all information about the Result type and the methods available on it. One of the methods is unwrap.
Calling unwrap
on Result<Sdl, String>
yields Sdl
Ok value, or terminates the program
with an error message that contains String
Err.
I recommend to get familiar with error handling in Rust by reading the book.
The Result type is very thin: if you are coming from C/C++, you can think of it as a union of two types, wrapped in a struct with a discriminator flag.
We assign the returned Sdl
to _sdl
value with let _sdl = ...
statement.
The _
before variable name _sdl
is useful to silence a warning about unused sdl
value.
Notice that there is no explicit SDL_Quit()
, but it is still executed automatically at the end of the
fn main
.
Rust language does not have garbage collector, instead variables are cleaned up as they leave scopes.
Sdl
struct has a drop
function that is executed before the clean up, and that’s where the SDL_Quit()
is invoked.
The story is not complete here however, because Sdl
is secretly internally reference-counted. Therefore
you need not to concern yourself of keeping it around, it will keep SDL alive as long as any other sdl object
has a clone of it.
Video subsystem
In addition to SDL, let’s initialize video subsystem and the window:
let sdl = sdl2::init().unwrap();
let video_subsystem = sdl.video().unwrap();
let window = video_subsystem
.window("Game", 900, 700)
.resizable()
.build()
.unwrap();
The sdl.video()
will create a video subsystem that internally will contain clone of Sdl
, so that
when video_subsystem
is dropped, the Sdl
reference-count is decreased, and it is dropped if
reference-count reaches zero.
This pattern continues all over the API.
The video_subsystem.window(...)
call does not return window itself. Instead, it creates a builder.
In sdl api reference, find VideoSubsystem
, and find
window
function in it, with this signature:
impl VideoSubsystem {
pub fn window(&self, title: &str, width: u32, height: u32) -> WindowBuilder
}
This is a method implemented for VideoSubsystem
, therefore the first special parameter
&self
refers to VideoSubsystem
itself. It is akin to this
in other languages.
When we call this method, we pass only the subsequent three parameters: title, width, height.
In this example, we set resizable
flag from all the other possible flags.
Call to build
creates the window, and the unwrap
handles possible error by terminating the program.
If you try to run this program, you may notice window flashing briefly before performing a graceful exit with correct resource cleanup. We are getting there.
Loop
To keep window open, we add an infinite loop:
loop {
}
You will notice that this window is “not responding”. This happens because when you do any action related to window, such as move a mouse around, click, or try to resize it, OS is sending messages to window, but no one is receiving them.
Receive window events
Before we start the loop, we initialize EventPump
to receive application events
(we may even have multiple windows).
Inside the loop, we iterate over all the received events, and then continue to loop:
let mut event_pump = sdl.event_pump().unwrap();
loop {
for _event in event_pump.poll_iter() {
// handle user input here
}
// render window contents here
}
We again use prefix _event
variable with underscore to silence unused event warning.
If we run the program now, the window comes to life! Albeit a bit empty inside. We will remedy that soon.
When we try to close the window, OS sends Quit
event, but our window is ignorant even of that.
Let’s handle it:
let mut event_pump = sdl.event_pump().unwrap();
'main: loop {
for event in event_pump.poll_iter() {
match event {
sdl2::event::Event::Quit {..} => break 'main,
_ => {},
}
}
// render window contents here
}
We use match
statement to match Quit event. There will be lots of pattern matching
when handling SDL events, so I suggest you learn all about it.
No that’s not all, here is more about patterns.
We have also annotated the outer loop with 'main
label, so that break 'main
breaks out of the loop
loop
instead of inner for
loop.
The code can be found here.
With that, our window can finally be closed, and we can start loading some color inside!