ScalaからJavaCPP + Java経由でネイティブライブラリーを使う

はじめに

未踏ターゲット2018年度ゲート式量子コンピュータにおける開発でC++で主に書かれた量子コンピューターのシミュレーターであるQulacsを利用している。QulacsはPythonバインディングがすでに用意されているが、やはりより慣れたプログラム言語であるScalaで開発をしたいと常に思っていた。そこでJavaCPPを利用してQulacsのようなネイティブライブラリーをJava経由でScalaから呼び出すために色々と調査したところ、最終的にはsbtプラグインを作ることができた。

実はJavaCPPのGitHubオーガナイゼーションにもsbtプラグインがあるが、調べたところこれはJavaCPPですでに利用できるOpenCVなどをScalaから使いやすくするためのものであり、新規にネイティブライブラリーをScalaから利用できるようにするものではなかったため、今回新規に作成することとした。この記事ではこのsbtプラグインの使い方や一部の内部実装について解説する。 この記事を読んで分からないことや改善点がある場合は、気軽にコメントで教えてほしい。

プラグインの概要

このsbtプラグインは次の3つを行う。

  1. .so(または.dylib)のようなネイティブライブラリーを作成する
  2. (1)のライブラリーとヘッダーファイルをJavaCPPに渡してJNI(Java Native Interface)用のファイルを作成する
  3. (2)をg++のような処理系でコンパイルしてJavaから利用できるネイティブライブラリーを作成する

まず、(1)はライブラリーによってそれの作り方は(makeを使うなど)色々異なると思われるので、sbtのsettingKeyとしてOSコマンドをわたすようになっている。そして、(2)もまたsbtから新たなJavaプロセスを起動している。これはJVMへ渡すオプションを適宜変える必要があり、たしかにsbtのrunコマンドやjavaOptionsなどを適切に変更すれば達成できた可能性もあるが、労力のわりに最終的に起きることにたいした変わりがないということでこのようにした。そしてそのままJavaCPPが(3)を行いJavaから呼びだせるようなダイナミックリンクライブラリーとなる。

設定と使い方

ここからはサンプルのプロジェクトを元に解説していく。今、C++のソースコードとヘッダーファイルが次のように存在するものする。

class HelloWorld {
public:
    int printN(int n);
};
#include <iostream>
#include <string>
#include "HelloWorld.hpp"

using namespace std;

int HelloWorld::printN(int n) {
    for (int i = 0; i < n; i++) {
        cout << "Hello World!\n";
    }

    return n;
}

まず、これをコンパイルするための設定をbuild.sbtへ記述する。

includePath := (baseDirectory in Compile).value / "cpp_src"

libraryName := "libHelloWorld"

makeLibraryCommands := Seq(
  gppCompilerPath.value,
  "-I", includePath.value.toString,
  currentLibraryMeta.value.option,
  "-o",
  (libraryDestinationPath.value / s"${libraryName.value}.${currentLibraryMeta.value.extension}").toString,
  ((baseDirectory in Compile).value / "cpp_src" / "HelloWorld.cpp").toString
)

ここでmakeLibraryCommandslibHelloWorldを作成するために利用されるOSコマンドである。順番に解説していく。 まずgppCompilerPathはデフォルトでclang++が利用される。 次にcurrentLibraryMetaはOSによるclang++といった処理系のオプションと、生成されるネイティブライブラリーの拡張子を吸収するためのデータ構造DynamicLibraryMetaが保存されている。

sealed abstract class DynamicLibraryMeta(
  val option: String,
  val extension: String
)

object DynamicLibraryMeta {
  case object Mac extends DynamicLibraryMeta("-dynamiclib", "dylib")
  case object Linux extends DynamicLibraryMeta("-shared", "so")
}

そしてcurrentLibraryMetaはデフォルトはJavaのプロパティであるSystem.getProperty("os.name")から自動で選択される。libraryDestinationPathもデフォルトではtargetディレクトリ下の専用ディレクトリを利用する。

次にネイティブライブラリーを利用するJavaコードを作る。

package javacpp.sbt;

import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform(include = {"HelloWorld.hpp"}, link = "HelloWorld")
public class HelloWorld extends Pointer {
    static {
        Loader.load();
    }

    public HelloWorld() {
        allocate();
    }

    public native void allocate();

    public native int printN(int n);
}

再びbuild.sbtへ戻り、ネイティブライブラリーと対応するJavaのクラスパスを与える。

nativeJavaClassPath := "javacpp.sbt.*"

enablePlugins(SbtJavaCPP4S)

このようにワイルドカードを使うこともできる。また今回のプラグインを有効にしてbuild.sbtにおける作業は終了となる。

最後にScalaからJava部分の呼び出しコードを作っておく。

object HelloWorld {
  def main(args: Array[String]): Unit = {
    val instance = new HelloWorld()
    instance.printN(5)
  }
}

あとはこれをsbt runすると次のようになる。

$ sbt run
[info] Loading settings for project example from build.sbt ...
[info] Set current project to example (in build file:/Users/yyu/Desktop/javacpp-sbt/example/)
[info] Compiling 1 Scala source to /Users/yyu/Desktop/javacpp-sbt/example/target/scala-2.12/classes ...
[info] Success!
[info] running (fork) org.bytedeco.javacpp.tools.Builder -cp /Users/yyu/Desktop/javacpp-sbt/example/target/scala-2.12/classes:/Users/yyu/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/bytedeco/javacpp/1.5.1/javacpp-1.5.1.jar:/Users/yyu/.sbt/boot/scala-2.12.10/lib/scala-library.jar -Dplatform.compiler=clang++ -Dplatform.includepath=/Users/yyu/Desktop/javacpp-sbt/example/cpp_src -Dplatform.linkpath=/Users/yyu/Desktop/javacpp-sbt/example/target/libjni -d /Users/yyu/Desktop/javacpp-sbt/example/target/libjni javacpp.sbt.*mple / Compile / generateJNILibrary 0s
[info] Generating /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jnijavacpp.cpp
[info] Generating /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jniHelloWorld.cpp
[info] Compiling /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/libjniHelloWorld.dylib
[info] clang++ -I/Users/yyu/Desktop/javacpp-sbt/example/cpp_src -I/Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home/include/darwin /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jniHelloWorld.cpp /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jnijavacpp.cpp -march=x86-64 -m64 -O3 -Wl,-rpath,@loader_path/. -Wall -fPIC -dynamiclib -undefined dynamic_lookup -o libjniHelloWorld.dylib -L/Users/yyu/Desktop/javacpp-sbt/example/target/libjni -Wl,-rpath,/Users/yyu/Desktop/javacpp-sbt/example/target/libjni -lHelloWorld -framework JavaVM
[info] Deleting /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jniHelloWorld.cpp
[info] Deleting /Users/yyu/Desktop/javacpp-sbt/example/target/libjni/jnijavacpp.cpp
[info] running (fork) javacpp.HelloWorld
[info] Hello World!
[info] Hello World!
[info] Hello World!
[info] Hello World!
[info] Hello World!
[success] Total time: 6 s, completed 2019/11/09 4:57:50

このように上手く動作している。

おわりに

小さな例では上手くいったので、次はQulacsのような大きなC++ライブラリーを扱えるようにしていきたい。今回の開発はコード量に対して大変な労力がかかっている。やはりネイティブのエラーをよく分かっていなかったり、ダイナミックリンクについて知っていなかったりと色々な苦労があった。余談となるがGo言語などがシングルバイナリとなっているのは、こういったダイナミックリンクの煩雑さも1つの要因ではないかと思う。

コメント