[Rust] C/C++のソースコードをCMakeでビルドして利用する方法

4 min read読了の目安(約4100字

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-dependenciescmakeを追加します。さらに、dependencieslibcを追加します。

lib/cmake-example/Cargo.toml
[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リンクする必要があります。

build.rs
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を必ず書く必要がある

サンプルを以下に示します。

CMakeLists.txtの例
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側から参照利用できるようになります。

lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!("bindings.rs");

5. Rust (ルート) 側の対応

次にRustメイン側の対応内容について説明していきます。

Cargo.tomlの編集

dependencieslib/cmake-exampleを追加します。

Cargo.toml
[dependencies]
cmake-example = { path = "lib/cmake-example" }

src/main.rsの作成

あとはRustのコードからC/C++のコードを呼び出すだけです。

main.rs
extern crate cmake_example;

fn main() {
    unsafe {
        cmake_example::foo();
    }
}