org.gradle.internal.classpath.transforms.InstrumentingClassTransform Maven / Gradle / Ivy
Show all versions of gradle-api Show documentation
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.internal.classpath.transforms;
import com.google.common.collect.ImmutableList;
import org.codehaus.groovy.runtime.callsite.CallSiteArray;
import org.codehaus.groovy.vmplugin.v8.IndyInterface;
import org.gradle.api.file.RelativePath;
import org.gradle.internal.Pair;
import org.gradle.internal.classpath.CallInterceptionClosureInstrumentingClassVisitor;
import org.gradle.internal.classpath.ClassData;
import org.gradle.internal.classpath.ClasspathEntryVisitor;
import org.gradle.internal.classpath.Instrumented;
import org.gradle.internal.classpath.intercept.CallInterceptorRegistry;
import org.gradle.internal.classpath.intercept.JvmBytecodeInterceptorSet;
import org.gradle.internal.hash.Hasher;
import org.gradle.internal.instrumentation.api.jvmbytecode.BridgeMethodBuilder;
import org.gradle.internal.instrumentation.api.jvmbytecode.JvmBytecodeCallInterceptor;
import org.gradle.internal.instrumentation.api.metadata.InstrumentationMetadata;
import org.gradle.internal.instrumentation.api.types.BytecodeInterceptorFilter;
import org.gradle.internal.lazy.Lazy;
import org.gradle.model.internal.asm.MethodVisitorScope;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.MethodNode;
import javax.annotation.Nullable;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static org.gradle.internal.classanalysis.AsmConstants.ASM_LEVEL;
import static org.gradle.internal.classpath.transforms.CommonTypes.NO_EXCEPTIONS;
import static org.gradle.internal.classpath.transforms.CommonTypes.STRING_TYPE;
import static org.gradle.internal.instrumentation.api.types.BytecodeInterceptorFilter.INSTRUMENTATION_ONLY;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Type.INT_TYPE;
import static org.objectweb.asm.Type.getMethodDescriptor;
import static org.objectweb.asm.Type.getType;
public class InstrumentingClassTransform implements ClassTransform {
/**
* Decoration format. Increment this when making changes.
*/
private static final int DECORATION_FORMAT = 36;
private static final Type INSTRUMENTED_TYPE = getType(Instrumented.class);
private static final Type BYTECODE_INTERCEPTOR_FILTER_TYPE = Type.getType(BytecodeInterceptorFilter.class);
private static final String RETURN_CALL_SITE_ARRAY = getMethodDescriptor(getType(CallSiteArray.class));
private static final String RETURN_VOID_FROM_CALL_SITE_ARRAY_BYTECODE_INTERCEPTOR = getMethodDescriptor(Type.VOID_TYPE, getType(CallSiteArray.class), BYTECODE_INTERCEPTOR_FILTER_TYPE);
private static final String GROOVY_INDY_INTERFACE_TYPE = getType(IndyInterface.class).getInternalName();
@SuppressWarnings("deprecation")
private static final String GROOVY_INDY_INTERFACE_V7_TYPE = getType(org.codehaus.groovy.vmplugin.v7.IndyInterface.class).getInternalName();
private static final String GROOVY_INDY_INTERFACE_BOOTSTRAP_METHOD_DESCRIPTOR = getMethodDescriptor(getType(CallSite.class), getType(MethodHandles.Lookup.class), STRING_TYPE, getType(MethodType.class), STRING_TYPE, INT_TYPE);
private static final String INSTRUMENTED_CALL_SITE_METHOD = "$instrumentedCallSiteArray";
private static final String CREATE_CALL_SITE_ARRAY_METHOD = "$createCallSiteArray";
private static final AdhocInterceptors ADHOC_INTERCEPTORS = new AdhocInterceptors();
private final JvmBytecodeInterceptorSet externalInterceptors;
@Override
public void applyConfigurationTo(Hasher hasher) {
hasher.putString(InstrumentingClassTransform.class.getSimpleName());
hasher.putInt(DECORATION_FORMAT);
}
public InstrumentingClassTransform() {
this(INSTRUMENTATION_ONLY);
}
public InstrumentingClassTransform(BytecodeInterceptorFilter interceptorFilter) {
this.externalInterceptors = CallInterceptorRegistry.getJvmBytecodeInterceptors(interceptorFilter);
}
private BytecodeInterceptorFilter interceptorFilter() {
return externalInterceptors.getOriginalFilter();
}
private List buildInterceptors(InstrumentationMetadata metadata) {
return externalInterceptors.getInterceptors(metadata);
}
@Override
public Pair apply(ClasspathEntryVisitor.Entry entry, ClassVisitor visitor, ClassData classData) {
// TODO(mlopatkin) can we reuse interceptors in a bigger scope, not per class, but per artifact?
List interceptors = buildInterceptors(classData);
if (interceptorFilter().matches(ADHOC_INTERCEPTORS)) {
interceptors = ImmutableList.builderWithExpectedSize(interceptors.size() + 1).add(ADHOC_INTERCEPTORS).addAll(interceptors).build();
}
return Pair.of(entry.getPath(),
new InstrumentingVisitor(
new CallInterceptionClosureInstrumentingClassVisitor(
new LambdaSerializationTransformer(new InstrumentingBackwardsCompatibilityVisitor(visitor)),
interceptorFilter()
),
classData,
interceptors,
interceptorFilter()
)
);
}
private static class BridgeMethod {
final Handle bridgeMethodHandle;
final BridgeMethodBuilder bridgeMethodBuilder;
private BridgeMethod(Handle bridgeMethodHandle, BridgeMethodBuilder bridgeMethodBuilder) {
this.bridgeMethodHandle = bridgeMethodHandle;
this.bridgeMethodBuilder = bridgeMethodBuilder;
}
}
private static class InstrumentingVisitor extends ClassVisitor {
private final ClassData classData;
private final List interceptors;
private final BytecodeInterceptorFilter interceptorFilter;
private final Map bridgeMethods = new LinkedHashMap<>();
private int nextBridgeMethodIndex;
private String className;
private boolean hasGroovyCallSites;
public InstrumentingVisitor(ClassVisitor visitor, ClassData classData, List interceptors, BytecodeInterceptorFilter interceptorFilter) {
super(ASM_LEVEL, visitor);
this.classData = classData;
this.interceptors = interceptors;
this.interceptorFilter = interceptorFilter;
}
@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.className = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals(CREATE_CALL_SITE_ARRAY_METHOD) && descriptor.equals(RETURN_CALL_SITE_ARRAY)) {
hasGroovyCallSites = true;
}
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
Lazy asMethodNode = Lazy.unsafe().of(() -> {
Optional methodNode = classData.readClassAsNode().methods.stream().filter(method ->
Objects.equals(method.name, name) && Objects.equals(method.desc, descriptor) && Objects.equals(method.signature, signature)
).findFirst();
return methodNode.orElseThrow(() -> new IllegalStateException("could not find method " + name + " with descriptor " + descriptor));
});
return new InstrumentingMethodVisitor(this, methodVisitor, asMethodNode, interceptors, interceptorFilter);
}
@Override
public void visitEnd() {
if (hasGroovyCallSites) {
generateCallSiteFactoryMethod();
}
bridgeMethods.values().forEach(this::generateBridgeMethod);
super.visitEnd();
}
private void generateCallSiteFactoryMethod() {
new MethodVisitorScope(visitStaticPrivateMethod(INSTRUMENTED_CALL_SITE_METHOD, RETURN_CALL_SITE_ARRAY)) {{
_INVOKESTATIC(className, CREATE_CALL_SITE_ARRAY_METHOD, RETURN_CALL_SITE_ARRAY);
_DUP();
_GETSTATIC(BYTECODE_INTERCEPTOR_FILTER_TYPE, interceptorFilter.name(), BYTECODE_INTERCEPTOR_FILTER_TYPE.getDescriptor());
_INVOKESTATIC(INSTRUMENTED_TYPE, "groovyCallSites", RETURN_VOID_FROM_CALL_SITE_ARRAY_BYTECODE_INTERCEPTOR);
_ARETURN();
visitMaxs(2, 0);
visitEnd();
}};
}
private void generateBridgeMethod(BridgeMethod bridgeMethod) {
bridgeMethod.bridgeMethodBuilder.buildBridgeMethod(visitStaticPrivateMethod(bridgeMethod.bridgeMethodHandle.getName(), bridgeMethod.bridgeMethodHandle.getDesc()));
}
private MethodVisitor visitStaticPrivateMethod(String name, String descriptor) {
return super.visitMethod(ACC_STATIC | ACC_SYNTHETIC | ACC_PRIVATE, name, descriptor, null, NO_EXCEPTIONS);
}
/**
* Finds the {@link BridgeMethod} for the method handle. May return null if nothing intercepts the method.
* For each method at most one bridge method is produced, regardless the number of handles encountered
* (i.e. all method references to e.g. {@code ProcessBuilder::start} in the class are re-routed to a single bridge method).
*
* @param originalHandle the original method handle
* @return the bridge method that intercepts the original method or null if there is no interceptor
*/
@Nullable
public BridgeMethod findBridgeMethodFor(Handle originalHandle) {
return bridgeMethods.computeIfAbsent(originalHandle, this::maybeBuildBridgeMethod);
}
@Nullable
private BridgeMethod maybeBuildBridgeMethod(Handle handle) {
for (JvmBytecodeCallInterceptor interceptor : interceptors) {
BridgeMethodBuilder methodBuilder = interceptor.findBridgeMethodBuilder(className, handle.getTag(), handle.getOwner(), handle.getName(), handle.getDesc());
if (methodBuilder != null) {
return new BridgeMethod(makeBridgeMethodHandle(makeBridgeMethodName(handle), methodBuilder.getBridgeMethodDescriptor()), methodBuilder);
}
}
return null;
}
/**
* Builds a unique bridge method name for the given method handle based on the owner and the name of the original method.
* For example, a bridge method for {@code com.foo.Bar.baz(...)} will be named {@code gradle$intercept$$com$foo$Bar$$baz$},
* where {@code } is a number to make the resulting name unique. The number starts from 0 and increases each time this method is called.
*
* Note that calling this method multiple times returns different names for the same bridge method.
*
* Most of the name decorations are added only to make stack traces easier to understand.
*
* @param originalHandle the original method handle to build bridge method for
* @return the unique bridge method name.
*/
private String makeBridgeMethodName(Handle originalHandle) {
// Index ensures that the generated name is unique for this class.
int index = nextBridgeMethodIndex++;
// com/foo/Bar -> com$foo$Bar
String mangledOwner = originalHandle.getOwner().replace("/", "$");
// Only and are allowed to have <> in the name.
// As we're intercepting these too, we strip prohibited symbols from the bridge method's name.
String safeName = originalHandle.getName().replace("<", "_").replace(">", "_");
return "gradle$intercept$$" + mangledOwner + "$$" + safeName + "$" + index;
}
private Handle makeBridgeMethodHandle(String name, String desc) {
return new Handle(H_INVOKESTATIC, className, name, desc, false);
}
}
private static class InstrumentingMethodVisitor extends MethodVisitorScope {
private final InstrumentingVisitor owner;
private final String className;
private final Lazy asNode;
private final Collection interceptors;
private final BytecodeInterceptorFilter interceptorFilter;
public InstrumentingMethodVisitor(
InstrumentingVisitor owner,
MethodVisitor methodVisitor,
Lazy asNode,
Collection interceptors,
BytecodeInterceptorFilter interceptorFilter
) {
super(methodVisitor);
this.owner = owner;
this.className = owner.className;
this.asNode = asNode;
this.interceptors = interceptors;
this.interceptorFilter = interceptorFilter;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKESTATIC && visitINVOKESTATIC(owner, name, descriptor)) {
return;
}
for (JvmBytecodeCallInterceptor interceptor : interceptors) {
if (interceptor.visitMethodInsn(this, className, opcode, owner, name, descriptor, isInterface, asNode)) {
return;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
private boolean visitINVOKESTATIC(String owner, String name, String descriptor) {
if (owner.equals(className) && name.equals(CREATE_CALL_SITE_ARRAY_METHOD) && descriptor.equals(RETURN_CALL_SITE_ARRAY)) {
_INVOKESTATIC(className, INSTRUMENTED_CALL_SITE_METHOD, RETURN_CALL_SITE_ARRAY);
return true;
}
return false;
}
@Override
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
if (isGroovyIndyCallsite(bootstrapMethodHandle)) {
Handle interceptor = new Handle(
H_INVOKESTATIC,
INSTRUMENTED_TYPE.getInternalName(),
getBoostrapMethodName(interceptorFilter),
GROOVY_INDY_INTERFACE_BOOTSTRAP_METHOD_DESCRIPTOR,
false
);
super.visitInvokeDynamicInsn(name, descriptor, interceptor, bootstrapMethodArguments);
} else {
for (int i = 0; i < bootstrapMethodArguments.length; i++) {
bootstrapMethodArguments[i] = maybeInstrumentBootstrapArgument(bootstrapMethodArguments[i]);
}
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
}
private static String getBoostrapMethodName(BytecodeInterceptorFilter interceptorFilter) {
switch (interceptorFilter) {
case INSTRUMENTATION_ONLY:
return "bootstrapInstrumentationOnly";
case ALL:
return "bootstrapAll";
default:
throw new UnsupportedOperationException("Unknown interceptor request: " + interceptorFilter);
}
}
private boolean isGroovyIndyCallsite(Handle bootstrapMethodHandle) {
return (bootstrapMethodHandle.getOwner().equals(GROOVY_INDY_INTERFACE_TYPE) ||
bootstrapMethodHandle.getOwner().equals(GROOVY_INDY_INTERFACE_V7_TYPE)) &&
bootstrapMethodHandle.getName().equals("bootstrap") &&
bootstrapMethodHandle.getDesc().equals(GROOVY_INDY_INTERFACE_BOOTSTRAP_METHOD_DESCRIPTOR);
}
private Object maybeInstrumentBootstrapArgument(Object argument) {
if (argument instanceof Handle) {
return maybeInstrumentHandle((Handle) argument);
}
return argument;
}
private Handle maybeInstrumentHandle(Handle handle) {
BridgeMethod bridgeMethod = owner.findBridgeMethodFor(handle);
if (bridgeMethod != null) {
return bridgeMethod.bridgeMethodHandle;
}
return handle; // No instrumentation requested.
}
}
}