This is a simple example project to show how to safely call C++ code from Rust using conan-rs and autocxx.
The c++ targets uses conan1 as the package manager and CMake. This is a very common setup in enterprise/legacy c++ codebases.
In this example, we have a static library, that answers the ultimate question to life, universe and everything, and a command line interface to it:
- Static lib:
#include <deep_thought/answer.hpp>
namespace deep_thought {
int answer() {
return 42;
}
} // namespace deep_thought
- Cli:
#include <deep_thought/answer.hpp>
#include <chrono>
#include <iostream>
#include <thread>
auto main() -> int {
auto task = []() {
std::cout << "Thinking..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "The answer is " << deep_thought::answer() << std::endl;
};
std::thread thread(task);
thread.join();
return 0;
}
After building it, we have the following:
We can take advantage of the many application areas that Rust is more suited than C++ for extending the codebase. For example, Writing C++ desktop applications is a very painful process, so we can resource to the marvelous Rust Tauri Framework for developing a good user experience, while maintaining the existing c++ code:
- Here we use the conan-rs crate to abstract the deps instalation, building and packaging of our c++ artifacs. Also, we automatically generate Rust bindings "reading" the codebase header files using the wonderful google autocxx crate. Finally, we link the c++ static lib to our Tauri binary to have a working application.
- In the "Tauri backend"(in the
./src/app-rs/src/ext.rs
file) we actually tell autocxx which c++ functions we want to generate code:
use autocxx::prelude::*;
include_cpp! {
#include "deep_thought/answer.hpp"
safety!(unsafe_ffi)
generate!("deep_thought::answer")
}
pub fn answer() -> i32 {
ffi::deep_thought::answer().into()
}
And in the main Tauri binary file we can enroll this function to be used as a Tauri command:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use app_rs::ext;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn answer() -> String {
ext::answer().to_string()
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![answer])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- Finally, we can invoke this file in the Tauri frontend (
./src/app-rs/src/app.rs
file):
...
let answer = {
let answer_msg_rc = answer_msg_rc.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
let answer_msg = answer_msg_rc.clone();
spawn_local(async move {
let new_msg = invoke("answer", JsValue::UNDEFINED)
.await
.as_string()
.unwrap();
answer_msg.set(new_msg);
});
})
};
...
With this, we have a fully fledged application, combining the strenghts of both Rust and C++:
For more details, read the full article.