-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add project with access to JNI / JVMTI functionality (#69)
- Loading branch information
Showing
17 changed files
with
831 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.