package org.jruby.ir.targets;
import com.headius.invokebinder.Binder;
import com.headius.invokebinder.Signature;
import com.headius.invokebinder.SmartBinder;
import com.headius.invokebinder.SmartHandle;
import org.jruby.Ruby;
import org.jruby.RubyBasicObject;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyFloat;
import org.jruby.RubyModule;
import org.jruby.RubyNil;
import org.jruby.RubyStruct;
import org.jruby.RubySymbol;
import org.jruby.internal.runtime.methods.AliasMethod;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.internal.runtime.methods.PartialDelegatingMethod;
import org.jruby.ir.JIT;
import org.jruby.java.invokers.InstanceFieldGetter;
import org.jruby.java.invokers.InstanceFieldSetter;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallType;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.callsite.CacheEntry;
import org.jruby.runtime.invokedynamic.JRubyCallSite;
import org.jruby.util.cli.Options;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.MutableCallSite;
import java.lang.invoke.SwitchPoint;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import static java.lang.invoke.MethodHandles.constant;
import static java.lang.invoke.MethodHandles.insertArguments;
import static java.lang.invoke.MethodHandles.lookup;
public abstract class InvokeSite extends MutableCallSite {
private static final Logger LOG = LoggerFactory.getLogger(InvokeSite.class);
static {
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.setDebugEnable(true);
}
private static final boolean LOG_BINDING = LOG.isDebugEnabled();
private static final AtomicLong SITE_ID = new AtomicLong(1);
final Signature signature;
final Signature fullSignature;
final int arity;
protected final String methodName;
final MethodHandle fallback;
private final SiteTracker tracker = new SiteTracker();
private final long siteID = SITE_ID.getAndIncrement();
private final int argOffset;
protected final String file;
protected final int line;
private boolean boundOnce;
private boolean literalClosure;
CacheEntry cache = CacheEntry.NULL_CACHE;
public String name() {
return methodName;
}
public final CallType callType;
public InvokeSite(MethodType type, String name, CallType callType, String file, int line) {
this(type, name, callType, false, file, line);
}
public InvokeSite(MethodType type, String name, CallType callType, boolean literalClosure, String file, int line) {
super(type);
this.methodName = name;
this.callType = callType;
this.literalClosure = literalClosure;
this.file = file;
this.line = line;
Signature startSig;
if (callType == CallType.SUPER) {
startSig = JRubyCallSite.STANDARD_SUPER_SIG;
argOffset = 4;
} else {
startSig = JRubyCallSite.STANDARD_SITE_SIG;
argOffset = 3;
}
int arity;
if (type.parameterType(type.parameterCount() - 1) == Block.class) {
arity = type.parameterCount() - (argOffset + 1);
if (arity == 1 && type.parameterType(argOffset) == IRubyObject[].class) {
arity = -1;
startSig = startSig.appendArg("args", IRubyObject[].class);
} else {
for (int i = 0; i < arity; i++) {
startSig = startSig.appendArg("arg" + i, IRubyObject.class);
}
}
startSig = startSig.appendArg("block", Block.class);
fullSignature = signature = startSig;
} else {
arity = type.parameterCount() - argOffset;
if (arity == 1 && type.parameterType(argOffset) == IRubyObject[].class) {
arity = -1;
startSig = startSig.appendArg("args", IRubyObject[].class);
} else {
for (int i = 0; i < arity; i++) {
startSig = startSig.appendArg("arg" + i, IRubyObject.class);
}
}
signature = startSig;
fullSignature = startSig.appendArg("block", Block.class);
}
this.arity = arity;
this.fallback = prepareBinder(true).invokeVirtualQuiet(Bootstrap.LOOKUP, "invoke");
}
public static CallSite bootstrap(InvokeSite site, MethodHandles.Lookup lookup) {
site.setInitialTarget(site.fallback);
return site;
}
public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject[] args, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
SwitchPoint switchPoint = (SwitchPoint) selfClass.getInvalidator().getData();
CacheEntry entry = selfClass.searchWithCache(methodName);
DynamicMethod method = entry.method;
if (methodMissing(entry, caller)) {
if (testThresholds(selfClass) == CacheAction.FAIL) {
logFail();
bindToFail();
} else {
logMethodMissing();
}
return callMethodMissing(entry, callType, context, self, selfClass, methodName, args, block);
}
MethodHandle mh = getHandle(self, entry);
if (literalClosure) {
mh = Binder.from(mh.type())
.tryFinally(getBlockEscape(signature))
.invoke(mh);
}
updateInvocationTarget(mh, self, selfClass, entry.method, switchPoint);
if (literalClosure) {
try {
return method.call(context, self, entry.sourceModule, methodName, args, block);
} finally {
block.escape();
}
}
return method.call(context, self, entry.sourceModule, methodName, args, block);
}
private static final MethodHandles.Lookup LOOKUP = lookup();
private static final MethodHandle ESCAPE_BLOCK = Binder.from(void.class, Block.class).invokeVirtualQuiet(LOOKUP, "escape");
private static final Map<Signature, MethodHandle> BLOCK_ESCAPES = Collections.synchronizedMap(new HashMap<Signature, MethodHandle>());
private static MethodHandle getBlockEscape(Signature signature) {
Signature voidSignature = signature.changeReturn(void.class);
MethodHandle escape = BLOCK_ESCAPES.get(voidSignature);
if (escape == null) {
escape = SmartBinder.from(voidSignature)
.permute("block")
.invoke(ESCAPE_BLOCK)
.handle();
BLOCK_ESCAPES.put(voidSignature, escape);
}
return escape;
}
public IRubyObject fail(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject[] args, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
String name = methodName;
CacheEntry entry = cache;
if (entry.typeOk(selfClass)) {
return entry.method.call(context, self, entry.sourceModule, name, args, block);
}
entry = selfClass.searchWithCache(name);
if (methodMissing(entry, caller)) {
return callMethodMissing(entry, callType, context, self, selfClass, name, args, block);
}
cache = entry;
return entry.method.call(context, self, entry.sourceModule, name, args, block);
}
public IRubyObject fail(ThreadContext context, IRubyObject caller, IRubyObject self, Block block) throws Throwable {
return fail(context, caller, self, IRubyObject.NULL_ARRAY, block);
}
public IRubyObject fail(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
String name = methodName;
CacheEntry entry = cache;
if (entry.typeOk(selfClass)) {
return entry.method.call(context, self, entry.sourceModule, name, arg0, block);
}
entry = selfClass.searchWithCache(name);
if (methodMissing(entry, caller)) {
return callMethodMissing(entry, callType, context, self, selfClass, name, arg0, block);
}
cache = entry;
return entry.method.call(context, self, entry.sourceModule, name, arg0, block);
}
public IRubyObject fail(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
String name = methodName;
CacheEntry entry = cache;
if (entry.typeOk(selfClass)) {
return entry.method.call(context, self, entry.sourceModule, name, arg0, arg1, block);
}
entry = selfClass.searchWithCache(name);
if (methodMissing(entry, caller)) {
return callMethodMissing(entry, callType, context, self, selfClass, name, arg0, arg1, block);
}
cache = entry;
return entry.method.call(context, self, entry.sourceModule, name, arg0, arg1, block);
}
public IRubyObject fail(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
String name = methodName;
CacheEntry entry = cache;
if (entry.typeOk(selfClass)) {
return entry.method.call(context, self, entry.sourceModule, name, arg0, arg1, arg2, block);
}
entry = selfClass.searchWithCache(name);
if (methodMissing(entry, caller)) {
return callMethodMissing(entry, callType, context, self, selfClass, name, arg0, arg1, arg2, block);
}
cache = entry;
return entry.method.call(context, self, entry.sourceModule, name, arg0, arg1, arg2, block);
}
public Binder prepareBinder(boolean varargs) {
SmartBinder binder = SmartBinder.from(signature);
if (varargs || arity > 3) {
if (arity == -1) {
} else if (arity == 0) {
binder = binder.insert(argOffset, "args", IRubyObject.NULL_ARRAY);
} else {
binder = binder
.collect("args", "arg[0-9]+");
}
}
if (signature.lastArgType() != Block.class) {
binder = binder.append("block", Block.NULL_BLOCK);
}
binder = binder.insert(0, "site", this);
return binder.binder();
}
MethodHandle getHandle(IRubyObject self, CacheEntry entry) throws Throwable {
boolean blockGiven = signature.lastArgType() == Block.class;
MethodHandle mh = buildNewInstanceHandle(entry, self);
if (mh == null) mh = buildNotEqualHandle(entry, self);
if (mh == null) mh = Bootstrap.buildNativeHandle(this, entry, blockGiven);
if (mh == null) mh = buildJavaFieldHandle(this, entry, self);
if (mh == null) mh = Bootstrap.buildIndyHandle(this, entry);
if (mh == null) mh = Bootstrap.buildJittedHandle(this, entry, blockGiven);
if (mh == null) mh = Bootstrap.buildAttrHandle(this, entry, self);
if (mh == null) mh = buildAliasHandle(entry, self);
if (mh == null) mh = buildStructHandle(entry);
if (mh == null) mh = Bootstrap.buildGenericHandle(this, entry);
assert mh != null : "we should have a method handle of some sort by now";
return mh;
}
MethodHandle buildJavaFieldHandle(InvokeSite site, CacheEntry entry, IRubyObject self) throws Throwable {
DynamicMethod method = entry.method;
if (method instanceof InstanceFieldGetter) {
if (site.arity != 0 || site.signature.lastArgType() == Block.class) return null;
Field field = ((InstanceFieldGetter) method).getField();
if (!IRubyObject.class.isAssignableFrom(field.getType())) return null;
MethodHandle fieldHandle = (MethodHandle) method.getHandle();
if (fieldHandle != null) {
return fieldHandle;
}
fieldHandle = LOOKUP.unreflectGetter(field);
MethodHandle filter = self.getRuntime().getNullToNilHandle();
MethodHandle receiverConverter = Binder
.from(field.getDeclaringClass(), IRubyObject.class)
.cast(Object.class, IRubyObject.class)
.invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy");
fieldHandle = Binder
.from(site.type())
.permute(2)
.filter(0, receiverConverter)
.filterReturn(filter)
.cast(fieldHandle.type())
.invoke(fieldHandle);
method.setHandle(fieldHandle);
return fieldHandle;
} else if (method instanceof InstanceFieldSetter) {
if (site.arity != 1 || site.signature.lastArgType() == Block.class) return null;
Field field = ((InstanceFieldSetter) method).getField();
if (!IRubyObject.class.isAssignableFrom(field.getType())) return null;
MethodHandle fieldHandle = (MethodHandle) method.getHandle();
if (fieldHandle != null) {
return fieldHandle;
}
fieldHandle = LOOKUP.unreflectSetter(field);
MethodHandle receiverConverter = Binder
.from(field.getDeclaringClass(), IRubyObject.class)
.cast(Object.class, IRubyObject.class)
.invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy");
fieldHandle = Binder
.from(site.type())
.permute(2, 3)
.filter(0, receiverConverter)
.filterReturn(constant(IRubyObject.class, self.getRuntime().getNil()))
.cast(fieldHandle.type())
.invoke(fieldHandle);
method.setHandle(fieldHandle);
return fieldHandle;
}
return null;
}
MethodHandle buildNewInstanceHandle(CacheEntry entry, IRubyObject self) {
MethodHandle mh = null;
DynamicMethod method = entry.method;
if (method == self.getRuntime().getBaseNewMethod()) {
RubyClass recvClass = (RubyClass) self;
CallSite initSite = SelfInvokeSite.bootstrap(LOOKUP, "callFunctional:initialize", type(), literalClosure ? 1 : 0, file, line);
MethodHandle initHandle = initSite.dynamicInvoker();
MethodHandle allocFilter = Binder.from(IRubyObject.class, IRubyObject.class)
.cast(IRubyObject.class, RubyClass.class)
.insert(0, new Class[] {ObjectAllocator.class, Ruby.class}, recvClass.getAllocator(), self.getRuntime())
.invokeVirtualQuiet(LOOKUP, "allocate");
mh = SmartBinder.from(LOOKUP, signature)
.filter("self", allocFilter)
.fold("dummy", initHandle)
.permute("self")
.identity()
.handle();
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tbound as new instance creation " + Bootstrap.logMethod(method));
}
}
return mh;
}
MethodHandle buildNotEqualHandle(CacheEntry entry, IRubyObject self) {
MethodHandle mh = null;
DynamicMethod method = entry.method;
Ruby runtime = self.getRuntime();
if (method.isBuiltin()) {
CallSite equalSite = null;
if (method.getImplementationClass() == runtime.getBasicObject() && name().equals("!=")) {
equalSite = SelfInvokeSite.bootstrap(LOOKUP, "callFunctional:==", type(), literalClosure ? 1 : 0, file, line);
} else if (method.getImplementationClass() == runtime.getKernel() && name().equals("!~")) {
equalSite = SelfInvokeSite.bootstrap(LOOKUP, "callFunctional:=~", type(), literalClosure ? 1 : 0, file, line);
}
if (equalSite != null) {
MethodHandle equalHandle = equalSite.dynamicInvoker();
MethodHandle filter = insertArguments(NEGATE, 1, runtime.getNil(), runtime.getTrue(), runtime.getFalse());
mh = MethodHandles.filterReturnValue(equalHandle, filter);
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tbound as specialized " + name() + ":" + Bootstrap.logMethod(method));
}
}
}
return mh;
}
public static final MethodHandle NEGATE = Binder.from(IRubyObject.class, IRubyObject.class, RubyNil.class, RubyBoolean.True.class, RubyBoolean.False.class).invokeStaticQuiet(LOOKUP, InvokeSite.class, "negate");
public static IRubyObject negate(IRubyObject object, RubyNil nil, RubyBoolean.True tru, RubyBoolean.False fals) {
return object == nil || object == fals ? tru : fals;
}
MethodHandle buildAliasHandle(CacheEntry entry, IRubyObject self) throws Throwable {
MethodHandle mh = null;
DynamicMethod method = entry.method;
if (method instanceof PartialDelegatingMethod) {
mh = getHandle(self, new CacheEntry(((PartialDelegatingMethod) method).getDelegate(), entry.sourceModule, entry.token));
} else if (method instanceof AliasMethod) {
AliasMethod alias = (AliasMethod) method;
DynamicMethod innerMethod = alias.getRealMethod();
String name = alias.getName();
InvokeSite innerSite = (InvokeSite) SelfInvokeSite.bootstrap(LOOKUP, "callFunctional:" + name, type(), literalClosure ? 1 : 0, file, line);
mh = innerSite.getHandle(self, new CacheEntry(innerMethod, entry.sourceModule, entry.token));
alias.setHandle(mh);
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tbound directly through alias to " + Bootstrap.logMethod(method));
}
}
return mh;
}
MethodHandle buildStructHandle(CacheEntry entry) throws Throwable {
MethodHandle mh = null;
DynamicMethod method = entry.method;
if (method instanceof RubyStruct.Accessor) {
if (arity == 0) {
RubyStruct.Accessor accessor = (RubyStruct.Accessor) method;
int index = accessor.getIndex();
mh = Binder.from(type())
.cast(type().changeParameterType(2, RubyStruct.class))
.permute(2)
.append(index)
.invokeVirtual(LOOKUP, "get");
method.setHandle(mh);
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tbound directly as Struct accessor " + Bootstrap.logMethod(method));
}
} else {
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tcalled struct accessor with arity > 0 " + Bootstrap.logMethod(method));
}
}
} else if (method instanceof RubyStruct.Mutator) {
if (arity == 1) {
RubyStruct.Mutator mutator = (RubyStruct.Mutator) method;
int index = mutator.getIndex();
mh = Binder.from(type())
.cast(type().changeParameterType(2, RubyStruct.class))
.permute(2, 3)
.append(index)
.invokeVirtual(LOOKUP, "set");
method.setHandle(mh);
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tbound directly as Struct mutator " + Bootstrap.logMethod(method));
}
} else {
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
LOG.info(name() + "\tcalled struct mutator with arity > 1 " + Bootstrap.logMethod(method));
}
}
}
return mh;
}
MethodHandle updateInvocationTarget(MethodHandle target, IRubyObject self, RubyModule testClass, DynamicMethod method, SwitchPoint switchPoint) {
MethodHandle fallback;
MethodHandle gwt;
CacheAction cacheAction = testThresholds(testClass);
switch (cacheAction) {
case FAIL:
logFail();
return bindToFail();
case PIC:
logPic(method);
fallback = getTarget();
break;
case REBIND:
case BIND:
logBind(cacheAction);
fallback = this.fallback;
break;
default:
throw new RuntimeException("invalid cache action: " + cacheAction);
}
tracker.addType(testClass.id);
SmartHandle test;
if (self instanceof RubySymbol ||
self instanceof RubyFixnum ||
self instanceof RubyFloat ||
self instanceof RubyNil ||
self instanceof RubyBoolean.True ||
self instanceof RubyBoolean.False) {
test = SmartBinder
.from(signature.asFold(boolean.class))
.permute("self")
.insert(1, "selfJavaType", self.getClass())
.cast(boolean.class, Object.class, Class.class)
.invoke(TEST_CLASS);
} else {
test = SmartBinder
.from(signature.changeReturn(boolean.class))
.permute("self")
.insert(0, "selfClass", RubyClass.class, testClass)
.invokeStaticQuiet(Bootstrap.LOOKUP, Bootstrap.class, "testType");
}
gwt = MethodHandles.guardWithTest(test.handle(), target, fallback);
gwt = switchPoint.guardWithTest(gwt, fallback);
setTarget(gwt);
return target;
}
private void logMethodMissing() {
if (LOG_BINDING) {
LOG.debug(methodName + "\ttriggered site #" + siteID + " method_missing (" + file + ":" + line + ")");
}
}
private void logBind(CacheAction action) {
if (LOG_BINDING) {
LOG.debug(methodName + "\ttriggered site #" + siteID + " " + action + " (" + file + ":" + line + ")");
}
}
private void logPic(DynamicMethod method) {
if (LOG_BINDING) {
LOG.debug(methodName + "\tadded to PIC " + logMethod(method));
}
}
private void logFail() {
if (LOG_BINDING) {
if (tracker.clearCount() > Options.INVOKEDYNAMIC_MAXFAIL.load()) {
LOG.info(methodName + "\tat site #" + siteID + " failed more than " + Options.INVOKEDYNAMIC_MAXFAIL.load() + " times; bailing out (" + file + ":" + line + ")");
} else if (tracker.seenTypesCount() + 1 > Options.INVOKEDYNAMIC_MAXPOLY.load()) {
LOG.info(methodName + "\tat site #" + siteID + " encountered more than " + Options.INVOKEDYNAMIC_MAXPOLY.load() + " types; bailing out (" + file + ":" + line + ")");
}
}
}
private MethodHandle bindToFail() {
MethodHandle target;
setTarget(target = prepareBinder(false).invokeVirtualQuiet(LOOKUP, "fail"));
return target;
}
enum CacheAction { FAIL, BIND, REBIND, PIC }
CacheAction testThresholds(RubyModule testClass) {
if (tracker.clearCount() > Options.INVOKEDYNAMIC_MAXFAIL.load() ||
(!tracker.hasSeenType(testClass.id)
&& tracker.seenTypesCount() + 1 > Options.INVOKEDYNAMIC_MAXPOLY.load())) {
return CacheAction.FAIL;
} else {
if (tracker.seenTypesCount() > 0 && getTarget() != null && !tracker.hasSeenType(testClass.id)) {
tracker.addType(testClass.id);
return CacheAction.PIC;
} else {
tracker.clearTypes();
tracker.addType(testClass.id);
return boundOnce ? CacheAction.REBIND : CacheAction.BIND;
}
}
}
public static RubyClass pollAndGetClass(ThreadContext context, IRubyObject self) {
context.callThreadPoll();
return RubyBasicObject.getMetaClass(self);
}
@Override
public void setTarget(MethodHandle target) {
super.setTarget(target);
boundOnce = true;
}
public void setInitialTarget(MethodHandle target) {
super.setTarget(target);
}
public abstract boolean methodMissing(CacheEntry entry, IRubyObject caller);
public static IRubyObject callMethodMissing(CacheEntry entry, CallType callType, ThreadContext context, IRubyObject self,
RubyClass selfClass, String name, IRubyObject[] args, Block block) {
return Helpers.callMethodMissing(context, self, selfClass, entry.method.getVisibility(), name, callType, args, block);
}
public static IRubyObject callMethodMissing(CacheEntry entry, CallType callType, ThreadContext context, IRubyObject self,
RubyClass selfClass, String name, IRubyObject arg0, Block block) {
return Helpers.callMethodMissing(context, self, selfClass, entry.method.getVisibility(), name, callType, arg0, block);
}
public static IRubyObject callMethodMissing(CacheEntry entry, CallType callType, ThreadContext context, IRubyObject self,
RubyClass selfClass, String name, IRubyObject arg0, IRubyObject arg1, Block block) {
return Helpers.callMethodMissing(context, self, selfClass, entry.method.getVisibility(), name, callType, arg0, arg1, block);
}
public static IRubyObject callMethodMissing(CacheEntry entry, CallType callType, ThreadContext context, IRubyObject self,
RubyClass selfClass, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) {
return Helpers.callMethodMissing(context, self, selfClass, entry.method.getVisibility(), name, callType, arg0, arg1, arg2, block);
}
private static String logMethod(DynamicMethod method) {
return "[#" + method.getSerialNumber() + ' ' + method.getImplementationClass() + ']';
}
@JIT
public static boolean testClass(Object object, Class clazz) {
return object.getClass() == clazz;
}
public String toString() {
return getClass().getName() + "[name=" + name() + ",arity=" + arity + ",type=" + type() + ",file=" + file + ",line=" + line + ']';
}
private static final MethodHandle TEST_CLASS = Binder
.from(boolean.class, Object.class, Class.class)
.invokeStaticQuiet(LOOKUP, InvokeSite.class, "testClass");
}