Skip to content

Integrating JniGen

Eugene Gershnik edited this page Apr 19, 2023 · 14 revisions

On the most basic level to use JniGen you need to

  1. Configure its annotation processor to run. This can be either during Java or Kotlin compilation or separately but must happen before C++ compilation
  2. Use its annotations in your Java code
  3. Include generated headers in your C++ code

Maven packages

  • Repository: Maven Central
  • Group Id: io.github.gershnik
  • Version: corresponds to Releases on GitHub
  • Annotations Artifact Id: smjni-jnigen-annotations
  • Java/KAPT Annotation processor Artifact Id: smjni-jnigen-processor
  • KSP Annotation processor Artifact Id: smjni-jnigen-kprocessor

Configuring annotation processor

The instructions below are for Gradle. If you use something else to build your Java code you will need to figure out the equivalent steps.

Android: pure Java codebase

Code (Groovy)


For a complete example see build.gradle in samples/android/java

repositories {
    google()
    mavenCentral()
}

dependencies {
    //JNI annotations
    compileOnly("io.github.gershnik:smjni-jnigen-annotations:3.7")
    //JNI code generator
    annotationProcessor("io.github.gershnik:smjni-jnigen-processor:3.7")
}

//JniGen settings
def jniGenProps = new Object() {
    //Where to put the generated files
    //Make sure there is nothing else in that folder (it shouldn't even exist). 
    //This will allow removal of stale files
    def generatedPath = file("src/main/cpp/generated").absolutePath

    //Name of the file listing all other generated files 
    def outputListName = "outputs.txt"

    //Additional classes to expose
    def additionalClasses = ["java.lang.Byte"]
}

android {

    //Pass options for JniGen
    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [
                    "smjni.jnigen.dest.path"       : file(jniGenProps.generatedPath).path,
                    "smjni.jnigen.own.dest.path"   : "true",
                    "smjni.jnigen.output.list.name": jniGenProps.outputListName,
                    "smjni.jnigen.expose.extra"    : jniGenProps.additionalClasses.join(";").toString()
            ]
        }
    }
}

//This makes Gradle rebuild Java compilation (and so run annotation processor)
//when any of the generated files are missing
//Use libraryVariants if you are building a library
android.applicationVariants.all { variant ->

    variant.javaCompileProvider.get().outputs.upToDateWhen {

        def jniGenOutputList = file("${jniGenProps.generatedPath}/${jniGenProps.outputListName}")

        if (!jniGenOutputList.exists()) {
            return false
        }

        for(line in jniGenOutputList) {
            if (!file("${jniGenProps.generatedPath}/$line").exists()) {
                return false
            }
        }
        return true
    }
}


//Clean generated headers on project clean
task cleanJNIHeaders(type: Delete) {
    delete file("${jniGenProps.generatedPath}")
}
clean.dependsOn cleanJNIHeaders

//Make Java compilation (and JniGen code generation) run before CMake build
tasks.whenTaskAdded { theTask ->
    def match = theTask.name =~ ~/^buildCMake(.*)$/
    if (match) {
        def config
        switch(match.group(1)) {
            case "RelWithDebInfo": config = "Release"; break
            default: config = match.group(1); break
        }
        theTask.dependsOn "compile${config}JavaWithJavac"
    }
}

Android: Kotlin + Java codebase with KAPT annotation processing

Code (Groovy)


For a complete example see build.gradle in samples/android/kotlin-kapt

repositories {
    google()
    mavenCentral()
}

dependencies {
    //JNI annotations
    compileOnly("io.github.gershnik:smjni-jnigen-annotations:3.7")
    //JNI code generator
    kapt("io.github.gershnik:smjni-jnigen-processor:3.7")
}

//JniGen settings
def jniGenProps = new Object() {
    //Where to put the generated files
    //Make sure there is nothing else in that folder (it shouldn't even exist). 
    //This will allow removal of stale files
    def generatedPath = "src/main/cpp/generated"

    //Name of the file listing all other generated files 
    def outputListName = "outputs.txt"

    //Additional classes to expose
    def additionalClasses = ["java.lang.Byte"]
}

//Pass options for JniGen via KAPT
kapt {
    useBuildCache = false
    arguments {
        arg("smjni.jnigen.dest.path", jniGenProps.generatedPath)
        arg("smjni.jnigen.own.dest.path", "true")
        arg("smjni.jnigen.output.list.name", jniGenProps.outputListName)
        arg("smjni.jnigen.expose.extra", jniGenProps.additionalClasses.join(";").toString())
    }
}

//This makes Gradle rebuild Kotlin compilation (and so run annotation processor)
//when any of the generated files are missing
//Note: use org.jetbrains.kotlin.gradle.tasks.KotlinCompile instead of 
//      org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask for older
//      versions of kotlin-gradle-plugin plugin 
tasks.withType(org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask).all {
    outputs.upToDateWhen {

        def jniGenOutputList = file("${jniGenProps.generatedPath}/${jniGenProps.outputListName}")

        if (!jniGenOutputList.exists()) {
            return false
        }

        for(line in jniGenOutputList) {
            if (!file("${jniGenProps.generatedPath}/$line").exists()) {
                return false
            }
        }
        return true
    }
}

//Clean generated headers on project clean
task cleanJNIHeaders(type: Delete) {
    delete file("${jniGenProps.generatedPath}")
}
clean.dependsOn cleanJNIHeaders

//Make KAPT (and so JniGen code generation) run before CMake build
tasks.whenTaskAdded { theTask ->
    def match = theTask.name =~ ~/^buildCMake(.*)$/
    if (match) {
        def config
        switch(match.group(1)) {
            case "RelWithDebInfo": config = "Release"; break
            default: config = match.group(1); break
        }
        theTask.dependsOn "kapt${config}Kotlin"
    }
}

Android: Kotlin + Java codebase with KSP annotation processing

Groovy


For a complete example see build.gradle in samples/android/kotlin-ksp

repositories {
    google()
    mavenCentral()
}

dependencies {
    //JNI annotations
    compileOnly("io.github.gershnik:smjni-jnigen-annotations:3.7")
    //JNI code generator
    ksp("io.github.gershnik:smjni-jnigen-kprocessor:3.7")
}

//JniGen settings
def jniGenProps = new Object() {
    //Where to put the generated files
    //Make sure there is nothing else in that folder (it shouldn't even exist). 
    //This will allow removal of stale files
    def generatedPath = file("src/main/cpp/generated").absolutePath

    //Name of the file listing all other generated files 
    def outputListName = "outputs.txt"

    //Additional classes to expose
    def additionalClasses = ["java.lang.Byte"]
}

//Pass options for JniGen via KSP
ksp {
    arg("smjni.jnigen.dest.path", jniGenProps.generatedPath)
    arg("smjni.jnigen.own.dest.path", "true")
    arg("smjni.jnigen.output.list.name", jniGenProps.outputListName)
    arg("smjni.jnigen.expose.extra", jniGenProps.additionalClasses.join(";").toString())
}

//This makes Gradle rebuild Kotlin compilation (and so run annotation processor)
//when any of the generated files are missing
//Note: use org.jetbrains.kotlin.gradle.tasks.KotlinCompile instead of
//      com.google.devtools.ksp.gradle.KspTaskJvm for older 
//      versions of kotlin-gradle-plugin
tasks.withType(com.google.devtools.ksp.gradle.KspTaskJvm).all {
    outputs.upToDateWhen {

        def jniGenOutputList = file("${jniGenProps.generatedPath}/${jniGenProps.outputListName}")

        if (!jniGenOutputList.exists()) {
            return false
        }

        for(line in jniGenOutputList) {
            if (!file("${jniGenProps.generatedPath}/$line").exists()) {
                return false
            }
        }
        return true
    }
}

//Clean generated headers on project clean
task cleanJNIHeaders(type: Delete) {
    delete file("${jniGenProps.generatedPath}")
}
clean.dependsOn cleanJNIHeaders

//Make KSP (and so JniGen code generation) run before CMake build
tasks.whenTaskAdded { theTask ->
    def match = theTask.name =~ ~/^buildCMake(.*)$/
    if (match) {
        def config
        switch(match.group(1)) {
            case "RelWithDebInfo": config = "Release"; break
            default: config = match.group(1); break
        }
        theTask.dependsOn "ksp${config}Kotlin"
    }
}

Kotlin


For a complete example see build.gradle.kts in samples/android/kotlin-ksp-kts

repositories {
    google()
    mavenCentral()
}

dependencies {
    //JNI annotations
    compileOnly("io.github.gershnik:smjni-jnigen-annotations:3.7")
    //JNI code generator
    ksp("io.github.gershnik:smjni-jnigen-kprocessor:3.7")
}

//JniGen settings
class JniGenProps{
    //Where to put the generated files
    //Make sure there is nothing else in that folder (it shouldn't even exist). 
    //This will allow removal of stale files
    val generatedPath: String = file("src/main/cpp/generated").absolutePath

    //Name of the file listing all other generated files
    val outputListName = "outputs.txt"

    //Additional classes to expose
    val additionalClasses = arrayOf("java.lang.Byte")
}
val jniGenProps = JniGenProps()


//Pass options for JniGen via KSP
ksp {
    arg("smjni.jnigen.dest.path", jniGenProps.generatedPath)
    arg("smjni.jnigen.own.dest.path", "true")
    arg("smjni.jnigen.output.list.name", jniGenProps.outputListName)
    arg("smjni.jnigen.expose.extra", jniGenProps.additionalClasses.joinToString(";"))
}

//This makes Gradle rebuild Kotlin compilation (and so run annotation processor)
//when any of the generated files are missing
//Note: use org.jetbrains.kotlin.gradle.tasks.KotlinCompile instead of
//      com.google.devtools.ksp.gradle.KspTaskJvm for older 
//      versions of kotlin-gradle-plugin
tasks.withType<com.google.devtools.ksp.gradle.KspTaskJvm> {
    outputs.upToDateWhen utd@{

        val jniGenOutputList = file("${jniGenProps.generatedPath}/${jniGenProps.outputListName}")

        if (!jniGenOutputList.exists()) {
            return@utd false
        }

        for(line in jniGenOutputList.readLines()) {
            if (!file("${jniGenProps.generatedPath}/$line").exists()) {
                return@utd false
            }
        }

        return@utd true
    }
}


//Clean generated headers on project clean
tasks.register<Delete>("cleanJNIHeaders") {
    delete(file(jniGenProps.generatedPath))
}
tasks.named("clean") {
    dependsOn("cleanJNIHeaders")
}

//Make KSP (and so JniGen code generation) run before CMake build
tasks.whenTaskAdded {
    val match = Regex("""^buildCMake([^\[]*).*$""").matchEntire(name)
    if (match != null) {
        val config = when(match.groupValues[1]) {
            "RelWithDebInfo" -> "Release"
            else -> match.groupValues[1]
        }
        dependsOn("ksp${config}Kotlin")
    }
}