Skip to content

Integrating JniGen

Eugene Gershnik edited this page Oct 9, 2021 · 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

  • Java/KAPT Annotation processor
    Group Id: io.github.gershnik
    Artifact Id: smjni-jnigen-processor
    Version: corresponds to releases on GitHub

  • KSP Annotation processor
    Group Id: io.github.gershnik
    Artifact Id: smjni-jnigen-kprocessor
    Version: corresponds to releases on GitHub

  • Annotations
    Group Id: io.github.gershnik
    Artifact Id: smjni-jnigen-annotations
    Version: corresponds to releases on GitHub

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

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.5")
    //JNI code generator
    annotationProcessor("io.github.gershnik:smjni-jnigen-processor:3.5")
}

//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"]
}

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

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.5")
    //JNI code generator
    kapt("io.github.gershnik:smjni-jnigen-processor:3.5")
}

//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
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).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

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:${gradle.ext.jniGenVersion}")
    //JNI code generator
    ksp("io.github.gershnik:smjni-jnigen-kprocessor:${gradle.ext.jniGenVersion}")
}

//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 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
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).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"
    }
}