Skip to content

XiangpengHao/cxx-cmake-example: CXX with CMake build system #436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CXX CMake CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
ubuntu-build:
runs-on: ubuntu-20.04
strategy:
matrix:
lto: ["ON", "OFF"]
env:
DEBIAN_FRONTEND: noninteractive
CC: clang-11
CXX: clang++-11
steps:
- name: Checkout
uses: actions/[email protected]
- name: Setup rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- run: cargo install cxxbridge-cmd
- run: sudo apt update
- run: sudo apt install -y cmake git
- run: sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
- run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO=${{matrix.lto}} ..
- run: cd build && make
- run: cd build && ./main
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build*
cmake-build*
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",

}
32 changes: 32 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
if (POLICY CMP0048)
cmake_policy(SET CMP0048 NEW)
endif ()

project(cxx_example)
cmake_minimum_required(VERSION 3.15)

set (CMAKE_CXX_STANDARD 17)

option(ENABLE_LTO "Enable cross language linking time optimization" ON)
if(ENABLE_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error)
if(supported)
message(STATUS "IPO / LTO enabled")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
add_link_options(-fuse-ld=lld)
else()
message(STATUS "IPO / LTO not supported: <${error}>")
endif()
endif()

include_directories(${CMAKE_BINARY_DIR}/rust_part)
include_directories(include)

add_subdirectory(rust_part)

add_executable(main main.cpp)

add_dependencies(main rust_part)

target_link_libraries(main rust_part_cxx)
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CXX with CMake build system
![CXX CMake CI](https://github.com/XiangpengHao/cxx-cmake-example/workflows/CXX%20CMake%20CI/badge.svg)

This is an example repo to setup [cxx](https://github.com/dtolnay/cxx) with the cmake build system.

The official [demo](https://github.com/dtolnay/cxx/tree/master/demo) used `cargo` to orchestrate the two build systems and place the `main` function inside the rust project.

In a lot of other applications, however, we want to embed rust into a large cpp project where we don't have a chance to choose build systems.
This template repo shows how to use cmake with cxx.


The cmake files do the following things:
1. Call `cargo build [--release]` to build a static library
2. Call `cxxbridge src/lib.rs > ...` to generate the source/header files (as specified [here](https://github.com/dtolnay/cxx#non-cargo-setup))
3. Create a library from the cxx generated source and link to the rust static library
4. Link and include the libraray to the corresponding targets


## Cross-language LTO (linking time optimization)
More details in [my blog](https://blog.haoxp.xyz/posts/cross-lang-lto-guide/).

### Why?
Calling rust function from c++ (and vice versa) is not zero-overhead because the LLVM optimizer by default will not optimize across shared libraries, let alone across different languages.

[LTO](https://llvm.org/docs/LinkTimeOptimization.html) (linking time optimization) allows the optimizers to perform optimization during linking time (instead of compile time), thus enables cross library optimization.

### How?
```bash
cmake -DENABLE_LTO=ON -DCMAKE_BUILD_TYPE=Release ..
make -j
./main
```

The `-DENABLE_LTO=ON` will compile and link both libraries with proper parameters.
Note that cross language LTO between rust and c/c++ is only possible with clang toolchain,
meaning that you need to have a very recent clang/lld installed.

## Example output

### With LTO
```
Points {
x: [
1,
2,
3,
],
y: [
4,
5,
6,
],
}
"cpp expert!"
Calling rust function, time elapsed: 100 ns.
Calling c++ function, time elapsed: 100 ns.
```

#### Without LTO
```
Points {
x: [
1,
2,
3,
],
y: [
4,
5,
6,
],
}
"cpp expert!"
Calling rust function, time elapsed: 1176600 ns.
Calling c++ function, time elapsed: 100 ns.
```


## Credits
The cmake files are largely inspired by [Using unsafe for Fun and Profit](https://github.com/Michael-F-Bryan/rust-ffi-guide).

I learned a lot about cross language LTO from this post: [Closing the gap: cross-language LTO between Rust and C/C++](https://blog.llvm.org/2019/09/closing-gap-cross-language-lto-between.html)
23 changes: 23 additions & 0 deletions include/cpp_part.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <iostream>
#include <memory>

struct Person
{
std::string name;

Person()
{
this->name = "cpp expert!";
}

void print_name()
{
std::cout << this->name << std::endl;
}
};

const std::string &get_name(const Person &person);

std::unique_ptr<Person> make_person();
67 changes: 67 additions & 0 deletions main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <iostream>
#include "rust_part.h"
#include <chrono>

const std::string &get_name(const Person &person)
{
return person.name;
}

std::unique_ptr<Person> make_person()
{
return std::make_unique<Person>();
}

int cpp_echo(int val)
{
return val;
}

int test_fun()
{
int sum = 0;
for (int i = 0; i < 1000000; i += 1)
{
sum += rust_part::rust_echo(i);
}
return sum;
}

int test_inline()
{
int sum = 0;
for (int i = 0; i < 1000000; i += 1)
{
sum += cpp_echo(i);
}
return sum;
}

void test_lto()
{
auto t1 = std::chrono::high_resolution_clock::now();
auto sum = test_fun();
auto t2 = std::chrono::high_resolution_clock::now();

auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();

std::cout << "Calling rust function"
<< ", time elapsed: " << duration << " ns." << std::endl;

t1 = std::chrono::high_resolution_clock::now();
sum = test_inline();
t2 = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();

std::cout << "Calling c++ function"
<< ", time elapsed: " << duration << " ns." << std::endl;
}

int main()
{
auto thing = rust_part::make_shared_thing();
rust_part::print_shared_thing(thing);

test_lto();
return 0;
}
2 changes: 2 additions & 0 deletions rust_part/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
36 changes: 36 additions & 0 deletions rust_part/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CARGO_CMD cargo build --verbose)
set(TARGET_DIR "debug")
else ()
set(CARGO_CMD cargo build --release --verbose)
set(TARGET_DIR "release")
endif ()

if(ENABLE_LTO)
set(RUST_FLAGS "-Clinker-plugin-lto" "-Clinker=clang-11" "-Clink-arg=-fuse-ld=lld-11")
endif()

set(RUST_PART_LIB "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR}/librust_part.a")

add_custom_target(rust_part ALL
COMMENT "Compiling rust_part module"
COMMAND CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUSTFLAGS="${RUST_FLAGS}" ${CARGO_CMD}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(rust_part PROPERTIES LOCATION ${CMAKE_CURRENT_BINARY_DIR})

set(RUST_PART_CXX "${CMAKE_CURRENT_BINARY_DIR}/rust_part.cpp")
add_library(rust_part_cxx STATIC ${RUST_PART_CXX})
add_custom_command(
OUTPUT ${RUST_PART_CXX}
COMMAND cxxbridge src/lib.rs --header > ${CMAKE_CURRENT_BINARY_DIR}/rust_part.h
COMMAND cxxbridge src/lib.rs > ${RUST_PART_CXX}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/lib.rs
)

target_link_libraries(rust_part_cxx pthread dl ${RUST_PART_LIB})

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

16 changes: 16 additions & 0 deletions rust_part/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rust_part"
version = "0.1.0"
authors = ["Xiangpeng Hao <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cxx = "0.5"

[build-dependencies]
cxx-build = "0.5"

[lib]
crate-type = ["staticlib"]
79 changes: 79 additions & 0 deletions rust_part/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::fmt;

#[cxx::bridge(namespace = rust_part)]
mod ffi {
struct Color {
r: u8,
g: u8,
b: u8,
}

struct SharedThing {
points: Box<Points>,
persons: UniquePtr<Person>,
pixels: Vec<Color>,
}

extern "C++" {
include!("cpp_part.h");
type Person;

fn get_name(person: &Person) -> &CxxString;
fn make_person() -> UniquePtr<Person>;
}

extern "Rust" {
type Points;
fn print_shared_thing(points: &SharedThing);
fn make_shared_thing() -> SharedThing;
fn rust_echo(val: i32) -> i32;
}
}

#[derive(Debug)]
pub struct Points {
x: Vec<u8>,
y: Vec<u8>,
}

impl ffi::Color {
pub fn new() -> Self {
Self {
r: 255,
g: 255,
b: 255,
}
}
}

impl fmt::Debug for ffi::Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Color")
.field("r", &self.r)
.field("g", &self.g)
.field("b", &self.b)
.finish()
}
}

fn print_shared_thing(thing: &ffi::SharedThing) {
println!("{:#?}", thing.points);
println!("{:#?}", thing.pixels);
println!("{:#?}", ffi::get_name(thing.persons.as_ref().unwrap()));
}

fn make_shared_thing() -> ffi::SharedThing {
ffi::SharedThing {
points: Box::new(Points {
x: vec![1, 2, 3],
y: vec![4, 5, 6],
}),
persons: ffi::make_person(),
pixels: vec![ffi::Color::new(), ffi::Color::new()],
}
}

#[inline(always)]
fn rust_echo(val: i32) -> i32 {
val
}