Ostatnimi czasy zastanawiałem się nad tym, czy można w prosty sposób zintegrować projekt napisany w C++ przy użyciu CMake i Conan z projektem napisanym w Rust.

Okazało się, że już są takie biblioteki w Rust jak Conan i CMake, więc nie zostało mi nic innego jak zrobić przykładowy projekt i poskładać to w całość.

Zaczynamy od projektu C++ z CMake, mój projekt nazywa się regexp_pcre

Plik CMakeLists.txt u mnie wygląda tak:

cmake_minimum_required(VERSION 3.16)
project(regexp_pcre)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_COMPILER clang++)

include_directories(include)

execute_process(COMMAND conan install ${CMAKE_CURRENT_SOURCE_DIR}/ -if=${CMAKE_BINARY_DIR}/ -pr=default)

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS)

include_directories(${CONAN_INCLUDE_DIRS})

add_executable(regexp_pcre main.cpp )
target_link_libraries(regexp_pcre -lstdc++ CONAN_PKG::jsoncpp CONAN_PKG::pcre)

add_library(regexp_pcre_lib lib.cpp )
target_link_libraries(regexp_pcre_lib -lstdc++ CONAN_PKG::jsoncpp CONAN_PKG::pcre CONAN_PKG::glog )

install(TARGETS ${PROJECT_NAME}_lib DESTINATION .)
file(GLOB HEADERS include/*.h)
install(FILES ${HEADERS} DESTINATION ../../../../include/)

Dodałem sobie execute_process(..) żeby przy każdym odświeżeniu CMake budowały mi się zależności z Conan jak również żeby nie było problemu gdy projekt będzie kompilowany przez Cargo

Nagłówki do projektu również umieszczam w dość egzotycznym miejscu, ale wynika to z tego, że chce mieć nagłówki w katalogu target w projekcie Rust, tak żebym miał do nich łatwy dostęp z poziomu Rust

Kolejnym niezbędnym elementem jest plik: conanfile.txt który u mnie wygląda tak:

[requires]
jsoncpp/1.9.4
pcre/8.44
glog/0.4.0

[generators]
cmake

[options]
pcre:with_jit=True

Pozostaje jeszcze stworzyć profil Conan, ja używam default, ale jak to woli, nie ma to znaczenia, natomiast trzeba pamiętać o zmianie w CMakeLists.txt

Moja definicja profilu wygląda tak:

[settings]
os=Linux
os_build=Linux
arch=x86_64
arch_build=x86_64
compiler=gcc
compiler.version=10
compiler.libcxx=libstdc++11
build_type=Release
[options]
[build_requires]
[env]

Plik nagłówka wygląda następująco:

#ifndef REGEXP_PCRE_LIB_H
#define REGEXP_PCRE_LIB_H


void example(const std::string& s);

#endif //REGEXP_PCRE_LIB_H

Logiki funkcji nie będę omawiać ponieważ nie ma ona żadnego znaczenia w naszym przykładzie

Więc mając już kompletną część związaną z C++ przechodzimy do części związanej z Rust

Mój projekt nazywa się cxx_test

Zaczynamy od zależności jakie potrzebujemy, dopisujemy je do pliku Cargo.toml

Blok build-dependencies niezbędne do wykonania operacji kompilacji podczas budowania projektu

[build-dependencies]
cxx-build = "1.0"
cmake = "0.1"
conan = "0.1"

Dodatkowo potrzebujemy jeden zależności w dependencies

[dependencies]
cxx = "1.0"

Mając już niezbędne zależności możemy przejść do utworzenia pliku: build.rs – pliku z definicją kompilacji zależności

U mnie wygląda on tak:

use cmake::Config;
extern crate conan;
use conan::*;
use std::env;
use std::path::Path;

fn main() {

    let conan_profile = "default";
    let command = InstallCommandBuilder::new()
        .with_profile(&conan_profile)
        .build_policy(BuildPolicy::Missing)
        .recipe_path(Path::new("../../cpp/regexp_pcre/conanfile.txt"))
        .build();

    let build_info = command.generate().unwrap();
    let json_include = build_info.get_dependency("jsoncpp").unwrap().get_include_dir().unwrap();
    let glog_include = build_info.get_dependency("glog").unwrap().get_include_dir().unwrap();
    let gflag_include = build_info.get_dependency("gflags").unwrap().get_include_dir().unwrap();

    let dst = Config::new("../../cpp/regexp_pcre")
        .cxxflag("-O3")
        .build();

    cxx_build::bridge("src/main.rs")
        .file("src/blodstone.cpp")
        .include("include")
        .include("target/cxxbridge/rust")
        .include("target/cxxbridge/cxx_test/src")
        .include(json_include)
        .include(glog_include)
        .include(gflag_include)
        .opt_level_str("fast")
        .flag("-std=c++20")
        .compiler("clang++")
        .compile("cxx-test");

    println!("cargo:rustc-link-search=native={}", dst.display());
    println!("cargo:rustc-link-lib=regexp_pcre_lib");
    build_info.cargo_emit();

}

Szczegóły jak zbudować plik znajdują się na następujących stronach:

Poniżej fragment budujący zależności pobrane z conanfile.txt projektu w CMake

let conan_profile = "default";
    let command = InstallCommandBuilder::new()
        .with_profile(&conan_profile)
        .build_policy(BuildPolicy::Missing)
        .recipe_path(Path::new("../../cpp/regexp_pcre/conanfile.txt"))
        .build();
let build_info = command.generate().unwrap();

Następnie wyciągamy sobie niezbędne lokalizacje katalogów include, po to żeby móc użyć w naszym projekcie zależności z conan

let json_include = build_info.get_dependency("jsoncpp").unwrap().get_include_dir().unwrap();
let glog_include = build_info.get_dependency("glog").unwrap().get_include_dir().unwrap();
let gflag_include = build_info.get_dependency("gflags").unwrap().get_include_dir().unwrap();

Kompilacja projektu CMake

let dst = Config::new("../../cpp/regexp_pcre")
        .cxxflag("-O3")
        .build();

Nasz projekt łączący świat Rust z C++ przy użyciu biblioteki cxx

cxx_build::bridge("src/main.rs")
        .file("src/blodstone.cpp")
        .include("include")
        .include("target/cxxbridge/rust")
        .include("target/cxxbridge/cxx_test/src")
        .include(json_include)
        .include(glog_include)
        .include(gflag_include)
        .opt_level_str("fast")
        .flag("-std=c++20")
        .compiler("clang++")
        .compile("cxx-test");

Ostatni element to jest dodanie zależności do bibliotek z conan i projektu CMake do linkera

println!("cargo:rustc-link-search=native={}", dst.display());
println!("cargo:rustc-link-lib=regexp_pcre_lib");
build_info.cargo_emit();

Następnie tworzymy katalog include w katalogu głównym projektu, w nim tworzymy plik blodstone.hz definicjami funkcje które będziemy wykonywać w C++

Następnie w src tworzymy plik blodstone.cpp z kodem źródłowym, więcej informacji o przykładzie można znaleźć na stronie: https://cxx.rs/build/cargo.html

Potem przechodzimy do naszego głównego pliku projektu Rust main.rs lub lib.rs

Dodajemy w nim taki fragment kody:

#[cxx::bridge]
mod bridge {

unsafe extern "C++" {
    include!("cxx_test/include/blodstone.h");
    include!("cxx_test/target/include/lib.h");

    pub(crate) fn format_stream();
    pub(crate) fn example(s: &CxxString);
}
}

Gdzie pierwsza funkcja to proxy do naszego wewnętrznego projektu a druga do projektu w CMake

Dzięki temu że nagłówki projektu CMake instalowaliśmy w takim dziwnym miejscu to teraz możemy się do nich dostać z takiej lokalizacji

include!("cxx_test/target/include/lib.h");

To co nam pozostaje to np, w funkcji main() wywołać wcześniej zdefiniowane funkcje w C++

fn main() {
	
    unsafe {
      // funckaj z projektu wewnętrznego
        format_stream();
	 // funkcja z projektu CMake
        let_cxx_string!(s = "hello world");
        example(&s);
    }

}

Kompilujemy, ja kompiluje i uruchamiamy z opcją -vv żeby wiedzieć dokładnie co się dzieje

cargo run --release -vv

W wyniku dostajemy

I0327 15:30:10.541882 90404 blodstone.cpp:73] json_file {"action":"run","data":{"number":1}}
I0327 15:30:11.542809 90404 lib.cpp:26] pcre found: 0
I0327 15:30:11.542840 90404 lib.cpp:43] {
	"action" : "run",
	"data" : 
	{
		"number" : 1
	}
}

Oczywiście jest to zupełnie przykładowy wynik działania, ale widać, że odpalił się kod z blodstone.cpp oraz z lib.cpp

Przykładowy kod można znaleźć tutaj:

2 komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *