本記事について
SWIGを使ってJavaからC++のコードを呼び出す方法について、サンプルコードを載せながら紹介する。この記事ではhello worldからclassの定義と簡単なSTLの使い方くらいまで書く。
※ SWIG歴1週間くらいで記事を書いているため、「こうすればとりあえず動く」ようなコードです。必ずしもベストな方法ではないかもしれません。また、誤り等あればご指摘いただけると幸いです。
サンプルコード: https://github.com/TkrUdagawa/swig_java_sample
SWIGとは
Simplified Wrapper and Interface Generator の略でC/C++で書かれたコードを他の言語から呼び出せるラッパーを生成するツール。Java以外にもPython, Ruby, PHPなどのスクリプト言語もサポートされている。
公式ページ:
http://www.swig.org/
手元の実行環境
- Ubuntu 18.04
- gcc 7.3.0
- swig 4.0.0 (ソースからビルド)
- java openjdk version "11.0.3"
SWIGのインストール
執筆時の最新版は4.0.0で2019年4月末にリリースされている。リリースノートを見ると4.0.0でC++11のSTL containerが加わったりしているようなので、少し面倒でも4.0.0をインストールするのが良さそう。
ダウンロード
公式サイトのダウンロードページ からダウンロードする。
インストール
ダウンロードしてきたファイルを展開して、configure, makeでインストールする
$ tar xvfz swig-4.0.0.tar.gz
$ cd swig-4.0.0
$ ./configure
$ make
$ make install
サンプル
1. Hello world
まずは最も単純な例。C++で定義した関数をJavaから呼び出すサンプルを作る。
自分で用意するファイルは以下の3つ
├── Main.java
├── hello.cpp
└── hello.i
C++側の用意
まずはJavaから呼び出されるC++のコードを用意する。
#include <iostream>
void hello() {
std::cout << "Hello World!" << std::endl;
}
次にこれをラップするためのSWIGのインターフェースファイルを作成する
%module hello
%{
void hello(); <-①
%}
void hello(); <- ②
%module では作成するモジュールの名前を指定する。
%{ %}の間にC/C++のheaderや宣言を記述し、最後に他言語から呼び出すときのインターフェースとして提供するものを書く。
同じvoid hello() を2回書いているが、①を書かないとコンパイル時にundeclared errorで失敗、②を消すとJavaの実行時に関数が見つからずにこける。
ここまで用意できたらswig
コマンドでラッパーコードを生成する。
$ swig -java -c++ -cppext cpp hello.i
引数の意味は -java
で生成するソースコードのターゲット言語を指定、-c++
でC++の機能を処理できるようにする、-cppext cpp
で生成されるC++のコードの拡張子を.cpp
にしている。何も指定しないと拡張子はcxxになる。
このコマンドを実行すると以下の3つのファイルが生成される。
- hello_wrap.cpp
- hello.java
- helloJNI.java
次にC++のコードをコンパイルして共有ライブラリを作成する。
$ g++ -shared -o libhello.so -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -fPIC hello.cpp hello_wrap.cpp
includeパスには jni.h
, jni_md.h
などがあるパスを指定する必要がある。
指定しない場合、コンパイルエラーで特定のヘッダファイルが見つからないと怒られるので、その都度locate
コマンドなどでファイルを探してinclude pathに追加していけば大丈夫。
Java側の用意
C++のライブラリを読み込むJava側の実装を行う。
public class Main {
static {
System.loadLibrary("hello"); // <- C++ライブラリのロード
}
public static void main(String[] args) {
hello.hello();
}
}
これをSWIGで生成されたファイルとともにコンパイルして実行する。これくらいの例なら何も考えずに javac *.java
などとやってしまっても良い
$ javac hello.java helloJNI.java Main.java
$ java Main
Hello World! <- C++の関数の実行結果
C++側のhello関数を呼び出して、文字列をプリントすることができた。
2. Classを使う
縦と横の長さをセットして面積を計算できる長方形クラスを作る。手順は基本的に1. と一緒。
今回用意するファイル
├── Main.java
├── square.cpp
├── square.hpp
└── square.i
今回はヘッダとソースを分けてみる。
C++側の用意
C++側の長方形のクラスとして SquareC
クラスを実装する。なお、インターフェースファイルで定義するモジュール名とクラス名が重複してはならない。
class SquareC {
public:
SquareC(double x, double y);
double area();
private:
double height_;
double width_;
};
#include "square.hpp"
SquareC::SquareC(double x, double y) : height_(x), width_(y) {
}
// 面積を返す
double SquareC::area() {
return height_ * width_;
}
次にインターフェースファイルを用意する。
%module square
%{
#include "square.hpp" <- ①
%}
%include "square.hpp" <- ②
1.でも書いたように、①のsquare.hppは宣言、②のsquare.hppはインターフェースの定義のために読み込んでいる。
swigコマンドを実行すると今回は4つのファイルが生成される。C++で定義しているclassに対応するSquareC.java
というのがHello Worldの例ではなかったファイルである。
$ swig -java -c++ -cppext cpp square.i
- square_wrap.cpp
- squareJNI.java
- square.java
- SquareC.java
あとは同様にコンパイルする
$ g++ -shared -o libsquare.so -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -fPIC square.cpp square_wrap.cpp
Java側の用意
square ライブラリをロードする。C++側で定義していたclassは通常のJavaのクラスと同様に使用することができる。
public class Main {
static {
System.loadLibrary("square");
}
public static void main(String[] args) {
SquareC s = new SquareC(3.0, 4.0);
System.out.println(s.area());
}
}
コンパイルして実行
$javac Main.java squareJNI.java SquareC.java square.java
$java Main
12.0
SquareCクラスのインスタンスを作成し、そのメソッドを呼び出して結果をJavaでプリントすることができた。
3. stringを使う
std::stringを使うサンプル。簡単なsetterとgetterでstringの入出力を使う例を作る。
用意するファイルは以下の4つ
├── Main.java
├── person.cpp
├── person.hpp
└── person.i
C++側の用意
名前と年齢のメンバを持つPersonC classを作成する。
#include <string>
class PersonC {
public:
PersonC(const std::string&, int);
const std::string& get_name() const;
void set_name(std::string&);
int get_age() const;
void set_age(int);
private:
std::string name_;
int age_;
};
#include <string>
#include "person.hpp"
PersonC::PersonC(const std::string& name, int age) : name_(name), age_(age) {}
const std::string& PersonC::get_name() const {
return name_;
}
void PersonC::set_name(std::string& name) {
name_ = name;
}
int PersonC::get_age() const {
return age_;
}
void PersonC::set_age(int age) {
age_ = age;
}
インターフェースファイルには新たに%include <std_string.i>
を追加する。これはSWIGがあらかじめ用意しているstd::string
を扱うためのインターフェースファイルである。
%module person
%include <std_string.i>
%{
#include "person.hpp"
%}
%include "person.hpp"
これまで同様swigコマンドを実行する。
$ swig -java -c++ -cppext cpp person.i
以下の5つのファイルが生成される。
- personJNI.java
- person.java
- SWIGTYPE_p_std__string.java
- PersonC.java
- person_wrap.cpp
std::stringをラップするSWIGTYPE_p_std__string.java
というファイルが新たに生成されている。
続いてC++のライブラリを作成する。
g++ -shared -o libperson.so -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -fPIC person.cpp person_wrap.cpp
Java側の用意
特に意識することなくString classを使って以下のように書くことができる。
public class Main {
static {
System.loadLibrary("person");
}
public static void main(String[] args) {
String p_name = "Taro";
PersonC p = new PersonC(p_name, 30);
String n = p.get_name();
System.out.println(n);
}
}
$ javac *.java
$ java Main
Taro
4. Vectorを使う
std::vectorを使って内積を計算させるサンプル。インターフェースファイル内でtemplateを使ってあげるとvectorを簡単にJavaでも使うことができる。
├── Main.java
├── inner.cpp
├── inner.hpp
└── inner.i
C++側の用意
C++側は特に何も意識することなくdoubleのvectorの内積を計算するプログラムを書いていく。
#include<vector>
double inner_product(const std::vector<double>&, const std::vector<double>&);
#include "inner.hpp"
double inner_product(const std::vector<double>& a,
const std::vector<double>& b) {
double ret_val = 0;
for (size_t i = 0; i < a.size(); ++i) {
ret_val += a[i] * b[i];
}
return ret_val;
}
次にインターフェースファイルを書く。
%include <std_vector.i>
%{
#include "inner.hpp"
%}
%include "inner.hpp"
%template(DVec) std::vector<double>;
<std_vector.i>
をインクルードしてvectorのインターフェースを使うとともに、新たに %template
という行が登場する。これはその名の通りC++のtemplateをラップするためのインターフェースを定義するもので、こうすることでターゲット言語でもC++のtemplateを使ったオブジェクトを扱うことができるようになる。この例では std::vector<double>
に DVec
という型でアクセスできるようになる。
swig コマンドを実行すると以下のようなファイルが生成される。
- DVec.java
- inner.java
- innerJNI.java
- inner_wrap.cpp
DVecに対応するJavaソースが生成されている。そして、これまで同様共有ライブラリを作成する。
$ g++ -shared -o libinner.so -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -fPIC inner.cpp inner_wrap.cpp
Java側の用意
DVecとして定義した <std::vector>
を使って実装していく。生成された DVec.java
を覗くとJavaでどのような実装になっているのか見ることができる。
public class DVec extends java.util.AbstractList<Double> implements java.util.RandomAccess {
private transient long swigCPtr;
protected transient boolean swigCMemOwn;
(以下略)
AbstractListを継承する形で実装されていることがわかる。従って、Javaから使用する際には
std::vectorではなくAbstractListのメソッド(addなど)を呼んであげる必要がある。
public class Main {
static {
System.loadLibrary("inner");
}
public static void main(String[] args) {
DVec a = new DVec();
a.add(1.0);
a.add(2.0);
a.add(3.0);
DVec b = new DVec();
b.add(3.0);
b.add(4.0);
b.add(5.0);
System.out.println(inner.inner_product(a, b));
}
}
これをコンパイルして実行する。
$ javac *.java
$ java Main
26.0
5. vectorのvectorを使う
templateが入れ子になっている例を扱う。中間の型をターゲット言語側で使う予定がないなら一番外側のテンプレートのみをインターフェースファイルに書いておけばOK。
ランダムな3×3の行列を作って表示する例を作成する。
C++側の用意
3×3の行列を作成するcreate関数と表示するprint_matrix関数を実装していく。
#include <vector>
std::vector<std::vector<double>> create();
void print_matrix(const std::vector<std::vector<double>>&);
#include <random>
#include <iostream>
#include "matrix.hpp"
std::vector<std::vector<double>> create() {
std::random_device rnd {};
std::vector<std::vector<double>> m {};
for (size_t i = 0; i < 3; ++i) {
std::vector<double> v {};
for (size_t j = 0; j < 3; ++j) {
v.push_back(rnd());
}
m.push_back(v);
}
return m;
}
void print_matrix(const std::vector<std::vector<double>>& m) {
std::cout << "[" << std::endl;
for (const auto& r : m) {
std::cout << " ";
for (const auto& e : r) {
std::cout << e << " " ;
}
std::cout << std::endl;
}
std::cout << "]" << std::endl;
}
次にインタフェースファイルを作成する。
%module matrix
%include <std_vector.i>
%{
#include "matrix.hpp"
%}
%include "matrix.hpp"
%template (DMatrix) std::vector<std::vector<double>>;
DMatrixのみをインターフェースファイルに記述し、std::vectorについては何も書かないようにしてみる。
この状態でswigコマンドを実行すると以下のファイルが作成される。
- matrixJNI.java
- matrix.java
- DMatrix.java
- matrix_wrap.cpp
- SWIGTYPE_p_std__vectorT_double_t.java
vector double に対応するようなファイルも作成されているように見える。
これまで同様共有ライブラリを作成してC++側の準備を終える。
$ g++ -shared -o libmatrix.so -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -fPIC matrix.cpp matrix_wrap.cpp
Java側の用意
Java側のコードを実装する。DMatrixクラスの変数を作成し、関数を読んだり要素にアクセスしてみる。
public class Main {
static {
System.loadLibrary("matrix");
}
public static void main(String[] args) {
DMatrix m = matrix.create();
matrix.print_matrix(m);
System.out.println(m.get(0)); <- mの要素0をプリントしてみる
System.out.println(m); <- m自体をプリントしてみる
}
}
これを実行すると以下のような結果が得られる。
$ javac *.java
$ java Main
[
1.99974e+09 2.96596e+08 1.57757e+09
1.71478e+09 6.51067e+08 2.89146e+09
1.63441e+09 9.24007e+08 2.31229e+09
]
SWIGTYPE_p_std__vectorT_double_t@27c170f0
[SWIGTYPE_p_std__vectorT_double_t@2626b418, SWIGTYPE_p_std__vectorT_double_t@5a07e868, SWIGTYPE_p_std__vectorT_double_t@76ed5528]
C++側はきちんとDMatrix型の変数を作成し、print_matrixも期待通り動作している。一方でJava側はインスタンスの情報がプリントされるだけでベクトルの中身はプリントされていない。
SWIGTYPE_p_std__vectorT_double_tというのがDVecに対応しそうなクラスではあるが、それのソースコードを見ると4. のサンプルで見たものとは明らかに実装が違うことがわかる。
public class SWIGTYPE_p_std__vectorT_double_t {
private transient long swigCPtr;
protected SWIGTYPE_p_std__vectorT_double_t(long cPtr, @SuppressWarnings("unused") boolean futureUse) {
swigCPtr = cPtr;
}
protected SWIGTYPE_p_std__vectorT_double_t() {
swigCPtr = 0;
}
protected static long getCPtr(SWIGTYPE_p_std__vectorT_double_t obj) {
return (obj == null) ? 0 : obj.swigCPtr;
}
}
このクラスはswigCPtrというメンバ変数とそれのgetterしか提供していないことがわかる。
なお、インターフェースファイルでDMatrixの要素であるDVecもtemplateで宣言してあげるとJavaからもベクトルの中身をプリントすることができるようになる。
%module matrix
%include <std_vector.i>
%{
#include "matrix.hpp"
%}
%include "matrix.hpp"
%template (DMatrix) std::vector<std::vector<double>>;
%template (DVec) std::vector<double>;
$ java Main
java Main
[
3.37025e+09 3.25125e+09 1.91348e+08
2.32276e+09 2.57749e+09 3.0991e+09
1.9426e+09 2.75113e+09 4.03224e+09
]
[3.370253657E9, 3.251246011E9, 1.91347842E8]
[[3.370253657E9, 3.251246011E9, 1.91347842E8], [2.322762433E9, 2.577487148E9, 3.099102289E9], [1.942601516E9, 2.751128283E9, 4.032242749E9]]
SWIGでサポートされているSTLについては同様の方法で扱うことができるはず。
長くなったので一旦ここまで。