diff --git a/README.md b/README.md
index d5c557f..75c934d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,18 @@
-Experiment with java22 and Rust to monitor exceptions in a JVM
\ No newline at end of file
+Experiment with java22 and Rust to monitor exceptions in a JVM
+
+Running:
+* Update the path to the rust lib (temp fix) in ExceptionLogger for your setup
+* mvn clean install
+* cd rustlib; cargo build
+* create a minimal class in a separate project
+```java
+public class Main {
+ public static void main(String[] args) throws Throwable {
+ throw new Throwable();
+ }
+}
+```
+* run it with (adjust paths):
+``` bash
+java22 -javaagent:$EXCEPTIONAL_PROJECT/exceptional/agent/target/exceptional-agent-1.0-SNAPSHOT.jar --enable-preview -classpath $YOUR_CLASSPATH Main
+```
diff --git a/agent/pom.xml b/agent/pom.xml
index deb8ba9..2ea49d2 100644
--- a/agent/pom.xml
+++ b/agent/pom.xml
@@ -18,6 +18,15 @@
22
UTF-8
+
+
+
+ org.github.shautvast.exceptional
+ exceptional-lib
+ 1.0-SNAPSHOT
+
+
+
@@ -38,7 +47,9 @@
com.github.shautvast.exceptional.Agent
true
true
- /Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar
+
+ /Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar
+
diff --git a/agent/src/main/java/com/github/shautvast/exceptional/Agent.java b/agent/src/main/java/com/github/shautvast/exceptional/Agent.java
index 86885fe..e06a45f 100644
--- a/agent/src/main/java/com/github/shautvast/exceptional/Agent.java
+++ b/agent/src/main/java/com/github/shautvast/exceptional/Agent.java
@@ -1,80 +1,80 @@
package com.github.shautvast.exceptional;
-import java.lang.classfile.*;
-import java.lang.classfile.instruction.ReturnInstruction;
+import java.lang.classfile.ClassBuilder;
+import java.lang.classfile.ClassElement;
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.MethodModel;
+import java.lang.classfile.instruction.ThrowInstruction;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
-import java.lang.instrument.ClassDefinition;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.lang.reflect.AccessFlag;
import java.security.ProtectionDomain;
public class Agent {
- @SuppressWarnings("DataFlowIssue")
+ private static final String EXCEPTIONLOGGER = ExceptionLogger.class.getName();
+
public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception {
System.err.println("--->Exceptional agent active");
// add transformer
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
- return instrumentThrowable(className, classfileBuffer);
+ return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
}
@Override
public byte[] transform(Module module, ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
- return instrumentThrowable(className, classfileBuffer);
+ return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
}
}, true);
-
- // we only want to redefine Throwable
- byte[] bytecode = Files.readAllBytes(
- Path.of(Agent.class.getResource("/java/lang/Throwable.class").toURI()));
- instrumentation.redefineClasses(new ClassDefinition(Throwable.class, bytecode));
}
- private static byte[] instrumentThrowable(String className, byte[] bytecode) {
- // we only want to instrument Throwable
- // or rather,,, is this the right way? This way we also intercept new any Exception that is not thrown
- // But,,, who does that?? (famous last words)
- if (className.equals("java/lang/Throwable")) {
- ClassFile classFile = ClassFile.of();
- ClassModel classModel = classFile.parse(bytecode);
- return instrumentConstructors(classFile, classModel);
- } else {
- return bytecode;
- }
- }
-
- private static byte[] instrumentConstructors(ClassFile classFile, ClassModel classModel) {
+ /*
+ * Every throw opcode will be preceded by a call to our ExceptionLogger
+ */
+ private static byte[] instrumentExceptionLoggerAtThrow(String className, byte[] classfileBuffer) {
+ var classFile = ClassFile.of();
+ var classModel = classFile.parse(classfileBuffer);
return classFile.build(classModel.thisClass().asSymbol(), classBuilder -> {
for (ClassElement ce : classModel) {
- if (ce instanceof MethodModel mm && mm.methodName().toString().equals("")) {
- instrumentMethodWithMyExceptionLogger(classBuilder, mm);
- continue;
+ // not all methods, TODO include synthetic?
+ if (ce instanceof MethodModel methodModel && !methodModel.flags().has(AccessFlag.ABSTRACT)
+ && !methodModel.flags().has(AccessFlag.NATIVE)
+ && !methodModel.flags().has(AccessFlag.BRIDGE)) {
+ transform(classBuilder, methodModel);
+ } else {
+ // keep all other class elements
+ classBuilder.with(ce);
}
- classBuilder.with(ce);
}
});
}
- private static void instrumentMethodWithMyExceptionLogger(ClassBuilder classBuilder, MethodModel methodModel) {
- methodModel.code().ifPresent(code ->
- classBuilder.withMethod(methodModel.methodName(), methodModel.methodType(), methodModel.flags().flagsMask(),
- methodBuilder ->
- methodBuilder.transformCode(code, ((builder, element) -> {
- if (element instanceof ReturnInstruction) {
- builder.aload(0); // load `this` on the stack, ie the Throwable instance
- builder.invokestatic( // call my code
- ClassDesc.of("com.github.shautvast.exceptional.ExceptionLogger"), "log",
- MethodTypeDesc.ofDescriptor("(Ljava/lang/Throwable;)V"));
- builder.return_();
- } else {
- builder.with(element); // leave any other bytecode in place
- }
- })
- )));
+ private static void transform(ClassBuilder classBuilder, MethodModel methodModel) {
+ methodModel.code().ifPresent(code -> classBuilder.withMethod(
+ methodModel.methodName(), // copy name, type and modifiers from the original
+ methodModel.methodType(),
+ methodModel.flags().flagsMask(),
+ methodBuilder -> {
+ // keep the existing annotations (and other method elements) in place
+ methodModel.forEachElement(methodBuilder::with);
+ // change the code
+ methodBuilder.transformCode(code, (builder, element) -> {
+ // this way of instrumenting may miss the already loaded classes, java.lang.String for example.
+ // May need to circumvent that
+ if (element instanceof ThrowInstruction) {
+ builder.dup(); // on top of the stack is the current exception instance
+ // duplicate it to make sure the `athrow` op has something to throw
+ // after the invoke to ExceptionLogger has popped one off
+ builder.invokestatic( // call my code with the exception as argument
+ ClassDesc.of(EXCEPTIONLOGGER), "log",
+ MethodTypeDesc.ofDescriptor("(Ljava/lang/Throwable;)V"));
+ }
+ builder.with(element); // leave any other element in place
+ });
+ }));
}
}
\ No newline at end of file
diff --git a/lib/dependency-reduced-pom.xml b/lib/dependency-reduced-pom.xml
new file mode 100644
index 0000000..d4cf151
--- /dev/null
+++ b/lib/dependency-reduced-pom.xml
@@ -0,0 +1,63 @@
+
+
+
+ exceptional-parent
+ org.github.shautvast.exceptional
+ 1.0-SNAPSHOT
+
+ 4.0.0
+ exceptional-lib
+ 1.0-SNAPSHOT
+
+
+
+ maven-compiler-plugin
+
+ 22
+ 22
+ --enable-preview
+
+
+
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ test
+
+
+ junit-jupiter-api
+ org.junit.jupiter
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+
+
+
+
+
+ 22
+ 22
+ UTF-8
+
+
diff --git a/lib/pom.xml b/lib/pom.xml
index 31b0304..99ea5bb 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -18,6 +18,19 @@
22
UTF-8
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.16.0
+
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ test
+
+
@@ -30,8 +43,23 @@
--enable-preview
-
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java b/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java
index b8df9ae..303a283 100644
--- a/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java
+++ b/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java
@@ -1,11 +1,38 @@
package com.github.shautvast.exceptional;
-import java.util.Arrays;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.lang.foreign.*;
+import java.lang.invoke.MethodHandle;
@SuppressWarnings("unused") // this code is called from the instrumented code
public class ExceptionLogger {
+ private static final Arena arena = Arena.ofConfined();
+ private static final Linker linker = Linker.nativeLinker();
+ // //TODO relative path, or configurable
+ private static final SymbolLookup rustlib = SymbolLookup.libraryLookup("/Users/Shautvast/dev/exceptional/rustlib/target/debug/librustlib.dylib", arena);
+ private final static MethodHandle logNative;
+ private final static ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ MemorySegment logFunction = rustlib.find("log_java_exception").orElseThrow();
+ logNative = linker.downcallHandle(logFunction, FunctionDescriptor.ofVoid(
+ ValueLayout.ADDRESS
+ ));
+ }
+
+ // how does this behave in a multithreaded context??
+ // probably need a ringbuffer with fixed memory to make this work efficiently
public static void log(Throwable throwable) {
- System.out.print("Logging exception:");
- Arrays.stream(throwable.getStackTrace()).forEach(System.out::println);
+ try {
+ // use json for now because of ease of integration
+ if (throwable != null) {
+ String json = objectMapper.writeValueAsString(throwable);
+ var data = arena.allocateFrom(json); // reuse instead of reallocating?
+ logNative.invoke(data); // invoke the rust function
+ }
+ } catch (Throwable e) {
+ e.printStackTrace(System.err);
+ }
}
}
diff --git a/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java b/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java
new file mode 100644
index 0000000..8ed57a2
--- /dev/null
+++ b/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java
@@ -0,0 +1,14 @@
+package com.github.shautvast.exceptional;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ExceptionLoggerTest {
+
+ @Test
+ void test(){
+ ExceptionLogger.log(new Throwable());
+ }
+
+}
\ No newline at end of file
diff --git a/rustlib/Cargo.lock b/rustlib/Cargo.lock
new file mode 100644
index 0000000..6236b71
--- /dev/null
+++ b/rustlib/Cargo.lock
@@ -0,0 +1,96 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustlib"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
diff --git a/rustlib/Cargo.toml b/rustlib/Cargo.toml
new file mode 100644
index 0000000..f7a7650
--- /dev/null
+++ b/rustlib/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rustlib"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+bench = false
+
+[dependencies]
+serde_json = { version = "1.0", features = [] }
+serde = { version = "1.0", features = ["derive"] }
+anyhow = "1.0"
\ No newline at end of file
diff --git a/rustlib/src/Throwable.rs b/rustlib/src/Throwable.rs
new file mode 100644
index 0000000..9c1de6e
--- /dev/null
+++ b/rustlib/src/Throwable.rs
@@ -0,0 +1,32 @@
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug)]
+pub struct Throwable {
+ cause: Option>,
+ #[serde(rename (deserialize = "stackTrace"))]
+ stack_trace: Vec,
+ message: Option,
+ suppressed: Vec,
+ #[serde(rename (deserialize = "localizedMessage"))]
+ localized_message: Option,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Stacktrace{
+ #[serde(rename (deserialize = "classLoaderName"))]
+ class_loader_name: Option,
+ #[serde(rename (deserialize = "moduleName"))]
+ module_name: Option,
+ #[serde(rename (deserialize = "moduleVersion"))]
+ module_version: Option,
+ #[serde(rename (deserialize = "methodName"))]
+ method_name: Option,
+ #[serde(rename (deserialize = "fileName"))]
+ file_name: Option,
+ #[serde(rename (deserialize = "lineNumber"))]
+ line_number: Option,
+ #[serde(rename (deserialize = "className"))]
+ class_name: Option,
+ #[serde(rename (deserialize = "nativeMethod"))]
+ native_method: Option,
+}
\ No newline at end of file
diff --git a/rustlib/src/lib.rs b/rustlib/src/lib.rs
new file mode 100644
index 0000000..a7f876d
--- /dev/null
+++ b/rustlib/src/lib.rs
@@ -0,0 +1,17 @@
+mod throwable;
+
+use std::ffi::{c_char, CStr};
+
+#[no_mangle]
+pub extern "C" fn log_java_exception(raw_string: *const c_char) {
+ let c_str = unsafe { CStr::from_ptr(raw_string) };
+ let string = c_str.to_str();
+ if let Ok(json) = string {
+ println!("receiving {}", json);
+ let error: throwable::Throwable = serde_json::from_str(json).unwrap();
+ println!("{:?}", error);
+ }
+}
+
+#[cfg(test)]
+mod tests {}