本日は
- Julia アドベントカレンダーネタです.
- Julia で C++ の資源を CxxWrap.jl というパッケージを使って呼び出す方法について書きます.Python だと C++ を呼び出すために pybind11 や
Boost.Python を使っている方もいると思います.CxxWrap.jl の思想もそれらを受け継いでいるのでそれらのツールを使ったことがある場合はすぐに雰囲気を掴むことができると思います. - 因みに Cxx.jl という REPL に C++ のコードを貼り付けるとそれを Julia から使えるというすごいパッケージも存在しますが,特定のバージョンでのみしか動作を確認していないのでここではスコープ外とします.
準備
前提
- Julia をインストールする
- Linux 環境 または Docker が動く環境を用意する.
- CxxWrap.jl 自体は Linux 限定のパッケージではありませんがこの記事に書いてあることの再現を容易にするためです.
- BinaryBuilder.jl を使えば Windows 環境がなくてもクロスコンパイルによって Linux 環境がホストのマシーンで Windows 用の dll を構成することも可能です.
CxxWrap.jl の導入
- 次のようにして
CxxWrap.jl
をインストールします.
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.5.3 (2020-11-09)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using Pkg; Pkg.add("CxxWrap")
# ... インストールのログが流れる
ひとまず準備は整いました.
コードを用意する
- 以下では C++ の資源として
hello.cpp
, ビルドするためのシェルスクリプトbuild.sh
及び 動作確認のためのコードを用意します.全ては gist にあります.必要に応じてDockerfile
もご利用ください.
2024 年版 はこちら
C++
ひとまずハローワールドでやってみましょう. ここでは C++ の資源が greet
というもので "hello, world" を返す関数であるとします.
#include <string>
#include "jlcxx/jlcxx.hpp"
std::string greet()
{
return "hello, world";
}
JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
{
mod.method("greet", &greet);
}
-
"jlcxx/jlcxx.hpp"
は CxxWrap.jl の裏に隠れているヘッダーファイルを指しています.あとでビルドするときにそのインクルードパスを指定します. - 今回は単純な例なので
hello.cpp
というファイルで閉じていますが,大規模なものになるとヘッダーファイルをインクルードして,C++ の関数と Julia から見える関数や型を対応させます. - 基本的には
mod.method("<Juliaから呼び出したい関数名>", &<C++の関数>);
のなかにJuliaとC++の対応を記述していくことになります.
ビルドスクリプト
- CxxWrap.jl に依存するパッケージは CMake を使ってビルドすることが多いみたいです.ここでは
-I
オプションを使ってビルド方針を取ります.
JL=`julia -e 'joinpath(Sys.BINDIR, "..") |> abspath |> print'`
PREFIX=`julia -e 'using CxxWrap; CxxWrap.prefix_path() |> print'`
g++ -fPIC -shared -std=c++17 \
-I${PREFIX}/include/ \
-I${JL}/include/julia \
hello.cpp -o libhello.so
julia.h
をインクルードできるようにする.
ビルドする際には julia.h
を要請します.ですので julia.h
が住んでいるパスをインクルードディレクトリとして指定する必要があります.
JL
の右辺は julia のパスの2つ階層下のパスを指しています.これはどのように Julia をインストールしたかによって依存します.たとえば,Julia が公式に提供する Docker のコンテナの中では /usr/local/julia
になっています.
# Julia の Docker コンテナ内で実行
root@xxx echo $JL
/usr/local/julia
root@xxx ls $JL
LICENSE.md bin etc include lib libexec share
Mac であれば JL
の値として /Applications/Julia-1.5.app/Contents/Resources/julia
が得られます.
jlcxx/jlcxx.hpp
をインクルードできるようにする.
実は CxxWrap.jl は C++ のコードを呼び出すために使われて,ビルドする際は CxxWrap.jl が依存する C++ のライブラリの libcxxwrap-julia の方が活躍します.
julia -e 'using CxxWrap; CxxWrap.prefix_path() |> print'
をすることで jlcxx/jlcxx.hpp
のヘッダーがあるプレフィックスを教えてくれます.これは Julia の REPL で次のようにして確認することができます.
julia> using CxxWrap
julia> prefix = CxxWrap.prefix_path()
# 実際は ~ の部分は展開されて出力されているはず
~/.julia/artifacts/6017255205dc4fbf4d962903a855a0c631f092dc
julia> ; を入力して REPL をシェルモードに切り替える
shell> tree $prefix -d # tree コマンドでディレクトリの雰囲気を掴んでいる
~/.julia/artifacts/6017255205dc4fbf4d962903a855a0c631f092dc
├── include
│ └── jlcxx
├── lib
│ └── cmake
│ └── JlCxx
├── logs
└── share
└── licenses
└── libcxxwrap_julia
9 directories
CMake を使ってビルドするときは lib/cmake/JlCxx
のパスを指定することになります.
補足 artifacts/...
について
- この
artifact/とても長いアルファベットと数字の文字列
について説明するとなると Julia チームのブログである Pkg + BinaryBuilder -- The Next Generation の記事にあるPkg Artifacts
の部分を追う必要があります.Artifacts について簡単にいうば Julia パッケージが依存する Julia 以外の資源,(たとえば共有ライブラリ,実行形式,画像,データセット,コンパイラ,などなど)をいい感じに提供するシステムです. - CxxWrap.jl の文脈で言えば libcxxwrap-julia のビルド済みプロジェクトがインターネットの世界から降ってきていると理解すれば十分です.
CxxWrap.prefix_path()
はそのビルドずみプロジェクトが実際にどこにダウンロードされたのかを知るための方法の一つです.
とりあえずビルドすると libhello.so
が生成されるはずです.拡張子は必要に応じて変更してください.
Julia から呼び出すぞ!
いよいよ libhello.so
という資源を使って C++ の greet
関数を呼びだそう!!!
module CppHello
using CxxWrap
# ↓の書き方はもう古いらしい
# @wrapmodule(joinpath(".","libhello.so"))
# Julia 1.10.3 が最新安定版の時代だとパスを与える関数を与えなければいけないようだ↓
@wrapmodule(() -> "./libhello")
function __init__()
@initcxx
end
end
# Call greet and show the result
@show CppHello.greet()
CppHello
(なまえはなんでも良い)という名前のモジュールを作って起きます. module の中身は libhello.so
を読み込んで マクロを使って C++ と対応させた Julia の greet
関数を定義させています.これによって CppHello を使うユーザーは CppHello.greet()
というように通常の Julia のモジュールのように扱うことが可能になります. 必要に応じて export
させることもできます.
実応用例
libcxxwrap-julia
- これは CxxWrap.jl が依存する C++ ライブラリです.このリポジトリ libcxxwrap-julia リポジトリの
example
ディレクトリを見ると良いでしょう.テストコードはこれらを用いています. - CxxWrap.jl 近辺のメンテナーの barche/cxxwrap-juliacon2020 さんのリポジトリ,その付属品のスライドページ を見る手もあります.
- YouTube 資料もあります.
OpenCV.jl
-
TakekazuKATO さんの記事 opencv_contribのjulia bindingを使ってみた 及び juliaからOpenCVを使う(opencv_contrib版)ビルド編 で言及されているように Julia のバインディングを作る際に
CxxWrap.jl
が使われています.C++の独自の構造体と Julia の型の対応を取ったりC++の関数ごとに対応するJuliaのコードを書くという作業が出ます.その手間を自動化させているようです.ただし,ソースからビルドする手間がああり,対応しているプラットフォームは限られています. -
とりあえず使ってみたい!っていう人は Linux 環境だと この gist を見ると幸せになると思います.
VideoCaptureWrap.jl
- 自作野良パッケージである VideoCaptureWrap.jl は
opencv_contrib
が提供しているプロジェクトとは独立に C++ と Julia の対応を手書きで作った物です.機能は Web カメラの画像をキャプチャーして C++ と Julia のデータ変換をして cv2::imshow でGUIで表示するという限定的な物ですが,BinaryBuilder の力で Mac/Linux/Windows/RPI3,4/Jetson nano といった幅広いプラットフォームで動作させることができています.opencv_contrib の Julia バィンディングも将来的には BinaryBuilder を使ってくれるようになるのではと心の中で期待しています.
MatplotWrap.jl
- Matplot++ という C++ グラフ描画ライブラリの一部の機能をラップした自作野良パッケージ MatplotWrap.jl も CxxWrap.jl を使っています.
Matplot++
が要請している gcc のバージョンが比較的新しいものを要請するので BinaryBuilder が提供するコンパイラーと齟齬がとれてないので GitHub Actions で Linux と Mac でのみのサポートになっています.
Appendix
今後発展が必要なところ
- ヘッダー作るってところがボトルネックですね・・・
C の場合だと Clang.jl というパッケージでヘッダーを自動生成してくれるらしいのですが,OpenCV.jl のテクニックを一般化した物が Julia パッケージで出てくると幸せになるのかなという気がします.
CMake, BinaryBuilder
- 今回は
g++ ...
という形でビルドしましたが,多くのパッケージは CMake を使ってビルドしています.また,多くのプラットフォームで動作させるために BinaryBuilder.jl を使ってコードをクロスコンパイルさせています.これによりLinuxマシンしか持っていない人でも Windows 向けのバイナリーを提供したり armv7l 向けのバイナリーを提供することもできます. - BinaryBuilder.jl の解説は 私のスライド を見ると解説を書いています.Artifacts システムや, JLL パッケージについての説明も簡単にですが書いてあります.
合わせて
もどうぞ
まとめ
CxxWrap.jl の紹介をしました.Hello World な資源を活用する方法を通して CxxWrap.jl とその取り巻きの技術を紹介しました.応用例も紹介しました.