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

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:

Roughly we are going to do the following:

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!

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).

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.