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:
Thanks for one’s marvelous posting! I truly enjoyed reading it, you will be a great author. I will make sure to bookmark your blog and will come back at some point. I want to encourage you to continue your great writing, have a nice day!