Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ dependencies {
fun Provider<MinimalExternalModuleDependency>.withVersion(version: String): Provider<String> {
return map { "${it.module.group}:${it.module.name}:$version" }
}

kotlin {
jvmToolchain(21)
}
79 changes: 79 additions & 0 deletions build-logic/src/main/kotlin/config-java25.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import org.gradle.accessors.dm.LibrariesForLibs
import org.incendo.cloudbuildlogic.jmp

plugins {
id("net.kyori.indra")
id("net.kyori.indra.publishing")
id("net.kyori.indra.checkstyle")
id("org.incendo.cloud-build-logic.javadoc-links")
}

val libs = the<LibrariesForLibs>()

indra {
javaVersions {
target(25)
strictVersions(true)
}

publishSnapshotsTo("paperSnapshots", "https://artifactory.papermc.io/artifactory/snapshots/")
publishReleasesTo("paperReleases", "https://artifactory.papermc.io/artifactory/releases/")
signWithKeyFromProperties("signingKey", "signingPassword")

apache2License()

github("PaperMC", "asm-utils") {
ci(true)
}

configurePublications {
pom {
developers {
jmp()
developer {
id = "Machine-Maker"
name = "Jake Potrebic"
url = "https://github.com/Machine-Maker"
}
developer {
id = "kennytv"
name = "Nassim Jahnke"
url = "https://github.com/kennytv"
}
}
}
}
}

repositories {
mavenCentral()
}

val mockitoAgent = configurations.create("mockitoAgent")

dependencies {
compileOnlyApi(libs.jspecify)
testCompileOnly(libs.jspecify)
compileOnly(libs.jetbrainsAnnotations)
testCompileOnly(libs.jetbrainsAnnotations)

mockitoAgent(libs.mockito.core) { isTransitive = false }
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit)
testImplementation(libs.assertj)
testImplementation(libs.jupiterApi)
testImplementation(libs.jupiterParams)
testRuntimeOnly(libs.jupiterEngine)
testRuntimeOnly(libs.platformLauncher)
}

tasks {
test {
useJUnitPlatform()
jvmArgs("-javaagent:${mockitoAgent.asPath}")
}
}

javadocLinks {
override(libs.jspecify, "https://jspecify.dev/docs/api/")
}
88 changes: 88 additions & 0 deletions classfile-utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import org.gradle.kotlin.dsl.register
import java.nio.file.Files
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.invariantSeparatorsPathString
import kotlin.io.path.isDirectory
import kotlin.use

plugins {
id("config-java25")
}

val mainForNewTargets = sourceSets.create("mainForNewTargets")

val testDataSet = sourceSets.create("testData")
val testDataNewTargets = sourceSets.create("testDataNewTargets")

val filtered = tasks.register<FilterTestClasspath>("filteredTestClasspath") {
outputDir.set(layout.buildDirectory.dir("filteredTestClasspath"))
old.from(testDataSet.output)
new.from(testDataNewTargets.output)
}

dependencies {
api(mainForNewTargets.output)
testRuntimeOnly(files(filtered.flatMap { it.outputDir })) // only have access to old targets at runtime, don't use them in actual tests
testImplementation(testDataNewTargets.output)

testDataSet.compileOnlyConfigurationName(libs.jspecify)
testDataNewTargets.compileOnlyConfigurationName(libs.jspecify)
testDataNewTargets.implementationConfigurationName(mainForNewTargets.output)
}

abstract class FilterTestClasspath : DefaultTask() {
@get:InputFiles
abstract val old: ConfigurableFileCollection

@get:InputFiles
abstract val new: ConfigurableFileCollection

@get:OutputDirectory
abstract val outputDir: DirectoryProperty

@get:Inject
abstract val fsOps: FileSystemOperations

@TaskAction
fun run() {
if (!outputDir.get().asFile.toPath().exists()) {
outputDir.get().asFile.mkdirs()
} else {
fsOps.delete {
delete(outputDir.get())
}
outputDir.get().asFile.mkdirs()
}

val newExisting = mutableListOf<String>()
for (file in new.files) {
if (file.exists()) {
Files.walk(file.toPath()).use { s ->
s.forEach {
if (it.isDirectory()) {
return@forEach
}
newExisting += file.toPath().relativize(it).invariantSeparatorsPathString
}
}
}
}
for (file in old.files) {
if (file.exists()) {
Files.walk(file.toPath()).use { s ->
s.forEach {
if (it.isDirectory()) {
return@forEach
}
val rel = file.toPath().relativize(it).invariantSeparatorsPathString
if (rel !in newExisting) {
it.copyTo(outputDir.get().asFile.toPath().resolve(rel).also { f -> f.parent.createDirectories() })
}
}
}
}
}
}
}
105 changes: 105 additions & 0 deletions classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.papermc.classfile;

import io.papermc.classfile.method.transform.MethodTransformContext;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.Opcode;
import java.lang.classfile.TypeKind;
import java.lang.constant.ClassDesc;
import java.lang.constant.DirectMethodHandleDesc;
import java.lang.constant.MethodTypeDesc;
import java.lang.invoke.LambdaMetafactory;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;

public final class ClassFiles {

public static final int BOOTSTRAP_HANDLE_IDX = 1;
public static final int DYNAMIC_TYPE_IDX = 2;
public static final String CONSTRUCTOR_METHOD_NAME = "<init>";
public static final ClassDesc LAMBDA_METAFACTORY = desc(LambdaMetafactory.class);
public static final String GENERATED_PREFIX = "paperClassfileGenerated$";
private static final String DEFAULT_CTOR_METHOD_PREFIX = "create";

private ClassFiles() {
}

public static String toInternalName(final ClassDesc clazz) {
if (!clazz.isClassOrInterface()) {
throw new IllegalArgumentException("Not a class or interface: " + clazz);
}
return clazz.descriptorString().substring(1, clazz.descriptorString().length() - 1);
}

public static ClassDesc desc(final Class<?> clazz) {
return clazz.describeConstable().orElseThrow();
}

public static MethodTypeDesc adjustForStatic(final Opcode opcode, final ClassDesc owner, final MethodTypeDesc descriptor) {
return switch (opcode) {
// for INVOKEVIRTUAL, INVOKEINTERFACE methods, we have to add the receiver as the first param for the static replacement
case INVOKEVIRTUAL, INVOKEINTERFACE -> descriptor.insertParameterTypes(0, owner);
// for INVOKESPECIAL, we have to add a return type; constructors have a void return type
case INVOKESPECIAL -> descriptor.changeReturnType(owner);
case INVOKESTATIC -> descriptor;
default -> throw new IllegalArgumentException("Unexpected opcode: " + opcode);
};
}

public static MethodTypeDesc adjustForStatic(final DirectMethodHandleDesc.Kind kind, final ClassDesc owner, final MethodTypeDesc descriptor) {
return switch (kind) {
// for VIRTUAL, INTERFACE_VIRTUAL methods, we have to add the receiver as the first param for the static replacement
case VIRTUAL, INTERFACE_VIRTUAL -> descriptor.insertParameterTypes(0, owner);
// for CONSTRUCTOR, we have to add a return type; constructors have a void return type
case CONSTRUCTOR -> descriptor.changeReturnType(owner);
case STATIC, INTERFACE_STATIC -> descriptor;
default -> throw new IllegalArgumentException("Unexpected kind: " + kind);
};
}

public static MethodTypeDesc replaceParameters(MethodTypeDesc descriptor, final Predicate<ClassDesc> oldParam, final ClassDesc newParam) {
for (int i = 0; i < descriptor.parameterCount(); i++) {
if (oldParam.test(descriptor.parameterType(i))) {
descriptor = descriptor.changeParameterType(i, newParam);
}
}
return descriptor;
}

public static String constructorMethodName(final ClassDesc owner) {
// strip preceding "L" and trailing ";""
final String ownerName = owner.descriptorString().substring(1, owner.descriptorString().length() - 1);
return DEFAULT_CTOR_METHOD_PREFIX + ownerName.substring(ownerName.lastIndexOf('/') + 1);
}

public static void emitInvoke(final CodeBuilder cb, final Opcode opcode, final MethodTransformContext.MethodInfo info, final MethodTypeDesc callDesc, final boolean includeSpecial) {
switch (opcode) {
case INVOKEVIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc);
case INVOKEINTERFACE -> cb.invokeinterface(info.owner(), info.name(), callDesc);
case INVOKESTATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, info.isInterface());
case INVOKESPECIAL -> {
if (!includeSpecial) {
throw new IllegalArgumentException("INVOKESPECIAL is not supported here");
}
cb.invokespecial(info.owner(), info.name(), callDesc, false);
}
default -> throw new IllegalArgumentException(opcode.toString());
}
}

public static void emitInvoke(final CodeBuilder cb, final DirectMethodHandleDesc.Kind kind, final MethodTransformContext.MethodInfo info, final MethodTypeDesc callDesc, final boolean includeSpecial) {
switch (kind) {
case VIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc);
case INTERFACE_VIRTUAL -> cb.invokeinterface(info.owner(), info.name(), callDesc);
case STATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, false);
case INTERFACE_STATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, true);
case CONSTRUCTOR -> {
if (!includeSpecial) {
throw new IllegalArgumentException("INVOKESPECIAL is not supported here");
}
cb.invokespecial(info.owner(), CONSTRUCTOR_METHOD_NAME, callDesc, false);
}
default -> throw new IllegalArgumentException(kind.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.papermc.classfile;

public enum MethodType {


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.papermc.classfile;

import io.papermc.classfile.method.MethodRewrite;
import io.papermc.classfile.method.MethodRewriteIndex;
import io.papermc.classfile.method.transform.BridgeMethodRegistry;
import io.papermc.classfile.transform.TransformContext;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.ClassTransform;
import java.util.List;

public class RewriteProcessor {

private static final ClassFile CLASS_FILE = ClassFile.of();

private final MethodRewriteIndex methodIndex;

public RewriteProcessor(final List<MethodRewrite> methodRewrites) {
this.methodIndex = new MethodRewriteIndex(methodRewrites);
}

public byte[] rewrite(final byte[] input) {
final ClassModel inputModel = CLASS_FILE.parse(input);
final BridgeMethodRegistry bridges = new BridgeMethodRegistry();
final TransformContext context = TransformContext.create(inputModel.thisClass().asSymbol(), bridges);
final ClassTransform transform = ClassTransform.transformingMethods(MethodRewrite.createTransform(this.methodIndex, context))
.andThen(ClassTransform.endHandler(bridges::emitAll));
return CLASS_FILE.transformClass(inputModel, transform);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.papermc.classfile.generation;

import java.lang.classfile.CodeBuilder;
import java.lang.classfile.TypeKind;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import java.util.function.Consumer;

@FunctionalInterface
public interface ParameterGeneration {

static ParameterGeneration standard() {
return Standard.INSTANCE;
}

static ParameterGeneration standard(final Consumer<CodeBuilder> prefix) {
return (descriptor, builder) -> {
prefix.accept(builder);
Standard.INSTANCE.generateParameters(descriptor, builder);
};
}

static ParameterGeneration mutating(final Mutating mutator) {
return mutating($ -> {}, mutator);
}

static ParameterGeneration mutating(final Consumer<CodeBuilder> prefix, final Mutating mutator) {
return (descriptor, builder) -> {
prefix.accept(builder);
mutator.generateParameters(descriptor, builder);
};
}

void generateParameters(MethodTypeDesc descriptor, CodeBuilder builder);

record Standard() implements ParameterGeneration {

private static final Standard INSTANCE = new Standard();

@Override
public void generateParameters(final MethodTypeDesc descriptor, final CodeBuilder builder) {
// Load all parameters (using correct slots for wide types)
int slot = 0;
for (final ClassDesc paramType : descriptor.parameterList()) {
final TypeKind typeKind = TypeKind.from(paramType);
builder.loadLocal(typeKind, slot);
slot += typeKind.slotSize();
}
}
}

@FunctionalInterface
interface Mutating extends ParameterGeneration {

@Override
default void generateParameters(final MethodTypeDesc descriptor, final CodeBuilder builder) {
// Load all parameters (using correct slots for wide types)
int slot = 0;
for (final ClassDesc paramType : descriptor.parameterList()) {
final TypeKind typeKind = TypeKind.from(paramType);
builder.loadLocal(typeKind, slot);
this.mutate(paramType, builder);
slot += typeKind.slotSize();
}
}

void mutate(ClassDesc paramType, CodeBuilder builder);
}
}
Loading
Loading