Skip to content

Commit

Permalink
add a java-agent that emulate value classes runtime behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
forax committed Apr 13, 2024
1 parent 111fb9d commit 72e0ec5
Show file tree
Hide file tree
Showing 5 changed files with 689 additions and 1 deletion.
22 changes: 21 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
<version>3.2.5</version>
</plugin>

<plugin>
Expand All @@ -75,6 +75,9 @@
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.github.forax.einherjar.cli.Main</mainClass>
<manifestEntries>
<Premain-Class>com.github.forax.einherjar.agent.Agent</Premain-Class>
</manifestEntries>
</transformer>
</transformers>
<filters>
Expand All @@ -90,6 +93,23 @@
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<argLine>-javaagent:target/einherjar.jar</argLine>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
94 changes: 94 additions & 0 deletions src/main/java/com/github/forax/einherjar/agent/Agent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.github.forax.einherjar.agent;

import com.github.forax.einherjar.agent.runtime.AgentRuntime;
import com.github.forax.einherjar.api.ValueType;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.ProtectionDomain;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

public class Agent {
static final String AGENT_RUNTIME_NAME = AgentRuntime.class.getName().replace('.', '/');

private static void copy(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[8_192];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}

private static void addEntryToBootstrapJarFile(JarOutputStream jarOutputStream, String entryClassName) throws IOException {
try(InputStream input = AgentRuntime.class.getResourceAsStream("/" + entryClassName + ".class")) {
if (input == null) {
throw new AssertionError("can not find " + entryClassName + " bytecode");
}
jarOutputStream.putNextEntry(new JarEntry(entryClassName + ".class"));
copy(input, jarOutputStream);
jarOutputStream.closeEntry();
}
}

private static Path createBootstrapJarFile() throws IOException {
Path bootstrapJarFile = Files.createTempFile("--agent-runtime-jar--", "");
try(OutputStream output = Files.newOutputStream(bootstrapJarFile);
JarOutputStream jarOutputStream = new JarOutputStream(output)) {
addEntryToBootstrapJarFile(jarOutputStream, AGENT_RUNTIME_NAME);
addEntryToBootstrapJarFile(jarOutputStream, AgentRuntime.Cache.class.getName().replace('.', '/'));
addEntryToBootstrapJarFile(jarOutputStream, ValueType.class.getName().replace('.', '/'));
}
return bootstrapJarFile;
}

public static void premain(String agentArgs, Instrumentation instrumentation) throws IOException {
Path bootstrapJarFile = createBootstrapJarFile();
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(bootstrapJarFile.toFile()));

instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {

try {
if (className.equals(AGENT_RUNTIME_NAME)) {
//System.err.println("bailout " + className);
return null;
}

ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ValueTypeInstrRewriter valueTypeInstrRewriter = new ValueTypeInstrRewriter(writer);
reader.accept(valueTypeInstrRewriter, 0);

if (valueTypeInstrRewriter.isTransformed()) {
//System.err.println("transform " + className);

//if (className.equals("org/opentest4j/AssertionFailedError")) { // DEBUG
// CheckClassAdapter.verify(new ClassReader(writer.toByteArray()), true, new PrintWriter(System.err));
//}

return writer.toByteArray();
}
return null;
} catch (Throwable t) {
t.printStackTrace(System.err);
throw t;
}
}
}, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.github.forax.einherjar.agent;

import com.github.forax.einherjar.agent.runtime.AgentRuntime;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import static com.github.forax.einherjar.agent.Agent.AGENT_RUNTIME_NAME;
import static java.lang.invoke.MethodType.methodType;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.DUP2;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.IFNE;
import static org.objectweb.asm.Opcodes.IF_ACMPEQ;
import static org.objectweb.asm.Opcodes.IF_ACMPNE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.MONITORENTER;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.V1_6;
import static org.objectweb.asm.Opcodes.V1_7;

class ValueTypeInstrRewriter extends ClassVisitor {
private static final Handle BSM;
static {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh;
try {
mh = lookup.findStatic(AgentRuntime.class, "bsm",
methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
MethodHandleInfo mhInfo = lookup.revealDirect(mh);
BSM = new Handle(mhInfo.getReferenceKind(),
mhInfo.getDeclaringClass().getName().replace('.', '/'),
mhInfo.getName(),
mhInfo.getMethodType().toMethodDescriptorString(),
false);
}

private boolean doNotUseInvokedynamic;
private boolean transformed;

public ValueTypeInstrRewriter(ClassVisitor classVisitor) {
super(ASM9, classVisitor);
}

public boolean isTransformed() {
return transformed;
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.doNotUseInvokedynamic = /*version < V1_7 */ true;
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(ASM9, mv) {
@Override
public void visitInsn(int opcode) {
if (opcode == MONITORENTER) {
mv.visitInsn(DUP);
if (doNotUseInvokedynamic) {
mv.visitMethodInsn(INVOKESTATIC, AGENT_RUNTIME_NAME, "monitorenter", "(Ljava/lang/Object;)V", false);
} else {
mv.visitInvokeDynamicInsn("monitorenter", "(Ljava/lang/Object;)V", BSM);
}
transformed = true;
}
super.visitInsn(opcode);
}

@Override
public void visitJumpInsn(int opcode, Label label) {
if (opcode == IF_ACMPEQ || opcode == IF_ACMPNE) {
if (doNotUseInvokedynamic) {
mv.visitMethodInsn(INVOKESTATIC, AGENT_RUNTIME_NAME, "acmp", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false);
} else {
mv.visitInvokeDynamicInsn( "acmp", "(Ljava/lang/Object;Ljava/lang/Object;)Z", BSM);
}
mv.visitJumpInsn(opcode == IF_ACMPEQ ? IFNE : IFEQ, label);
transformed = true;
return;
}
super.visitJumpInsn(opcode, label);
}

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKESTATIC && owner.equals("java/lang/System") && name.equals("identityHashCode") && descriptor.equals("(Ljava/lang/Object;)I")) {
if (doNotUseInvokedynamic) {
mv.visitMethodInsn(INVOKESTATIC, AGENT_RUNTIME_NAME, "identityHashCode", "(Ljava/lang/Object;)I", false);
} else {
mv.visitInvokeDynamicInsn( "identityHashCode", "(Ljava/lang/Object;)I", BSM);
}
transformed = true;
return;
}
if (opcode == INVOKESPECIAL && name.equals("<init>")) {
switch (owner) {
case "java/lang/ref/PhantomReference":
case "java/lang/ref/SoftReference":
case "java/lang/ref/WeakReference":
switch (descriptor) {
case "(Ljava/lang/Object;Ljava/lang/ref/ReferenceQueue;)V":
mv.visitInsn(DUP2);
mv.visitInsn(POP);
break;
case "(Ljava/lang/Object;)V":
mv.visitInsn(DUP);
break;
default:
throw new AssertionError("invalid descriptor " + descriptor);
}
if (doNotUseInvokedynamic) {
mv.visitMethodInsn(INVOKESTATIC, AGENT_RUNTIME_NAME, "identityCheck", "(Ljava/lang/Object;)V", false);
} else {
mv.visitInvokeDynamicInsn( "identityCheck", "(Ljava/lang/Object;)V", BSM);
}
transformed = true;
break;
default:
break;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
};
}
}
Loading

0 comments on commit 72e0ec5

Please sign in to comment.