Skip to content

Commit

Permalink
lua4jvm: Replace hidden classes with single-use ClassLoaders
Browse files Browse the repository at this point in the history
Hidden classes have hidden stack frames, which is not ok when they are
compiled from user code that might throw exceptions.
  • Loading branch information
bensku committed Aug 2, 2024
1 parent 6ae5186 commit 71dc004
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package fi.benjami.code4jvm.lua.compiler;

import java.lang.invoke.MethodHandles;

import fi.benjami.code4jvm.Type;
import fi.benjami.code4jvm.call.CallTarget;
import fi.benjami.code4jvm.call.FixedCallTarget;

/**
* Support code for custom class data dynamic constants.
* JDK already has {@link MethodHandles#classDataAt(java.lang.invoke.MethodHandles.Lookup, String, Class, int) a form of this},
* but it only works with actual JDK class data. That, unfortunately, is only
* available for hidden classes, which we can't use because they're invisible
* in stack frames...
*
*/
public class ClassData {

static final String FIELD_NAME = "$CONSTANTS";
static final FixedCallTarget BOOTSTRAP = CallTarget.staticMethod(Type.of(ClassData.class), Type.OBJECT, "get",
Type.of(MethodHandles.Lookup.class), Type.STRING, Type.of(Class.class), Type.INT);

public static Object get(MethodHandles.Lookup lookup, String ignoredName, Class<?> type, int index) {
Object[] array;
try {
array = (Object[]) lookup.findStaticGetter(lookup.lookupClass(), FIELD_NAME, Object[].class)
.invokeExact();
} catch (Throwable e) {
throw new AssertionError(e);
}
return array[index];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,25 @@ public static MethodHandle callTarget(LuaType[] argTypes, LuaFunction function,
var ctx = LuaContext.forFunction(function.owner(), function.type(), truncateReturn, argTypes);
var code = generateCode(ctx, function.type(), argTypes, upvalueTypes);
try {
var lookup = LOOKUP.defineHiddenClassWithClassData(code, ctx.allClassData(), true);
// Load the class with single-use class loader
// Using hidden classes would be preferable, but JVM hides them from stack frames
// ... which really screws up stack traces of Lua code
// See https://bugs.openjdk.org/browse/JDK-8212620
var implClass = SingleClassLoader.load("unknown", code);
try {
LOOKUP.findStaticSetter(implClass, ClassData.FIELD_NAME, Object[].class)
.invokeExact(ctx.allClassData());
} catch (Throwable e) {
throw new AssertionError(e); // We just generated the field!
}

// Cache the constructor and actual function MHs
// They'll hold references to the underlying class
var jvmUpvalueTypes = Arrays.stream(upvalueTypes)
.map(LuaType::backingType)
.map(Type::loadedClass)
.toArray(Class[]::new);
var constructor = lookup.findConstructor(lookup.lookupClass(),
var constructor = LOOKUP.findConstructor(implClass,
MethodType.methodType(void.class, jvmUpvalueTypes));

var normalArgCount = function.type().acceptedArgs().size();
Expand All @@ -90,7 +100,7 @@ public static MethodHandle callTarget(LuaType[] argTypes, LuaFunction function,

var jvmReturnType = ctx.returnType().equals(LuaType.NIL)
? Type.VOID : ctx.returnType().backingType();
var method = LOOKUP.findVirtual(lookup.lookupClass(), "call",
var method = LOOKUP.findVirtual(implClass, "call",
MethodType.methodType(jvmReturnType.loadedClass(), jvmArgTypes));

return new CompiledFunction(constructor, method);
Expand Down Expand Up @@ -118,7 +128,10 @@ private static byte[] generateCode(LuaContext ctx, LuaType.Function type,
LuaType[] argTypes, LuaType[] upvalueTypes) {
// Create class that wraps the method acting as function body
// TODO store name in function if available?
var def = ClassDef.create("fi.benjami.code4jvm.lua.compiler.CompiledFunction", Access.PUBLIC);
var def = ClassDef.create("unknown", Access.PUBLIC);

// Class data constants
def.addStaticField(Access.PUBLIC, Type.OBJECT.array(1), ClassData.FIELD_NAME);

// Add fields for upvalues
for (var i = 0; i < upvalueTypes.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fi.benjami.code4jvm.lua.compiler;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -199,11 +198,11 @@ public Constant addClassData(Object value) {
public Constant addClassData(Object value, Type type) {
var index = classData.size();
classData.add(value);
return Constant.classDataAt(type, index);
return Constant.dynamic(type, ClassData.BOOTSTRAP, Constant.of(index));
}

public Object allClassData() {
return classData;
public Object[] allClassData() {
return classData.toArray();
}

public <T> T cached(Object key, T value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package fi.benjami.code4jvm.lua.compiler;

public class SingleClassLoader extends ClassLoader {
class SingleClassLoader extends ClassLoader {

public static Class<?> load(String name, byte[] code) {
return new SingleClassLoader(code).findClass(name);
}

private final byte[] code;

Expand All @@ -9,8 +13,7 @@ public SingleClassLoader(byte[] code) {
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
public Class<?> findClass(String name) {
return defineClass(name, code, 0, code.length);

}
}
12 changes: 12 additions & 0 deletions src/main/java/fi/benjami/code4jvm/Constant.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package fi.benjami.code4jvm;

import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Optional;

import org.objectweb.asm.ConstantDynamic;

import fi.benjami.code4jvm.call.CallTarget;
import fi.benjami.code4jvm.call.FixedCallTarget;

public class Constant implements Value {

Expand Down Expand Up @@ -71,6 +73,16 @@ public static Constant classDataAt(Type type, int index) {
return new Constant(dynamic, type);
}

public static Constant dynamic(Type type, FixedCallTarget bootstrap, Constant... args) {
var handle = bootstrap.toMethodHandle();
var argValues = Arrays.stream(args)
.map(Constant::value)
.toArray();
var dynamic = new ConstantDynamic("_", type.descriptor(), handle, argValues);
return new Constant(dynamic, type);

}

// TODO method handle, (more) constant dynamic

private final Object value;
Expand Down

0 comments on commit 71dc004

Please sign in to comment.