Skip to content

Commit 8f1bfba

Browse files
authored
Fix loading cached inner classes (#230)
* custom meta class for scripts * custom class loader to cache script classes * custom meta class for class class
1 parent 73dbdf1 commit 8f1bfba

17 files changed

+175
-116
lines changed

src/main/java/com/cleanroommc/groovyscript/GroovyScript.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager;
2323
import com.cleanroommc.groovyscript.sandbox.*;
2424
import com.cleanroommc.groovyscript.sandbox.mapper.GroovyDeobfMapper;
25-
import com.cleanroommc.groovyscript.sandbox.security.GrSMetaClassCreationHandle;
25+
import com.cleanroommc.groovyscript.sandbox.meta.GrSMetaClassCreationHandle;
2626
import com.cleanroommc.groovyscript.server.GroovyScriptLanguageServer;
2727
import com.google.gson.JsonElement;
2828
import com.google.gson.JsonObject;
@@ -106,6 +106,7 @@ public void onConstruction(FMLConstructionEvent event) {
106106
MinecraftForge.EVENT_BUS.register(EventHandler.class);
107107
NetworkHandler.init();
108108
GroovySystem.getMetaClassRegistry().setMetaClassCreationHandle(GrSMetaClassCreationHandle.INSTANCE);
109+
GroovySystem.getMetaClassRegistry().getMetaClassCreationHandler().setDisableCustomMetaClassLookup(true);
109110
GroovyDeobfMapper.init();
110111
LinkGeneratorHooks.init();
111112
ReloadableRegistryManager.init();

src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/GroovyClassLoaderMixin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class GroovyClassLoaderMixin {
1818
@Inject(method = "recompile", at = @At("HEAD"), cancellable = true)
1919
public void onRecompile(URL source, String className, Class<?> oldClass, CallbackInfoReturnable<Class<?>> cir) {
2020
if (source != null && oldClass == null) {
21-
Class<?> c = GroovyScript.getSandbox().onRecompileClass((GroovyClassLoader) (Object) this, source, className);
21+
Class<?> c = GroovyScript.getSandbox().onRecompileClass(source, className);
2222
if (c != null) {
2323
cir.setReturnValue(c);
2424
}

src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/MetaClassImplMixin.java

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.cleanroommc.groovyscript.GroovyScript;
44
import com.cleanroommc.groovyscript.api.IDynamicGroovyProperty;
5+
import com.cleanroommc.groovyscript.sandbox.meta.ClassMetaClass;
56
import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager;
67
import groovy.lang.*;
7-
import org.codehaus.groovy.reflection.CachedClass;
88
import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl;
99
import org.spongepowered.asm.mixin.Mixin;
1010
import org.spongepowered.asm.mixin.*;
@@ -20,20 +20,16 @@
2020
public abstract class MetaClassImplMixin {
2121

2222
@Shadow
23-
protected abstract Object doInvokeMethod(Class sender, Object object, String methodName, Object[] originalArguments, boolean isCallToSuper, boolean fromInsideClass);
23+
protected abstract Object doInvokeMethod(Class<?> sender, Object object, String methodName, Object[] originalArguments,
24+
boolean isCallToSuper, boolean fromInsideClass);
2425

2526
@Shadow
26-
protected abstract Object invokeMissingMethod(Object instance, String methodName, Object[] arguments, RuntimeException original, boolean isCallToSuper);
27+
protected abstract Object invokeMissingMethod(Object instance, String methodName, Object[] arguments, RuntimeException original,
28+
boolean isCallToSuper);
2729

2830
@Shadow
2931
protected abstract MetaProperty getMetaProperty(String name, boolean useStatic);
3032

31-
@Shadow
32-
protected abstract MetaMethod[] getNewMetaMethods(CachedClass c);
33-
34-
@Shadow
35-
public abstract CachedClass getTheCachedClass();
36-
3733
@Mutable
3834
@Shadow
3935
@Final
@@ -74,8 +70,11 @@ public void removeBlacklistedAdditional(Class<?> theClass, MetaMethod[] add, Cal
7470
}
7571
}
7672

77-
@Inject(method = "invokeMethod(Ljava/lang/Class;Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;ZZ)Ljava/lang/Object;", at = @At("HEAD"), cancellable = true)
78-
public void invokeMethod(Class<?> sender, Object object, String methodName, Object[] arguments, boolean isCallToSuper, boolean fromInsideClass, CallbackInfoReturnable<Object> cir) {
73+
@Inject(method = "invokeMethod(Ljava/lang/Class;Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;ZZ)Ljava/lang/Object;",
74+
at = @At("HEAD"),
75+
cancellable = true)
76+
public void invokeMethod(Class<?> sender, Object object, String methodName, Object[] arguments, boolean isCallToSuper,
77+
boolean fromInsideClass, CallbackInfoReturnable<Object> cir) {
7978
try {
8079
cir.setReturnValue(doInvokeMethod(sender, object, methodName, arguments, isCallToSuper, fromInsideClass));
8180
} catch (MissingMethodException mme) {
@@ -84,7 +83,8 @@ public void invokeMethod(Class<?> sender, Object object, String methodName, Obje
8483
}
8584

8685
@Inject(method = "invokeMissingProperty", at = @At("HEAD"), cancellable = true)
87-
public void invokeMissingProperty(Object instance, String propertyName, Object optionalValue, boolean isGetter, CallbackInfoReturnable<Object> cir) {
86+
public void invokeMissingProperty(Object instance, String propertyName, Object optionalValue, boolean isGetter,
87+
CallbackInfoReturnable<Object> cir) {
8888
if (instance instanceof IDynamicGroovyProperty) {
8989
if (isGetter) {
9090
Object prop = ((IDynamicGroovyProperty) instance).getProperty(propertyName);
@@ -99,25 +99,18 @@ public void invokeMissingProperty(Object instance, String propertyName, Object o
9999

100100
@Inject(method = "invokeStaticMissingMethod", at = @At("HEAD"), cancellable = true)
101101
public void invokeStaticMissingMethod(Class<?> sender, String methodName, Object[] arguments, CallbackInfoReturnable<Object> cir) {
102-
if (sender.getSuperclass() != Script.class && "main".equals(methodName)) {
103-
cir.setReturnValue(null);
102+
if ((Object) this instanceof ClassMetaClass cmc) {
103+
cmc.invokeStaticMissingMethod(sender, methodName, arguments, cir);
104104
}
105105
}
106106

107-
@Inject(method = "invokeStaticMissingProperty", at = @At("HEAD"), cancellable = true)
108-
public void invokeStaticMissingProperty(Object instance, String propertyName, Object optionalValue, boolean isGetter, CallbackInfoReturnable<Object> cir) {
109-
if (!isGetter) return;
110-
Object o = GroovyScript.getSandbox().getBindings().get(propertyName);
111-
if (o != null) cir.setReturnValue(o);
112-
}
113-
114107
/**
115108
* @author brachy
116109
* @reason class scripts being unable to use bindings and this method calling closures improperly
117110
*/
118111
@Overwrite
119-
private Object invokePropertyOrMissing(Object object, String methodName, Object[] originalArguments,
120-
boolean fromInsideClass, boolean isCallToSuper) {
112+
private Object invokePropertyOrMissing(Object object, String methodName, Object[] originalArguments, boolean fromInsideClass,
113+
boolean isCallToSuper) {
121114
MetaProperty metaProperty = getMetaProperty(methodName, false);
122115

123116
Object value = null;
@@ -134,15 +127,8 @@ private Object invokePropertyOrMissing(Object object, String methodName, Object[
134127
value = GroovyScript.getSandbox().getBindings().get(methodName);
135128
}
136129

137-
if (value instanceof Closure) {
138-
Closure<?> closure = (Closure<?>) value;
130+
if (value instanceof Closure<?> closure) {
139131
return closure.call(originalArguments);
140-
/*MetaClass metaClass = closure.getMetaClass();
141-
try {
142-
return metaClass.invokeMethod(closure.getClass(), closure, DO_CALL_METHOD, originalArguments, false, fromInsideClass);
143-
} catch (MissingMethodException mme) {
144-
// fall through -- "doCall" is not intrinsic to Closure
145-
}*/
146132
}
147133

148134
if (value != null && !(value instanceof Map) && !methodName.equals("call")) {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.cleanroommc.groovyscript.sandbox;
2+
3+
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
4+
import net.minecraft.launchwrapper.Launch;
5+
6+
import java.util.Map;
7+
8+
public class CachedClassLoader extends ClassLoader {
9+
10+
private final Map<String, Class<?>> cache = new Object2ObjectOpenHashMap<>();
11+
12+
public CachedClassLoader() {
13+
super(Launch.classLoader);
14+
}
15+
16+
public Class<?> defineClass(String name, byte[] bytes) {
17+
Class<?> clz = super.defineClass(name, bytes, 0, bytes.length);
18+
resolveClass(clz);
19+
this.cache.put(clz.getName(), clz);
20+
return clz;
21+
}
22+
23+
public void clearCache() {
24+
this.cache.clear();
25+
}
26+
27+
@Override
28+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
29+
Class<?> clz = tryLoadClass(name);
30+
if (clz != null) return clz;
31+
return super.loadClass(name, resolve);
32+
}
33+
34+
@Override
35+
protected Class<?> findClass(String name) throws ClassNotFoundException {
36+
Class<?> clz = tryLoadClass(name);
37+
if (clz != null) return clz;
38+
return super.findClass(name);
39+
}
40+
41+
public Class<?> tryLoadClass(String name) {
42+
Class<?> clz = this.cache.get(name);
43+
if (clz != null) return clz;
44+
try {
45+
return Launch.classLoader.findClass(name);
46+
} catch (ClassNotFoundException e) {
47+
return null;
48+
}
49+
}
50+
}

src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.cleanroommc.groovyscript.sandbox;
22

33
import com.cleanroommc.groovyscript.api.GroovyLog;
4-
import groovy.lang.GroovyClassLoader;
54
import org.apache.commons.lang3.builder.ToStringBuilder;
65

76
import java.io.File;
@@ -48,7 +47,7 @@ public void onCompile(Class<?> clazz, String basePath) {
4847
}
4948
}
5049

51-
protected void ensureLoaded(GroovyClassLoader classLoader, String basePath) {
50+
protected void ensureLoaded(CachedClassLoader classLoader, String basePath) {
5251
if (this.clazz == null) {
5352
this.clazz = classLoader.defineClass(this.name, this.data);
5453
}
@@ -88,8 +87,6 @@ public String getPath() {
8887

8988
@Override
9089
public String toString() {
91-
return new ToStringBuilder(this)
92-
.append("name", name)
93-
.toString();
90+
return new ToStringBuilder(this).append("name", name).toString();
9491
}
9592
}

src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.google.gson.JsonArray;
66
import com.google.gson.JsonElement;
77
import com.google.gson.JsonObject;
8-
import groovy.lang.GroovyClassLoader;
98
import org.apache.commons.lang3.builder.ToStringBuilder;
109
import org.jetbrains.annotations.NotNull;
1110

@@ -43,7 +42,7 @@ public CompiledClass findInnerClass(String clazz) {
4342
return comp;
4443
}
4544

46-
public void ensureLoaded(GroovyClassLoader classLoader, String basePath) {
45+
public void ensureLoaded(CachedClassLoader classLoader, String basePath) {
4746
for (CompiledClass comp : this.innerClasses) {
4847
if (comp.clazz == null) {
4948
if (comp.readData(basePath)) {
@@ -80,7 +79,8 @@ public JsonObject toJson() {
8079
}
8180

8281
public static CompiledScript fromJson(JsonObject json, String scriptRoot, String cacheRoot) {
83-
CompiledScript cs = new CompiledScript(json.get("path").getAsString(), JsonHelper.getString(json, null, "name"), json.get("lm").getAsLong());
82+
CompiledScript cs = new CompiledScript(json.get("path").getAsString(), JsonHelper.getString(json, null, "name"),
83+
json.get("lm").getAsLong());
8484
if (new File(scriptRoot, cs.path).exists()) {
8585
if (json.has("inner")) {
8686
for (JsonElement element : json.getAsJsonArray("inner")) {
@@ -109,18 +109,16 @@ public void deleteCache(String cachePath) {
109109
}
110110

111111
public boolean checkPreprocessors(File basePath) {
112-
return this.preprocessors == null ||
113-
this.preprocessors.isEmpty() ||
114-
Preprocessor.validatePreprocessor(new File(basePath, this.path), this.preprocessors);
112+
return this.preprocessors == null || this.preprocessors.isEmpty() || Preprocessor.validatePreprocessor(
113+
new File(basePath, this.path), this.preprocessors);
115114
}
116115

117116
@Override
118117
public String toString() {
119-
return new ToStringBuilder(this)
120-
.append("name", name)
121-
.append("path", path)
122-
.append("innerClasses", innerClasses)
123-
.append("lastEdited", lastEdited)
124-
.toString();
118+
return new ToStringBuilder(this).append("name", name)
119+
.append("path", path)
120+
.append("innerClasses", innerClasses)
121+
.append("lastEdited", lastEdited)
122+
.toString();
125123
}
126124
}

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import groovy.util.ResourceException;
1212
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
1313
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
14-
import net.minecraft.launchwrapper.Launch;
1514
import org.codehaus.groovy.control.CompilerConfiguration;
1615
import org.codehaus.groovy.runtime.InvokerHelper;
1716
import org.jetbrains.annotations.ApiStatus;
@@ -38,6 +37,7 @@ public abstract class GroovySandbox {
3837
private final ThreadLocal<Boolean> running = ThreadLocal.withInitial(() -> false);
3938
private final Map<String, Object> bindings = new Object2ObjectOpenHashMap<>();
4039
private final Set<Class<?>> staticImports = new HashSet<>();
40+
private final CachedClassLoader ccl = new CachedClassLoader();
4141

4242
protected GroovySandbox(URL[] scriptEnvironment) {
4343
if (scriptEnvironment == null || scriptEnvironment.length == 0) {
@@ -83,7 +83,7 @@ protected void stopRunning() {
8383
}
8484

8585
protected GroovyScriptEngine createScriptEngine() {
86-
GroovyScriptEngine engine = new GroovyScriptEngine(this.scriptEnvironment, Launch.classLoader);
86+
GroovyScriptEngine engine = new GroovyScriptEngine(this.scriptEnvironment, this.ccl);
8787
CompilerConfiguration config = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
8888
config.setSourceEncoding("UTF-8");
8989
engine.setConfig(config);
@@ -156,13 +156,11 @@ protected void loadClassScripts(GroovyScriptEngine engine, Binding binding, Set<
156156
// the superclass of class files is Object
157157
if (clazz.getSuperclass() != Script.class && shouldRunFile(classFile)) {
158158
executedClasses.add(classFile);
159-
Script script = InvokerHelper.createScript(clazz, binding);
160-
if (run) runScript(script);
161159
}
162160
}
163161
}
164162

165-
protected void runScript(Script script){
163+
protected void runScript(Script script) {
166164
setCurrentScript(script.getClass().getName());
167165
script.run();
168166
setCurrentScript(null);
@@ -228,6 +226,10 @@ protected void setCurrentScript(String currentScript) {
228226
this.currentScript = currentScript;
229227
}
230228

229+
public CachedClassLoader getClassLoader() {
230+
return ccl;
231+
}
232+
231233
public static String getRelativePath(String source) {
232234
try {
233235
Path path = Paths.get(new URL(source).toURI());

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,14 @@ public void onCompileClass(SourceUnit su, String path, Class<?> clazz, byte[] co
257257
* it. Groovy will then try to compile the script again. If we already compiled the class we just stop the compilation process.
258258
*/
259259
@ApiStatus.Internal
260-
public Class<?> onRecompileClass(GroovyClassLoader classLoader, URL source, String className) {
260+
public Class<?> onRecompileClass(URL source, String className) {
261261
String path = source.toExternalForm();
262-
CompiledScript cs = this.index.get(FileUtil.relativize(this.scriptRoot.getPath(), path));
262+
String rel = FileUtil.relativize(this.scriptRoot.getPath(), path);
263+
CompiledScript cs = this.index.get(rel);
263264
Class<?> c = null;
264265
if (cs != null) {
265266
if (cs.clazz == null && cs.readData(this.cacheRoot.getPath())) {
266-
cs.ensureLoaded(classLoader, this.cacheRoot.getPath());
267+
cs.ensureLoaded(getClassLoader(), this.cacheRoot.getPath());
267268
}
268269
c = cs.clazz;
269270
}
@@ -281,7 +282,7 @@ protected Class<?> loadScriptClass(GroovyScriptEngine engine, File file) {
281282
if (!comp.checkPreprocessors(this.scriptRoot)) {
282283
return GroovyLog.class; // failed preprocessor check
283284
}
284-
comp.ensureLoaded(engine.getGroovyClassLoader(), this.cacheRoot.getPath());
285+
comp.ensureLoaded(getClassLoader(), this.cacheRoot.getPath());
285286

286287
} else if (!ENABLE_CACHE || (comp == null || comp.clazz == null || lastModified > comp.lastEdited)) {
287288
// class is not loaded and class bytes don't exist yet or script has been edited
@@ -312,7 +313,7 @@ protected Class<?> loadScriptClass(GroovyScriptEngine engine, File file) {
312313
if (!comp.checkPreprocessors(this.scriptRoot)) {
313314
return GroovyLog.class; // failed preprocessor check
314315
}
315-
comp.ensureLoaded(engine.getGroovyClassLoader(), this.cacheRoot.getPath());
316+
comp.ensureLoaded(getClassLoader(), this.cacheRoot.getPath());
316317
}
317318
return comp.clazz;
318319
}
@@ -389,6 +390,7 @@ public File getScriptRoot() {
389390
@ApiStatus.Internal
390391
public boolean deleteScriptCache() {
391392
this.index.clear();
393+
getClassLoader().clearCache();
392394
try {
393395
FileUtils.cleanDirectory(this.cacheRoot);
394396
return true;

0 commit comments

Comments
 (0)