昔から多くのライブラリが作られていたが動かない・メンテナンスが止まってるものたち.
コミュニティベースで開発されてるからしょうがない…
2024 年時点で自分の手元 (Linux/macOS) で動いているもの
It uses C++20 features extensively and aims to support the newest Julia version, rather than focusing on backwards compatibility.
2024 年時点で自分の手元 (Linux/macOS) で動いているもの
今日はこっちを話す
bash
, make
, cmake
https://github.com/JuliaInterop/libcxxwrap-julia
を利用できる受け取った文字列をそのまま返す関数
git clone https://github.com/terasakisatoshi/cmake-playground.git
cd cmake-playground/cxxwrap1
docker build -t cxxwrap1 .
docker run --rm -it -v $PWD:/work -w /work cxxwrap1 bash -c 'bash build.sh && julia callcxx.jl'
下記のようなログが出力されればOK
greet
関数をラップする下記のような C++ コードを用意する
# build.sh の一部改変
SHARED_LIB_EXT=".so" # Linux
SHARED_LIB_EXT=".dylib" # Apple
# Get Julia installation paths
rm Manifest.toml
julia --project -e 'using Pkg; Pkg.instantiate()'
JL=`julia --project -e 'joinpath(Sys.BINDIR, "..") |> abspath |> print'`
PREFIX=`julia --project -e 'using CxxWrap; CxxWrap.prefix_path() |> print'`
# Build shared library with appropriate extension
g++ -fPIC -shared -std=c++17 \
-I${PREFIX}/include/ \
-L${PREFIX}/lib/ \
-I${JL}/include/julia \
-L${JL}/lib \
-ljulia -lcxxwrap_julia hello.cpp -o libhello${SHARED_LIB_EXT}
-I
でヘッダーファイルのパスを指定する
julia.h
, jlcxx/jlcxx.hpp
を使うため-L
でライブラリのパスを指定する
libjulia
, libcxxwrap_julia
とリンクするためbash build.sh
による生成物libhello<拡張子>
が生成される..so
, .dylib
, .dll
など Julia 側からはこの共有ライブラリを実行時に読み込むCppHello
モジュールの中に greet
という関数が定義される大事なことなので2回言いました
プラクティカルな例(大規模なC++プロジェクト)をラップする際,CMake(ソースコードのビルド管理ツール) と仲良くすることになる.
CMakeLists.txt
, cmake ..
みたいなやつ. 見たことあるでしょ?それです.cmake-playground/cxxwrap2
を見てねCMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(cxxwrap2)
# とりあえず書いておく
find_package(JlCxx)
get_target_property(JlCxx_location JlCxx::cxxwrap_julia LOCATION)
get_filename_component(JlCxx_location ${JlCxx_location} DIRECTORY)
# 皆さんが触る箇所はここ
# hello という共有ライブラリを作るためのターゲットを定義
add_library(hello SHARED hello.cpp)
message(STATUS "Found JlCxx at ${JlCxx_location}")
# hello というターゲットは何に依存しているか(リンクすべきか)を記述
target_link_libraries(hello JlCxx::cxxwrap_julia)
jlcxx/jlcxx.hpp
, julia.h
をインクルードするためのディレクトリを指定する必要があった
hello.cpp
をコンパイルするために表面上見えない julia.h
の場所を知る必要があったJlCxx::cxxwrap_julia
に押し付けることができる.ここ を参照cmake
コマンドでビルドができるfind_package(JlCxx)
によって C++ パッケージ JlCxx
の情報を取得することができる.
CXXWRAP_PREFIX
で指定-DCMAKE_PREFIX_PATH
オプションで指定export CMAKE_PREFIX_PATH=...
のようにして環境変数で定義# Get Julia installation paths
CXXWRAP_PREFIX=`julia --project -e 'using CxxWrap; CxxWrap.prefix_path() |> print'`
cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$CXXWRAP_PREFIX
cmake --build ./build --config Release -j `nproc`
./build/libhello<拡張子>
が出来上がる.cmake-playground/cxxwrap4
では Julia の標準のディレクトリ構造を採用している
./deps
ディレクトリに Julia パッケージのビルドをする際のスクリプト・ソースコードを配置するjulia> using Pkg; Pkg.build()
で Julia パッケージのビルドができるPkg.build()
は Julia スクリプト deps/build.jl
を実行する.例えば cmake-playground/cxxwrap4
では次のようにしている
cmake
)./deps
に)Julia のインターフェースしか興味ない人・環境に対してローカルでビルドさせるのはしんどい(環境構築の学習コストが高い)BinaryBuilder.jl を使えば LibHello_jll
のように事前ビルド済みの JLL packages (a pun on “Dynamic-Link Library”, with the J standing for Julia) パッケージを提供すれば良い
↑メンテしてないけれどコンセプトは今でも通じるところはある(はず)
C++ と Julia をつなげるためのコードを用意
ここをどうするか
数値計算の文脈だと配列を渡して配列を返すプログラムが書ければ良いけれど 気軽に試せる例が少ない…
というわけで cmake-playground/cxxwrap6
double(C++), Float64(Julia)
の要素を持つ配列(1, 2 次元配列)に対して演算を施す例を作った.
テンプレートを使っている関数をラップする方法誰か教えてください
要素を二倍にする例
void inplace_twice(jlcxx::ArrayRef<double, 2> jlx) {
for (size_t i = 0; i < jlx.size(); i++) {
jlx[i] = 2 * jlx[i];
}
}
対応する Julia の関数を呼び出すと二次元配列の各要素が二倍になる
CxxWrap.jl の README.md にある Const arrays を参照
const double* const_vector()
{
// static キーワードが重要
static double d[] = {1., 2., 3};
return d;
}
const double* const_matrix()
{
// static キーワードが重要
static double d[2][3] = {{1., 2., 3}, {4., 5., 6.}};
return &d[0][0];
}
// ...module definition skipped...
mymodule.method("const_vector", []() { return jlcxx::make_const_array(const_vector(), 3); });
mymodule.method("const_matrix", []() { return jlcxx::make_const_array(const_matrix(), 3, 2); });
↑の例だと固定長・固定サイズしか扱えなさそう?
動的なサイズを返したいぞ?
// これは実行時にセグフォする. 辛い
mymodule.method("array", [] () {
jlcxx::Array<int> data{ };
data.push_back(1);
data.push_back(2);
data.push_back(3);
return data;
});
std::vector
は返すことができた. Julia 側からは AbstractVector
のサブタイプである CxxWrap.StdVector
のインスタンスとして取得できる.static Eigen::MatrixXd y;
を宣言して y に値を格納する方法を採用すればできた.#include <Eigen/Dense>
// 要素を 3 倍にする
auto triple(jlcxx::ArrayRef<double, 2> jlx) {
size_t size0 = jl_array_dim(jlx.m_array, 0);
size_t size1 = jl_array_dim(jlx.m_array, 1);
// static キーワードをつけなければいけない
static Eigen::MatrixXd y;
auto x = Eigen::Map<Eigen::MatrixXd>(jlx.data(), size0, size1);
// Do something
y = 2 * x + x;
return jlcxx::make_julia_array(y.data(), size0, size1);
}
x::Matrix{Float64}
を入力として受け付けることができているのように Julia のデータ jlx
が持っている数値データを C++ 側にスムーズに渡せる.
Const Arrays の例の場合 Julia 側は 3x2 行列として理解される
列に関するループ変数 c
を先に回さないと直感的な結果を出力できない
void f(jlcxx::ArrayRef<double, 2> jlx) {
size_t size0 = jl_array_dim(jlx.m_array, 0);
size_t size1 = jl_array_dim(jlx.m_array, 1);
std::cout << "[";
for(size_t r = 0; r < size0; r++){
for(size_t c = 0; c < size1; c++){
std::cout << jlx[r + size0 * c];
if (c == size1 - 1){
if (r != size0 - 1){
std::cout << "; ";
}
} else {
std::cout << " ";
}
}
}
std::cout << "]";
std::cout << std::endl;
}
今までの例は便利ではあるが,C++ 側のクラスを返す関数に対応できない
mod.add_type
を使ってできる
下記が Julia のコードとして実行できる
ラッパーを作っているところはこちら EasyEigenInterface.jl/deps/src/jl_easy_eigen_interface.cpp
作ったのは良いが, EasyEigenInterface.jl
で対応づけた MatrixXd 型を他の Julia ライブラリで活用する方法がわからない…
いわゆる「ボクが作ったサイツヨオレオレ実装」にとどまっている.
まだ十分試しきれてないが Geant4 という高エネルギー物理の C++ 実装はラッパー関数を自動生成しているらしい
なんかよくわからないけれどすごそう(小並感) 時間がなかったので誰か解説書いてほしい
おめでとうございます.このスライドを読む必要は全くありません.Clang.jl を使いましょう.
Clang.jl を使って binding を作った例
libqrean をフォークしたもの - https://github.com/terasakisatoshi/libqrean/tree/julia/LibQREAN
go build -buildmode=c-shared -o export.so
とすれば export.h
を作ってくれる.Clang.jl と合わせ技でいい感じのものが作れそう?