PixInsight Class Library を自作プログラムから呼び出してみる

目次

  1. 1. あいさつ
  2. 2. やり方
    1. 2.1. 呼び出したい機能が格納されたスタティックライブラリを用意
    2. 2.2. C++ 側のインタフェースを用意,PCL とリンク,Rust バインディングの自動生成
    3. 2.3. Rust の本番アプリケーションから呼出し
  3. 3. まとめ

あいさつ

こんにちは.突然ですが,皆さんは PixInsight Class Library(PCL)をご存知でしょうか.PixInsight 内部の機能がプログラムの形で提供されています.

The PixInsight Class Library (PCL) is a C++ development framework to build PixInsight modules.
PixInsight modules are special shared libraries (.so files on FreeBSD and Linux; .dylib under macOS; .dll files on Windows) that communicate with the PixInsight core application through a high-level API provided by PCL. Along with a core communication API, PCL includes a comprehensive set of image processing algorithms, ranging from geometrical transformations to multiscale analysis algorithms, most of them available as multithreaded parallel implementations. PCL provides also rigorous and efficient implementations of essential astronomical algorithms, including state-of-the-art solar system ephemerides, vector astrometry, and reduction of positions of solar system and stellar objects.
PixInsight Class Library― by Pleiades Astrophoto S.L.

私は普段 Rust と呼ばれるちょっとモダンなプログラミング言語を使っているのですが,C++ との Foreign Function Interface(FFI)を学んでいるうちに,あれ,これ PCL も呼べるんじゃね?ということに気づいたので,せっかくならやってみることにしました.

やり方

PCL には Hello World のようなサンプルがないので,PCL から Version を表示する機能を呼んでやることを目標にします.
このへんのコードを呼び出したい↑のような PCL のコードを呼び出したいです.

以下にやり方をつらつらと書いていきます.
OS は Windows 10 で,Git と Rust コンパイラ,CMake,Visual Studio(2019 Community)がインストールされていることが前提です.
Rust から C++ を呼ぶのは若干めんどくさく,

  1. 呼び出したい機能が格納されたスタティックライブラリを用意する.

  2. C++ 側のインタフェースを用意して,1.のライブラリとリンクしてビルドする.(今回はCMakeを使います)

  3. C++ 側のヘッダーから Rust のバインディングを自動生成する.(bindgen crate を使います)

  4. Rust の本番アプリケーションから呼び出す.

みたいな流れになります.
2,3については,Rust の build script を使ってビルド時によしなにやってくれるようにします.
順番にやっていきましょう.

呼び出したい機能が格納されたスタティックライブラリを用意

呼び出したい機能が格納されたスタティックライブラリとは,PCL のことです.ただ,PCL はソースコードの形で提供されているため,頑張ってビルドします.
まず PCL リポジトリをクローン(ローカルにダウンロード)

1
$ git clone https://gitlab.com/pixinsight/PCL/

PCLDIR, PCLBINDIR64, PCLLIBDIR64, PCLINCDIR, PCLSRCDIRにパスを設定.(こちらを参考に)
Visual Studio 2019 を開き,\PCL\src\pcl\windows\vc16\PCL.vcxproj を開く.ビルドの設定をRelease, x64にします.そのままやるとこの後エラーが出るので,以下のコードをちょっと修正しました.

1
2
3
4
5
6
7
8
9
10
11
//DrizzleData.cpp 481行目あたり,
else if ( element.Name() == "CFASourceImage" )
{
// optional
m_cfaSourceFilePath = element.Text().Trimmed();
m_cfaSourcePattern = element.AttributeValue( "pattern" );
String channel = element.AttributeValue( "channel" );
if ( !channel.IsEmpty() )
//intへのキャストを追加,たぶんオーバーロードがうまくいってない
m_cfaSourceChannel = Range( (int)channel.ToInt(), -1, int32_max );
}

コンパイラによってはうまく動くのかもしれませんが,少なくとも自分の環境では無理でしたので,今のとここのような修正になっています.
ビルドすると,PCLLIBDIR64 に設定したパスにスタティックライブラリができます.

PCLのスタティックライブラリができた!

C++ 側のインタフェースを用意,PCL とリンク,Rust バインディングの自動生成

次に,C++ 側のインタフェースを用意します.

1
2
3
4
5
6
7
//pcl_cpp.hpp
class Pcl_rs
{
public:
Pcl_rs();
void print_pcl_version(void);
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//pcl_cpp.cpp
#define __PCL_WINDOWS
// include the local headers
#include <iostream>
#include <pcl/Version.h>
#include "pcl_cpp.hpp"

Pcl_rs::Pcl_rs() {}

void Pcl_rs::print_pcl_version(void)
{
//ここでpclの機能を実際に呼んでます
std::cout << pcl::Version::AsString() << std::endl;
return;
}

pcl_cpp.cpp は CMake を使ってビルドするので,CMakeList.txtを用意します.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#CMakeList.txt
cmake_minimum_required(VERSION 3.5)

project(pcl_cpp)
set(CMAKE_CXX_STANDARD 11)
set(TARGET pcl_cpp)

#自身の環境によって変更してください
include_directories(/path/to/PCLINCDIR)

add_library(${TARGET}
STATIC
pcl_cpp.cpp
)

target_link_libraries(${TARGET}
PRIVATE
)

install (TARGETS ${TARGET} DESTINATION .)

Rust の ライブラリプロジェクトを用意します.これが本番アプリケーションという体です.
ライブラリプロジェクトなので,別のプログラムに移植することは簡単です.

1
$ cargo new pcl-rs --lib //ライブラリプロジェクトを作る

上に書いた3つを以下のように配置します.

1
2
3
4
5
6
7
8
9
10
11
12
├─pcl-rs
│ │ build.rs
│ │ Cargo.toml
│ │
│ ├─src
│ │ │ lib.rs
│ │ │ main.rs
│ │ │
│ │ └─pcl_cpp
│ │ CMakeLists.txt
│ │ pcl_cpp.cpp
│ │ pcl_cpp.hpp

触れていないファイルがいつかありますが後述します.
build.rs が 本番アプリケーションのビルド時に走るスクリプトです.ここでCMake,bindgenを使って,Rust から pcl_cpp.cpp の機能を使えるようにします.
まず Cargo.toml に依存ライブラリを記載.

1
2
3
4
5
6
7
8
9
# Cargo.toml
[dependencies]
libc = "0.2.51"

[build-dependencies]
cc = "1.0.35"
bindgen = "0.52"
libc = "0.2.51"
cmake = "0.1"

ビルドスクリプトを書いていきます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//build.rs
extern crate bindgen;

use cmake;
use std::env;
use std::path::PathBuf;

fn main() {
//pcl_cpp.cppかpcl_hpp.cppが変更された場合に走る
println!("cargo:rerun-if-changed=src/pcl_cpp/pcl_cpp.cpp");
println!("cargo:rerun-if-changed=src/pcl_cpp/pcl_cpp.hpp");

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_path = out_path.to_str().unwrap();

//リンクするライブラリを探すパス
println!("cargo:rustc-link-search={}/build/Release/", out_path);
//自身の環境によって変更してください
println!("cargo:rustc-link-search=native=/path/to/PCLLIBDIR64");
//Windows でビルドする場合に必要
//User32.Lib の場所を指定
println!("cargo:rustc-link-search=native=C:/Program Files (x86)/Windows Kits/10/Lib/10.0.19041.0/um/x64");

//リンクするライブラリ
println!("cargo:rustc-link-lib=static=pcl_cpp");
println!("cargo:rustc-link-lib=static=PCL-pxi");
//Windows でビルドする場合に必要
println!("cargo:rustc-link-lib=static=User32");

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

let _dst = cmake::Config::new("src/pcl_cpp").generator("Visual Studio 16 2019").build();

//pcl_cpp.hpp からバインディング生成
let bindings = bindgen::Builder::default()
.clang_arg("-x")
.clang_arg("c++")
.header("src/pcl_cpp/pcl_cpp.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");

bindings
.write_to_file(out_path.join("pcl_cpp.rs"))
.expect("Couldn't write bindings!");
}

スクリプトの細かい説明は省きますが,ビルドスクリプトが走ると,OUT_DIR で設定されたパス以下に pcl_cpp.rs バインディングが自動生成されます.
ここに,C++側のクラス,メソッドの情報などが入っています.

Rust の本番アプリケーションから呼出し

あとは Rust 側のライブラリでこのバインディングを読み込めばOKです.
lib.rs に以下を書いて,

1
2
3
4
5
6
7
//lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/pcl_cpp.rs"));
//自動生成されたバインディングを読み込む

main アプリケーションで実際に使ってみます.

1
2
3
4
5
6
7
8
9
10
//main.rs
use pcl_rs::Pcl_rs;

fn main() {
//本番アプリケーションではこのように unsafe ブロックで囲んで使う
unsafe {
let mut my_pcl = Pcl_rs::new();
my_pcl.print_pcl_version();
}
}

Rust でビルドすると…

PCL のバージョンが出た!

Rust で作ったライブラリから PCL を呼び出して,バージョンが出ました!めでたしめでたし.

まとめ

まだ PCL でどれだけのことができるのかわかっていませんが,Rust 側とのデータのコミュニケーションができれば,PCL で提供されている数多くの機能を自作プログラムから呼び出して利用することができるようになりました.
これは実質 PixInsight のすべての力を手に入れたと言っても過言ではない…?
今回のテクニックは,PCL 以外にも C++ で書かれたライブラリを利用する時に有用です.
また何か発展があればブログに書きたいと思います!
それでは.