Skip to content

Commit

Permalink
Add project with access to JNI / JVMTI functionality (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKunz authored Jan 5, 2024
1 parent 6cd1403 commit 6d1541c
Show file tree
Hide file tree
Showing 17 changed files with 831 additions and 0 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ opentelemetryInstrumentationAlphaBom = { group = "io.opentelemetry.instrumentati
awsContribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-aws-resources", version.ref = "opentelemetryContribAlpha" }
contribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-resource-providers", version.ref = "opentelemetryContribAlpha" }

assertJ-core = "org.assertj:assertj-core:3.24.2"

[bundles]

[plugins]
Expand Down
17 changes: 17 additions & 0 deletions jvmti-access/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This project provides access to native functionality from within java, especially to `JVMTI`.
It is implemented by loading a native library. The sources of the native library can be found in `src/main/jni`.

## Building

The native library can be built via `./gradlew :jvmti-access:compileJni`.
This will run compilers for various OS and architecture combinations using docker.
Therefor docker must be running when building this project.
The `compileJni` task is integrated into the standard tasks, e.g. running `assemble` will
automatically rebuild the native library if required.

## Development

For the best development experience we recommend opening the `src/main/jni` directory
in Visual Studio Code with the CPP extension installed. The folder is configured to automatically
pick up the jni / jvmti header files from your `$JAVA_HOME` in order to provide autocompletion and
a pleasant development experience.
227 changes: 227 additions & 0 deletions jvmti-access/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import com.bmuschko.gradle.docker.tasks.image.DockerExistingImage
import com.github.dockerjava.api.async.ResultCallback
import com.github.dockerjava.api.command.CreateContainerResponse
import com.github.dockerjava.api.command.WaitContainerResultCallback
import com.github.dockerjava.api.model.*
import java.io.IOException
import java.util.*

plugins {
id("java")
id("com.bmuschko.docker-java-application") version "9.4.0"
}

dependencies {
testImplementation(libs.assertJ.core)
}

// we use Java 7 for this project so that it can be reused in the old elastic-apm-agent
// Subsequently, the newest Java compiler we can use is java 17
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks {
compileJava {
options.release.set(7)
}
}

val jniSrcDir = file("src/main/jni")
val jniBuildDir: Directory = layout.buildDirectory.dir("jni").get()

sourceSets {
main {
resources {
srcDir(jniBuildDir)
}
}
}

val sharedCompilerArgs = "-std=c++20 -O2 -ftls-model=global-dynamic -fPIC -Wall -Werror -Wextra -shared"
val nativeTargets = listOf(
NativeTarget(
"darwin-arm64.so",
"jni_darwin.Dockerfile",
"-arch arm64 $sharedCompilerArgs"
),
NativeTarget(
"darwin-x64.so",
"jni_darwin.Dockerfile",
"-arch x86_64 $sharedCompilerArgs"
),
NativeTarget(
"linux-arm64.so",
"jni_linux_arm64.Dockerfile",
"-mtls-dialect=desc $sharedCompilerArgs"
),
NativeTarget(
"linux-x64.so",
"jni_linux_x64.Dockerfile",
"-mtls-dialect=gnu2 $sharedCompilerArgs"
)
)

task("buildJavaIncludesImage", DockerBuildImage::class) {
dockerFile.set(file("jni-build/java_includes.Dockerfile"))
inputDir.set(file("jni-build"))
images.add("elastic_jni_build_java_includes:latest")
}

val compileJniTask = task("compileJni")
compileJniTask.group = "jni"
tasks.processResources {
dependsOn(compileJniTask)
}

nativeTargets.forEach {
val taskSuffix = it.getTaskSuffix();

val createImageTask = task("buildCompilerImage$taskSuffix", DockerBuildImage::class) {
dependsOn("buildJavaIncludesImage")
dockerFile.set(file("jni-build/"+it.dockerfile))
inputDir.set(file("jni-build"))
}

val artifactCompileTask = task("compileJni$taskSuffix", DockerRun::class) {
dependsOn(createImageTask)
//compileJava generates the JNI-headers from native methods
dependsOn(tasks.compileJava)

val artifactName = "elastic-jvmti-${it.artifactSuffix}"
val actualOutputDir = jniBuildDir.asFile.resolve("elastic-jvmti")
val artifactFile = actualOutputDir.resolve(artifactName)
val generatedHeadersDir = layout.buildDirectory.get().dir("generated/sources/headers/java/main")

inputs.dir(jniSrcDir)
inputs.dir(generatedHeadersDir)
outputs.file(artifactFile)

doFirst {
actualOutputDir.mkdirs()
if (artifactFile.exists()) {
artifactFile.delete()
}
}

targetImageId { createImageTask.imageId.get() }

binds.put(jniSrcDir.absolutePath, "/jni_src")
binds.put(generatedHeadersDir.asFile.absolutePath, "/jni_headers")
binds.put(actualOutputDir.absolutePath, "/jni_dest")
val args = "${it.compilerArgs} -I /jni_headers -I /jni_src -o /jni_dest/$artifactName /jni_src/*.cpp"
envVars.put("BUILD_ARGS", args)
}
compileJniTask.dependsOn(artifactCompileTask)
}


class NativeTarget(val artifactSuffix: String, val dockerfile: String, val compilerArgs: String) {

fun getTaskSuffix() : String {
var suffix = artifactSuffix;
//remove file suffix
if(suffix.contains('.')) {
suffix = suffix.substring(0, suffix.lastIndexOf('.'))
}
//replace kebab-case with upper CamelCase
var result = "";
for (segment in suffix.split("-")) {
result += Character.toUpperCase(segment[0]);
result += segment.substring(1);
}
return result;
}
}

/**
* Custom task combining creating, running and cleaning up a container.
*/
open class DockerRun : DockerExistingImage() {

@get:Optional
@get:Input
val envVars: MapProperty<String, String> = project.objects.mapProperty(
String::class.java,
String::class.java
)

@get:Optional
@get:Input
val binds: MapProperty<String, String> = project.objects.mapProperty(
String::class.java,
String::class.java
)

@Throws(IOException::class)
override fun runRemoteCommand() {
logger.debug("Creating container")
val container = createContainer()
try {

logger.debug("Starting container with ID '${container.id}'.")
dockerClient.startContainerCmd(container.id).exec()

logger.debug("Following logs of container with ID '${container.id}'.")
followContainerLogs(container)

val containerWait = dockerClient.waitContainerCmd(container.id)
val exitCode = containerWait.exec(WaitContainerResultCallback()).awaitStatusCode()

logger.debug("Container exited with code $exitCode")

if(exitCode != 0) {
throw GradleException("Container exited with status code $exitCode, check the logs for details")
}
} finally {
dockerClient.removeContainerCmd(container.id)
.withForce(true)
.exec()
}
}

private fun createContainer(): CreateContainerResponse {
val createContainerCommand = dockerClient.createContainerCmd(imageId.get())
createContainerCommand.withEnv(
envVars.get().entries.stream()
.map { "${it.key}=${it.value}" }
.toList()
)
createContainerCommand.hostConfig.withBinds(binds.get().entries.stream()
.map { "${it.key}:${it.value}" }
.map(Bind::parse)
.toList()
)
return createContainerCommand.exec()
}

private fun followContainerLogs(container: CreateContainerResponse) {
val logCommand = dockerClient.logContainerCmd(container.id)
.withFollowStream(true)
.withTailAll()
.withStdErr(true)
.withStdOut(true)
logCommand.exec(object : ResultCallback.Adapter<Frame?>() {
override fun onNext(frame: Frame?) {
if (frame != null) {
when (frame.streamType) {
StreamType.STDOUT, StreamType.RAW -> logger.quiet(
String(frame.payload).replaceFirst("/\\s+$/".toRegex(), "")
)

StreamType.STDERR -> logger.error(
String(frame.payload).replaceFirst("/\\s+$/".toRegex(), "")
)

else -> {}
}
}
super.onNext(frame)
}
}).awaitCompletion();
}

}

12 changes: 12 additions & 0 deletions jvmti-access/jni-build/java_includes.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ubuntu:jammy-20231128
RUN apt-get update && apt-get install -y curl unzip

RUN mkdir /java_linux && cd /java_linux \
&& curl -L https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.1%2B12/OpenJDK21U-jdk_x64_linux_hotspot_21.0.1_12.tar.gz --output jdk.tar.gz \
&& tar --strip-components 1 -xvf jdk.tar.gz --wildcards jdk*/include \
&& rm jdk.tar.gz

RUN mkdir /java_darwin && cd /java_darwin \
&& curl -L https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.1%2B12/OpenJDK21U-jdk_x64_mac_hotspot_21.0.1_12.tar.gz --output jdk.tar.gz \
&& tar --strip-components 3 -xvf jdk.tar.gz --wildcards jdk*/include \
&& rm jdk.tar.gz
12 changes: 12 additions & 0 deletions jvmti-access/jni-build/jni_darwin.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM crazymax/osxcross:13.1-r0-ubuntu AS osxcross
FROM elastic_jni_build_java_includes:latest AS java_includes

FROM ubuntu:jammy-20231128
RUN apt-get update && apt-get install -y curl clang lld libc6-dev
ENV PATH="/osxcross/bin:$PATH"
ENV LD_LIBRARY_PATH="/osxcross/lib:$LD_LIBRARY_PATH"

COPY --from=osxcross /osxcross /osxcross
COPY --from=java_includes /java_darwin /java_darwin

CMD o64-clang++ -I /java_darwin/include -I /java_darwin/include/darwin $BUILD_ARGS
6 changes: 6 additions & 0 deletions jvmti-access/jni-build/jni_linux_arm64.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM elastic_jni_build_java_includes:latest AS java_includes

FROM dockcross/linux-arm64@sha256:d1e0059c199d64c74f2e813ce71e210b55ec9c1b24fdb14520c6125d5119513f
COPY --from=java_includes /java_linux /java_linux

CMD $CXX -I /java_linux/include -I /java_linux/include/linux $BUILD_ARGS
6 changes: 6 additions & 0 deletions jvmti-access/jni-build/jni_linux_x64.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM elastic_jni_build_java_includes:latest AS java_includes

FROM dockcross/linux-x64@sha256:89a2c6061215d923a940902fbb2c3c42fdd8a4819d2bd3d7176602f34335f075
COPY --from=java_includes /java_linux /java_linux

CMD $CXX -I /java_linux/include -I /java_linux/include/linux $BUILD_ARGS
Loading

0 comments on commit 6d1541c

Please sign in to comment.