Rust: C/C++のソースファイルをCMakeでビルドして利用する方法
1. はじめに
C/C++のライブラリをRustで参照利用する場合、以下の方法が考えられます。
- 3rd partyのcrateを利用する
- 自分でインタフェースをFFI (Foreign Function Interface) する
しかし、該当ライブラリの全てのAPIを利用する訳ではなかったり、かと言って自分でFFIするもの面倒だったり、RustからC/C++へのインターフェースに取り周り的に難しかったりする事があります。そのような時には、ライブラリを参照する部分はC/C++でインターフェースをラップしたり、Rustから扱い易くモジュール化して、そのモジュールをFFIした方が遥かに簡単で便利な事があります。
本記事では、このようなケースに置いて、C/C++のソースコードはCMake (CMakeLists.txt) で管理ビルドし、RustのCargoで統合してビルドする方法をまとめています。
2. cmakeクレート
cmakeというクレートを利用します。これを利用することで、Cargoで既存のCMakeLists.txt
ファイルの内容に従ってC/C++のソースコードをビルドリンク出来るようになります。
3. ディレクトリ構成 (例)
今回、説明用のディレクトリ構成は以下のようにします。C/C++のソースコードはlib/cmake-example
ディレクトリ以下にモジュールとして配置し、それをsrc/main.rs
から呼び出すようにします。
Cargo.toml
├── src/
│ └── main.rs
├── lib/
│ └── cmake-example/
│ │ └── Cargo.toml
│ │ └── build.rs
│ │ └── src/
│ │ │ └── lib.rs
│ │ │ └── bindings.rs
│ │ │ └── cpp/
│ │ │ │ └── example.cc
│ │ │ │ └── example.h
│ │ │ │ └── CMakeLists.txt
4. C/C++ (lib/cmake-example) 側の対応
まずはC/C++側の対応内容について説明していきます。
lib/cmake-example/Cargo.tomlの作成
build-dependencies
にcmake
を追加します。さらに、dependencies
にlibc
を追加します。
[package]
name = "cmake-example"
version = "0.1.0"
authors = ["name"]
edition = "2018"
build = "build.rs" # ビルドスクリプトはbuild.rsファイル
[dependencies]
libc = "0.2.94"
[build-dependencies]
cmake = "0.1"
# bindgen = "0.58.1" # bindgenを利用すれば自動でC/C++のFFIのソースコードを生成可能
ビルドスクリプトのbuild.rsを作成
CMakeを実行してC/C++をビルドするためのビルドスクリプトを作成します。C/C++のコードをRustに組み込んで利用する場合、C/C++のビルド成果物はstaticライブラリ (.a)ファイルで良いと思います(Rustのコードで生成されたバイナリに組み込むだけだから)。一方、C/C++で利用している3rd partyのライブラリはdynamicリンクする必要があります。
use cmake;
fn main() {
// CMakeLists.txtが存在するディレクトリを指定します
// lib/cmake-exampleディレクトリからの相対位置となります
let dst = cmake::build("src/cpp");
println!("cargo:rustc-link-search=native={}", dst.display());
// staticライブラリとして他に利用するライブラリはなし
println!("cargo:rustc-link-lib=static=");
// C++ソースコードの場合は必ずこれを追加すること
println!("cargo:rustc-link-lib=dylib=stdc++");
// CMakeLists.txt内の記述とは別に、その他のライブラリは必要なものを全て記述する必要あり
println!("cargo:rustc-link-lib=dylib=EGL");
println!("cargo:rustc-link-lib=dylib=GLESv2");
println!("cargo:rustc-link-lib=dylib=X11");
// pkg-configでヒットしないライブラリは以下のように直接パス指定が可能
let soil_lib_dir = "/usr/lib";
println!("cargo:rustc-link-search={}", soil_lib_dir);
println!("cargo:rustc-link-lib=dylib=SOIL");
}
CMakeLists.txt
の用意
CMakeLists.txt
自体はC/C++でビルドする時に利用するそのままの形式で利用出来ますが、以下の2点だけRust向けの注意する必要があります。
- staticライブラリにビルドする必要がある
- installを必ず書く必要がある
サンプルを以下に示します。
cmake_minimum_required(VERSION 3.10)
project("cmake example for Rust" LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig)
pkg_check_modules(EGL REQUIRED egl)
pkg_check_modules(GLES2 REQUIRED glesv2)
pkg_check_modules(X11 REQUIRED x11)
set(TARGET cmake-example)
# 必ずstaticライブラリにビルドすること
add_library(${TARGET}
STATIC
example.cc
)
target_link_libraries(${TARGET}
PRIVATE
${EGL_LIBRARIES}
${GLES2_LIBRARIES}
${X11_LIBRARIES}
)
target_include_directories(${TARGET}
PRIVATE
${EGL_INCLUDE_DIR}
${GLES2_INCLUDE_DIR}
${X11_INCLUDE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
)
# 必ずこれが必要です
install (TARGETS ${TARGET} DESTINATION .)
bindings.rsの作成
C/C++のライブラリのバインディング用のコードを作成します。今回は説明しませんが、bindgen
クレートを利用することでこのソースコード自体も自動生成が可能です。今回は手動で作成します。これは自身のC/C++のソースコードの内容に合わせて作成して下さい。
extern "C" {
pub fn foo();
}
lib.rsの作成
上記で作成したbindings.rs
をインクルードするだけのlib.rs
を用意します。ここまでくればmain.rs側から参照利用できるようになります。
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!("bindings.rs");
5. Rust (ルート) 側の対応
次にRustメイン側の対応内容について説明していきます。
Cargo.tomlの編集
dependencies
にlib/cmake-example
を追加します。
[dependencies]
cmake-example = { path = "lib/cmake-example" }
src/main.rsの作成
あとはRustのコードからC/C++のコードを呼び出すだけです。
extern crate cmake_example;
fn main() {
unsafe {
cmake_example::foo();
}
}
Discussion
教えてください。
-(ハイフン) と _(アンダースコア) はこれであっているのでしょうか?
実際のコードで指定している
cmake_example
がどこでcmake-example
と紐づいているのかよくわからなかったので、質問させていただきました。詳しくは覚えていませんが、それで合っています。
cmake-example
はcmakeプロジェクトの名前かつc++コードのビルド結果のファイル名だったりします。Rust側から参照する場合には、仕組み上-
が_
に置き換わるイメージです。