Setting up development environment
In this chapter we will create our project directory structure, create our executable and figure out how to interface our Rust application with Vulkan. Since Vulkan has a C API, we will download the Vulkan C headers and generate a Rust binding library that will expose Vulkan structs and functions to our executable and link to it.
In addition if you are using Linux, we will generate Windows executables as well from Linux using MinGW.
This chapter assumes that you installed rustup and git.
Keep in mind, that on Windows I only tested this tutorial with the x86_64-pc-windows-msvc toolchain.
If you are a Windows user, I recommend following this tutorial with the x86_64-pc-windows-msvc toolchain.
The x86_64-pc-windows-gnu toolchain for generating Windows binaries has only been tested on Linux,
and if you encounter troubles with it on Windows, fixing those will be out of the scope of this tutorial.
This tutorial is in open beta. There may be bugs in the code and misinformation and inaccuracies in the text. If you find any, feel free to open a ticket on the repo of the code samples.
Setting up the Cargo Workspace
We will split our program into multiple crates, so we are going to use a cargo workspace. We are going to organize our crates the following way
-
We will create a library crate called
vk_bindingsthat will interface our application with Vulkan. This crate will be used to generate bindings for the C API of Vulkan and link our application with the Vulkan library. -
Then we will have an executable crate called
vk_tutorialwhere we will develop our application.
Let's create our workspace! Create a directory somewhere on your file system! Afterwards let's open a terminal, (on Windows I tested with Git Bash) and after navigating into it let's create the previously mentioned two crates using the following command:
cargo new --vcs none --lib vk_bindings
cargo new --vcs none --bin vk_tutorial
Then let's create our workspace configuration file, Cargo.toml in the top level directory with the following content:
[workspace]
members = [
"vk_bindings",
"vk_tutorial"
]
Since the vk_tutorial needs to access the bindings in crate vk_bindings, we need to add a dependency.
This is what the Cargo.toml of vk_tutorial is going to look like:
[package]
name = "vk_tutorial"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
vk_bindings = { path = "../vk_bindings" }
Now our directory structure looks like this:
┣━ Cargo.toml
┣━ vk_bindings
┃ ┣━ Cargo.toml
┃ ┗━ src
┃ ┗━ lib.rs
┗━ vk_tutorial
┣━ Cargo.toml
┗━ src
┗━ main.rs
Now it's time to write our binding library!
Writing our binding library
Previously we have created our vk_bindings library and now we are going to write its
contents.
Vulkan is exposed as a C API and in order to use it, we must generate some kind of a bridge between C and Rust. This bridge is called a Rust binding.
A Rust binding is a set of structs and functions that represent structs and functions in a C header. The structs' memory layout must match what the C library expects, and the functions' parameter lists, return values and calling conventions must match the parameter lists, return values and calling conventions of the compiled C functions exposed by the library.
We could write such things by hand, but the Vulkan API is enormous, so instead we use a tool to generate it from the Vulkan C headers.
The vk_bindings crate is responsible for two things:
- Generate Rust bindings that will allow us to call Vulkan from Rust
- Export the structs and functions in the binding to our executable
- Link our executable to the Vulkan library
Roughly we are going to do the following:
- Download the Vulkan headers
- Install the Vulkan library
- Create a build script and generate the bindings
- Include it in the Rust code
- Link to Vulkan
Downloading the Vulkan headers
Khronos group hosts the Vulkan headers in the Vulkan-Headers repository on github. There is a CMake project in the repository that supports installing the Vulkan headers into a directory of our choice.
CMake is a cross platform build automation tool that is generally used to build C/C++ programs.
CMake will be required to install the Vulkan headers once the repository is cloned, and later in this tutorial we will use CMake to build and install several development tools, so let's install it!
- For a Windows installer, check out the website, download the installer and follow the instructions.
- On Linux, CMake is generally available from the repositories. For Arch Linux the package is cmake. You will need additional programs, such as make, and these will be in the base-devel package. For any other distro, follow your distro specific instructions.
CMake supports an install command that will put the generated binaries into a directory
structure. The CMake project in the Vulkan-Headers repository uses this to install the Vulkan headers into
a directory of choice. My choice is the build_tools directory under the project directory,
so let's create it!
┣━ Cargo.toml
┣━ build_tools
┣━ vk_bindings
┃ ┣━ Cargo.toml
┃ ┗━ src
┃ ┗━ lib.rs
┗━ vk_tutorial
┣━ Cargo.toml
┗━ src
┗━ main.rs
Once the directory is created, open a terminal on Linux or git bash on Windows in your project directory, and download the Vulkan-Headers from git.
git clone https://github.com/KhronosGroup/Vulkan-Headers
After that navigate into the Vulkan-Headers directory use CMake to install the headers into the
previously chosen install directory. Just replace $install_dir with your choice. My choice will be build_tools in the project
directory as mentioned previously.
cd Vulkan-Headers && cmake -DCMAKE_INSTALL_PREFIX=$install_dir && cmake --build . && cmake --install .
Also it's probably smart to record these commands into a shell script, just in case you want to routinely run it, want other people to run it, etc.
Now that we have the C headers, it's time to get the Vulkan library.
Installing the Vulkan library
Beyond the headers, which contain the struct and function definitions of Vulkan, we need their implementation, which is in the Vulkan library. On Windows and Linux this library will be the Vulkan loader, which will search for Vulkan drivers on your machine.
Beyond this library you will need Vulkan drivers for your GPU(s).
-
On Windows you need to install the LunarG Vulkan SDK to get the Vulkan library. Download the installer from the LunarG website, run it and follow the instructions.
The Vulkan driver for your GPU(s) are probably downloaded by the Windows updater, but if not, check your GPU vendor's website for drivers.
-
On Linux the Vulkan libraries are generally available in the repositories. For Arch Linux, detailed info about Vulkan and the Arch Linux packages can be found in the Arch wiki. Pay attention that you also need the libraries that are required for application development, such as the validation layers!
Installing the Linux version of the Vulkan SDK may also come in handy, but you will probably have to install the Vulkan driver for your GPU(s) from the repositories.
Now we have everything to generate our Rust bindings.
Generating the bindings using bindgen
Now that we have the Vulkan headers and library, we can use a code generator to generate our Rust bindings.
In Rust, rust-bindgen is the ecosystem standard library for generating Rust bindings from C headers.
For rust-bindgen to work, you need to install one of its dependencies, clang, which is a C/C++ compiler. The manual of rust-bindgen explains how to install clang on Windows and many Linux distributions. Please, consult this document for clang installation.
Bindgen is available both as a standalone application and as a library downloadable from cargo. We will use the latter.
In our scheme we generate Rust bindings from C headers in a build script. The build script will pull in bindgen as a dependency, consume the Vulkan headers, generate the bindings and write them out to a Rust source file. Afterwards it will issue the link command to link with the Vulkan library. The lib.rs will include the generated file and expose it to the executable.
Now that we have the plan laid out, let's get started.
First we place our build script next to the Cargo.toml of the vk_bindings crate.
The new directory structure looks like this:
┣━ Cargo.toml
┣━ build_tools
┣━ vk_bindings
┃ ┣━ Cargo.toml
┃ ┣━ build.rs
┃ ┗━ src
┃ ┗━ lib.rs
┗━ vk_tutorial
┣━ Cargo.toml
┗━ src
┗━ main.rs
After that we need to add build.rs as build script and the bindgen crate as a build dependency:
[package]
name = "vk_bindings"
version = "0.1.0"
edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
bindgen = "0.63.0"
Then we need to write the build script like this. The code assumes that the Vulkan headers have been installed
into the build_tools directory. You may need to adjust the path to vulkan.h and the
include path if you installed it elsewhere.
use bindgen;
fn main()
{
//
// Generate bindings
//
let include_path = "../build_tools/include/";
let header_path = "../build_tools/include/vulkan/vulkan.h";
// Invalidate when VK header changes
println!("cargo:rerun-if-changed={}", header_path);
// Generate bindings
let bindings = bindgen::Builder::default()
.header(header_path)
.clang_arg(format!("-I{}", include_path))
.use_core()
.derive_default(true)
.prepend_enum_name(false)
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
// Save bindings
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_path = std::path::PathBuf::from(out_dir);
let vk_binding_path = out_path.join("vk_bindings.rs");
bindings.write_to_file(vk_binding_path)
.expect("Couldn't write bindings!");
// ...
}
Now running cargo will generate the Rust bindings, but we are still not including it in the
application yet. This is done by creating a Rust source file, and using the include! macro.
The following code goes into the lib.rs file of the vk_bindings
crate:
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/vk_bindings.rs"));
We will place a few extra macros in order to disable certain warnings during compiation. Vulkan does not follow the Rust naming conventions, and neither do the resulting bindings. The compiler would flood your terminal during compilation with useless warnings, so we disable these warnings. The resulting code looks like this.
There are extra steps you need to perform in order to actually generate binaries without errors. On Windows you
need the vulkan-1.lib library from the Vulkan SDK that we previously installed. On Linux you need
the vulkan.so library that you hopefully installed from the repos by now.
In the build script, you need to emit these commands that command rustc to link against one of the aforementioned libraries:
// ...
fn main()
{
//
// Generate bindings
//
// ...
//
// Link
//
let target = std::env::var("TARGET").unwrap();
if target == "x86_64-pc-windows-gnu" ||
target == "x86_64-pc-windows-msvc"
{
println!("cargo:rustc-link-lib=vulkan-1");
}
else
{
println!("cargo:rustc-link-lib=vulkan");
}
}
The complete build script looks like this:
use bindgen;
fn main()
{
//
// Generate bindings
//
let include_path = "../build_tools/include/";
let header_path = "../build_tools/include/vulkan/vulkan.h";
// Invalidate when VK header changes
println!("cargo:rerun-if-changed={}", header_path);
// Generate bindings
let bindings = bindgen::Builder::default()
.header(header_path)
.clang_arg(format!("-I{}", include_path))
.use_core()
.derive_default(true)
.prepend_enum_name(false)
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
// Save bindings
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_path = std::path::PathBuf::from(out_dir);
let vk_binding_path = out_path.join("vk_bindings.rs");
bindings.write_to_file(vk_binding_path)
.expect("Couldn't write bindings!");
//
// Link
//
let target = std::env::var("TARGET").unwrap();
if target == "x86_64-pc-windows-gnu" ||
target == "x86_64-pc-windows-msvc"
{
println!("cargo:rustc-link-lib=vulkan-1");
}
else
{
println!("cargo:rustc-link-lib=vulkan");
}
}
These commands assume that the vulkan-1.lib or the vulkan.so is present in the link directories.
If the linker does not find them, you may need to add the directory of these libraries as a link
library like this:
RUSTFLAGS="-L/path/to/vulkan/libraries/" cargo build
If you installed the Vulkan SDK, (on Windows you must have) then the VULKAN_SDK environment
variable is set to its install directory, and you will find the Vulkan library in the Lib directory.
You can set the link directory using this environment variable like this:
RUSTFLAGS="-L$VULKAN_SDK/Lib/" cargo build
Now that we have generated Rust bindings for Vulkan and linked to the Vulkan library, I want to add just a little bit of extra info on how to build a Windows executable on Linux.
Bonus: Cross compiling on Linux using MinGW
You may want to build Windows binaries of your application on Linux, for instance, because you are using
jenkins on a Linux build server, or you are developing on linux, like me.
For cross compilation for Windows on Linux you need to install the x86_64-pc-windows-gnu toolchain and
MinGW. You can install the x86_64-pc-windows-gnu toolchain using rustup:
rustup target add x86_64-pc-windows-gnu
For the installation of MinGW, follow your distro specific instructions.
On Arch Linux the package is
mingw-w64-gcc.
You also need to supply the path of vulkan-1.lib, and you will probably get this from a Windows
Vulkan SDK installation. The installer works with wine,
and you can install it into your wine prefix. Then you can supply it in the RUSTFLAGS
environment variable like this:
RUSTFLAGS="-L/home/username/.wine/drive_c/VulkanSDK/1.3.216.0/Lib/" cargo build --target=x86_64-pc-windows-gnu
If you miss this, you may receive errors such as
/usr/lib/gcc/x86_64-w64-mingw32/12.1.0/../../../../x86_64-w64-mingw32/bin/ld: cannot find -lvulkan-1: No such file or directory
If you check out the tutorial samples from git, you can try the following example command in the downloaded repository which starts one of the applications among the samples with a Vulkan 1.3.216.0 SDK installed. Windows binaries on Linux are automatically ran by cargo in wine:
RUSTFLAGS="-L/home/username/.wine/drive_c/VulkanSDK/1.3.216.0/Lib/" cargo run --target=x86_64-pc-windows-gnu --bin vk_00_instance_and_device
Now that we can build a windows binary on Linux, it's time to wrap up this lesson.
Wrapping up
In this tutorial we created our project directory, installed the Vulkan headers and library, generated Rust bindings for Vulkan, linked our application with Vulkan and possibly figured out how to build windows executables on Linux.
In the next chapter we will open our vk_tutorial crate, where our application is still a hello world
program, and start calling into Vulkan.
The tutorial continues here.