support AOT-compilation of Java 8 lambda expressions

These expressions are tricky because they rely on invokedynamic, which
normally implies runtime code generation.  However, since lambdas
don't actually use the "dynamicness" of invokedynamic, we can convert
them into static calls to synthetic classes at compile time.

Since I had already written code to synthesize such classes in Java
and I didn't want to rewrite it in C++, I needed to add support for
running Java code to the bootimage generator.  And since the primary
VM used by the generator is purpose-built to generate AOT-compiled
code for a specific target architecture and is not capable of
generating or running JIT-compiled code for the host architecture, I
added support for loading a second, independent, host-specific VM for
running Java code.

The rest of the patch handles the fact that each method compilation
might cause new, synthetic classes to be created, so we need to make
sure those classes and their methods are included in the final heap
and code images.  This required breaking some giant code blocks out of
makeCodeImage into their own methods, which makes the diff look
scarier than it really is.
This commit is contained in:
Joel Dice 2015-09-12 20:15:46 -06:00
parent 763aada4b0
commit d5a5b5309a
18 changed files with 893 additions and 479 deletions

View File

@ -168,8 +168,8 @@ Library" below for details.
These flags determine the name of the directory used for the build.
The name always starts with _${platform}-${arch}_, and each non-default
build option is appended to the name. For example, a debug build with
bootimage enabled on Linux/i386 would be built in
_build/linux-i386-debug-bootimage_. This allows you to build with
bootimage enabled on Linux/x86_64 would be built in
_build/linux-x86_64-debug-bootimage_. This allows you to build with
several different sets of options independently and even
simultaneously without doing a clean build each time.
@ -575,7 +575,7 @@ For boot image builds:
Note you can use ProGuard without using a boot image and vice-versa,
as desired.
The following instructions assume we are building for Linux/i386.
The following instructions assume we are building for Linux/x86_64.
Please refer to the previous example for guidance on other platforms.
__1.__ Build Avian, create a new directory, and populate it with the
@ -584,13 +584,13 @@ VM object files.
$ make bootimage=true
$ mkdir hello
$ cd hello
$ ar x ../build/linux-i386-bootimage/libavian.a
$ ar x ../build/linux-x86_64-bootimage/libavian.a
__2.__ Create a stage1 directory and extract the contents of the
class library jar into it.
$ mkdir stage1
$ (cd stage1 && jar xf ../../build/linux-i386-bootimage/classpath.jar)
$ (cd stage1 && jar xf ../../build/linux-x86_64-bootimage/classpath.jar)
__3.__ Build the Java code and add it to stage1.
@ -630,10 +630,11 @@ using the OpenJDK library.)
__6.__ Build the boot and code images.
$ ../build/linux-i386-bootimage/bootimage-generator \
$ ../build/linux-x86_64-bootimage/bootimage-generator \
-cp stage2 \
-bootimage bootimage-bin.o \
-codeimage codeimage-bin.o
-codeimage codeimage-bin.o \
-hostvm ../build/linux-x86_64-interpret/libjvm.so
Note that you can override the default names for the start and end
symbols in the boot/code image by also passing:

View File

@ -423,6 +423,27 @@ public class Classes {
}
}
public static VMMethod findMethod(ClassLoader loader,
String class_,
String name,
String spec)
throws ClassNotFoundException
{
VMClass c = SystemClassLoader.vmClass(loader.loadClass(class_));
VMMethod[] methodTable = c.methodTable;
if (methodTable != null) {
link(c);
for (int i = 0; i < methodTable.length; ++i) {
VMMethod m = methodTable[i];
if (toString(m.name).equals(name) && toString(m.spec).equals(spec)) {
return m;
}
}
}
return null;
}
public static int findMethod(VMClass vmClass, String name,
Class[] parameterTypes)
{

View File

@ -20,6 +20,8 @@ import java.util.Enumeration;
import java.util.NoSuchElementException;
public class SystemClassLoader extends ClassLoader {
public static native ClassLoader appLoader();
private native VMClass findVMClass(String name)
throws ClassNotFoundException;

View File

@ -187,14 +187,28 @@ public class LambdaMetafactory {
return result;
}
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType methodType,
MethodHandle methodImplementation,
MethodType instantiatedMethodType)
throws LambdaConversionException
public static byte[] makeLambda(String invokedName,
String invokedType,
String methodType,
String implementationClass,
String implementationName,
String implementationSpec,
int implementationKind)
{
return makeLambda(invokedName,
new MethodType(invokedType),
new MethodType(methodType),
new MethodHandle(implementationClass,
implementationName,
implementationSpec,
implementationKind));
}
private static byte[] makeLambda(String invokedName,
MethodType invokedType,
MethodType methodType,
MethodHandle methodImplementation)
{
String className;
{ int number;
@ -265,8 +279,19 @@ public class LambdaMetafactory {
throw error;
}
byte[] classData = out.toByteArray();
return out.toByteArray();
}
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType methodType,
MethodHandle methodImplementation,
MethodType instantiatedMethodType)
throws LambdaConversionException
{
byte[] classData = makeLambda(invokedName, invokedType, methodType, methodImplementation);
try {
return new CallSite
(new MethodHandle

View File

@ -1,6 +1,7 @@
package java.lang.invoke;
import avian.Classes;
import avian.SystemClassLoader;
public class MethodHandle {
static final int REF_invokeStatic = 6;
@ -17,6 +18,20 @@ public class MethodHandle {
this.method = method;
}
MethodHandle(String class_,
String name,
String spec,
int kind)
{
this.kind = kind;
this.loader = SystemClassLoader.appLoader();
try {
this.method = Classes.findMethod(this.loader, class_, name, spec);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
if (method.class_ != null) {

View File

@ -4,6 +4,7 @@ import static avian.Assembler.*;
import avian.Assembler;
import avian.Classes;
import avian.SystemClassLoader;
import avian.VMClass;
import java.util.List;
@ -25,6 +26,12 @@ public final class MethodType implements java.io.Serializable {
this.spec = spec;
}
MethodType(String spec) {
this.loader = SystemClassLoader.appLoader();
this.spec = new byte[spec.length() + 1];
spec.getBytes(0, spec.length(), this.spec, 0);
}
public String toMethodDescriptorString() {
return Classes.makeString(spec, 0, spec.length - 1);
}

View File

@ -1333,6 +1333,12 @@ bootimage-generator-objects = \
$(call cpp-objects,$(bootimage-generator-sources),$(src),$(build))
bootimage-generator = $(build)/bootimage-generator
ifneq ($(mode),fast)
host-vm-options := -$(mode)
endif
host-vm = build/$(build-platform)-$(build-arch)-interpret$(host-vm-options)/libjvm.so
bootimage-object = $(build)/bootimage-bin.o
codeimage-object = $(build)/codeimage-bin.o
@ -2010,7 +2016,8 @@ $(bootimage-object) $(codeimage-object): $(bootimage-generator) \
@echo "generating bootimage and codeimage binaries from $(classpath-build) using $(<)"
$(<) -cp $(classpath-build) -bootimage $(bootimage-object) -codeimage $(codeimage-object) \
-bootimage-symbols $(bootimage-symbols) \
-codeimage-symbols $(codeimage-symbols)
-codeimage-symbols $(codeimage-symbols) \
-hostvm $(host-vm)
executable-objects = $(vm-objects) $(classpath-objects) $(driver-object) \
$(vm-heapwalk-objects) $(boot-object) $(vm-classpath-objects) \
@ -2065,6 +2072,7 @@ $(unittest-executable): $(unittest-executable-objects)
$(bootimage-generator): $(bootimage-generator-objects) $(vm-objects)
echo building $(bootimage-generator) arch=$(build-arch) platform=$(bootimage-platform)
$(MAKE) process=interpret bootimage= mode=$(mode)
$(MAKE) mode=$(mode) \
build=$(host-build-root) \
arch=$(build-arch) \

View File

@ -55,10 +55,13 @@ class BootImage {
} PACKED;
class GcField;
class GcClass;
class OffsetResolver {
public:
virtual unsigned fieldOffset(Thread*, GcField*) = 0;
virtual void addClass(Thread*, GcClass*, const uint8_t*, size_t) = 0;
};
#define NAME(x) Target##x

View File

@ -20,6 +20,7 @@
#define EMBED_PREFIX_PROPERTY "avian.embed.prefix"
#define CLASSPATH_PROPERTY "java.class.path"
#define JAVA_HOME_PROPERTY "java.home"
#define REENTRANT_PROPERTY "avian.reentrant"
#define BOOTCLASSPATH_PREPEND_OPTION "bootclasspath/p"
#define BOOTCLASSPATH_OPTION "bootclasspath"
#define BOOTCLASSPATH_APPEND_OPTION "bootclasspath/a"

View File

@ -198,6 +198,14 @@ const unsigned ConstructorFlag = 1 << 1;
#define JNI_VERSION_1_6 0x00010006
#endif
#ifndef JNI_TRUE
#define JNI_TRUE 1
#endif
#ifndef JNI_OK
#define JNI_OK 0
#endif
typedef Machine JavaVM;
typedef Thread JNIEnv;
@ -207,6 +215,19 @@ struct JNINativeMethod {
void* function;
};
struct JavaVMOption {
char* optionString;
void* extraInfo;
};
struct JavaVMInitArgs {
jint version;
jint nOptions;
JavaVMOption* options;
jboolean ignoreUnrecognized;
};
struct JavaVMVTable {
void* reserved0;
void* reserved1;
@ -3737,10 +3758,10 @@ void populateMultiArray(Thread* t,
GcMethod* getCaller(Thread* t, unsigned target, bool skipMethodInvoke = false);
object defineClass(Thread* t,
GcClassLoader* loader,
const uint8_t* buffer,
unsigned length);
GcClass* defineClass(Thread* t,
GcClassLoader* loader,
const uint8_t* buffer,
unsigned length);
inline GcMethod* methodClone(Thread* t, GcMethod* method)
{

View File

@ -170,7 +170,8 @@ class Processor {
GcTriple** calls,
avian::codegen::DelayedPromise** addresses,
GcMethod* method,
OffsetResolver* resolver) = 0;
OffsetResolver* resolver,
Machine* hostVM) = 0;
virtual void visitRoots(Thread* t, HeapWalker* w) = 0;

View File

@ -183,6 +183,12 @@ extern "C" AVIAN_EXPORT int64_t JNICALL
t->m->classpath->makeString(t, array, offset, length));
}
extern "C" AVIAN_EXPORT int64_t JNICALL
Avian_avian_SystemClassLoader_appLoader(Thread* t, object, uintptr_t*)
{
return reinterpret_cast<int64_t>(roots(t)->appLoader());
}
extern "C" AVIAN_EXPORT int64_t JNICALL
Avian_avian_SystemClassLoader_findLoadedVMClass(Thread* t,
object,

View File

@ -909,14 +909,16 @@ class BootContext {
GcTriple* calls,
avian::codegen::DelayedPromise* addresses,
Zone* zone,
OffsetResolver* resolver)
OffsetResolver* resolver,
JavaVM* hostVM)
: protector(t, this),
constants(constants),
calls(calls),
addresses(addresses),
addressSentinal(addresses),
zone(zone),
resolver(resolver)
resolver(resolver),
hostVM(hostVM)
{
}
@ -927,6 +929,7 @@ class BootContext {
avian::codegen::DelayedPromise* addressSentinal;
Zone* zone;
OffsetResolver* resolver;
JavaVM* hostVM;
};
class Context {
@ -3991,6 +3994,34 @@ void checkField(Thread* t, GcField* field, bool shouldBeStatic)
}
}
bool isLambda(Thread* t,
GcClassLoader* loader,
GcCharArray* bootstrapArray,
GcInvocation* invocation)
{
GcMethod* bootstrap = cast<GcMethod>(t,
resolve(t,
loader,
invocation->pool(),
bootstrapArray->body()[0],
findMethodInClass,
GcNoSuchMethodError::Type));
PROTECT(t, bootstrap);
return vm::strcmp(reinterpret_cast<const int8_t*>(
"java/lang/invoke/LambdaMetafactory"),
bootstrap->class_()->name()->body().begin()) == 0
and vm::strcmp(reinterpret_cast<const int8_t*>("metafactory"),
bootstrap->name()->body().begin()) == 0
and vm::strcmp(
reinterpret_cast<const int8_t*>(
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/"
"String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/"
"MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/"
"invoke/MethodType;)Ljava/lang/invoke/CallSite;"),
bootstrap->spec()->body().begin()) == 0;
}
void compile(MyThread* t,
Frame* initialFrame,
unsigned initialIp,
@ -5054,36 +5085,168 @@ loop:
invocation->setClass(t, context->method->class_());
unsigned index = addDynamic(t, invocation);
BootContext* bc = context->bootContext;
if (bc) {
// When we're AOT-compiling an application, we can't handle
// invokedynamic in general, since it usually implies runtime
// code generation. However, Java 8 lambda expressions are a
// special case for which we can generate code ahead of time.
//
// The only tricky part about it is that the class synthesis
// code resides in LambdaMetaFactory, which means we need to
// call out to a separate Java VM to execute it (the VM we're
// currently executing in won't work because it only knows how
// to compile code for the target machine, which might not be
// the same as the host; plus we don't want to pollute the
// runtime heap image with stuff that's only needed at compile
// time).
GcMethod* template_ = invocation->template_();
unsigned returnCode = template_->returnCode();
unsigned rSize = resultSize(t, returnCode);
unsigned parameterFootprint = template_->parameterFootprint();
GcClass* c = context->method->class_();
PROTECT(t, c);
// TODO: can we allow tailCalls in general?
// e.g. what happens if the call site is later bound to a method that can't be tail called?
// NOTE: calling isTailCall right now would cause an segfault, since
// invocation->template_()->class_() will be null.
// bool tailCall
// = isTailCall(t, code, ip, context->method, invocation->template_());
bool tailCall = false;
GcCharArray* bootstrapArray = cast<GcCharArray>(
t,
cast<GcArray>(t, c->addendum()->bootstrapMethodTable())
->body()[invocation->bootstrap()]);
PROTECT(t, bootstrapArray);
// todo: do we need to tell the compiler to add a load barrier
// here for VolatileCallSite instances?
if (isLambda(t, c->loader(), bootstrapArray, invocation)) {
JNIEnv* e;
if (bc->hostVM->vtable->AttachCurrentThread(bc->hostVM, &e, 0) == 0) {
e->vtable->PushLocalFrame(e, 256);
ir::Value* result = c->stackCall(
c->memory(c->memory(c->threadRegister(), ir::Type::object(),
TARGET_THREAD_DYNAMICTABLE),
ir::Type::object(), index * TargetBytesPerWord),
tailCall ? Compiler::TailJump : 0, frame->trace(0, 0),
operandTypeForFieldCode(t, returnCode),
frame->peekMethodArguments(parameterFootprint));
jclass lmfClass
= e->vtable->FindClass(e, "java/lang/invoke/LambdaMetafactory");
jmethodID makeLambda = e->vtable->GetStaticMethodID(
e,
lmfClass,
"makeLambda",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/"
"lang/String;Ljava/lang/String;Ljava/lang/String;I)[B");
frame->popFootprint(parameterFootprint);
GcReference* reference = cast<GcReference>(
t,
singletonObject(
t, invocation->pool(), bootstrapArray->body()[2]));
int kind = reference->kind();
if (rSize) {
frame->pushReturnValue(returnCode, result);
GcMethod* method
= cast<GcMethod>(t,
resolve(t,
c->loader(),
invocation->pool(),
bootstrapArray->body()[2],
findMethodInClass,
GcNoSuchMethodError::Type));
jarray lambda = e->vtable->CallStaticObjectMethod(
e,
lmfClass,
makeLambda,
e->vtable->NewStringUTF(
e,
reinterpret_cast<const char*>(
invocation->template_()->name()->body().begin())),
e->vtable->NewStringUTF(
e,
reinterpret_cast<const char*>(
invocation->template_()->spec()->body().begin())),
e->vtable->NewStringUTF(
e,
reinterpret_cast<const char*>(
cast<GcByteArray>(
t,
singletonObject(t,
invocation->pool(),
bootstrapArray->body()[1]))
->body()
.begin())),
e->vtable->NewStringUTF(
e,
reinterpret_cast<const char*>(
method->class_()->name()->body().begin())),
e->vtable->NewStringUTF(e,
reinterpret_cast<const char*>(
method->name()->body().begin())),
e->vtable->NewStringUTF(e,
reinterpret_cast<const char*>(
method->spec()->body().begin())),
kind);
uint8_t* bytes = reinterpret_cast<uint8_t*>(
e->vtable->GetPrimitiveArrayCritical(e, lambda, 0));
GcClass* lambdaClass
= defineClass(t,
roots(t)->appLoader(),
bytes,
e->vtable->GetArrayLength(e, lambda));
bc->resolver->addClass(
t, lambdaClass, bytes, e->vtable->GetArrayLength(e, lambda));
e->vtable->ReleasePrimitiveArrayCritical(e, lambda, bytes, 0);
e->vtable->PopLocalFrame(e, 0);
THREAD_RUNTIME_ARRAY(
t, char, spec, invocation->template_()->spec()->length());
memcpy(RUNTIME_ARRAY_BODY(spec),
invocation->template_()->spec()->body().begin(),
invocation->template_()->spec()->length());
GcMethod* target = resolveMethod(
t, lambdaClass, "make", RUNTIME_ARRAY_BODY(spec));
bool tailCall = isTailCall(t, code, ip, context->method, target);
compileDirectInvoke(t, frame, target, tailCall);
} else {
throwNew(
t, GcVirtualMachineError::Type, "unable to attach to host VM");
}
} else {
throwNew(t,
GcVirtualMachineError::Type,
"invokedynamic not supported for AOT-compiled code except "
"in the case of lambda expressions");
}
} else {
unsigned index = addDynamic(t, invocation);
GcMethod* template_ = invocation->template_();
unsigned returnCode = template_->returnCode();
unsigned rSize = resultSize(t, returnCode);
unsigned parameterFootprint = template_->parameterFootprint();
// TODO: can we allow tailCalls in general?
// e.g. what happens if the call site is later bound to a method that
// can't be tail called?
// NOTE: calling isTailCall right now would cause an segfault, since
// invocation->template_()->class_() will be null.
// bool tailCall
// = isTailCall(t, code, ip, context->method,
// invocation->template_());
bool tailCall = false;
// todo: do we need to tell the compiler to add a load barrier
// here for VolatileCallSite instances?
ir::Value* result
= c->stackCall(c->memory(c->memory(c->threadRegister(),
ir::Type::object(),
TARGET_THREAD_DYNAMICTABLE),
ir::Type::object(),
index * TargetBytesPerWord),
tailCall ? Compiler::TailJump : 0,
frame->trace(0, 0),
operandTypeForFieldCode(t, returnCode),
frame->peekMethodArguments(parameterFootprint));
frame->popFootprint(parameterFootprint);
if (rSize) {
frame->pushReturnValue(returnCode, result);
}
}
} break;
@ -9134,10 +9297,12 @@ class MyProcessor : public Processor {
GcTriple** calls,
avian::codegen::DelayedPromise** addresses,
GcMethod* method,
OffsetResolver* resolver)
OffsetResolver* resolver,
JavaVM* hostVM)
{
MyThread* t = static_cast<MyThread*>(vmt);
BootContext bootContext(t, *constants, *calls, *addresses, zone, resolver);
BootContext bootContext(
t, *constants, *calls, *addresses, zone, resolver, hostVM);
compile(t, &codeAllocator, &bootContext, method);

View File

@ -3512,7 +3512,8 @@ class MyProcessor : public Processor {
GcTriple**,
avian::codegen::DelayedPromise**,
GcMethod*,
OffsetResolver*)
OffsetResolver*,
JavaVM*)
{
abort(s);
}

View File

@ -3617,6 +3617,7 @@ extern "C" AVIAN_EXPORT jint JNICALL
const char* bootLibraries = 0;
const char* classpath = 0;
const char* javaHome = AVIAN_JAVA_HOME;
bool reentrant = false;
const char* embedPrefix = AVIAN_EMBED_PREFIX;
const char* bootClasspathPrepend = "";
const char* bootClasspath = 0;
@ -3667,6 +3668,9 @@ extern "C" AVIAN_EXPORT jint JNICALL
} else if (strncmp(p, JAVA_HOME_PROPERTY "=", sizeof(JAVA_HOME_PROPERTY))
== 0) {
javaHome = p + sizeof(JAVA_HOME_PROPERTY);
} else if (strncmp(p, REENTRANT_PROPERTY "=", sizeof(REENTRANT_PROPERTY))
== 0) {
reentrant = strcmp(p + sizeof(REENTRANT_PROPERTY), "true") == 0;
} else if (strncmp(p,
EMBED_PREFIX_PROPERTY "=",
sizeof(EMBED_PREFIX_PROPERTY)) == 0) {
@ -3689,7 +3693,7 @@ extern "C" AVIAN_EXPORT jint JNICALL
++propertyCount;
}
System* s = makeSystem();
System* s = makeSystem(reentrant);
Heap* h = makeHeap(s, heapLimit);
Classpath* c = makeClasspath(s, h, javaHome, embedPrefix);

View File

@ -5889,14 +5889,14 @@ GcMethod* getCaller(Thread* t, unsigned target, bool skipMethodInvoke)
return v.method;
}
object defineClass(Thread* t,
GcClassLoader* loader,
const uint8_t* buffer,
unsigned length)
GcClass* defineClass(Thread* t,
GcClassLoader* loader,
const uint8_t* buffer,
unsigned length)
{
PROTECT(t, loader);
object c = parseClass(t, loader, buffer, length);
GcClass* c = parseClass(t, loader, buffer, length);
// char name[byteArrayLength(t, className(t, c))];
// memcpy(name, &byteArrayBody(t, className(t, c), 0),
@ -5915,7 +5915,7 @@ object defineClass(Thread* t,
PROTECT(t, c);
saveLoadedClass(t, loader, cast<GcClass>(t, c));
saveLoadedClass(t, loader, c);
return c;
}

View File

@ -1018,6 +1018,7 @@ class MySystem : public System {
}
HANDLE mutex;
bool reentrant;
};
} // namespace

File diff suppressed because it is too large Load Diff