Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype CGLIB replacement (Issue 1133) #1298

Closed

Conversation

mcculls
Copy link
Contributor

@mcculls mcculls commented Mar 13, 2020

This PR replaces CGLIB with prototype code to generate "enhancers" and "fast-classes".

I've tried this with a number of existing applications, however since this is new code and the whole area of proxying can be tricky (especially involving bridge methods) there will likely be tweaks and fixes needed :)

available for early testing, but not yet ready for review - more details to be added

This avoids the need for a marker interface, which in turn reduces the need for bridge classloaders.
Since we store bridge classloaders in a weak cache, we're effectively just trading one for another.
a sorted list of strings. It assumes only those strings will
be queried and therefore may produce false-positive results
for strings not in the list.

This trie will be used in the replacement FastClass/Enhancer.
@google google deleted a comment from mcculls May 12, 2020
Copy link
Contributor

@markmarch markmarch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing all my comments.

This LGTM but I'll have sameb@ have a second look to make sure I haven't missed anything. Once Sam has LGTMed this PR, I'll work on submitting it internal (as the repo is not properly set up to auto accept PR).

@markmarch markmarch requested a review from sameb May 13, 2020 02:16
}
} catch (net.sf.cglib.core.CodeGenerationException e) {
} catch (Exception | LinkageError e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have any narrower type we can catch here, to avoid over-catching exceptions & suppressing real errors?

(same elsewhere we catch Exception or Throwable)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is meant to catch any exceptions that occurred while creating the fast constructor (not invocation as that happens inside FastClassProxy) and fall-through to use JDK reflection instead. CGLIB wrapped anything extending Exception inside CodeGenerationException so this just widens it to include linkage errors which can sometimes be thrown during class-loading. We'd want to fall back to JDK reflection in that case too.

We could consider logging the exception, but we'd still want to fall back to JDK reflection when we can't create the fast constructor.

*/
ANONYMOUS,

/** Prefer Unsafe, but fall back to child class loaders if Unsafe is not available (Default) */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we clarify how "Prefer Unsafe" differs from "Unsafe.defineAnonymousClass"? what Unsafe method is it using? (or is it defineAnonymousClass?)

same in OFF's docs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the general Unsafe case we use Unsafe.defineAnonymousClass to get access to the host classloader's defineClass method. Once we have access we then define classes using that classloader. We only use defineAnonymousClass to define all classes when guice_custom_class_loading is set to ANONYMOUS.

I'll expand on this in the javadoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try {
return (T) fastMethod.apply(instance, parameters);
} catch (Throwable e) {
throw new InvocationTargetException(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the the FastClass/Enhancer code prewrap in InvocationTargetExceptions?

otherwise it looks like all callers need to remember to do that, and if they don't then (i'm assuming) checked exceptions from the user code can slip out? unless it's throwing the user-errors inside of some kind of runtime exception (to make it work w/ BiFunction, in which case do we need to unwrap from that?)

(also it looks like IllegalAccessException isn't possible anymore?)

same comment throughout where we do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced classes don't pre-wrap exceptions with InvocationTargetException because they need to match the behaviour of the original type as closely as possible. To to keep class generation simple, fast-classes also don't do any pre-wrapping. In other words the fast-class behaves as if we were calling the original class directly (from an exception PoV).

Instead we apply InvocationTargetException to the few fast-constructor/fast-method places that need it to match JDK reflection behaviour. (This behaviour is checked by some internal tests.) This is also closer to the calling code that unwraps the invocation exception and reports the original cause, rather than have it hidden inside the generated code.

Wrt. checked exceptions from user code: those are propagated from the BiFunction by using a common trick to get the Java compiler to treat them as unchecked (all exceptions are unchecked from the perspective of the JVM.) This means the generated code doesn't need to do any wrapping itself, only the couple of places that need to match JDK reflection behaviour.

Also wrt. IllegalAccessException - that doesn't apply to the 'fast-class' case because we only use 'fast' invocation when we know we have enough access to benefit from it, and the generated code deliberately gives enough access to Guice to use the generated invoker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a bit of javadoc in 1386c48 and removed IllegalAccessException for fast-invocation

return fastClass.invoke(index, target, parameters);
try {
return fastMethod.apply(target, parameters);
} catch (Throwable e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same q about wrapping w/ InvocationTargetException here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

}
};
}
} catch (net.sf.cglib.core.CodeGenerationException e) {
} catch (Exception | LinkageError e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same q about catching narrower exceptions here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above - we want to fall back to JDK reflection if any exception or class-loading linkage error occurs when creating the fast-method (note this doesn't cover the actual invocation.)

try {
// pass this signature's index into the table function to retrieve the invoker
return (BiFunction) invokerTable.invokeExact(signatureTable.applyAsInt(signature));
} catch (Throwable e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should WrongMethodTypeException be handled separately here? javadoc describes that as thrown when "if the target's type is not identical with the caller's symbolic type descriptor", as opposed to every other kind of exception, which is just propagated from the underlying call.

same below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we need to handle WrongMethodTypeException separately here.

}

/**
* Generate trampoline that takes an index, along with a context object and array of argument
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does "and array of argument objects" imply there's a penalty for using the trampoline, in order to allocate the array & stuff the args into it? is this a change from how FastClass or Enhancer worked?

[have we done any benchmarks on the old vs new AOP approaches?]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For fast-invocation we are given an array of objects (which gets passed to either the fast-invoker or JDK reflection.)

Likewise for enhanced types, any intercepted methods need to convert their original arguments into an array of objects so they can be passed through an InvocationHandler and onto the method interceptor as MethodInvocation details. Methods that don't have any interceptors are not enhanced.

So the object array is a necessary detail of any solution (either having it passed in or expecting it to be provided for interception purposes.)

Re: benchmarks, I've run various tests that show performance is better using the new approach. For method interception the improvement per-invocation is small, but there's a much larger benefit when it comes to the up-front cost of interception when creating new injectors.

protected byte[] generateGlue(Collection<Executable> members) {
ClassWriter cw = new ClassWriter(COMPUTE_MAXS);

cw.visit(V1_8, PUBLIC | ACC_SUPER, proxyName, null, hostName, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment on why we're using v1_8 compatible classes (as opposed to anything older or newer)?

also: does this limit our ability to interact with host code that uses newer JDK features?

also: we use the constant in a bunch of places, should we extract to a shared constant so we can increment throughout the codebase more easily? or is that not a meaningful thing to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use V1_8 as a minimum because that's the base version that Guice currently supports. The bytecode generated for the fast-class and enhancer glue is simple and doesn't need anything more than Java8. This is because we're not copying the actual method code, only generating invokers that match the expected signature.

I'd prefer not to use a shared constant here because it makes it harder to see what level we're targeting. Also in the future we may decide we want to change the version for one of the cases, but not the others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkurz
Copy link
Contributor

mkurz commented Jun 26, 2020

@mcculls Scopes.java conflicts.

kevinb9n pushed a commit that referenced this pull request Jul 9, 2020
Replace CGLIB with custom code to generate "enhancers" and "fast-classes".

Some user visible changes from using cglib:
- intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown.
- Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy
- Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable
- Generated class name is slightly longer.

Closes #1298

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=320433559
@kevinb9n kevinb9n mentioned this pull request Jul 9, 2020
kevinb9n pushed a commit that referenced this pull request Jul 10, 2020
Replace CGLIB with custom code to generate "enhancers" and "fast-classes".

Some user visible changes from using cglib:
- intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown.
- Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy
- Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable
- Generated class name is slightly longer.

Closes #1298

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=320433559
ShoOgino pushed a commit to ShoOgino/guiceFile that referenced this pull request Oct 14, 2020
Replace CGLIB with custom code to generate "enhancers" and "fast-classes".

Some user visible changes from using cglib:
- intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown.
- Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy
- Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable
- Generated class name is slightly longer.

Closes #1298

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=320433559
ShoOgino pushed a commit to ShoOgino/guiceMethod that referenced this pull request Oct 14, 2020
Replace CGLIB with custom code to generate "enhancers" and "fast-classes".

Some user visible changes from using cglib:
- intercepted method that has a return type of int but returns null from the interceptor will no longer be automatically converted to 0, instead a NullPointerException will be thrown.
- Scope implementation can no longer check for circular proxy instance using CircularDependencyProxy marker class, instead should use Scopes.isCircularProxy
- Depending on which custom class loading option is used, Guice enhanced class may no longer be mockable/spyable
- Generated class name is slightly longer.

Closes #1298

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=320433559
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants