Skip to content

Commit

Permalink
Merge pull request #4 from Animeshz/jni-setup
Browse files Browse the repository at this point in the history
Set up JNI along with cross compilations of C/C++ code with docker.
  • Loading branch information
Animeshz committed Dec 30, 2020
2 parents f0ff3b8 + 0c878e3 commit c080b0a
Show file tree
Hide file tree
Showing 11 changed files with 361 additions and 58 deletions.
21 changes: 21 additions & 0 deletions docker/jvm-build/linux-x64/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM dockcross/linux-x64

LABEL maintainer="Animesh Sahu animeshsahu19@yahoo.com"

ENV DEFAULT_DOCKCROSS_IMAGE animeshz/keyboard-mouse-kt:jni-build-linux-x64
ENV WORK_DIR=/work
ENV JNI_HEADERS_DIR=${WORK_DIR}/support-files/headers/jni
ENV X11_HEADERS_DIR=/usr/include/X11/

RUN \
apt-get update && \
apt-get install --no-install-recommends --yes \
curl \
libx11-dev \
libxi-dev && \
mkdir -p ${JNI_HEADERS_DIR} && \
cd ${JNI_HEADERS_DIR} && \
curl 'https://raw.githubusercontent.com/openjdk/jdk/master/src/java.base/share/native/include/jni.h' > jni.h && \
curl 'https://raw.githubusercontent.com/openjdk/jdk/master/src/java.base/unix/native/include/jni_md.h' > jni_md.h

WORKDIR ${WORK_DIR}
17 changes: 17 additions & 0 deletions docker/jvm-build/windows-x64/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM dockcross/windows-shared-x64

LABEL maintainer="Animesh Sahu animeshsahu19@yahoo.com"

ENV DEFAULT_DOCKCROSS_IMAGE animeshz/keyboard-mouse-kt:jni-build-windows-x64
ENV WORK_DIR=/work
ENV JNI_HEADERS_DIR=${WORK_DIR}/support-files/headers/jni

RUN \
apt-get update && \
apt-get install --no-install-recommends --yes curl && \
mkdir -p ${JNI_HEADERS_DIR} && \
cd ${JNI_HEADERS_DIR} && \
curl 'https://raw.githubusercontent.com/openjdk/jdk/master/src/java.base/share/native/include/jni.h' > jni.h && \
curl 'https://raw.githubusercontent.com/openjdk/jdk/master/src/java.base/windows/native/include/jni_md.h' > jni_md.h

WORKDIR ${WORK_DIR}
197 changes: 147 additions & 50 deletions keyboard/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
@file:Suppress("UNUSED_VARIABLE")

import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.internal.jvm.Jvm
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import kotlin.system.exitProcess

plugins {
id("cpp-library")
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
}

Expand All @@ -21,70 +23,165 @@ fun KotlinMultiplatformExtension.configureJvm() {
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
tasks.getByName<Test>("jvmTest") {
useJUnitPlatform()
}

val jvmMain by sourceSets.getting
val jvmTest by sourceSets.getting { dependsOn(jvmMain) }
val jvmTest by sourceSets.getting {
dependsOn(jvmMain)
dependencies {
implementation("io.kotest:kotest-runner-junit5:4.3.2")
implementation("io.kotest:kotest-assertions-core:4.3.2")
}
}
mainSourceSets.add(jvmMain)
testSourceSets.add(jvmTest)

// JNI-C++ configuration
val jniImplementation by configurations.creating
val jniHeaderDirectory = file("src/jvmMain/generated/jni").apply { mkdirs() }
jvmMain.resources.srcDir("build/jni")
jvmTest.resources.srcDir("build/jni")

configurations.matching {
it.name.startsWith("cppCompile") || it.name.startsWith("nativeLink") || it.name.startsWith("nativeRuntime")
}.all { extendsFrom(jniImplementation) }
// Generating Jni headers
val generateJniHeaders by tasks.creating {
group = "build"
dependsOn(tasks.getByName("compileKotlinJvm"))

val jniHeaderDirectory = layout.buildDirectory.dir("jniHeaders")
// For caching
inputs.dir("src/jvmMain/kotlin")
outputs.dir("src/jvmMain/generated/jni")

val generateJniHeaders by tasks.creating(Exec::class) {
group = "build"
doLast {
val javaHome = Jvm.current().javaHome
val javap = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javap") }?.absolutePath ?: error("javap not found")
val javac = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javac") }?.absolutePath ?: error("javac not found")
val buildDir = file("build/classes/kotlin/jvm/main")
val tmpDir = file("build/tmp/jvmJni").apply { mkdirs() }

// if (HostManager.hostIsMingw) {
// commandLine(
// "cmd", "/c", "'${Jvm.current().javaHome.canonicalPath}\\bin\\javah'",
// "-d", "src/jvmMain/jni/generated",
// "-classpath", "src/jvmMain/kotlin",
// "com.github.animeshz.keyboard.jni.TestKt"
// )
// } else {
// commandLine(
// "sh", "-c", "${Jvm.current().javaHome.canonicalPath}/bin/javah",
// "-d", "src/jvmMain/jni/generated",
// "-classpath", "src/jvmMain/kotlin",
// "com.github.animeshz.keyboard.jni.TestKt"
// )
// }
}
buildDir.walkTopDown()
.filter { "META" !in it.absolutePath }
.forEach { file ->
if (!file.isFile) return@forEach

val output = ByteArrayOutputStream().use {
project.exec {
commandLine(javap, "-cp", buildDir.absolutePath, file.absolutePath)
standardOutput = it
}.assertNormalExitValue()
it.toString()
}

library {
binaries.configureEach {
val compileTask = compileTask.get()

compileTask.dependsOn(generateJniHeaders)
compileTask.compilerArgs.addAll(jniHeaderDirectory.map { listOf("-I", it.asFile.canonicalPath) })
compileTask.compilerArgs.addAll(compileTask.targetPlatform.map {
listOf("-I", "${Jvm.current().javaHome.canonicalPath}/include") + when {
it.operatingSystem.isMacOsX ->
listOf("-I", "${Jvm.current().javaHome.canonicalPath}/include/darwin")
it.operatingSystem.isLinux ->
listOf("-I", "${Jvm.current().javaHome.canonicalPath}/include/linux")
it.operatingSystem.isWindows ->
listOf("/I", "${Jvm.current().javaHome.canonicalPath}/include/win32")
else -> emptyList()
val (packageName, className, methodInfo) =
"""public \w*\s*class (.+)\.(\w+) (?:implements.*)\{\R([^\}]*)\}""".toRegex().find(output)?.destructured ?: return@forEach
val nativeMethods =
""".*\bnative\b.*""".toRegex().findAll(methodInfo).mapNotNull { it.groups }.flatMap { it.asSequence().mapNotNull { group -> group?.value } }.toList()
if (nativeMethods.isEmpty()) return@forEach

val source = buildString {
appendln("package $packageName;")
appendln("public class $className {")
for (method in nativeMethods) {
if ("()" in method) appendln(method)
else {
val updatedMethod = StringBuilder(method).apply {
var count = 0
for (i in indices) if (this[i] == ',' || this[i] == ')') insert(i, " arg${count++}")
}
appendln(updatedMethod)
}
}
appendln("}")
}
val outputFile = tmpDir.resolve(packageName.replace(".", "/")).apply { mkdirs() }.resolve("$className.java").apply { createNewFile() }
outputFile.writeText(source)

project.exec {
commandLine(javac, "-h", jniHeaderDirectory.absolutePath, outputFile.absolutePath)
}.assertNormalExitValue()
}
})
}
}

// tasks.getByName<Test>("jvmTest") {
// val sharedLib = library.developmentBinary.get() as CppSharedLibrary
// dependsOn(sharedLib.linkTask)
// systemProperty("java.library.path", sharedLib.linkFile.get().asFile.parentFile)
// }
//
// tasks.getByName<Jar>("jvmJar") {
// from(library.developmentBinary.flatMap { (it as CppSharedLibrary).linkFile })
// }
// For building shared libraries out of C/C++ sources

val compileJni by tasks.creating {
group = "build"
dependsOn(generateJniHeaders)
tasks.getByName("jvmProcessResources").dependsOn(this)

// For caching
inputs.dir("src/jvmMain/jni")
outputs.dir("build/jni")

doFirst {
println("Checking docker installation")

val exit = project.exec { commandLine("where", "docker") }.exitValue
if (exit != 0) {
println("Please install docker before running this task")
exitProcess(1)
}
}

doLast {
class Target(val os: String, val arch: String, val dockerImage: String)

val targets = listOf(
Target("windows", "x64", "animeshz/keyboard-mouse-kt:jni-build-windows-x64"),
Target("linux", "x64", "animeshz/keyboard-mouse-kt:jni-build-linux-x64")
)

for (target in targets) {
// Integrate with CMake
val tmpVar = file(".").absolutePath
val path = if (Os.isFamily(Os.FAMILY_WINDOWS)) "/run/desktop/mnt/host/${tmpVar[0].toLowerCase()}${tmpVar.substring(2 until tmpVar.length).replace('\\', '/')}"
else tmpVar

val work: () -> String = {
ByteArrayOutputStream().use {
project.exec {
val args = arrayOf(
"docker",
"run",
"--rm",
"-v",
"$path:/work/project",
target.dockerImage,
"bash",
"-c",
"mkdir -p \$WORK_DIR/project/build/jni && " +
"mkdir -p \$WORK_DIR/project/build/tmp/compile-jni-${target.os}-${target.arch} && " +
"cd \$WORK_DIR/project/build/tmp/compile-jni-${target.os}-${target.arch} && " +
"cmake \$WORK_DIR/project/src/jvmMain/jni/${target.os}-${target.arch} && " +
"cmake --build . --config Release && " +
"cp -rf libKeyboardKt${target.arch}.{dll,so,dylib} \$WORK_DIR/project/build/jni 2>/dev/null || :"
)
println(args.joinToString(" "))
commandLine(*args)

isIgnoreExitValue = true
standardOutput = System.out
errorOutput = it
}
it.toString()
}
}
var output = work()
val nonDaemonError = "docker: error during connect: This error may indicate that the docker daemon is not running."
if (Os.isFamily(Os.FAMILY_WINDOWS) && output.startsWith(nonDaemonError)) {
project.exec { commandLine("C:\\Program Files\\Docker\\Docker\\DockerCli.exe", "-SwitchDaemon") }.assertNormalExitValue()

do {
Thread.sleep(500)
output = work()
} while (output.startsWith(nonDaemonError))
}

println(output)
}
}
}
}

fun KotlinMultiplatformExtension.configureLinux() {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions keyboard/src/jvmMain/jni/linux-x64/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.10)
project(KeyboardKtx64)

include_directories(KeyboardKtx64 PRIVATE $ENV{JNI_HEADERS_DIR})
include_directories(KeyboardKtx64 PRIVATE "../../generated/jni")

include_directories($ENV{X11_HEADERS_DIR})
include_directories($ENV{X11_HEADERS_DIR}/extensions)

add_library(
KeyboardKtx64 SHARED
JvmKeyboardHandler.cpp
)
24 changes: 24 additions & 0 deletions keyboard/src/jvmMain/jni/linux-x64/JvmKeyboardHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "com_github_animeshz_keyboard_JvmKeyboardHandler.h"

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isCapsLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isNumLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isScrollLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

#ifdef __cplusplus
}
#endif
10 changes: 10 additions & 0 deletions keyboard/src/jvmMain/jni/windows-x64/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.10)
project(KeyboardKtx64)

include_directories(KeyboardKtx64 PRIVATE $ENV{JNI_HEADERS_DIR})
include_directories(KeyboardKtx64 PRIVATE "../../generated/jni")

add_library(
KeyboardKtx64 SHARED
JvmKeyboardHandler.cpp
)
24 changes: 24 additions & 0 deletions keyboard/src/jvmMain/jni/windows-x64/JvmKeyboardHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "com_github_animeshz_keyboard_JvmKeyboardHandler.h"

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isCapsLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isNumLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isScrollLockOn
(JNIEnv *env, jobject obj) {
return 1;
}

#ifdef __cplusplus
}
#endif

0 comments on commit c080b0a

Please sign in to comment.