Setting Up

Before we can start doing any coding we need to get a build environment set up and run a hello world program to check everything works.

This chapter will cover:

  • Setting up a C++ build system
  • Integrating cargo into the build system transparently
  • A "hello world" to test that C++ can call Rust functions

Setting up Qt and the Build System

First, create a new cmake project in a directory of your choosing.

$ mkdir rest_client && cd rest_client
$ mkdir gui
$ touch gui/main.cpp
$ touch CMakeLists.txt

You'll then want to make sure your CMakeLists.txt file (the file specifying the project and build settings) looks something like this.

# CMakeLists.txt

cmake_minimum_required(VERSION 3.7)
project(rest-client)

enable_testing()
add_subdirectory(client)
add_subdirectory(gui)

This says we're building a project called rest-client that requires at least cmake version 3.7. We've also enabled testing and added two subdirectories to the project (client and gui).

Our main.cpp is still empty, lets rectify that by adding in a button.

// gui/main.cpp

#include <QtWidgets/QPushButton>
#include <QtWidgets/QApplication>

int main(int argc, char **argv) {
  QApplication app(argc, argv);

  QPushButton button("Hello World");
  button.show();

  app.exec();
}

We need to add a CMakeLists.txt to the gui/ directory to let cmake know how to build our GUI.

# gui/CMakeLists.txt

set(CMAKE_CXX_STANDARD 14)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
find_package(Qt5Widgets)

set(SOURCE main.cpp)
add_executable(gui ${SOURCE})
target_link_libraries(gui Qt5::Widgets)
add_dependencies(gui client)

This is mostly concerned with adding the correct options so Qt's meta-object compiler can do its thing and we can locate the correct Qt libraries, however right down the bottom you'll notice that we create a new executable with add_executable(). This says our gui target has right now one source file, main.cpp. It also needs to link to Qt5::Widgets and depends on our client (the Rust library), which hasn't yet been configured.

Building Rust with CMake

Next we need to create the Rust project.

$ cargo new --lib client

To make it accessible from C++ we need to make sure cargo generates a dynamically linked library. This is just a case of tweaking our Cargo.toml to tell cargo we're creating a cdylib instead of the usual library format.

# client/Cargo.toml

[package]
name = "client"
version = "0.1.0"
authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
description = "The business logic for a REST client"
repository = "https://github.com/Michael-F-Bryan/rust-ffi-guide"

[dependencies]

[lib]
crate-type = ["cdylib"]

If you then compile the project you'll see cargo build a shared object (libclient.so) instead of the normal *.rlib file.

$ cargo build
$ ls target/debug/
build  deps  examples  incremental  libclient.d  libclient.so  native

Note: You don't technically need to make a dynamic library (cdylib) for your Rust code to be callable from other languages. You can always use static linking with a staticlib, however that can be a bit more annoying to set up because you need to remember to link in a bunch of other things that the Rust standard library uses (mainly libc and the C runtime).

With a dynamic library all the work for dependency resolution is handled by the loader when your program gets loaded into memory on startup. Meaning things should Just Work.

Now we know the Rust compiles natively with cargo, we need to hook it up to cmake. We do this by writing a CMakeLists.txt in the client/ directory. As a general rule, you'll have one CMakeLists.txt for every "area" of your code. This usually up being one per directory, but not always.

# client/CMakeLists.txt

if (CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(CARGO_CMD cargo build)
    set(TARGET_DIR "debug")
else ()
    set(CARGO_CMD cargo build --release)
    set(TARGET_DIR "release")
endif ()

set(CLIENT_SO "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR}/libclient.so")

add_custom_target(client ALL
    COMMENT "Compiling client module"
    COMMAND CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} ${CARGO_CMD} 
    COMMAND cp ${CLIENT_SO} ${CMAKE_CURRENT_BINARY_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(client PROPERTIES LOCATION ${CMAKE_CURRENT_BINARY_DIR})

add_test(NAME client_test 
    COMMAND cargo test
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})

This is our first introduction to the difference between a debug and release build. So we know whether to compile our program using different optimisation levels and debug symbols, cmake will set a CMAKE_BUILD_TYPE variable containing either Debug or Release.

Here we're just using an if statement to set the cargo build command and the target directory, then using those to add a custom target which will first build the library, then copy the generated binary to the CMAKE_BINARY_DIR.

For good measure, lets add a test (client_test) which lets cmake know how to test our Rust module.

To make sure cargo puts all compiled artefacts in the correct spot within build/, we set the CARGO_TARGET_DIR environment variable while invoking the CARGO_CMD. The compiled library is then copied from into CMAKE_CURRENT_BINARY_DIR and we set the LOCATION property on the overall target to be CMAKE_CURRENT_BINARY_DIR.

The purpose of that little dance is so that no matter what type of build (release or debug) we do, the compiled library will be in the same spot. We then set the client target's LOCATION property so that anyone else who needs to use client's outputs knows which directory they'll be in.

Now we know where the compiled client module will be, we can tell our gui to link to it.

# gui/CMakeLists.txt

...

set(SOURCE main.cpp)
add_executable(gui ${SOURCE})
+ get_target_property(CLIENT_DIR client LOCATION)
target_link_libraries(gui Qt5::Widgets)
+ target_link_libraries(gui ${CLIENT_DIR}/libclient.so)
add_dependencies(gui client)

Now we can compile and run this basic program to make sure everything is working. You'll probably want to create a separate build/ directory so you don't pollute the rest of the project with random build artefacts.

$ mkdir build && cd build
$ cmake ..
$ make
$ ./gui/gui

Calling Rust from C++

So far we've just made sure everything compiles, however the C++ and Rust code are still completely independent. The next task is to check the Rust library is linked to properly by calling a function from C++.

First we add a dummy function to the lib.rs.


# #![allow(unused_variables)]
#fn main() {
#[no_mangle]
pub extern "C" fn hello_world() {
    println!("Hello World!");
}
#}

There's a lot going on here, so lets step through it bit by bit.

The #[no_mangle] attribute indicates to the compiler that it shouldn't mangle the function's name during compilation. According to Wikipedia, name mangling:

In compiler construction, name mangling (also called name decoration) is a technique used to solve various problems caused by the need to resolve unique names for programming entities in many modern programming languages.

It provides a way of encoding additional information in the name of a function, structure, class or another datatype in order to pass more semantic information from the compilers to linkers.

The need arises where the language allows different entities to be named with the same identifier as long as they occupy a different namespace (where a namespace is typically defined by a module, class, or explicit namespace directive) or have different signatures (such as function overloading).

TL:DR; it's a way for compilers to generate multiple instances of a function which accepts different types or parameters. Without it we wouldn't be able to have things like generics or function overloading without name clashes.

If this function is going to be called from C++ we need to specify the calling convention (the extern "C" bit). This tells the compiler low level things like how arguments are passed between functions. By far the most common convention is to "just do what C does".

The rest of the function declaration should be fairly intuitive.

After recompiling (cd build && cmake .. && make) you can inspect the generated binary using nm to make sure the hello_world() function is there.

$ nm libclient.so | grep ' T '
0000000000003330 T hello_world          <-- the function we created
00000000000096c0 T __rdl_alloc
00000000000098d0 T __rdl_alloc_excess
0000000000009840 T __rdl_alloc_zeroed
0000000000009760 T __rdl_dealloc
0000000000009a20 T __rdl_grow_in_place
0000000000009730 T __rdl_oom
0000000000009780 T __rdl_realloc
0000000000009950 T __rdl_realloc_excess
0000000000009a30 T __rdl_shrink_in_place
0000000000009770 T __rdl_usable_size
0000000000015ad0 T rust_eh_personality

The nm tool lists all the symbols in a binary as well as their addresses (the hex bit in the first column) and what type of symbol they are. All functions are in the Text section of the binary, so you can use grep to view only the exported functions.

Now we have a working library, why don't we make the GUI program less like a contrived example and more like a real-life application?

The first thing is to pull our main window out into its own source files.

$ touch gui/main_window.hpp gui/main_window.cpp
# gui/CMakeLists.txt

...

- set(SOURCE main.cpp)
+ set(SOURCE main_window.cpp main_window.hpp main.cpp)
add_executable(gui ${SOURCE})
// gui/main_window.hpp

#include <QtWidgets/QMainWindow>
#include <QtWidgets/QPushButton>

class MainWindow : public QMainWindow {
  Q_OBJECT

public:
  MainWindow(QWidget *parent = nullptr);
private slots:
  void onClick();

private:
  QPushButton *button;
};

Here we've declared a MainWindow class which contains our trusty QPushButton and has a single constructor and click handler.

We also need to fill out the MainWindow methods and hook up the button's released signal to our onClick() click handler.

// gui/main_window.cpp

#include "main_window.hpp"

extern "C" {
void hello_world();
}

void MainWindow::onClick() { 
    // Call the `hello_world` function to print a message to stdout
    hello_world(); 
}

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
  button = new QPushButton("Click Me", this);
  
  // Connect the button's `released` signal to `this->onClick()`
  connect(button, SIGNAL(released()), this, SLOT(onClick()));
}

Don't forget to update main.cpp to use the new MainWindow.

// gui/main.cpp

#include "main_window.hpp"
#include <QtWidgets/QApplication>

int main(int argc, char **argv) {
  QApplication app(argc, argv);

  MainWindow mainWindow;
  mainWindow.show();

  app.exec();
}

Now when you compile and run ./gui, "Hello World" wil be printed to the console every time you click on the button.

If you got to this point then congratulations, you've just finished the most difficult part - getting everything to build!