JNIを使う(2) Gradleを用いたビルド
この記事は「大分高専 Advent Calendar 2024」3日目の記事です。
はじめに
前回は、コマンドライン上でJNIを使ったプログラムをビルドしました。しかしコマンドライン上でのビルドは手順が多く、コードを変更する度に毎回手動でビルドするのは大変です。そこで、今回はJavaもC/C++もどちらもビルドできるGradleを使って、JNIのビルドを自動化してみようと思います。
プロジェクト構成
今回は:cpp
というプロジェクトにネイティブのC++コード、:java
というプロジェクトにJavaコードを格納し、自動でビルドさせます。
$ ./gradlew -q projects
Projects:
------------------------------------------------------------
Root project 'jni_practice'
------------------------------------------------------------
Root project 'jni_practice'
+--- Project ':cpp'
\--- Project ':java'
cpp側のビルドスクリプト設定
まず、:cpp
プロジェクトのビルドスクリプトを作成します。
plugins {
id("cpp-library")
}
group = "com.example"
version = "1.0-SNAPSHOT"
tasks.withType<CppCompile> {
dependsOn(":java:classes")
}
library {
linkage = listOf(Linkage.SHARED)
privateHeaders {
// for jni.h
val javaHome = System.getProperty("java.home")
from("$javaHome/include")
// OS-specific include directories
val os = System.getProperty("os.name").toLowerCase()
when {
os.contains("win") -> from("$javaHome/include/windows")
os.contains("mac") -> from("$javaHome/include/darwin")
os.contains("nix") || os.contains("nux") -> from("$javaHome/include/linux")
else -> throw GradleException("Unsupported operating system for JNI headers")
}
// For generated headers
val javaProject = project(":java")
from("${javaProject.layout.buildDirectory.get()}/generated/sources/headers/java/main")
}
}
Java側のソースコードから生成されるヘッダファイルが必要なので、CppCompile
タスクを:java:classes
タスクに依存させています。
また、linkageにLinkage.SHARED
を指定して、共有ライブラリを作成するようにしています。
また、インクルードパスについては、まずfrom("$javaHome/include")
でjni.h
用のパスを追加しています。OS固有のヘッダーは、使っているOS毎にインクルード先を変化させる処理を入れています。最後に、:java
プロジェクトによって生成されるネイティブメソッドの関数プロトタイプが列挙されるヘッダーへのパスは、Project
のlayout.buildDirectory.get()
でビルドディレクトリを取得し、指定しています。
java側のビルドスクリプト設定
次に、:java
プロジェクトのビルドスクリプトを作成しましょう。
plugins {
id("java")
id("application")
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
application{
mainClass = "com.example.hello_world.Main"
}
tasks.named<JavaExec>("run") {
dependsOn(":cpp:assembleRelease")
val cLib = project(":cpp")
jvmArgs = listOf("-Djava.library.path=${cLib.layout.buildDirectory.get()}/lib/main/release")
}
tasks.test {
useJUnitPlatform()
}
run
タスクを、:cpp:assembleRelease
タスクに依存させることで、必ず:cpp
が共有ライブラリを出力した後にjavaコードが実行されることを保証しています。また、jvmArgs
プロパティに:cpp
ライブラリが生成した共有ライブラリへのパスを入れ、実行時に共有ライブラリを解決できるようにしています。
classesタスクによるJNI用ヘッダファイルの生成と実装
javaコードにネイティブメソッドを定義したら、:java:classes
タスクを実行し、JNI用のヘッダファイルを生成しましょう。ヘッダファイルの名前は、デフォルトではネイティブメソッドを持つクラスへの完全修飾パスのスネークケースとなっています。
ヘッダファイルが出来たら、C++コードでヘッダファイルをインクルードし、関数プロトタイプを実装すればOKです。先ほどのビルドスクリプト設定のお陰で、生成されたヘッダファイルはビルド時に自動的に解決されるようになっています。
#include "com_example_hello_world_JniClass.h"
JNIEXPORT void JNICALL Java_com_example_hello_1world_JniClass_theNativeMethod(JNIEnv *, jobject)
{
printf("Hello JNI World\n");
}
(因みに、ここでhello_1world
と謎の1が入っているのが不思議ですが、どうやら後置の1がエスケープ文字になっている模様。com.example.hello_world.JniClass
-> com_example_hello_1world_JniClass
)
実行
- ネイティブメソッドの定義
- それによるヘッダファイル生成
- C++側でヘッダファイルを読み込み、実装
このステップが完了したら、実際にプログラムを実行しましょう。
./gradlew run
試しに前回のコードを実行してみると、
> Task :java:run
Hello JNI World
きちんとC++とjavaが連携されていますね🎉
おわりに
今回は、煩雑なJNIのビルドを、Gradleを使って自動化してみました。コマンドライン上でのビルドは骨が折れるので、改めてビルドツールの有り難みを感じますね…。
Discussion