public class Bootstrap { private static final Logger LOG = LoggerFactory.getLogger("Bootstrap"); static final Lookup LOOKUP = MethodHandles.lookup(); public static CallSite string( Lookup lookup, String name, MethodType type, String value, String encodingName) { Encoding encoding; EncodingDB.Entry entry = EncodingDB.getEncodings().get(encodingName.getBytes()); if (entry == null) entry = EncodingDB.getAliases().get(encodingName.getBytes()); if (entry == null) throw new RuntimeException("could not find encoding: " + encodingName); encoding = entry.getEncoding(); ByteList byteList = new ByteList(value.getBytes(RubyEncoding.ISO), encoding); MutableCallSite site = new MutableCallSite(type); Binder binder = Binder.from(RubyString.class, ThreadContext.class) .insert(0, arrayOf(MutableCallSite.class, ByteList.class), site, byteList); if (name.equals("frozen")) { site.setTarget(binder.invokeStaticQuiet(lookup, Bootstrap.class, "frozenString")); } else { site.setTarget(binder.invokeStaticQuiet(lookup, Bootstrap.class, "string")); } return site; } public static CallSite bytelist( Lookup lookup, String name, MethodType type, String value, String encodingName) { Encoding encoding; EncodingDB.Entry entry = EncodingDB.getEncodings().get(encodingName.getBytes()); if (entry == null) entry = EncodingDB.getAliases().get(encodingName.getBytes()); if (entry == null) throw new RuntimeException("could not find encoding: " + encodingName); encoding = entry.getEncoding(); ByteList byteList = new ByteList(value.getBytes(RubyEncoding.ISO), encoding); return new ConstantCallSite(constant(ByteList.class, byteList)); } public static CallSite array(Lookup lookup, String name, MethodType type) { MethodHandle handle = Binder.from(type) .collect(1, IRubyObject[].class) .invokeStaticQuiet(LOOKUP, Bootstrap.class, "array"); CallSite site = new ConstantCallSite(handle); return site; } public static CallSite hash(Lookup lookup, String name, MethodType type) { MethodHandle handle = Binder.from(lookup, type) .collect(1, IRubyObject[].class) .invokeStaticQuiet(LOOKUP, Bootstrap.class, "hash"); CallSite site = new ConstantCallSite(handle); return site; } public static CallSite kwargsHash(Lookup lookup, String name, MethodType type) { MethodHandle handle = Binder.from(lookup, type) .collect(2, IRubyObject[].class) .invokeStaticQuiet(LOOKUP, Bootstrap.class, "kwargsHash"); CallSite site = new ConstantCallSite(handle); return site; } public static CallSite ivar(Lookup lookup, String name, MethodType type) throws Throwable { String[] names = name.split(":"); String operation = names[0]; String varName = names[1]; VariableSite site = new VariableSite(type, varName, "noname", 0); MethodHandle handle; handle = lookup.findStatic( Bootstrap.class, operation, type.insertParameterTypes(0, VariableSite.class)); handle = handle.bindTo(site); site.setTarget(handle.asType(site.type())); return site; } public static CallSite searchConst( Lookup lookup, String name, MethodType type, int noPrivateConsts) { MutableCallSite site = new MutableCallSite(type); String[] bits = name.split(":"); String constName = bits[1]; MethodHandle handle = Binder.from(lookup, type) .append(site, constName.intern()) .append(noPrivateConsts == 0 ? false : true) .invokeStaticQuiet(LOOKUP, Bootstrap.class, bits[0]); site.setTarget(handle); return site; } public static Handle string() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "string", sig( CallSite.class, Lookup.class, String.class, MethodType.class, String.class, String.class)); } public static Handle bytelist() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "bytelist", sig( CallSite.class, Lookup.class, String.class, MethodType.class, String.class, String.class)); } public static Handle array() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "array", sig(CallSite.class, Lookup.class, String.class, MethodType.class)); } public static Handle hash() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "hash", sig(CallSite.class, Lookup.class, String.class, MethodType.class)); } public static Handle kwargsHash() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "kwargsHash", sig(CallSite.class, Lookup.class, String.class, MethodType.class)); } public static Handle invokeSuper() { return SuperInvokeSite.BOOTSTRAP; } public static Handle ivar() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "ivar", sig(CallSite.class, Lookup.class, String.class, MethodType.class)); } public static Handle searchConst() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "searchConst", sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class)); } public static RubyString string(MutableCallSite site, ByteList value, ThreadContext context) throws Throwable { MethodHandle handle = SmartBinder.from(STRING_SIGNATURE) .invoke(NEW_STRING_SHARED_HANDLE.apply("byteList", value)) .handle(); site.setTarget(handle); return RubyString.newStringShared(context.runtime, value); } public static RubyString frozenString(MutableCallSite site, ByteList value, ThreadContext context) throws Throwable { RubyString frozen = context.runtime.freezeAndDedupString(RubyString.newStringShared(context.runtime, value)); MethodHandle handle = Binder.from(RubyString.class, ThreadContext.class).dropAll().constant(frozen); site.setTarget(handle); return frozen; } private static final Signature STRING_SIGNATURE = Signature.from(RubyString.class, arrayOf(ThreadContext.class), "context"); private static final Signature NEW_STRING_SHARED_SIGNATURE = Signature.from( RubyString.class, arrayOf(ThreadContext.class, ByteList.class), "context", "byteList"); private static final SmartHandle NEW_STRING_SHARED_HANDLE = SmartBinder.from(NEW_STRING_SHARED_SIGNATURE) .invokeStaticQuiet(MethodHandles.lookup(), Bootstrap.class, "newStringShared"); @JIT private static RubyString newStringShared(ThreadContext context, ByteList byteList) { return RubyString.newStringShared(context.runtime, byteList); } public static IRubyObject array(ThreadContext context, IRubyObject[] elts) { return RubyArray.newArrayNoCopy(context.runtime, elts); } public static Handle contextValue() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "contextValue", sig(CallSite.class, Lookup.class, String.class, MethodType.class)); } public static Handle contextValueString() { return new Handle( Opcodes.H_INVOKESTATIC, p(Bootstrap.class), "contextValueString", sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class)); } public static CallSite contextValue(Lookup lookup, String name, MethodType type) { MutableCallSite site = new MutableCallSite(type); site.setTarget(Binder.from(type).append(site).invokeStaticQuiet(lookup, Bootstrap.class, name)); return site; } public static CallSite contextValueString( Lookup lookup, String name, MethodType type, String str) { MutableCallSite site = new MutableCallSite(type); site.setTarget( Binder.from(type).append(site, str).invokeStaticQuiet(lookup, Bootstrap.class, name)); return site; } public static IRubyObject nil(ThreadContext context, MutableCallSite site) { MethodHandle constant = (MethodHandle) ((RubyNil) context.nil).constant(); if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(IRubyObject.class, context.nil); site.setTarget(constant); return context.nil; } public static IRubyObject True(ThreadContext context, MutableCallSite site) { MethodHandle constant = (MethodHandle) context.runtime.getTrue().constant(); if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(IRubyObject.class, context.runtime.getTrue()); site.setTarget(constant); return context.runtime.getTrue(); } public static IRubyObject False(ThreadContext context, MutableCallSite site) { MethodHandle constant = (MethodHandle) context.runtime.getFalse().constant(); if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(IRubyObject.class, context.runtime.getFalse()); site.setTarget(constant); return context.runtime.getFalse(); } public static Ruby runtime(ThreadContext context, MutableCallSite site) { MethodHandle constant = (MethodHandle) context.runtime.constant(); if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(Ruby.class, context.runtime); site.setTarget(constant); return context.runtime; } public static RubyEncoding encoding(ThreadContext context, MutableCallSite site, String name) { RubyEncoding rubyEncoding = IRRuntimeHelpers.retrieveEncoding(context, name); MethodHandle constant = (MethodHandle) rubyEncoding.constant(); if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(RubyEncoding.class, rubyEncoding); site.setTarget(constant); return rubyEncoding; } public static IRubyObject hash(ThreadContext context, IRubyObject[] pairs) { Ruby runtime = context.runtime; RubyHash hash = RubyHash.newHash(runtime); for (int i = 0; i < pairs.length; ) { hash.fastASetCheckString(runtime, pairs[i++], pairs[i++]); } return hash; } public static IRubyObject kwargsHash(ThreadContext context, RubyHash hash, IRubyObject[] pairs) { return IRRuntimeHelpers.dupKwargsHashAndPopulateFromArray(context, hash, pairs); } static MethodHandle buildGenericHandle( InvokeSite site, DynamicMethod method, RubyClass dispatchClass) { SmartBinder binder; binder = SmartBinder.from(site.signature) .permute("context", "self", "arg.*", "block") .insert( 2, new String[] {"rubyClass", "name"}, new Class[] {RubyModule.class, String.class}, dispatchClass, site.name()) .insert(0, "method", DynamicMethod.class, method); if (site.arity > 3) { binder = binder.collect("args", "arg.*"); } return binder.invokeVirtualQuiet(LOOKUP, "call").handle(); } static MethodHandle buildJittedHandle(InvokeSite site, DynamicMethod method, boolean blockGiven) { MethodHandle mh = null; SmartBinder binder; CompiledIRMethod compiledIRMethod = null; if (method instanceof CompiledIRMethod) { compiledIRMethod = (CompiledIRMethod) method; } else if (method instanceof InterpretedIRMethod) { DynamicMethod actualMethod = ((InterpretedIRMethod) method).getActualMethod(); if (actualMethod instanceof CompiledIRMethod) { compiledIRMethod = (CompiledIRMethod) actualMethod; } } if (compiledIRMethod != null) { // attempt IR direct binding // TODO: this will have to expand when we start specializing arities binder = SmartBinder.from(site.signature).permute("context", "self", "arg.*", "block"); if (site.arity == -1) { // already [], nothing to do mh = (MethodHandle) compiledIRMethod.getHandle(); } else if (site.arity == 0) { MethodHandle specific; if ((specific = compiledIRMethod.getHandleFor(site.arity)) != null) { mh = specific; } else { mh = (MethodHandle) compiledIRMethod.getHandle(); binder = binder.insert(2, "args", IRubyObject.NULL_ARRAY); } } else { MethodHandle specific; if ((specific = compiledIRMethod.getHandleFor(site.arity)) != null) { mh = specific; } else { mh = (MethodHandle) compiledIRMethod.getHandle(); binder = binder.collect("args", "arg.*"); } } if (!blockGiven) { binder = binder.append("block", Block.class, Block.NULL_BLOCK); } binder = binder .insert(1, "scope", StaticScope.class, compiledIRMethod.getStaticScope()) .append("class", RubyModule.class, compiledIRMethod.getImplementationClass()); mh = binder.invoke(mh).handle(); } return mh; } static MethodHandle buildNativeHandle(InvokeSite site, DynamicMethod method, boolean blockGiven) { MethodHandle mh = null; SmartBinder binder = null; if (method.getNativeCall() != null) { int nativeArgCount = getNativeArgCount(method, method.getNativeCall()); DynamicMethod.NativeCall nc = method.getNativeCall(); if (nc.isJava()) { // not supported yet, use DynamicMethod.call } else { if (nativeArgCount >= 0) { // native methods only support arity 3 if (nativeArgCount == site.arity) { // nothing to do binder = SmartBinder.from(lookup(), site.signature); } else { // arity mismatch...leave null and use DynamicMethod.call below } } else { // varargs if (site.arity == -1) { // ok, already passing [] binder = SmartBinder.from(lookup(), site.signature); } else if (site.arity == 0) { // no args, insert dummy binder = SmartBinder.from(lookup(), site.signature) .insert(2, "args", IRubyObject.NULL_ARRAY); } else { // 1 or more args, collect into [] binder = SmartBinder.from(lookup(), site.signature).collect("args", "arg.*"); } } if (binder != null) { // clean up non-arguments, ordering, types if (!nc.hasContext()) { binder = binder.drop("context"); } if (nc.hasBlock() && !blockGiven) { binder = binder.append("block", Block.NULL_BLOCK); } else if (!nc.hasBlock() && blockGiven) { binder = binder.drop("block"); } if (nc.isStatic()) { mh = binder .permute("context", "self", "arg.*", "block") // filter caller .cast(nc.getNativeReturn(), nc.getNativeSignature()) .invokeStaticQuiet(LOOKUP, nc.getNativeTarget(), nc.getNativeName()) .handle(); } else { mh = binder .permute("self", "context", "arg.*", "block") // filter caller, move self .castArg("self", nc.getNativeTarget()) .castVirtual( nc.getNativeReturn(), nc.getNativeTarget(), nc.getNativeSignature()) .invokeVirtualQuiet(LOOKUP, nc.getNativeName()) .handle(); } } } } return mh; } public static int getNativeArgCount(DynamicMethod method, DynamicMethod.NativeCall nativeCall) { // if non-Java, must: // * exactly match arities or both are [] boxed // * 3 or fewer arguments return getArgCount(nativeCall.getNativeSignature(), nativeCall.isStatic()); } private static int getArgCount(Class[] args, boolean isStatic) { int length = args.length; boolean hasContext = false; if (isStatic) { if (args.length > 1 && args[0] == ThreadContext.class) { length--; hasContext = true; } // remove self object assert args.length >= 1; length--; if (args.length > 1 && args[args.length - 1] == Block.class) { length--; } if (length == 1) { if (hasContext && args[2] == IRubyObject[].class) { length = -1; } else if (args[1] == IRubyObject[].class) { length = -1; } } } else { if (args.length > 0 && args[0] == ThreadContext.class) { length--; hasContext = true; } if (args.length > 0 && args[args.length - 1] == Block.class) { length--; } if (length == 1) { if (hasContext && args[1] == IRubyObject[].class) { length = -1; } else if (args[0] == IRubyObject[].class) { length = -1; } } } return length; } public static IRubyObject ivarGet(VariableSite site, IRubyObject self) throws Throwable { RubyClass realClass = self.getMetaClass().getRealClass(); VariableAccessor accessor = realClass.getVariableAccessorForRead(site.name()); // produce nil if the variable has not been initialize MethodHandle nullToNil = findStatic( Helpers.class, "nullToNil", methodType(IRubyObject.class, IRubyObject.class, IRubyObject.class)); nullToNil = insertArguments(nullToNil, 1, self.getRuntime().getNil()); nullToNil = explicitCastArguments(nullToNil, methodType(IRubyObject.class, Object.class)); // get variable value and filter with nullToNil MethodHandle getValue; boolean direct = false; if (accessor instanceof FieldVariableAccessor) { direct = true; int offset = ((FieldVariableAccessor) accessor).getOffset(); Class cls = REIFIED_OBJECT_CLASSES[offset]; getValue = lookup().findGetter(cls, "var" + offset, Object.class); getValue = explicitCastArguments(getValue, methodType(Object.class, IRubyObject.class)); } else { getValue = findStatic( VariableAccessor.class, "getVariable", methodType(Object.class, RubyBasicObject.class, int.class)); getValue = explicitCastArguments(getValue, methodType(Object.class, IRubyObject.class, int.class)); getValue = insertArguments(getValue, 1, accessor.getIndex()); } getValue = filterReturnValue(getValue, nullToNil); // prepare fallback MethodHandle fallback = null; if (site.chainCount() + 1 > Options.INVOKEDYNAMIC_MAXPOLY.load()) { if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info( site.name() + "\tqet on type " + self.getMetaClass().id + " failed (polymorphic)" + extractSourceInfo(site)); fallback = findStatic( Bootstrap.class, "ivarGetFail", methodType(IRubyObject.class, VariableSite.class, IRubyObject.class)); fallback = fallback.bindTo(site); site.setTarget(fallback); return (IRubyObject) fallback.invokeWithArguments(self); } else { if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { if (direct) { LOG.info( site.name() + "\tget field on type " + self.getMetaClass().id + " added to PIC" + extractSourceInfo(site)); } else { LOG.info( site.name() + "\tget on type " + self.getMetaClass().id + " added to PIC" + extractSourceInfo(site)); } } fallback = site.getTarget(); site.incrementChainCount(); } // prepare test MethodHandle test = findStatic( InvocationLinker.class, "testRealClass", methodType(boolean.class, int.class, IRubyObject.class)); test = insertArguments(test, 0, accessor.getClassId()); getValue = guardWithTest(test, getValue, fallback); if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info( site.name() + "\tget on class " + self.getMetaClass().id + " bound directly" + extractSourceInfo(site)); site.setTarget(getValue); return (IRubyObject) getValue.invokeExact(self); } public static IRubyObject ivarGetFail(VariableSite site, IRubyObject self) throws Throwable { return site.getVariable(self); } public static void ivarSet(VariableSite site, IRubyObject self, IRubyObject value) throws Throwable { RubyClass realClass = self.getMetaClass().getRealClass(); VariableAccessor accessor = realClass.getVariableAccessorForWrite(site.name()); // set variable value and fold by returning value MethodHandle setValue; boolean direct = false; if (accessor instanceof FieldVariableAccessor) { direct = true; int offset = ((FieldVariableAccessor) accessor).getOffset(); Class cls = REIFIED_OBJECT_CLASSES[offset]; setValue = findStatic(cls, "setVariableChecked", methodType(void.class, cls, Object.class)); setValue = explicitCastArguments( setValue, methodType(void.class, IRubyObject.class, IRubyObject.class)); } else { setValue = findStatic( accessor.getClass(), "setVariableChecked", methodType( void.class, RubyBasicObject.class, RubyClass.class, int.class, Object.class)); setValue = explicitCastArguments( setValue, methodType( void.class, IRubyObject.class, RubyClass.class, int.class, IRubyObject.class)); setValue = insertArguments(setValue, 1, realClass, accessor.getIndex()); } // prepare fallback MethodHandle fallback = null; if (site.chainCount() + 1 > Options.INVOKEDYNAMIC_MAXPOLY.load()) { if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info( site.name() + "\tset on type " + self.getMetaClass().id + " failed (polymorphic)" + extractSourceInfo(site)); fallback = findStatic( Bootstrap.class, "ivarSetFail", methodType(void.class, VariableSite.class, IRubyObject.class, IRubyObject.class)); fallback = fallback.bindTo(site); site.setTarget(fallback); fallback.invokeExact(self, value); } else { if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { if (direct) { LOG.info( site.name() + "\tset field on type " + self.getMetaClass().id + " added to PIC" + extractSourceInfo(site)); } else { LOG.info( site.name() + "\tset on type " + self.getMetaClass().id + " added to PIC" + extractSourceInfo(site)); } } fallback = site.getTarget(); site.incrementChainCount(); } // prepare test MethodHandle test = findStatic( InvocationLinker.class, "testRealClass", methodType(boolean.class, int.class, IRubyObject.class)); test = insertArguments(test, 0, accessor.getClassId()); test = dropArguments(test, 1, IRubyObject.class); setValue = guardWithTest(test, setValue, fallback); if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info( site.name() + "\tset on class " + self.getMetaClass().id + " bound directly" + extractSourceInfo(site)); site.setTarget(setValue); setValue.invokeExact(self, value); } public static void ivarSetFail(VariableSite site, IRubyObject self, IRubyObject value) throws Throwable { site.setVariable(self, value); } private static MethodHandle findStatic(Class target, String name, MethodType type) { return findStatic(lookup(), target, name, type); } private static MethodHandle findStatic( Lookup lookup, Class target, String name, MethodType type) { try { return lookup.findStatic(target, name, type); } catch (Exception e) { throw new RuntimeException(e); } } public static boolean testType(RubyClass original, IRubyObject self) { // naive test return ((RubyBasicObject) self).getMetaClass() == original; } /////////////////////////////////////////////////////////////////////////// // constant lookup public static IRubyObject searchConst( ThreadContext context, StaticScope staticScope, MutableCallSite site, String constName, boolean noPrivateConsts) throws Throwable { // Lexical lookup Ruby runtime = context.getRuntime(); RubyModule object = runtime.getObject(); IRubyObject constant = (staticScope == null) ? object.getConstant(constName) : staticScope.getConstantInner(constName); // Inheritance lookup RubyModule module = null; if (constant == null) { // SSS FIXME: Is this null check case correct? module = staticScope == null ? object : staticScope.getModule(); constant = noPrivateConsts ? module.getConstantFromNoConstMissing(constName, false) : module.getConstantNoConstMissing(constName); } // Call const_missing or cache if (constant == null) { return module.callMethod(context, "const_missing", context.runtime.fastNewSymbol(constName)); } SwitchPoint switchPoint = (SwitchPoint) runtime.getConstantInvalidator(constName).getData(); // bind constant until invalidated MethodHandle target = Binder.from(site.type()).drop(0, 2).constant(constant); MethodHandle fallback = Binder.from(site.type()) .append(site, constName) .append(noPrivateConsts) .invokeStatic(LOOKUP, Bootstrap.class, "searchConst"); site.setTarget(switchPoint.guardWithTest(target, fallback)); return constant; } public static IRubyObject inheritanceSearchConst( ThreadContext context, IRubyObject cmVal, MutableCallSite site, String constName, boolean noPrivateConsts) throws Throwable { Ruby runtime = context.runtime; RubyModule module; if (cmVal instanceof RubyModule) { module = (RubyModule) cmVal; } else { throw runtime.newTypeError(cmVal + " is not a type/class"); } IRubyObject constant = noPrivateConsts ? module.getConstantFromNoConstMissing(constName, false) : module.getConstantNoConstMissing(constName); if (constant == null) { constant = UndefinedValue.UNDEFINED; } SwitchPoint switchPoint = (SwitchPoint) runtime.getConstantInvalidator(constName).getData(); // bind constant until invalidated MethodHandle target = Binder.from(site.type()).drop(0, 2).constant(constant); MethodHandle fallback = Binder.from(site.type()) .append(site, constName) .append(noPrivateConsts) .invokeStatic(LOOKUP, Bootstrap.class, "inheritanceSearchConst"); // test that module is same as before MethodHandle test = Binder.from(site.type().changeReturnType(boolean.class)) .drop(0, 1) .insert(1, module.id) .invokeStaticQuiet(LOOKUP, Bootstrap.class, "testArg0ModuleMatch"); target = guardWithTest(test, target, fallback); site.setTarget(switchPoint.guardWithTest(target, fallback)); return constant; } public static IRubyObject lexicalSearchConst( ThreadContext context, StaticScope scope, MutableCallSite site, String constName, boolean noPrivateConsts) throws Throwable { Ruby runtime = context.runtime; IRubyObject constant = scope.getConstantInner(constName); if (constant == null) { constant = UndefinedValue.UNDEFINED; } SwitchPoint switchPoint = (SwitchPoint) runtime.getConstantInvalidator(constName).getData(); // bind constant until invalidated MethodHandle target = Binder.from(site.type()).drop(0, 2).constant(constant); MethodHandle fallback = Binder.from(site.type()) .append(site, constName) .append(noPrivateConsts) .invokeStatic(LOOKUP, Bootstrap.class, "lexicalSearchConst"); site.setTarget(switchPoint.guardWithTest(target, fallback)); return constant; } /////////////////////////////////////////////////////////////////////////// // Fixnum binding public static IRubyObject instVarNullToNil(IRubyObject value, IRubyObject nil, String name) { if (value == null) { Ruby runtime = nil.getRuntime(); if (runtime.isVerbose()) { nil.getRuntime() .getWarnings() .warning( IRubyWarnings.ID.IVAR_NOT_INITIALIZED, "instance variable " + name + " not initialized"); } return nil; } return value; } public static boolean testArg0ModuleMatch(IRubyObject arg0, int id) { return arg0 instanceof RubyModule && ((RubyModule) arg0).id == id; } private static String extractSourceInfo(VariableSite site) { return " (" + site.file() + ":" + site.line() + ")"; } }
/** This file implements a seekable IO file. */ public class ChannelStream implements Stream, Finalizable { private static final Logger LOG = LoggerFactory.getLogger("ChannelStream"); private static final boolean DEBUG = false; /** * The size of the read/write buffer allocated for this stream. * * <p>This size has been scaled back from its original 16k because although the larger buffer size * results in raw File.open times being rather slow (due to the cost of instantiating a relatively * large buffer). We should try to find a happy medium, or potentially pool buffers, or perhaps * even choose a value based on platform(??), but for now I am reducing it along with changes for * the "large read" patch from JRUBY-2657. */ public static final int BUFSIZE = 4 * 1024; /** * The size at which a single read should turn into a chunkier bulk read. Currently, this size is * about 4x a normal buffer size. * * <p>This size was not really arrived at experimentally, and could potentially be increased. * However, it seems like a "good size" and we should probably only adjust it if it turns out we * would perform better with a larger buffer for large bulk reads. */ private static final int BULK_READ_SIZE = 16 * 1024; private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); /** * A cached EOFException. Since EOFException is only used by us internally, we create a single * instance to avoid stack trace generation. Comment out the initialization of this field to cause * a new one each time. */ private static EOFException eofException = new EOFException(); private volatile Ruby runtime; protected ModeFlags modes; protected boolean sync = false; protected volatile ByteBuffer buffer; // r/w buffer protected boolean reading; // are we reading or writing? private ChannelDescriptor descriptor; private boolean blocking = true; protected int ungotc = -1; private volatile boolean closedExplicitly = false; private volatile boolean eof = false; private volatile boolean autoclose = true; private ChannelStream(Ruby runtime, ChannelDescriptor descriptor, boolean autoclose) { this.runtime = runtime; this.descriptor = descriptor; this.modes = descriptor.getOriginalModes(); buffer = ByteBuffer.allocate(BUFSIZE); buffer.flip(); this.reading = true; this.autoclose = autoclose; runtime.addInternalFinalizer(this); } private ChannelStream( Ruby runtime, ChannelDescriptor descriptor, ModeFlags modes, boolean autoclose) { this(runtime, descriptor, autoclose); this.modes = modes; } public Ruby getRuntime() { return runtime; } public void checkReadable() throws IOException { if (!modes.isReadable()) throw new IOException("not opened for reading"); } public void checkWritable() throws IOException { if (!modes.isWritable()) throw new IOException("not opened for writing"); } public void checkPermissionsSubsetOf(ModeFlags subsetModes) { subsetModes.isSubsetOf(modes); } public ModeFlags getModes() { return modes; } public boolean isSync() { return sync; } public void setSync(boolean sync) { this.sync = sync; } public void setBinmode() { // No-op here, no binmode handling needed. } public boolean isBinmode() { return false; } public boolean isAutoclose() { return autoclose; } public void setAutoclose(boolean autoclose) { this.autoclose = autoclose; } /** * Implement IO#wait as per io/wait in MRI. waits until input available or timed out and returns * self, or nil when EOF reached. * * <p>The default implementation loops while ready returns 0. */ public void waitUntilReady() throws IOException, InterruptedException { while (ready() == 0) { Thread.sleep(10); } } public boolean readDataBuffered() { return reading && (ungotc != -1 || buffer.hasRemaining()); } public boolean writeDataBuffered() { return !reading && buffer.position() > 0; } private final int refillBuffer() throws IOException { buffer.clear(); int n = ((ReadableByteChannel) descriptor.getChannel()).read(buffer); buffer.flip(); return n; } public synchronized ByteList fgets(ByteList separatorString) throws IOException, BadDescriptorException { checkReadable(); ensureRead(); if (separatorString == null) { return readall(); } final ByteList separator = (separatorString == PARAGRAPH_DELIMETER) ? PARAGRAPH_SEPARATOR : separatorString; descriptor.checkOpen(); if (feof()) { return null; } int c = read(); if (c == -1) { return null; } // unread back buffer.position(buffer.position() - 1); ByteList buf = new ByteList(40); byte first = separator.getUnsafeBytes()[separator.getBegin()]; LineLoop: while (true) { ReadLoop: while (true) { byte[] bytes = buffer.array(); int offset = buffer.position(); int max = buffer.limit(); // iterate over remainder of buffer until we find a match for (int i = offset; i < max; i++) { c = bytes[i]; if (c == first) { // terminate and advance buffer when we find our char buf.append(bytes, offset, i - offset); if (i >= max) { buffer.clear(); } else { buffer.position(i + 1); } break ReadLoop; } } // no match, append remainder of buffer and continue with next block buf.append(bytes, offset, buffer.remaining()); int read = refillBuffer(); if (read == -1) break LineLoop; } // found a match above, check if remaining separator characters match, appending as we go for (int i = 0; i < separator.getRealSize(); i++) { if (c == -1) { break LineLoop; } else if (c != separator.getUnsafeBytes()[separator.getBegin() + i]) { buf.append(c); continue LineLoop; } buf.append(c); if (i < separator.getRealSize() - 1) { c = read(); } } break; } if (separatorString == PARAGRAPH_DELIMETER) { while (c == separator.getUnsafeBytes()[separator.getBegin()]) { c = read(); } ungetc(c); } return buf; } public synchronized int getline(ByteList dst, byte terminator) throws IOException, BadDescriptorException { checkReadable(); ensureRead(); descriptor.checkOpen(); int totalRead = 0; boolean found = false; if (ungotc != -1) { dst.append((byte) ungotc); found = ungotc == terminator; ungotc = -1; ++totalRead; } while (!found) { final byte[] bytes = buffer.array(); final int begin = buffer.arrayOffset() + buffer.position(); final int end = begin + buffer.remaining(); int len = 0; for (int i = begin; i < end && !found; ++i) { found = bytes[i] == terminator; ++len; } if (len > 0) { dst.append(buffer, len); totalRead += len; } if (!found) { int n = refillBuffer(); if (n <= 0) { if (n < 0 && totalRead < 1) { return -1; } break; } } } return totalRead; } public synchronized int getline(ByteList dst, byte terminator, long limit) throws IOException, BadDescriptorException { checkReadable(); ensureRead(); descriptor.checkOpen(); int totalRead = 0; boolean found = false; if (ungotc != -1) { dst.append((byte) ungotc); found = ungotc == terminator; ungotc = -1; limit--; ++totalRead; } while (!found) { final byte[] bytes = buffer.array(); final int begin = buffer.arrayOffset() + buffer.position(); final int end = begin + buffer.remaining(); int len = 0; for (int i = begin; i < end && limit-- > 0 && !found; ++i) { found = bytes[i] == terminator; ++len; } if (limit < 1) found = true; if (len > 0) { dst.append(buffer, len); totalRead += len; } if (!found) { int n = refillBuffer(); if (n <= 0) { if (n < 0 && totalRead < 1) { return -1; } break; } } } return totalRead; } /** * @deprecated readall do busy loop for the IO which has NONBLOCK bit. You should implement the * logic by yourself with fread(). */ @Deprecated public synchronized ByteList readall() throws IOException, BadDescriptorException { final long fileSize = descriptor.isSeekable() && descriptor.getChannel() instanceof FileChannel ? ((FileChannel) descriptor.getChannel()).size() : 0; // // Check file size - special files in /proc have zero size and need to be // handled by the generic read path. // if (fileSize > 0) { ensureRead(); FileChannel channel = (FileChannel) descriptor.getChannel(); final long left = fileSize - channel.position() + bufferedInputBytesRemaining(); if (left <= 0) { eof = true; return null; } if (left > Integer.MAX_VALUE) { if (getRuntime() != null) { throw getRuntime().newIOError("File too large"); } else { throw new IOException("File too large"); } } ByteList result = new ByteList((int) left); ByteBuffer buf = ByteBuffer.wrap(result.getUnsafeBytes(), result.begin(), (int) left); // // Copy any buffered data (including ungetc byte) // copyBufferedBytes(buf); // // Now read unbuffered directly from the file // while (buf.hasRemaining()) { final int MAX_READ_CHUNK = 1 * 1024 * 1024; // // When reading into a heap buffer, the jvm allocates a temporary // direct ByteBuffer of the requested size. To avoid allocating // a huge direct buffer when doing ludicrous reads (e.g. 1G or more) // we split the read up into chunks of no more than 1M // ByteBuffer tmp = buf.duplicate(); if (tmp.remaining() > MAX_READ_CHUNK) { tmp.limit(tmp.position() + MAX_READ_CHUNK); } int n = channel.read(tmp); if (n <= 0) { break; } buf.position(tmp.position()); } eof = true; result.length(buf.position()); return result; } else if (descriptor.isNull()) { return new ByteList(0); } else { checkReadable(); ByteList byteList = new ByteList(); ByteList read = fread(BUFSIZE); if (read == null) { eof = true; return byteList; } while (read != null) { byteList.append(read); read = fread(BUFSIZE); } return byteList; } } /** * Copies bytes from the channel buffer into a destination <tt>ByteBuffer</tt> * * @param dst A <tt>ByteBuffer</tt> to place the data in. * @return The number of bytes copied. */ private final int copyBufferedBytes(ByteBuffer dst) { final int bytesToCopy = dst.remaining(); if (ungotc != -1 && dst.hasRemaining()) { dst.put((byte) ungotc); ungotc = -1; } if (buffer.hasRemaining() && dst.hasRemaining()) { if (dst.remaining() >= buffer.remaining()) { // // Copy out any buffered bytes // dst.put(buffer); } else { // // Need to clamp source (buffer) size to avoid overrun // ByteBuffer tmp = buffer.duplicate(); tmp.limit(tmp.position() + dst.remaining()); dst.put(tmp); buffer.position(tmp.position()); } } return bytesToCopy - dst.remaining(); } /** * Copies bytes from the channel buffer into a destination <tt>ByteBuffer</tt> * * @param dst A <tt>ByteBuffer</tt> to place the data in. * @return The number of bytes copied. */ private final int copyBufferedBytes(byte[] dst, int off, int len) { int bytesCopied = 0; if (ungotc != -1 && len > 0) { dst[off++] = (byte) ungotc; ungotc = -1; ++bytesCopied; } final int n = Math.min(len - bytesCopied, buffer.remaining()); buffer.get(dst, off, n); bytesCopied += n; return bytesCopied; } /** * Copies bytes from the channel buffer into a destination <tt>ByteBuffer</tt> * * @param dst A <tt>ByteList</tt> to place the data in. * @param len The maximum number of bytes to copy. * @return The number of bytes copied. */ private final int copyBufferedBytes(ByteList dst, int len) { int bytesCopied = 0; dst.ensure(Math.min(len, bufferedInputBytesRemaining())); if (bytesCopied < len && ungotc != -1) { ++bytesCopied; dst.append((byte) ungotc); ungotc = -1; } // // Copy out any buffered bytes // if (bytesCopied < len && buffer.hasRemaining()) { int n = Math.min(buffer.remaining(), len - bytesCopied); dst.append(buffer, n); bytesCopied += n; } return bytesCopied; } /** * Returns a count of how many bytes are available in the read buffer * * @return The number of bytes that can be read without reading the underlying stream. */ private final int bufferedInputBytesRemaining() { return reading ? (buffer.remaining() + (ungotc != -1 ? 1 : 0)) : 0; } /** * Tests if there are bytes remaining in the read buffer. * * @return <tt>true</tt> if there are bytes available in the read buffer. */ private final boolean hasBufferedInputBytes() { return reading && (buffer.hasRemaining() || ungotc != -1); } /** * Returns a count of how many bytes of space is available in the write buffer. * * @return The number of bytes that can be written to the buffer without flushing to the * underlying stream. */ private final int bufferedOutputSpaceRemaining() { return !reading ? buffer.remaining() : 0; } /** * Tests if there is space available in the write buffer. * * @return <tt>true</tt> if there are bytes available in the write buffer. */ private final boolean hasBufferedOutputSpace() { return !reading && buffer.hasRemaining(); } /** * Closes IO handler resources. * * @throws IOException * @throws BadDescriptorException */ public void fclose() throws IOException, BadDescriptorException { try { synchronized (this) { closedExplicitly = true; close(); // not closing from finalize } } finally { Ruby localRuntime = getRuntime(); // Make sure we remove finalizers while not holding self lock, // otherwise there is a possibility for a deadlock! if (localRuntime != null) localRuntime.removeInternalFinalizer(this); // clear runtime so it doesn't get stuck in memory (JRUBY-2933) runtime = null; } } /** * Internal close. * * @throws IOException * @throws BadDescriptorException */ private void close() throws IOException, BadDescriptorException { // finish and close ourselves finish(true); } private void finish(boolean close) throws BadDescriptorException, IOException { try { flushWrite(); if (DEBUG) LOG.info("Descriptor for fileno {} closed by stream", descriptor.getFileno()); } finally { buffer = EMPTY_BUFFER; // clear runtime so it doesn't get stuck in memory (JRUBY-2933) runtime = null; // finish descriptor descriptor.finish(close); } } /** * @throws IOException * @throws BadDescriptorException */ public synchronized int fflush() throws IOException, BadDescriptorException { checkWritable(); try { flushWrite(); } catch (EOFException eofe) { return -1; } return 0; } /** * Flush the write buffer to the channel (if needed) * * @throws IOException */ private void flushWrite() throws IOException, BadDescriptorException { if (reading || !modes.isWritable() || buffer.position() == 0) return; // Don't bother int len = buffer.position(); buffer.flip(); int n = descriptor.write(buffer); if (n != len) { // TODO: check the return value here } buffer.clear(); } /** * Flush the write buffer to the channel (if needed) * * @throws IOException */ private boolean flushWrite(final boolean block) throws IOException, BadDescriptorException { if (reading || !modes.isWritable() || buffer.position() == 0) return false; // Don't bother int len = buffer.position(); int nWritten = 0; buffer.flip(); // For Sockets, only write as much as will fit. if (descriptor.getChannel() instanceof SelectableChannel) { SelectableChannel selectableChannel = (SelectableChannel) descriptor.getChannel(); synchronized (selectableChannel.blockingLock()) { boolean oldBlocking = selectableChannel.isBlocking(); try { if (oldBlocking != block) { selectableChannel.configureBlocking(block); } nWritten = descriptor.write(buffer); } finally { if (oldBlocking != block) { selectableChannel.configureBlocking(oldBlocking); } } } } else { nWritten = descriptor.write(buffer); } if (nWritten != len) { buffer.compact(); return false; } buffer.clear(); return true; } public InputStream newInputStream() { InputStream in = descriptor.getBaseInputStream(); return in == null ? new InputStreamAdapter(this) : in; } public OutputStream newOutputStream() { return new OutputStreamAdapter(this); } public void clearerr() { eof = false; } /** * @throws IOException * @throws BadDescriptorException */ public boolean feof() throws IOException, BadDescriptorException { checkReadable(); if (eof) { return true; } else { return false; } } /** @throws IOException */ public synchronized long fgetpos() throws IOException, PipeException, InvalidValueException, BadDescriptorException { // Correct position for read / write buffering (we could invalidate, but expensive) if (descriptor.isSeekable()) { FileChannel fileChannel = (FileChannel) descriptor.getChannel(); long pos = fileChannel.position(); // Adjust for buffered data if (reading) { pos -= buffer.remaining(); return pos - (pos > 0 && ungotc != -1 ? 1 : 0); } else { return pos + buffer.position(); } } else if (descriptor.isNull()) { return 0; } else { throw new PipeException(); } } /** * Implementation of libc "lseek", which seeks on seekable streams, raises EPIPE if the fd is * assocated with a pipe, socket, or FIFO, and doesn't do anything for other cases (like stdio). * * @throws IOException * @throws InvalidValueException */ public synchronized void lseek(long offset, int type) throws IOException, InvalidValueException, PipeException, BadDescriptorException { if (descriptor.isSeekable()) { FileChannel fileChannel = (FileChannel) descriptor.getChannel(); ungotc = -1; int adj = 0; if (reading) { // for SEEK_CUR, need to adjust for buffered data adj = buffer.remaining(); buffer.clear(); buffer.flip(); } else { flushWrite(); } try { switch (type) { case SEEK_SET: fileChannel.position(offset); break; case SEEK_CUR: fileChannel.position(fileChannel.position() - adj + offset); break; case SEEK_END: fileChannel.position(fileChannel.size() + offset); break; } } catch (IllegalArgumentException e) { throw new InvalidValueException(); } catch (IOException ioe) { throw ioe; } } else if (descriptor.getChannel() instanceof SelectableChannel) { // TODO: It's perhaps just a coincidence that all the channels for // which we should raise are instanceof SelectableChannel, since // stdio is not...so this bothers me slightly. -CON throw new PipeException(); } else { } } public synchronized void sync() throws IOException, BadDescriptorException { flushWrite(); } /** * Ensure buffer is ready for reading, flushing remaining writes if required * * @throws IOException */ private void ensureRead() throws IOException, BadDescriptorException { if (reading) return; flushWrite(); buffer.clear(); buffer.flip(); reading = true; } /** * Ensure buffer is ready for reading, flushing remaining writes if required * * @throws IOException */ private void ensureReadNonBuffered() throws IOException, BadDescriptorException { if (reading) { if (buffer.hasRemaining()) { Ruby localRuntime = getRuntime(); if (localRuntime != null) { throw localRuntime.newIOError("sysread for buffered IO"); } else { throw new IOException("sysread for buffered IO"); } } } else { // libc flushes writes on any read from the actual file, so we flush here flushWrite(); buffer.clear(); buffer.flip(); reading = true; } } private void resetForWrite() throws IOException { if (descriptor.isSeekable()) { FileChannel fileChannel = (FileChannel) descriptor.getChannel(); if (buffer.hasRemaining()) { // we have read ahead, and need to back up fileChannel.position(fileChannel.position() - buffer.remaining()); } } // FIXME: Clearing read buffer here...is this appropriate? buffer.clear(); reading = false; } /** * Ensure buffer is ready for writing. * * @throws IOException */ private void ensureWrite() throws IOException { if (!reading) return; resetForWrite(); } public synchronized ByteList read(int number) throws IOException, BadDescriptorException { checkReadable(); ensureReadNonBuffered(); ByteList byteList = new ByteList(number); // TODO this should entry into error handling somewhere int bytesRead = descriptor.read(number, byteList); if (bytesRead == -1) { eof = true; } return byteList; } private ByteList bufferedRead(int number) throws IOException, BadDescriptorException { checkReadable(); ensureRead(); int resultSize = 0; // 128K seems to be the minimum at which the stat+seek is faster than reallocation final int BULK_THRESHOLD = 128 * 1024; if (number >= BULK_THRESHOLD && descriptor.isSeekable() && descriptor.getChannel() instanceof FileChannel) { // // If it is a file channel, then we can pre-allocate the output buffer // to the total size of buffered + remaining bytes in file // FileChannel fileChannel = (FileChannel) descriptor.getChannel(); resultSize = (int) Math.min( fileChannel.size() - fileChannel.position() + bufferedInputBytesRemaining(), number); } else { // // Cannot discern the total read length - allocate at least enough for the buffered data // resultSize = Math.min(bufferedInputBytesRemaining(), number); } ByteList result = new ByteList(resultSize); bufferedRead(result, number); return result; } private int bufferedRead(ByteList dst, int number) throws IOException, BadDescriptorException { int bytesRead = 0; // // Copy what is in the buffer, if there is some buffered data // bytesRead += copyBufferedBytes(dst, number); boolean done = false; // // Avoid double-copying for reads that are larger than the buffer size // while ((number - bytesRead) >= BUFSIZE) { // // limit each iteration to a max of BULK_READ_SIZE to avoid over-size allocations // final int bytesToRead = Math.min(BULK_READ_SIZE, number - bytesRead); final int n = descriptor.read(bytesToRead, dst); if (n == -1) { eof = true; done = true; break; } else if (n == 0) { done = true; break; } bytesRead += n; } // // Complete the request by filling the read buffer first // while (!done && bytesRead < number) { int read = refillBuffer(); if (read == -1) { eof = true; break; } else if (read == 0) { break; } // append what we read into our buffer and allow the loop to continue final int len = Math.min(buffer.remaining(), number - bytesRead); dst.append(buffer, len); bytesRead += len; } if (bytesRead == 0 && number != 0) { if (eof) { throw newEOFException(); } } return bytesRead; } private EOFException newEOFException() { if (eofException != null) { return eofException; } else { return new EOFException(); } } private int bufferedRead(ByteBuffer dst, boolean partial) throws IOException, BadDescriptorException { checkReadable(); ensureRead(); boolean done = false; int bytesRead = 0; // // Copy what is in the buffer, if there is some buffered data // bytesRead += copyBufferedBytes(dst); // // Avoid double-copying for reads that are larger than the buffer size, or // the destination is a direct buffer. // while ((bytesRead < 1 || !partial) && (dst.remaining() >= BUFSIZE || dst.isDirect())) { ByteBuffer tmpDst = dst; if (!dst.isDirect()) { // // We limit reads to BULK_READ_SIZED chunks to avoid NIO allocating // a huge temporary native buffer, when doing reads into a heap buffer // If the dst buffer is direct, then no need to limit. // int bytesToRead = Math.min(BULK_READ_SIZE, dst.remaining()); if (bytesToRead < dst.remaining()) { tmpDst = dst.duplicate(); tmpDst.limit(tmpDst.position() + bytesToRead); } } int n = descriptor.read(tmpDst); if (n == -1) { eof = true; done = true; break; } else if (n == 0) { done = true; break; } else { bytesRead += n; } } // // Complete the request by filling the read buffer first // while (!done && dst.hasRemaining() && (bytesRead < 1 || !partial)) { int read = refillBuffer(); if (read == -1) { eof = true; done = true; break; } else if (read == 0) { done = true; break; } else { // append what we read into our buffer and allow the loop to continue bytesRead += copyBufferedBytes(dst); } } if (eof && bytesRead == 0 && dst.remaining() != 0) { throw newEOFException(); } return bytesRead; } private int bufferedRead() throws IOException, BadDescriptorException { ensureRead(); if (!buffer.hasRemaining()) { int len = refillBuffer(); if (len == -1) { eof = true; return -1; } else if (len == 0) { return -1; } } return buffer.get() & 0xFF; } /** * @throws IOException * @throws BadDescriptorException */ private int bufferedWrite(ByteList buf) throws IOException, BadDescriptorException { checkWritable(); ensureWrite(); // Ruby ignores empty syswrites if (buf == null || buf.length() == 0) return 0; if (buf.length() > buffer.capacity()) { // Doesn't fit in buffer. Write immediately. flushWrite(); // ensure nothing left to write int n = descriptor.write(ByteBuffer.wrap(buf.getUnsafeBytes(), buf.begin(), buf.length())); if (n != buf.length()) { // TODO: check the return value here } } else { if (buf.length() > buffer.remaining()) flushWrite(); buffer.put(buf.getUnsafeBytes(), buf.begin(), buf.length()); } if (isSync()) flushWrite(); return buf.getRealSize(); } /** * @throws IOException * @throws BadDescriptorException */ private int bufferedWrite(ByteBuffer buf) throws IOException, BadDescriptorException { checkWritable(); ensureWrite(); // Ruby ignores empty syswrites if (buf == null || !buf.hasRemaining()) return 0; final int nbytes = buf.remaining(); if (nbytes >= buffer.capacity()) { // Doesn't fit in buffer. Write immediately. flushWrite(); // ensure nothing left to write descriptor.write(buf); // TODO: check the return value here } else { if (nbytes > buffer.remaining()) flushWrite(); buffer.put(buf); } if (isSync()) flushWrite(); return nbytes - buf.remaining(); } /** * @throws IOException * @throws BadDescriptorException */ private int bufferedWrite(int c) throws IOException, BadDescriptorException { checkWritable(); ensureWrite(); if (!buffer.hasRemaining()) flushWrite(); buffer.put((byte) c); if (isSync()) flushWrite(); return 1; } public synchronized void ftruncate(long newLength) throws IOException, BadDescriptorException, InvalidValueException { Channel ch = descriptor.getChannel(); if (!(ch instanceof FileChannel)) { throw new InvalidValueException(); } invalidateBuffer(); FileChannel fileChannel = (FileChannel) ch; long position = fileChannel.position(); if (newLength > fileChannel.size()) { // truncate can't lengthen files, so we save position, seek/write, and go back int difference = (int) (newLength - fileChannel.size()); fileChannel.position(fileChannel.size()); // FIXME: This worries me a bit, since it could allocate a lot with a large newLength fileChannel.write(ByteBuffer.allocate(difference)); } else { fileChannel.truncate(newLength); } fileChannel.position(position); } /** * Invalidate buffer before a position change has occurred (e.g. seek), flushing writes if * required, and correcting file position if reading * * @throws IOException */ private void invalidateBuffer() throws IOException, BadDescriptorException { if (!reading) flushWrite(); int posOverrun = buffer.remaining(); // how far ahead we are when reading buffer.clear(); if (reading) { buffer.flip(); // if the read buffer is ahead, back up FileChannel fileChannel = (FileChannel) descriptor.getChannel(); if (posOverrun != 0) fileChannel.position(fileChannel.position() - posOverrun); } } /** Ensure close (especially flush) when we're finished with. */ @Override public void finalize() throws Throwable { super.finalize(); if (closedExplicitly) return; if (DEBUG) { LOG.info("finalize() for not explicitly closed stream"); } // FIXME: I got a bunch of NPEs when I didn't check for nulls here...HOW?! if (descriptor != null && descriptor.isOpen()) { // tidy up finish(autoclose); } } public int ready() throws IOException { if (descriptor.getChannel() instanceof SelectableChannel) { int ready_stat = 0; java.nio.channels.Selector sel = SelectorFactory.openWithRetryFrom( null, ((SelectableChannel) descriptor.getChannel()).provider()); SelectableChannel selchan = (SelectableChannel) descriptor.getChannel(); synchronized (selchan.blockingLock()) { boolean is_block = selchan.isBlocking(); try { selchan.configureBlocking(false); selchan.register(sel, java.nio.channels.SelectionKey.OP_READ); ready_stat = sel.selectNow(); sel.close(); } catch (Throwable ex) { } finally { if (sel != null) { try { sel.close(); } catch (Exception e) { } } selchan.configureBlocking(is_block); } } return ready_stat; } else { return newInputStream().available(); } } public synchronized void fputc(int c) throws IOException, BadDescriptorException { bufferedWrite(c); } public int ungetc(int c) { if (c == -1) { return -1; } // putting a bit back, so we're not at EOF anymore eof = false; // save the ungot ungotc = c; return c; } public synchronized int fgetc() throws IOException, BadDescriptorException { if (eof) { return -1; } checkReadable(); int c = read(); if (c == -1) { eof = true; return c; } return c & 0xff; } public synchronized int fwrite(ByteList string) throws IOException, BadDescriptorException { return bufferedWrite(string); } public synchronized int write(ByteBuffer buf) throws IOException, BadDescriptorException { return bufferedWrite(buf); } public synchronized int writenonblock(ByteList buf) throws IOException, BadDescriptorException { checkWritable(); ensureWrite(); // Ruby ignores empty syswrites if (buf == null || buf.length() == 0) return 0; if (buffer.position() != 0 && !flushWrite(false)) return 0; if (descriptor.getChannel() instanceof SelectableChannel) { SelectableChannel selectableChannel = (SelectableChannel) descriptor.getChannel(); synchronized (selectableChannel.blockingLock()) { boolean oldBlocking = selectableChannel.isBlocking(); try { if (oldBlocking) { selectableChannel.configureBlocking(false); } return descriptor.write(ByteBuffer.wrap(buf.getUnsafeBytes(), buf.begin(), buf.length())); } finally { if (oldBlocking) { selectableChannel.configureBlocking(oldBlocking); } } } } else { // can't set nonblocking, so go ahead with it...not much else we can do return descriptor.write(ByteBuffer.wrap(buf.getUnsafeBytes(), buf.begin(), buf.length())); } } public synchronized ByteList fread(int number) throws IOException, BadDescriptorException { try { if (number == 0) { if (eof) { return null; } else { return new ByteList(0); } } return bufferedRead(number); } catch (EOFException e) { eof = true; return null; } } public synchronized ByteList readnonblock(int number) throws IOException, BadDescriptorException, EOFException { assert number >= 0; if (number == 0) { return null; } if (descriptor.getChannel() instanceof SelectableChannel) { SelectableChannel selectableChannel = (SelectableChannel) descriptor.getChannel(); synchronized (selectableChannel.blockingLock()) { boolean oldBlocking = selectableChannel.isBlocking(); try { selectableChannel.configureBlocking(false); return readpartial(number); } finally { selectableChannel.configureBlocking(oldBlocking); } } } else if (descriptor.getChannel() instanceof FileChannel) { return fread(number); } else { return null; } } public synchronized ByteList readpartial(int number) throws IOException, BadDescriptorException, EOFException { assert number >= 0; if (number == 0) { return null; } if (descriptor.getChannel() instanceof FileChannel) { return fread(number); } if (hasBufferedInputBytes()) { // already have some bytes buffered, just return those return bufferedRead(Math.min(bufferedInputBytesRemaining(), number)); } else { // otherwise, we try an unbuffered read to get whatever's available return read(number); } } public synchronized int read(ByteBuffer dst) throws IOException, BadDescriptorException, EOFException { return read(dst, !(descriptor.getChannel() instanceof FileChannel)); } public synchronized int read(ByteBuffer dst, boolean partial) throws IOException, BadDescriptorException, EOFException { assert dst.hasRemaining(); return bufferedRead(dst, partial); } public synchronized int read() throws IOException, BadDescriptorException { try { descriptor.checkOpen(); if (ungotc >= 0) { int c = ungotc; ungotc = -1; return c; } return bufferedRead(); } catch (EOFException e) { eof = true; return -1; } } public ChannelDescriptor getDescriptor() { return descriptor; } public void setBlocking(boolean block) throws IOException { if (!(descriptor.getChannel() instanceof SelectableChannel)) { return; } synchronized (((SelectableChannel) descriptor.getChannel()).blockingLock()) { blocking = block; try { ((SelectableChannel) descriptor.getChannel()).configureBlocking(block); } catch (IllegalBlockingModeException e) { // ignore this; select() will set the correct mode when it is finished } } } public boolean isBlocking() { return blocking; } public synchronized void freopen(Ruby runtime, String path, ModeFlags modes) throws DirectoryAsFileException, IOException, InvalidValueException, PipeException, BadDescriptorException { // flush first flushWrite(); // reset buffer buffer.clear(); if (reading) { buffer.flip(); } this.modes = modes; if (descriptor.isOpen()) { descriptor.close(); } if (path.equals("/dev/null") || path.equalsIgnoreCase("nul:") || path.equalsIgnoreCase("nul")) { descriptor = descriptor.reopen(new NullChannel(), modes); } else { String cwd = runtime.getCurrentDirectory(); JRubyFile theFile = JRubyFile.create(cwd, path); if (theFile.isDirectory() && modes.isWritable()) throw new DirectoryAsFileException(); if (modes.isCreate()) { if (theFile.exists() && modes.isExclusive()) { throw runtime.newErrnoEEXISTError("File exists - " + path); } theFile.createNewFile(); } else { if (!theFile.exists()) { throw runtime.newErrnoENOENTError("file not found - " + path); } } // We always open this rw since we can only open it r or rw. RandomAccessFile file = new RandomAccessFile(theFile, modes.toJavaModeString()); if (modes.isTruncate()) file.setLength(0L); descriptor = descriptor.reopen(file, modes); try { if (modes.isAppendable()) lseek(0, SEEK_END); } catch (PipeException pe) { // ignore, it's a pipe or fifo } } } public static Stream open(Ruby runtime, ChannelDescriptor descriptor) { return maybeWrapWithLineEndingWrapper( new ChannelStream(runtime, descriptor, true), descriptor.getOriginalModes()); } public static Stream fdopen(Ruby runtime, ChannelDescriptor descriptor, ModeFlags modes) throws InvalidValueException { // check these modes before constructing, so we don't finalize the partially-initialized stream descriptor.checkNewModes(modes); return maybeWrapWithLineEndingWrapper( new ChannelStream(runtime, descriptor, modes, true), modes); } public static Stream open(Ruby runtime, ChannelDescriptor descriptor, boolean autoclose) { return maybeWrapWithLineEndingWrapper( new ChannelStream(runtime, descriptor, autoclose), descriptor.getOriginalModes()); } public static Stream fdopen( Ruby runtime, ChannelDescriptor descriptor, ModeFlags modes, boolean autoclose) throws InvalidValueException { // check these modes before constructing, so we don't finalize the partially-initialized stream descriptor.checkNewModes(modes); return maybeWrapWithLineEndingWrapper( new ChannelStream(runtime, descriptor, modes, autoclose), modes); } private static Stream maybeWrapWithLineEndingWrapper(Stream stream, ModeFlags modes) { if (Platform.IS_WINDOWS && stream.getDescriptor().getChannel() instanceof FileChannel && !modes.isBinary()) { return new CRLFStreamWrapper(stream); } return stream; } public static Stream fopen(Ruby runtime, String path, ModeFlags modes) throws FileNotFoundException, DirectoryAsFileException, FileExistsException, IOException, InvalidValueException, PipeException, BadDescriptorException { ChannelDescriptor descriptor = ChannelDescriptor.open( runtime.getCurrentDirectory(), path, modes, runtime.getClassLoader()); Stream stream = fdopen(runtime, descriptor, modes); return stream; } public Channel getChannel() { return getDescriptor().getChannel(); } private static final class InputStreamAdapter extends java.io.InputStream { private final ChannelStream stream; public InputStreamAdapter(ChannelStream stream) { this.stream = stream; } @Override public int read() throws IOException { synchronized (stream) { // If it can be pulled direct from the buffer, don't go via the slow path if (stream.hasBufferedInputBytes()) { try { return stream.read(); } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } } } byte[] b = new byte[1]; // java.io.InputStream#read must return an unsigned value; return read(b, 0, 1) == 1 ? b[0] & 0xff : -1; } @Override public int read(byte[] bytes, int off, int len) throws IOException { if (bytes == null) { throw new NullPointerException("null destination buffer"); } if ((len | off | (off + len) | (bytes.length - (off + len))) < 0) { throw new IndexOutOfBoundsException(); } if (len == 0) { return 0; } try { synchronized (stream) { final int available = stream.bufferedInputBytesRemaining(); if (available >= len) { return stream.copyBufferedBytes(bytes, off, len); } else if (stream.getDescriptor().getChannel() instanceof SelectableChannel) { SelectableChannel ch = (SelectableChannel) stream.getDescriptor().getChannel(); synchronized (ch.blockingLock()) { boolean oldBlocking = ch.isBlocking(); try { if (!oldBlocking) { ch.configureBlocking(true); } return stream.bufferedRead(ByteBuffer.wrap(bytes, off, len), true); } finally { if (!oldBlocking) { ch.configureBlocking(oldBlocking); } } } } else { return stream.bufferedRead(ByteBuffer.wrap(bytes, off, len), true); } } } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } catch (EOFException ex) { return -1; } } @Override public int available() throws IOException { synchronized (stream) { return !stream.eof ? stream.bufferedInputBytesRemaining() : 0; } } @Override public void close() throws IOException { try { synchronized (stream) { stream.fclose(); } } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } } } private static final class OutputStreamAdapter extends java.io.OutputStream { private final ChannelStream stream; public OutputStreamAdapter(ChannelStream stream) { this.stream = stream; } @Override public void write(int i) throws IOException { synchronized (stream) { if (!stream.isSync() && stream.hasBufferedOutputSpace()) { stream.buffer.put((byte) i); return; } } byte[] b = {(byte) i}; write(b, 0, 1); } @Override public void write(byte[] bytes, int off, int len) throws IOException { if (bytes == null) { throw new NullPointerException("null source buffer"); } if ((len | off | (off + len) | (bytes.length - (off + len))) < 0) { throw new IndexOutOfBoundsException(); } try { synchronized (stream) { if (!stream.isSync() && stream.bufferedOutputSpaceRemaining() >= len) { stream.buffer.put(bytes, off, len); } else if (stream.getDescriptor().getChannel() instanceof SelectableChannel) { SelectableChannel ch = (SelectableChannel) stream.getDescriptor().getChannel(); synchronized (ch.blockingLock()) { boolean oldBlocking = ch.isBlocking(); try { if (!oldBlocking) { ch.configureBlocking(true); } stream.bufferedWrite(ByteBuffer.wrap(bytes, off, len)); } finally { if (!oldBlocking) { ch.configureBlocking(oldBlocking); } } } } else { stream.bufferedWrite(ByteBuffer.wrap(bytes, off, len)); } } } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } } @Override public void close() throws IOException { try { synchronized (stream) { stream.fclose(); } } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } } @Override public void flush() throws IOException { try { synchronized (stream) { stream.flushWrite(true); } } catch (BadDescriptorException ex) { throw new IOException(ex.getMessage()); } } } }
public class MixedModeIRMethod extends DynamicMethod implements IRMethodArgs, PositionAware, Compilable<DynamicMethod> { private static final Logger LOG = LoggerFactory.getLogger("InterpretedIRMethod"); private Signature signature; private boolean displayedCFG = false; // FIXME: Remove when we find nicer way of logging CFG protected final IRScope method; protected static class DynamicMethodBox { public DynamicMethod actualMethod; public int callCount = 0; } protected DynamicMethodBox box = new DynamicMethodBox(); public MixedModeIRMethod(IRScope method, Visibility visibility, RubyModule implementationClass) { super(implementationClass, visibility, CallConfiguration.FrameNoneScopeNone, method.getName()); this.method = method; getStaticScope().determineModule(); this.signature = getStaticScope().getSignature(); // disable JIT if JIT is disabled if (!implementationClass.getRuntime().getInstanceConfig().getCompileMode().shouldJIT()) { this.box.callCount = -1; } } public IRScope getIRScope() { return method; } public DynamicMethod getActualMethod() { return box.actualMethod; } public StaticScope getStaticScope() { return method.getStaticScope(); } public ArgumentDescriptor[] getArgumentDescriptors() { ensureInstrsReady(); // Make sure method is minimally built before returning this info return ((IRMethod) method).getArgumentDescriptors(); } public Signature getSignature() { return signature; } @Override public Arity getArity() { return signature.arity(); } protected void post(InterpreterContext ic, ThreadContext context) { // update call stacks (pop: ..) context.popFrame(); if (ic.popDynScope()) { context.popScope(); } } protected void pre( InterpreterContext ic, ThreadContext context, IRubyObject self, String name, Block block, RubyModule implClass) { // update call stacks (push: frame, class, scope, etc.) context.preMethodFrameOnly(implClass, name, self, block); if (ic.pushNewDynScope()) { context.pushScope(DynamicScope.newDynamicScope(ic.getStaticScope())); } } // FIXME: for subclasses we should override this method since it can be simple get // FIXME: to avoid cost of synch call in lazilyacquire we can save the ic here public InterpreterContext ensureInstrsReady() { if (method instanceof IRMethod) { return ((IRMethod) method).lazilyAcquireInterpreterContext(); } return method.getInterpreterContext(); } @Override public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) { if (IRRuntimeHelpers.isDebug()) doDebug(); DynamicMethodBox box = this.box; if (box.callCount >= 0) tryJit(context, box); DynamicMethod jittedMethod = box.actualMethod; if (jittedMethod != null) { return jittedMethod.call(context, self, clazz, name, args, block); } else { return INTERPRET_METHOD( context, ensureInstrsReady(), getImplementationClass().getMethodLocation(), self, name, args, block); } } private IRubyObject INTERPRET_METHOD( ThreadContext context, InterpreterContext ic, RubyModule implClass, IRubyObject self, String name, IRubyObject[] args, Block block) { try { ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine()); if (ic.hasExplicitCallProtocol()) { return ic.engine.interpret(context, self, ic, implClass, name, args, block, null); } else { try { this.pre(ic, context, self, name, block, implClass); return ic.engine.interpret(context, self, ic, implClass, name, args, block, null); } finally { this.post(ic, context); } } } finally { ThreadContext.popBacktrace(context); } } @Override public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, String name, Block block) { if (IRRuntimeHelpers.isDebug()) doDebug(); DynamicMethodBox box = this.box; if (box.callCount >= 0) tryJit(context, box); DynamicMethod jittedMethod = box.actualMethod; if (jittedMethod != null) { return jittedMethod.call(context, self, clazz, name, block); } else { return INTERPRET_METHOD( context, ensureInstrsReady(), getImplementationClass().getMethodLocation(), self, name, block); } } private IRubyObject INTERPRET_METHOD( ThreadContext context, InterpreterContext ic, RubyModule implClass, IRubyObject self, String name, Block block) { try { ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine()); if (ic.hasExplicitCallProtocol()) { return ic.engine.interpret(context, self, ic, implClass, name, block, null); } else { try { this.pre(ic, context, self, name, block, implClass); return ic.engine.interpret(context, self, ic, implClass, name, block, null); } finally { this.post(ic, context); } } } finally { ThreadContext.popBacktrace(context); } } @Override public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, Block block) { if (IRRuntimeHelpers.isDebug()) doDebug(); DynamicMethodBox box = this.box; if (box.callCount >= 0) tryJit(context, box); DynamicMethod jittedMethod = box.actualMethod; if (jittedMethod != null) { return jittedMethod.call(context, self, clazz, name, arg0, block); } else { return INTERPRET_METHOD( context, ensureInstrsReady(), getImplementationClass().getMethodLocation(), self, name, arg0, block); } } private IRubyObject INTERPRET_METHOD( ThreadContext context, InterpreterContext ic, RubyModule implClass, IRubyObject self, String name, IRubyObject arg1, Block block) { try { ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine()); if (ic.hasExplicitCallProtocol()) { return ic.engine.interpret(context, self, ic, implClass, name, arg1, block, null); } else { try { this.pre(ic, context, self, name, block, implClass); return ic.engine.interpret(context, self, ic, implClass, name, arg1, block, null); } finally { this.post(ic, context); } } } finally { ThreadContext.popBacktrace(context); } } @Override public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) { if (IRRuntimeHelpers.isDebug()) doDebug(); DynamicMethodBox box = this.box; if (box.callCount >= 0) tryJit(context, box); DynamicMethod jittedMethod = box.actualMethod; if (jittedMethod != null) { return jittedMethod.call(context, self, clazz, name, arg0, arg1, block); } else { return INTERPRET_METHOD( context, ensureInstrsReady(), getImplementationClass().getMethodLocation(), self, name, arg0, arg1, block); } } private IRubyObject INTERPRET_METHOD( ThreadContext context, InterpreterContext ic, RubyModule implClass, IRubyObject self, String name, IRubyObject arg1, IRubyObject arg2, Block block) { try { ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine()); if (ic.hasExplicitCallProtocol()) { return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, block, null); } else { try { this.pre(ic, context, self, name, block, implClass); return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, block, null); } finally { this.post(ic, context); } } } finally { ThreadContext.popBacktrace(context); } } @Override public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { if (IRRuntimeHelpers.isDebug()) doDebug(); DynamicMethodBox box = this.box; if (box.callCount >= 0) tryJit(context, box); DynamicMethod jittedMethod = box.actualMethod; if (jittedMethod != null) { return jittedMethod.call(context, self, clazz, name, arg0, arg1, arg2, block); } else { return INTERPRET_METHOD( context, ensureInstrsReady(), getImplementationClass().getMethodLocation(), self, name, arg0, arg1, arg2, block); } } private IRubyObject INTERPRET_METHOD( ThreadContext context, InterpreterContext ic, RubyModule implClass, IRubyObject self, String name, IRubyObject arg1, IRubyObject arg2, IRubyObject arg3, Block block) { try { ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine()); if (ic.hasExplicitCallProtocol()) { return ic.engine.interpret( context, self, ic, implClass, name, arg1, arg2, arg3, block, null); } else { try { this.pre(ic, context, self, name, block, implClass); return ic.engine.interpret( context, self, ic, implClass, name, arg1, arg2, arg3, block, null); } finally { this.post(ic, context); } } } finally { ThreadContext.popBacktrace(context); } } protected void doDebug() { // FIXME: This is printing out IRScope CFG but JIT may be active and it might not reflect // currently executing. Move into JIT and into interp since they will be getting CFG from // different sources // FIXME: This is only printing out CFG once. If we keep applying more passes then we // will want to print out after those new passes. ensureInstrsReady(); LOG.info("Executing '" + method.getName() + "'"); if (!displayedCFG) { LOG.info(method.debugOutput()); displayedCFG = true; } } public DynamicMethod getMethodForCaching() { DynamicMethod method = box.actualMethod; if (method instanceof CompiledIRMethod) { return method; } return this; } @Override public void completeBuild(DynamicMethod newMethod) { this.box.actualMethod = newMethod; this.box.actualMethod.serialNumber = this.serialNumber; this.box.callCount = -1; getImplementationClass().invalidateCacheDescendants(); } protected void tryJit(ThreadContext context, DynamicMethodBox box) { if (context.runtime.isBooting()) return; // don't JIT during runtime boot if (box.callCount++ >= Options.JIT_THRESHOLD.load()) context.runtime.getJITCompiler().buildThresholdReached(context, this); } public String getClassName(ThreadContext context) { String className; if (implementationClass.isSingleton()) { MetaClass metaClass = (MetaClass) implementationClass; RubyClass realClass = metaClass.getRealClass(); // if real class is Class if (realClass == context.runtime.getClassClass()) { // use the attached class's name className = ((RubyClass) metaClass.getAttached()).getName(); } else { // use the real class name className = realClass.getName(); } } else { // use the class name className = implementationClass.getName(); } return className; } public void setActualMethod(CompiledIRMethod method) { this.box.actualMethod = method; } protected void dupBox(MixedModeIRMethod orig) { this.box = orig.box; } @Override public DynamicMethod dup() { MixedModeIRMethod x = new MixedModeIRMethod(method, visibility, implementationClass); x.box = box; return x; } public String getFile() { return method.getFileName(); } public int getLine() { return method.getLineNumber(); } public void setCallCount(int callCount) { box.callCount = callCount; } }
public class Interpreter { private static final Logger LOG = LoggerFactory.getLogger("Interpreter"); public static IRubyObject interpret(Ruby runtime, Node rootNode, IRubyObject self) { IRScope scope = new IRBuilder().buildRoot((RootNode) rootNode); scope.prepareForInterpretation(); // scope.runCompilerPass(new CallSplitter()); return interpretTop(runtime, scope, self); } private static int interpInstrsCount = 0; public static boolean isDebug() { return RubyInstanceConfig.IR_DEBUG; } public static IRubyObject interpretTop(Ruby runtime, IRScope scope, IRubyObject self) { assert scope instanceof IRScript : "Must be an IRScript scope at Top!!!"; IRScript root = (IRScript) scope; // We get the live object ball rolling here. This give a valid value for the top // of this lexical tree. All new scope can then retrieve and set based on lexical parent. if (root.getStaticScope().getModule() == null) { // If an eval this may already be setup. root.getStaticScope().setModule(runtime.getObject()); } RubyModule currModule = root.getStaticScope().getModule(); IRMethod rootMethod = root.getRootClass().getRootMethod(); InterpretedIRMethod method = new InterpretedIRMethod(rootMethod, currModule); ThreadContext context = runtime.getCurrentContext(); IRubyObject rv = method.call(context, self, currModule, "", IRubyObject.NULL_ARRAY); if (isDebug()) LOG.debug("-- Interpreted instructions: {}", interpInstrsCount); return rv; } public static IRubyObject interpret(ThreadContext context, CFG cfg, InterpreterContext interp) { Ruby runtime = context.getRuntime(); boolean inClosure = (cfg.getScope() instanceof IRClosure); boolean passThroughBreak = false; try { interp.setMethodExitLabel( cfg.getExitBB().getLabel()); // used by return and break instructions! Instr[] instrs = cfg.prepareForInterpretation(); int n = instrs.length; int ipc = 0; Instr lastInstr = null; while (ipc < n) { interpInstrsCount++; lastInstr = instrs[ipc]; if (isDebug()) LOG.debug("I: {}", lastInstr); try { Label jumpTarget = lastInstr.interpret(interp); ipc = (jumpTarget == null) ? ipc + 1 : jumpTarget.getTargetPC(); } // SSS FIXME: This only catches Ruby exceptions // What about Java exceptions? catch (org.jruby.exceptions.RaiseException re) { ipc = cfg.getRescuerPC(lastInstr); if (ipc == -1) throw re; // No one rescued exception, pass it on! interp.setException(re.getException()); } } // If a closure, and lastInstr was a return, have to return from the nearest method! IRubyObject rv = (IRubyObject) interp.getReturnValue(); if (lastInstr instanceof ReturnInstr && inClosure && !interp.inLambda()) { throw new IRReturnJump(((ReturnInstr) lastInstr).methodToReturnFrom, rv); } // If a closure, and lastInstr was a break, have to return from the nearest closure! else if (lastInstr instanceof BREAK_Instr) { if (!inClosure) throw runtime.newLocalJumpError(Reason.BREAK, rv, "unexpected break"); passThroughBreak = true; RuntimeHelpers.breakJump(context, rv); } return rv; } catch (JumpException.BreakJump bj) { if (passThroughBreak) throw bj; return (IRubyObject) bj.getValue(); } catch (IRReturnJump rj) { // - If we are in a lambda, stop propagating // - If not in a lambda // - if in a closure, pass it along // - if not in a closure, we got this return jump from a closure further up the call stack. // So, continue popping the call stack till we get to the right method if (!interp.inLambda() && (inClosure || (rj.methodToReturnFrom != cfg.getScope()))) throw rj; // pass it along return (IRubyObject) rj.returnValue; } finally { if (interp.getFrame() != null) { context.popFrame(); interp.setFrame(null); } if (interp.hasAllocatedDynamicScope()) context.postMethodScopeOnly(); } } public static IRubyObject INTERPRET_METHOD( ThreadContext context, CFG cfg, InterpreterContext interp, String name, RubyModule implClass, boolean isTraceable) { Ruby runtime = interp.getRuntime(); boolean syntheticMethod = name == null || name.equals(""); try { String className = implClass.getName(); if (!syntheticMethod) ThreadContext.pushBacktrace(context, className, name, context.getFile(), context.getLine()); if (isTraceable) methodPreTrace(runtime, context, name, implClass); return interpret(context, cfg, interp); } finally { if (isTraceable) { try { methodPostTrace(runtime, context, name, implClass); } finally { if (!syntheticMethod) ThreadContext.popBacktrace(context); } } else { if (!syntheticMethod) ThreadContext.popBacktrace(context); } } } private static void methodPreTrace( Ruby runtime, ThreadContext context, String name, RubyModule implClass) { if (runtime.hasEventHooks()) context.trace(RubyEvent.CALL, name, implClass); } private static void methodPostTrace( Ruby runtime, ThreadContext context, String name, RubyModule implClass) { if (runtime.hasEventHooks()) context.trace(RubyEvent.RETURN, name, implClass); } }
/** * Represents the base build of a CFG. All information here is accessed via delegation from the CFG * itself so this is meant as an internal organizational structure for a build. */ public class CFG { public enum EdgeType { REGULAR, // Any non-special edge. Not really used. EXCEPTION, // Edge to exception handling basic blocks FALL_THROUGH, // Edge which is the natural fall through choice on a branch EXIT // Edge to dummy exit BB } private static final Logger LOG = LoggerFactory.getLogger("CFG"); private IRExecutionScope scope; private Map<Label, BasicBlock> bbMap = new HashMap<Label, BasicBlock>(); // Map of bb -> first bb of the rescue block that initiates exception handling for all exceptions // thrown within this bb private Map<BasicBlock, BasicBlock> rescuerMap = new HashMap<BasicBlock, BasicBlock>(); // Map of bb -> first bb of the ensure block that protects this bb private Map<BasicBlock, BasicBlock> ensurerMap = new HashMap<BasicBlock, BasicBlock>(); private List<ExceptionRegion> outermostERs = new ArrayList<ExceptionRegion>(); private BasicBlock entryBB = null; private BasicBlock exitBB = null; private DirectedGraph<BasicBlock> graph = new DirectedGraph<BasicBlock>(); private int nextBBId = 0; // Next available basic block id LinkedList<BasicBlock> postOrderList = null; // Post order traversal list of the cfg public CFG(IRExecutionScope scope) { this.scope = scope; } public int getNextBBID() { nextBBId++; return nextBBId; } public int getMaxNodeID() { return nextBBId; } public boolean bbIsProtected(BasicBlock b) { // No need to look in ensurerMap because (_bbEnsurerMap(b) != null) => (_bbResucerMap(b) != // null) return getRescuerBBFor(b) != null; } public BasicBlock getBBForLabel(Label label) { return bbMap.get(label); } public BasicBlock getEnsurerBBFor(BasicBlock block) { return ensurerMap.get(block); } public BasicBlock getEntryBB() { return entryBB; } public BasicBlock getExitBB() { return exitBB; } public List<ExceptionRegion> getOutermostExceptionRegions() { return outermostERs; } public LinkedList<BasicBlock> postOrderList() { if (postOrderList == null) postOrderList = buildPostOrderList(); return postOrderList; } public ListIterator<BasicBlock> getPostOrderTraverser() { return postOrderList().listIterator(); } public ListIterator<BasicBlock> getReversePostOrderTraverser() { return postOrderList().listIterator(size()); } public IRExecutionScope getScope() { return scope; } public int size() { return graph.size(); } public Collection<BasicBlock> getBasicBlocks() { return graph.allData(); } public Collection<BasicBlock> getSortedBasicBlocks() { return graph.getSortedData(); } public Iterable<BasicBlock> getIncomingSources(BasicBlock block) { return graph.vertexFor(block).getIncomingSourcesData(); } public Iterable<Edge<BasicBlock>> getIncomingEdges(BasicBlock block) { return graph.vertexFor(block).getIncomingEdges(); } public BasicBlock getIncomingSource(BasicBlock block) { return graph.vertexFor(block).getIncomingSourceData(); } public BasicBlock getIncomingSourceOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getIncomingSourceDataOfType(type); } public Edge<BasicBlock> getIncomingEdgeOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getIncomingEdgeOfType(type); } public Edge<BasicBlock> getOutgoingEdgeOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getOutgoingEdgeOfType(type); } public BasicBlock getOutgoingDestination(BasicBlock block) { return graph.vertexFor(block).getOutgoingDestinationData(); } public BasicBlock getOutgoingDestinationOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getOutgoingDestinationDataOfType(type); } public Iterable<BasicBlock> getOutgoingDestinations(BasicBlock block) { return graph.vertexFor(block).getOutgoingDestinationsData(); } public Iterable<BasicBlock> getOutgoingDestinationsOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getOutgoingDestinationsDataOfType(type); } public Iterable<BasicBlock> getOutgoingDestinationsNotOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getOutgoingDestinationsDataNotOfType(type); } public Set<Edge<BasicBlock>> getOutgoingEdges(BasicBlock block) { return graph.vertexFor(block).getOutgoingEdges(); } public Iterable<Edge<BasicBlock>> getOutgoingEdgesNotOfType(BasicBlock block, Object type) { return graph.vertexFor(block).getOutgoingEdgesNotOfType(type); } public BasicBlock getRescuerBBFor(BasicBlock block) { return rescuerMap.get(block); } public void addEdge(BasicBlock source, BasicBlock destination, Object type) { graph.vertexFor(source).addEdgeTo(destination, type); } /* Add 'b' as a global ensure block that protects all unprotected blocks in this scope */ public void addGlobalEnsureBlock(BasicBlock geb) { addEdge(geb, getExitBB(), EdgeType.EXIT); for (BasicBlock basicBlock : getBasicBlocks()) { if (basicBlock != geb && !bbIsProtected(basicBlock)) { addEdge(basicBlock, geb, EdgeType.EXCEPTION); setRescuerBB(basicBlock, geb); setEnsurerBB(basicBlock, geb); } } } public void putBBForLabel(Label label, BasicBlock block) { bbMap.put(label, block); } public void setEnsurerBB(BasicBlock block, BasicBlock ensureBlock) { ensurerMap.put(block, ensureBlock); } public void setRescuerBB(BasicBlock block, BasicBlock exceptionBlock) { rescuerMap.put(block, exceptionBlock); } /** Build the Control Flow Graph */ public DirectedGraph<BasicBlock> build(List<Instr> instrs) { // Map of label & basic blocks which are waiting for a bb with that label Map<Label, List<BasicBlock>> forwardRefs = new HashMap<Label, List<BasicBlock>>(); // Map of return address variable and all possible targets (required to connect up ensure blocks // with their targets) Map<Variable, Set<Label>> retAddrMap = new HashMap<Variable, Set<Label>>(); Map<Variable, BasicBlock> retAddrTargetMap = new HashMap<Variable, BasicBlock>(); // List of bbs that have a 'return' instruction List<BasicBlock> returnBBs = new ArrayList<BasicBlock>(); // List of bbs that have a 'throw' instruction List<BasicBlock> exceptionBBs = new ArrayList<BasicBlock>(); // Stack of nested rescue regions Stack<ExceptionRegion> nestedExceptionRegions = new Stack<ExceptionRegion>(); // List of all rescued regions List<ExceptionRegion> allExceptionRegions = new ArrayList<ExceptionRegion>(); // Dummy entry basic block (see note at end to see why) entryBB = createBB(nestedExceptionRegions); // First real bb BasicBlock firstBB = createBB(nestedExceptionRegions); // Build the rest! BasicBlock currBB = firstBB; BasicBlock newBB = null; boolean bbEnded = false; boolean nextBBIsFallThrough = true; for (Instr i : instrs) { Operation iop = i.getOperation(); if (iop == Operation.LABEL) { Label l = ((LabelInstr) i).label; newBB = createBB(l, nestedExceptionRegions); // Jump instruction bbs dont add an edge to the succeeding bb by default if (nextBBIsFallThrough) graph.addEdge(currBB, newBB, EdgeType.FALL_THROUGH); currBB = newBB; bbEnded = false; nextBBIsFallThrough = true; // Add forward reference edges List<BasicBlock> frefs = forwardRefs.get(l); if (frefs != null) { for (BasicBlock b : frefs) { graph.addEdge(b, newBB, EdgeType.REGULAR); } } } else if (bbEnded && (iop != Operation.EXC_REGION_END)) { newBB = createBB(nestedExceptionRegions); // Jump instruction bbs dont add an edge to the succeeding bb by default if (nextBBIsFallThrough) graph.addEdge(currBB, newBB, EdgeType.FALL_THROUGH); // currBB cannot be null! currBB = newBB; bbEnded = false; nextBBIsFallThrough = true; } if (i instanceof ExceptionRegionStartMarkerInstr) { // SSS: Do we need this anymore? // currBB.addInstr(i); ExceptionRegionStartMarkerInstr ersmi = (ExceptionRegionStartMarkerInstr) i; ExceptionRegion rr = new ExceptionRegion(ersmi.firstRescueBlockLabel, ersmi.ensureBlockLabel); rr.addBB(currBB); allExceptionRegions.add(rr); if (nestedExceptionRegions.empty()) { outermostERs.add(rr); } else { nestedExceptionRegions.peek().addNestedRegion(rr); } nestedExceptionRegions.push(rr); } else if (i instanceof ExceptionRegionEndMarkerInstr) { // SSS: Do we need this anymore? // currBB.addInstr(i); nestedExceptionRegions.pop().setEndBB(currBB); } else if (iop.endsBasicBlock()) { bbEnded = true; currBB.addInstr(i); Label tgt; nextBBIsFallThrough = false; if (i instanceof BranchInstr) { tgt = ((BranchInstr) i).getJumpTarget(); nextBBIsFallThrough = true; } else if (i instanceof JumpInstr) { tgt = ((JumpInstr) i).getJumpTarget(); } else if (iop.isReturn()) { // BREAK, RETURN, CLOSURE_RETURN tgt = null; returnBBs.add(currBB); } else if (i instanceof ThrowExceptionInstr) { tgt = null; exceptionBBs.add(currBB); } else if (i instanceof JumpIndirectInstr) { tgt = null; Set<Label> retAddrs = retAddrMap.get(((JumpIndirectInstr) i).getJumpTarget()); for (Label l : retAddrs) { addEdge(currBB, l, forwardRefs); } // Record the target bb for the retaddr var for any set_addr instrs that appear later and // use the same retaddr var retAddrTargetMap.put(((JumpIndirectInstr) i).getJumpTarget(), currBB); } else { throw new RuntimeException( "Unhandled case in CFG builder for basic block ending instr: " + i); } if (tgt != null) addEdge(currBB, tgt, forwardRefs); } else if (iop != Operation.LABEL) { currBB.addInstr(i); } if (i instanceof SetReturnAddressInstr) { Variable v = i.getResult(); Label tgtLbl = ((SetReturnAddressInstr) i).getReturnAddr(); BasicBlock tgtBB = retAddrTargetMap.get(v); // If we have the target bb, add the edge // If not, record it for fixup later if (tgtBB != null) { addEdge(tgtBB, tgtLbl, forwardRefs); } else { Set<Label> addrs = retAddrMap.get(v); if (addrs == null) { addrs = new HashSet<Label>(); retAddrMap.put(v, addrs); } addrs.add(tgtLbl); } } else if (i instanceof CallInstr) { // Build CFG for the closure if there exists one Operand closureArg = ((CallInstr) i).getClosureArg(); if (closureArg instanceof MetaObject) { ((IRClosure) ((MetaObject) closureArg).scope).buildCFG(); } } } // Process all rescued regions for (ExceptionRegion rr : allExceptionRegions) { BasicBlock firstRescueBB = bbMap.get(rr.getFirstRescueBlockLabel()); // 1. Tell the region that firstRescueBB is its protector! rr.setFirstRescueBB(firstRescueBB); // 2. Record a mapping from the region's exclusive basic blocks to the first bb that will // start exception handling for all their exceptions. // 3. Add an exception edge from every exclusive bb of the region to firstRescueBB BasicBlock ensureBlockBB = rr.getEnsureBlockLabel() == null ? null : bbMap.get(rr.getEnsureBlockLabel()); for (BasicBlock b : rr.getExclusiveBBs()) { rescuerMap.put(b, firstRescueBB); graph.addEdge(b, firstRescueBB, EdgeType.EXCEPTION); if (ensureBlockBB != null) { ensurerMap.put(b, ensureBlockBB); // SSS FIXME: This is a conservative edge because when a rescue block is present // that catches an exception, control never reaches the ensure block directly. // Only when we get an error or threadkill even, or when breaks propagate upward // do we need to hit an ensure directly. This edge is present to account for that // control-flow scneario. graph.addEdge(b, ensureBlockBB, EdgeType.EXCEPTION); } } } buildExitBasicBlock( nestedExceptionRegions, firstBB, returnBBs, exceptionBBs, nextBBIsFallThrough, currBB, entryBB); optimize(); // remove useless cfg edges & orphaned bbs return graph; } private void addEdge( BasicBlock src, Label targetLabel, Map<Label, List<BasicBlock>> forwardRefs) { BasicBlock target = bbMap.get(targetLabel); if (target != null) { graph.addEdge(src, target, EdgeType.REGULAR); return; } // Add a forward reference from target -> source List<BasicBlock> forwardReferences = forwardRefs.get(targetLabel); if (forwardReferences == null) { forwardReferences = new ArrayList<BasicBlock>(); forwardRefs.put(targetLabel, forwardReferences); } forwardReferences.add(src); } /** * Create special empty exit BasicBlock that all BasicBlocks will eventually flow into. All Edges * to this 'dummy' BasicBlock will get marked with an edge type of EXIT. * * <p>Special BasicBlocks worth noting: 1. Exceptions, Returns, Entry(why?) -> ExitBB 2. Returns * -> ExitBB */ private BasicBlock buildExitBasicBlock( Stack<ExceptionRegion> nestedExceptionRegions, BasicBlock firstBB, List<BasicBlock> returnBBs, List<BasicBlock> exceptionBBs, boolean nextIsFallThrough, BasicBlock currBB, BasicBlock entryBB) { exitBB = createBB(nestedExceptionRegions); graph.addEdge(entryBB, exitBB, EdgeType.EXIT); graph.addEdge(entryBB, firstBB, EdgeType.FALL_THROUGH); for (BasicBlock rb : returnBBs) { graph.addEdge(rb, exitBB, EdgeType.EXIT); } for (BasicBlock rb : exceptionBBs) { graph.addEdge(rb, exitBB, EdgeType.EXIT); } if (nextIsFallThrough) graph.addEdge(currBB, exitBB, EdgeType.EXIT); return exitBB; } private BasicBlock createBB(Label label, Stack<ExceptionRegion> nestedExceptionRegions) { BasicBlock basicBlock = new BasicBlock(this, label); bbMap.put(label, basicBlock); graph.vertexFor(basicBlock); if (!nestedExceptionRegions.empty()) nestedExceptionRegions.peek().addBB(basicBlock); return basicBlock; } private BasicBlock createBB(Stack<ExceptionRegion> nestedExceptionRegions) { return createBB(scope.getNewLabel(), nestedExceptionRegions); } public void removeEdge(Edge edge) { graph.removeEdge(edge); } private void deleteOrphanedBlocks(DirectedGraph<BasicBlock> graph) { // System.out.println("\nGraph:\n" + getGraph().toString()); // System.out.println("\nInstructions:\n" + toStringInstrs()); // FIXME: Quick and dirty implementation while (true) { BasicBlock bbToRemove = null; for (BasicBlock b : graph.allData()) { if (b == entryBB) continue; // Skip entry bb! // Every other bb should have at least one incoming edge if (graph.vertexFor(b).getIncomingEdges().isEmpty()) { bbToRemove = b; break; } } if (bbToRemove == null) break; removeBB(bbToRemove); } } void removeBB(BasicBlock b) { graph.removeVertexFor(b); bbMap.remove(b.getLabel()); rescuerMap.remove(b); ensurerMap.remove(b); // SSS FIXME: Patch up rescued regions as well?? } private void optimize() { // SSS FIXME: Can't we not add some of these exception edges in the first place?? // Remove exception edges from blocks that couldn't possibly thrown an exception! List<Edge> toRemove = new ArrayList<Edge>(); for (BasicBlock b : graph.allData()) { boolean noExceptions = true; for (Instr i : b.getInstrs()) { if (i.canRaiseException()) { noExceptions = false; break; } } if (noExceptions) { for (Edge<BasicBlock> e : graph.vertexFor(b).getOutgoingEdgesOfType(EdgeType.EXCEPTION)) { BasicBlock source = e.getSource().getData(); BasicBlock destination = e.getDestination().getData(); toRemove.add(e); if (rescuerMap.get(source) == destination) rescuerMap.remove(source); if (ensurerMap.get(source) == destination) ensurerMap.remove(source); } } } if (!toRemove.isEmpty()) { for (Edge edge : toRemove) { graph.removeEdge(edge); } } deleteOrphanedBlocks(graph); } public String toStringGraph() { return graph.toString(); } public String toStringInstrs() { StringBuilder buf = new StringBuilder(); for (BasicBlock b : getSortedBasicBlocks()) { buf.append(b.toStringInstrs()); } buf.append("\n\n------ Rescue block map ------\n"); for (BasicBlock bb : rescuerMap.keySet()) { buf.append("BB ") .append(bb.getID()) .append(" --> BB ") .append(rescuerMap.get(bb).getID()) .append("\n"); } buf.append("\n\n------ Ensure block map ------\n"); for (BasicBlock bb : ensurerMap.keySet()) { buf.append("BB ") .append(bb.getID()) .append(" --> BB ") .append(ensurerMap.get(bb).getID()) .append("\n"); } List<IRClosure> closures = scope.getClosures(); if (!closures.isEmpty()) { buf.append("\n\n------ Closures encountered in this scope ------\n"); for (IRClosure c : closures) { buf.append(c.toStringBody()); } buf.append("------------------------------------------------\n"); } return buf.toString(); } void removeEdge(BasicBlock a, BasicBlock b) { throw new UnsupportedOperationException("Not yet implemented"); } private LinkedList<BasicBlock> buildPostOrderList() { LinkedList<BasicBlock> list = new LinkedList<BasicBlock>(); BasicBlock root = getEntryBB(); Stack<BasicBlock> stack = new Stack<BasicBlock>(); stack.push(root); BitSet bbSet = new BitSet(1 + getMaxNodeID()); bbSet.set(root.getID()); // Non-recursive post-order traversal (the added flag is required to handle cycles and common // ancestors) while (!stack.empty()) { // Check if all children of the top of the stack have been added BasicBlock b = stack.peek(); boolean allChildrenDone = true; for (BasicBlock dst : getOutgoingDestinations(b)) { int dstID = dst.getID(); if (!bbSet.get(dstID)) { allChildrenDone = false; stack.push(dst); bbSet.set(dstID); } } // If all children have been added previously, we are ready with 'b' in this round! if (allChildrenDone) { stack.pop(); list.add(b); } } // Sanity check! for (BasicBlock b : getBasicBlocks()) { if (!bbSet.get(b.getID())) { printError("BB " + b.getID() + " missing from po list!"); break; } } return list; } private void printError(String message) { LOG.error(message + "\nGraph:\n" + this + "\nInstructions:\n" + toStringInstrs()); } }
/** * Implementation of Ruby's <code>Thread</code> class. Each Ruby thread is mapped to an underlying * Java Virtual Machine thread. * * <p>Thread encapsulates the behavior of a thread of execution, including the main thread of the * Ruby script. In the descriptions that follow, the parameter <code>aSymbol</code> refers to a * symbol, which is either a quoted string or a <code>Symbol</code> (such as <code>:name</code>). * * <p>Note: For CVS history, see ThreadClass.java. */ @JRubyClass(name = "Thread") public class RubyThread extends RubyObject implements ExecutionContext { private static final Logger LOG = LoggerFactory.getLogger("RubyThread"); /** The thread-like think that is actually executing */ private ThreadLike threadImpl; /** Normal thread-local variables */ private transient Map<IRubyObject, IRubyObject> threadLocalVariables; /** Context-local variables, internal-ish thread locals */ private final Map<Object, IRubyObject> contextVariables = new WeakHashMap<Object, IRubyObject>(); /** Whether this thread should try to abort the program on exception */ private boolean abortOnException; /** The final value resulting from the thread's execution */ private IRubyObject finalResult; /** * The exception currently being raised out of the thread. We reference it here to continue * propagating it while handling thread shutdown logic and abort_on_exception. */ private RaiseException exitingException; /** The ThreadGroup to which this thread belongs */ private RubyThreadGroup threadGroup; /** Per-thread "current exception" */ private IRubyObject errorInfo; /** Weak reference to the ThreadContext for this thread. */ private volatile WeakReference<ThreadContext> contextRef; private static final boolean DEBUG = false; /** Thread statuses */ public static enum Status { RUN, SLEEP, ABORTING, DEAD } /** Current status in an atomic reference */ private final AtomicReference<Status> status = new AtomicReference<Status>(Status.RUN); /** Mail slot for cross-thread events */ private volatile ThreadService.Event mail; /** The current task blocking a thread, to allow interrupting it in an appropriate way */ private volatile BlockingTask currentBlockingTask; /** The list of locks this thread currently holds, so they can be released on exit */ private final List<Lock> heldLocks = new ArrayList<Lock>(); /** Whether or not this thread has been disposed of */ private volatile boolean disposed = false; /** The thread's initial priority, for use in thread pooled mode */ private int initialPriority; protected RubyThread(Ruby runtime, RubyClass type) { super(runtime, type); finalResult = runtime.getNil(); errorInfo = runtime.getNil(); } public void receiveMail(ThreadService.Event event) { synchronized (this) { // if we're already aborting, we can receive no further mail if (status.get() == Status.ABORTING) return; mail = event; switch (event.type) { case KILL: status.set(Status.ABORTING); } // If this thread is sleeping or stopped, wake it notify(); } // interrupt the target thread in case it's blocking or waiting // WARNING: We no longer interrupt the target thread, since this usually means // interrupting IO and with NIO that means the channel is no longer usable. // We either need a new way to handle waking a target thread that's waiting // on IO, or we need to accept that we can't wake such threads and must wait // for them to complete their operation. // threadImpl.interrupt(); // new interrupt, to hopefully wake it out of any blocking IO this.interrupt(); } public synchronized void checkMail(ThreadContext context) { ThreadService.Event myEvent = mail; mail = null; if (myEvent != null) { switch (myEvent.type) { case RAISE: receivedAnException(context, myEvent.exception); case KILL: throwThreadKill(); } } } public IRubyObject getErrorInfo() { return errorInfo; } public IRubyObject setErrorInfo(IRubyObject errorInfo) { this.errorInfo = errorInfo; return errorInfo; } public void setContext(ThreadContext context) { this.contextRef = new WeakReference<ThreadContext>(context); } public ThreadContext getContext() { return contextRef.get(); } public Thread getNativeThread() { return threadImpl.nativeThread(); } /** * Perform pre-execution tasks once the native thread is running, but we have not yet called the * Ruby code for the thread. */ public void beforeStart() { // store initial priority, for restoring pooled threads to normal initialPriority = threadImpl.getPriority(); // set to "normal" priority threadImpl.setPriority(Thread.NORM_PRIORITY); } /** Dispose of the current thread by tidying up connections to other stuff */ public synchronized void dispose() { if (!disposed) { disposed = true; // remove from parent thread group threadGroup.remove(this); // unlock all locked locks unlockAll(); // reset thread priority to initial if pooling if (Options.THREADPOOL_ENABLED.load()) { threadImpl.setPriority(initialPriority); } // mark thread as DEAD beDead(); // unregister from runtime's ThreadService getRuntime().getThreadService().unregisterThread(this); } } public static RubyClass createThreadClass(Ruby runtime) { // FIXME: In order for Thread to play well with the standard 'new' behavior, // it must provide an allocator that can create empty object instances which // initialize then fills with appropriate data. RubyClass threadClass = runtime.defineClass( "Thread", runtime.getObject(), ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR); runtime.setThread(threadClass); threadClass.index = ClassIndex.THREAD; threadClass.setReifiedClass(RubyThread.class); threadClass.defineAnnotatedMethods(RubyThread.class); RubyThread rubyThread = new RubyThread(runtime, threadClass); // TODO: need to isolate the "current" thread from class creation rubyThread.threadImpl = new NativeThread(rubyThread, Thread.currentThread()); runtime.getThreadService().setMainThread(Thread.currentThread(), rubyThread); // set to default thread group runtime.getDefaultThreadGroup().addDirectly(rubyThread); threadClass.setMarshal(ObjectMarshal.NOT_MARSHALABLE_MARSHAL); return threadClass; } /** * <code>Thread.new</code> * * <p>Thread.new( <i>[ arg ]*</i> ) {| args | block } -> aThread * * <p>Creates a new thread to execute the instructions given in block, and begins running it. Any * arguments passed to Thread.new are passed into the block. * * <pre> * x = Thread.new { sleep .1; print "x"; print "y"; print "z" } * a = Thread.new { print "a"; print "b"; sleep .2; print "c" } * x.join # Let the threads finish before * a.join # main thread exits... * </pre> * * <i>produces:</i> abxyzc */ @JRubyMethod( name = {"new", "fork"}, rest = true, meta = true) public static IRubyObject newInstance(IRubyObject recv, IRubyObject[] args, Block block) { return startThread(recv, args, true, block); } /** * Basically the same as Thread.new . However, if class Thread is subclassed, then calling start * in that subclass will not invoke the subclass's initialize method. */ @JRubyMethod(rest = true, meta = true, compat = RUBY1_8) public static RubyThread start(IRubyObject recv, IRubyObject[] args, Block block) { return startThread(recv, args, false, block); } @JRubyMethod(rest = true, name = "start", meta = true, compat = RUBY1_9) public static RubyThread start19(IRubyObject recv, IRubyObject[] args, Block block) { Ruby runtime = recv.getRuntime(); // The error message may appear incongruous here, due to the difference // between JRuby's Thread model and MRI's. // We mimic MRI's message in the name of compatibility. if (!block.isGiven()) throw runtime.newArgumentError("tried to create Proc object without a block"); return startThread(recv, args, false, block); } public static RubyThread adopt(IRubyObject recv, Thread t) { return adoptThread(recv, t, Block.NULL_BLOCK); } private static RubyThread adoptThread(final IRubyObject recv, Thread t, Block block) { final Ruby runtime = recv.getRuntime(); final RubyThread rubyThread = new RubyThread(runtime, (RubyClass) recv); rubyThread.threadImpl = new NativeThread(rubyThread, t); ThreadContext context = runtime.getThreadService().registerNewThread(rubyThread); runtime.getThreadService().associateThread(t, rubyThread); context.preAdoptThread(); // set to default thread group runtime.getDefaultThreadGroup().addDirectly(rubyThread); return rubyThread; } @JRubyMethod(rest = true, visibility = PRIVATE) public IRubyObject initialize(ThreadContext context, IRubyObject[] args, Block block) { Ruby runtime = getRuntime(); if (!block.isGiven()) throw runtime.newThreadError("must be called with a block"); try { RubyRunnable runnable = new RubyRunnable(this, args, context.getFrames(0), block); if (RubyInstanceConfig.POOLING_ENABLED) { FutureThread futureThread = new FutureThread(this, runnable); threadImpl = futureThread; addToCorrectThreadGroup(context); threadImpl.start(); // JRUBY-2380, associate future early so it shows up in Thread.list right away, in case it // doesn't run immediately runtime.getThreadService().associateThread(futureThread.getFuture(), this); } else { Thread thread = new Thread(runnable); thread.setDaemon(true); thread.setName( "Ruby" + thread.getName() + ": " + context.getFile() + ":" + (context.getLine() + 1)); threadImpl = new NativeThread(this, thread); addToCorrectThreadGroup(context); // JRUBY-2380, associate thread early so it shows up in Thread.list right away, in case it // doesn't run immediately runtime.getThreadService().associateThread(thread, this); threadImpl.start(); } // We yield here to hopefully permit the target thread to schedule // MRI immediately schedules it, so this is close but not exact Thread.yield(); return this; } catch (OutOfMemoryError oome) { if (oome.getMessage().equals("unable to create new native thread")) { throw runtime.newThreadError(oome.getMessage()); } throw oome; } catch (SecurityException ex) { throw runtime.newThreadError(ex.getMessage()); } } private static RubyThread startThread( final IRubyObject recv, final IRubyObject[] args, boolean callInit, Block block) { RubyThread rubyThread = new RubyThread(recv.getRuntime(), (RubyClass) recv); if (callInit) { rubyThread.callInit(args, block); } else { // for Thread::start, which does not call the subclass's initialize rubyThread.initialize(recv.getRuntime().getCurrentContext(), args, block); } return rubyThread; } public synchronized void cleanTerminate(IRubyObject result) { finalResult = result; } public synchronized void beDead() { status.set(Status.DEAD); } public void pollThreadEvents() { pollThreadEvents(getRuntime().getCurrentContext()); } public void pollThreadEvents(ThreadContext context) { if (mail != null) checkMail(context); } private static void throwThreadKill() { throw new ThreadKill(); } /** * Returns the status of the global ``abort on exception'' condition. The default is false. When * set to true, will cause all threads to abort (the process will exit(0)) if an exception is * raised in any thread. See also Thread.abort_on_exception= . */ @JRubyMethod(name = "abort_on_exception", meta = true) public static RubyBoolean abort_on_exception_x(IRubyObject recv) { Ruby runtime = recv.getRuntime(); return runtime.isGlobalAbortOnExceptionEnabled() ? runtime.getTrue() : runtime.getFalse(); } @JRubyMethod(name = "abort_on_exception=", required = 1, meta = true) public static IRubyObject abort_on_exception_set_x(IRubyObject recv, IRubyObject value) { recv.getRuntime().setGlobalAbortOnExceptionEnabled(value.isTrue()); return value; } @JRubyMethod(name = "current", meta = true) public static RubyThread current(IRubyObject recv) { return recv.getRuntime().getCurrentContext().getThread(); } @JRubyMethod(name = "main", meta = true) public static RubyThread main(IRubyObject recv) { return recv.getRuntime().getThreadService().getMainThread(); } @JRubyMethod(name = "pass", meta = true) public static IRubyObject pass(IRubyObject recv) { Ruby runtime = recv.getRuntime(); ThreadService ts = runtime.getThreadService(); boolean critical = ts.getCritical(); ts.setCritical(false); Thread.yield(); ts.setCritical(critical); return recv.getRuntime().getNil(); } @JRubyMethod(name = "list", meta = true) public static RubyArray list(IRubyObject recv) { RubyThread[] activeThreads = recv.getRuntime().getThreadService().getActiveRubyThreads(); return recv.getRuntime().newArrayNoCopy(activeThreads); } private void addToCorrectThreadGroup(ThreadContext context) { // JRUBY-3568, inherit threadgroup or use default IRubyObject group = context.getThread().group(); if (!group.isNil()) { ((RubyThreadGroup) group).addDirectly(this); } else { context.runtime.getDefaultThreadGroup().addDirectly(this); } } private IRubyObject getSymbolKey(IRubyObject originalKey) { if (originalKey instanceof RubySymbol) { return originalKey; } else if (originalKey instanceof RubyString) { return getRuntime().newSymbol(originalKey.asJavaString()); } else if (originalKey instanceof RubyFixnum) { getRuntime().getWarnings().warn(ID.FIXNUMS_NOT_SYMBOLS, "Do not use Fixnums as Symbols"); throw getRuntime().newArgumentError(originalKey + " is not a symbol"); } else { throw getRuntime().newTypeError(originalKey + " is not a symbol"); } } private synchronized Map<IRubyObject, IRubyObject> getThreadLocals() { if (threadLocalVariables == null) { threadLocalVariables = new HashMap<IRubyObject, IRubyObject>(); } return threadLocalVariables; } private void clearThreadLocals() { threadLocalVariables = null; } public final Map<Object, IRubyObject> getContextVariables() { return contextVariables; } public boolean isAlive() { return threadImpl.isAlive() && status.get() != Status.ABORTING; } @JRubyMethod(name = "[]", required = 1) public IRubyObject op_aref(IRubyObject key) { IRubyObject value; if ((value = getThreadLocals().get(getSymbolKey(key))) != null) { return value; } return getRuntime().getNil(); } @JRubyMethod(name = "[]=", required = 2) public IRubyObject op_aset(IRubyObject key, IRubyObject value) { key = getSymbolKey(key); getThreadLocals().put(key, value); return value; } @JRubyMethod(name = "abort_on_exception") public RubyBoolean abort_on_exception() { return abortOnException ? getRuntime().getTrue() : getRuntime().getFalse(); } @JRubyMethod(name = "abort_on_exception=", required = 1) public IRubyObject abort_on_exception_set(IRubyObject val) { abortOnException = val.isTrue(); return val; } @JRubyMethod(name = "alive?") public RubyBoolean alive_p() { return isAlive() ? getRuntime().getTrue() : getRuntime().getFalse(); } @JRubyMethod(name = "join", optional = 1) public IRubyObject join(IRubyObject[] args) { Ruby runtime = getRuntime(); long timeoutMillis = Long.MAX_VALUE; if (args.length > 0 && !args[0].isNil()) { if (args.length > 1) { throw getRuntime().newArgumentError(args.length, 1); } // MRI behavior: value given in seconds; converted to Float; less // than or equal to zero returns immediately; returns nil timeoutMillis = (long) (1000.0D * args[0].convertToFloat().getValue()); if (timeoutMillis <= 0) { // TODO: not sure that we should skip calling join() altogether. // Thread.join() has some implications for Java Memory Model, etc. if (threadImpl.isAlive()) { return getRuntime().getNil(); } else { return this; } } } if (isCurrent()) { throw getRuntime().newThreadError("thread " + identityString() + " tried to join itself"); } try { if (runtime.getThreadService().getCritical()) { // If the target thread is sleeping or stopped, wake it synchronized (this) { notify(); } // interrupt the target thread in case it's blocking or waiting // WARNING: We no longer interrupt the target thread, since this usually means // interrupting IO and with NIO that means the channel is no longer usable. // We either need a new way to handle waking a target thread that's waiting // on IO, or we need to accept that we can't wake such threads and must wait // for them to complete their operation. // threadImpl.interrupt(); } RubyThread currentThread = getRuntime().getCurrentContext().getThread(); final long timeToWait = Math.min(timeoutMillis, 200); // We need this loop in order to be able to "unblock" the // join call without actually calling interrupt. long start = System.currentTimeMillis(); while (true) { currentThread.pollThreadEvents(); threadImpl.join(timeToWait); if (!threadImpl.isAlive()) { break; } if (System.currentTimeMillis() - start > timeoutMillis) { break; } } } catch (InterruptedException ie) { ie.printStackTrace(); assert false : ie; } catch (ExecutionException ie) { ie.printStackTrace(); assert false : ie; } if (exitingException != null) { // Set $! in the current thread before exiting getRuntime().getGlobalVariables().set("$!", (IRubyObject) exitingException.getException()); throw exitingException; } if (threadImpl.isAlive()) { return getRuntime().getNil(); } else { return this; } } @JRubyMethod public IRubyObject value() { join(new IRubyObject[0]); synchronized (this) { return finalResult; } } @JRubyMethod public IRubyObject group() { if (threadGroup == null) { return getRuntime().getNil(); } return threadGroup; } void setThreadGroup(RubyThreadGroup rubyThreadGroup) { threadGroup = rubyThreadGroup; } @JRubyMethod(name = "inspect") @Override public synchronized IRubyObject inspect() { // FIXME: There's some code duplication here with RubyObject#inspect StringBuilder part = new StringBuilder(); String cname = getMetaClass().getRealClass().getName(); part.append("#<").append(cname).append(":"); part.append(identityString()); part.append(' '); part.append(status.toString().toLowerCase()); part.append('>'); return getRuntime().newString(part.toString()); } @JRubyMethod(name = "key?", required = 1) public RubyBoolean key_p(IRubyObject key) { key = getSymbolKey(key); return getRuntime().newBoolean(getThreadLocals().containsKey(key)); } @JRubyMethod(name = "keys") public RubyArray keys() { IRubyObject[] keys = new IRubyObject[getThreadLocals().size()]; return RubyArray.newArrayNoCopy(getRuntime(), getThreadLocals().keySet().toArray(keys)); } @JRubyMethod(name = "critical=", required = 1, meta = true, compat = CompatVersion.RUBY1_8) public static IRubyObject critical_set(IRubyObject receiver, IRubyObject value) { receiver.getRuntime().getThreadService().setCritical(value.isTrue()); return value; } @JRubyMethod(name = "critical", meta = true, compat = CompatVersion.RUBY1_8) public static IRubyObject critical(IRubyObject receiver) { return receiver.getRuntime().newBoolean(receiver.getRuntime().getThreadService().getCritical()); } @JRubyMethod(name = "stop", meta = true) public static IRubyObject stop(ThreadContext context, IRubyObject receiver) { RubyThread rubyThread = context.getThread(); synchronized (rubyThread) { rubyThread.checkMail(context); try { // attempt to decriticalize all if we're the critical thread receiver.getRuntime().getThreadService().setCritical(false); rubyThread.status.set(Status.SLEEP); rubyThread.wait(); } catch (InterruptedException ie) { rubyThread.checkMail(context); rubyThread.status.set(Status.RUN); } } return receiver.getRuntime().getNil(); } @JRubyMethod(required = 1, meta = true) public static IRubyObject kill(IRubyObject receiver, IRubyObject rubyThread, Block block) { if (!(rubyThread instanceof RubyThread)) throw receiver.getRuntime().newTypeError(rubyThread, receiver.getRuntime().getThread()); return ((RubyThread) rubyThread).kill(); } @JRubyMethod(meta = true) public static IRubyObject exit(IRubyObject receiver, Block block) { RubyThread rubyThread = receiver.getRuntime().getThreadService().getCurrentContext().getThread(); synchronized (rubyThread) { rubyThread.status.set(Status.ABORTING); rubyThread.mail = null; receiver.getRuntime().getThreadService().setCritical(false); throw new ThreadKill(); } } @JRubyMethod(name = "stop?") public RubyBoolean stop_p() { // not valid for "dead" state return getRuntime().newBoolean(status.get() == Status.SLEEP || status.get() == Status.DEAD); } @JRubyMethod(name = "wakeup") public synchronized RubyThread wakeup() { if (!threadImpl.isAlive() && status.get() == Status.DEAD) { throw getRuntime().newThreadError("killed thread"); } status.set(Status.RUN); notifyAll(); return this; } @JRubyMethod(name = "priority") public RubyFixnum priority() { return RubyFixnum.newFixnum(getRuntime(), threadImpl.getPriority()); } @JRubyMethod(name = "priority=", required = 1) public IRubyObject priority_set(IRubyObject priority) { // FIXME: This should probably do some translation from Ruby priority levels to Java priority // levels (until we have green threads) int iPriority = RubyNumeric.fix2int(priority); if (iPriority < Thread.MIN_PRIORITY) { iPriority = Thread.MIN_PRIORITY; } else if (iPriority > Thread.MAX_PRIORITY) { iPriority = Thread.MAX_PRIORITY; } if (threadImpl.isAlive()) { threadImpl.setPriority(iPriority); } return RubyFixnum.newFixnum(getRuntime(), iPriority); } @JRubyMethod(optional = 3) public IRubyObject raise(IRubyObject[] args, Block block) { Ruby runtime = getRuntime(); ThreadContext context = runtime.getCurrentContext(); if (this == context.getThread()) { return RubyKernel.raise(context, runtime.getKernel(), args, block); } debug(this, "before raising"); RubyThread currentThread = getRuntime().getCurrentContext().getThread(); debug(this, "raising"); IRubyObject exception = prepareRaiseException(runtime, args, block); runtime .getThreadService() .deliverEvent( new ThreadService.Event( currentThread, this, ThreadService.Event.Type.RAISE, exception)); return this; } /** * This is intended to be used to raise exceptions in Ruby threads from non- Ruby threads like * Timeout's thread. * * @param args Same args as for Thread#raise * @param block Same as for Thread#raise */ public void internalRaise(IRubyObject[] args) { Ruby runtime = getRuntime(); IRubyObject exception = prepareRaiseException(runtime, args, Block.NULL_BLOCK); receiveMail(new ThreadService.Event(this, this, ThreadService.Event.Type.RAISE, exception)); } private IRubyObject prepareRaiseException(Ruby runtime, IRubyObject[] args, Block block) { if (args.length == 0) { IRubyObject lastException = errorInfo; if (lastException.isNil()) { return new RaiseException(runtime, runtime.getRuntimeError(), "", false).getException(); } return lastException; } IRubyObject exception; ThreadContext context = getRuntime().getCurrentContext(); if (args.length == 1) { if (args[0] instanceof RubyString) { return runtime.getRuntimeError().newInstance(context, args, block); } if (!args[0].respondsTo("exception")) { return runtime.newTypeError("exception class/object expected").getException(); } exception = args[0].callMethod(context, "exception"); } else { if (!args[0].respondsTo("exception")) { return runtime.newTypeError("exception class/object expected").getException(); } exception = args[0].callMethod(context, "exception", args[1]); } if (!runtime.getException().isInstance(exception)) { return runtime.newTypeError("exception object expected").getException(); } if (args.length == 3) { ((RubyException) exception).set_backtrace(args[2]); } return exception; } @JRubyMethod(name = "run") public synchronized IRubyObject run() { return wakeup(); } /** * We can never be sure if a wait will finish because of a Java "spurious wakeup". So if we * explicitly wakeup and we wait less than requested amount we will return false. We will return * true if we sleep right amount or less than right amount via spurious wakeup. */ public synchronized boolean sleep(long millis) throws InterruptedException { assert this == getRuntime().getCurrentContext().getThread(); boolean result = true; synchronized (this) { pollThreadEvents(); try { status.set(Status.SLEEP); if (millis == -1) { wait(); } else { wait(millis); } } finally { result = (status.get() != Status.RUN); pollThreadEvents(); status.set(Status.RUN); } } return result; } @JRubyMethod(name = "status") public synchronized IRubyObject status() { if (threadImpl.isAlive()) { // TODO: no java stringity return getRuntime().newString(status.toString().toLowerCase()); } else if (exitingException != null) { return getRuntime().getNil(); } else { return getRuntime().getFalse(); } } public static interface BlockingTask { public void run() throws InterruptedException; public void wakeup(); } public static final class SleepTask implements BlockingTask { private final Object object; private final long millis; private final int nanos; public SleepTask(Object object, long millis, int nanos) { this.object = object; this.millis = millis; this.nanos = nanos; } public void run() throws InterruptedException { synchronized (object) { object.wait(millis, nanos); } } public void wakeup() { synchronized (object) { object.notify(); } } } public void executeBlockingTask(BlockingTask task) throws InterruptedException { enterSleep(); try { currentBlockingTask = task; pollThreadEvents(); task.run(); } finally { exitSleep(); currentBlockingTask = null; pollThreadEvents(); } } public void enterSleep() { status.set(Status.SLEEP); } public void exitSleep() { status.set(Status.RUN); } @JRubyMethod(name = {"kill", "exit", "terminate"}) public IRubyObject kill() { // need to reexamine this RubyThread currentThread = getRuntime().getCurrentContext().getThread(); // If the killee thread is the same as the killer thread, just die if (currentThread == this) throwThreadKill(); debug(this, "trying to kill"); currentThread.pollThreadEvents(); getRuntime() .getThreadService() .deliverEvent(new ThreadService.Event(currentThread, this, ThreadService.Event.Type.KILL)); debug(this, "succeeded with kill"); return this; } private static void debug(RubyThread thread, String message) { if (DEBUG) LOG.debug(Thread.currentThread() + "(" + thread.status + "): " + message); } @JRubyMethod( name = {"kill!", "exit!", "terminate!"}, compat = RUBY1_8) public IRubyObject kill_bang() { throw getRuntime() .newNotImplementedError( "Thread#kill!, exit!, and terminate! are not safe and not supported"); } @JRubyMethod(name = "safe_level") public IRubyObject safe_level() { throw getRuntime().newNotImplementedError("Thread-specific SAFE levels are not supported"); } @JRubyMethod(compat = CompatVersion.RUBY1_9) public IRubyObject backtrace(ThreadContext context) { return getContext().createCallerBacktrace(context.runtime, 0); } public StackTraceElement[] javaBacktrace() { if (threadImpl instanceof NativeThread) { return ((NativeThread) threadImpl).getThread().getStackTrace(); } // Future-based threads can't get a Java trace return new StackTraceElement[0]; } private boolean isCurrent() { return threadImpl.isCurrent(); } public void exceptionRaised(RaiseException exception) { assert isCurrent(); RubyException rubyException = exception.getException(); Ruby runtime = rubyException.getRuntime(); if (runtime.getSystemExit().isInstance(rubyException)) { runtime .getThreadService() .getMainThread() .raise(new IRubyObject[] {rubyException}, Block.NULL_BLOCK); } else if (abortOnException(runtime)) { RubyException systemExit; if (!runtime.is1_9()) { runtime.printError(rubyException); systemExit = RubySystemExit.newInstance(runtime, 1); systemExit.message = rubyException.message; systemExit.set_backtrace(rubyException.backtrace()); } else { systemExit = rubyException; } runtime .getThreadService() .getMainThread() .raise(new IRubyObject[] {systemExit}, Block.NULL_BLOCK); return; } else if (runtime.getDebug().isTrue()) { runtime.printError(exception.getException()); } exitingException = exception; } /** * For handling all non-Ruby exceptions bubbling out of threads * * @param exception */ @SuppressWarnings("deprecation") public void exceptionRaised(Throwable exception) { if (exception instanceof RaiseException) { exceptionRaised((RaiseException) exception); return; } assert isCurrent(); Ruby runtime = getRuntime(); if (abortOnException(runtime) && exception instanceof Error) { // re-propagate on main thread runtime.getThreadService().getMainThread().getNativeThread().stop(exception); } else { // just rethrow on this thread, let system handlers report it UnsafeFactory.getUnsafe().throwException(exception); } } private boolean abortOnException(Ruby runtime) { return (runtime.isGlobalAbortOnExceptionEnabled() || abortOnException); } public static RubyThread mainThread(IRubyObject receiver) { return receiver.getRuntime().getThreadService().getMainThread(); } private volatile Selector currentSelector; @Deprecated public boolean selectForAccept(RubyIO io) { return select(io, SelectionKey.OP_ACCEPT); } private synchronized Selector getSelector(SelectableChannel channel) throws IOException { return SelectorFactory.openWithRetryFrom(getRuntime(), channel.provider()); } public boolean select(RubyIO io, int ops) { return select(io.getChannel(), io, ops); } public boolean select(RubyIO io, int ops, long timeout) { return select(io.getChannel(), io, ops, timeout); } public boolean select(Channel channel, RubyIO io, int ops) { return select(channel, io, ops, -1); } public boolean select(Channel channel, RubyIO io, int ops, long timeout) { if (channel instanceof SelectableChannel) { SelectableChannel selectable = (SelectableChannel) channel; synchronized (selectable.blockingLock()) { boolean oldBlocking = selectable.isBlocking(); SelectionKey key = null; try { selectable.configureBlocking(false); if (io != null) io.addBlockingThread(this); currentSelector = getRuntime().getSelectorPool().get(selectable.provider()); key = selectable.register(currentSelector, ops); beforeBlockingCall(); int result; if (timeout < 0) { result = currentSelector.select(); } else if (timeout == 0) { result = currentSelector.selectNow(); } else { result = currentSelector.select(timeout); } // check for thread events, in case we've been woken up to die pollThreadEvents(); if (result == 1) { Set<SelectionKey> keySet = currentSelector.selectedKeys(); if (keySet.iterator().next() == key) { return true; } } return false; } catch (IOException ioe) { throw getRuntime().newIOErrorFromException(ioe); } finally { // Note: I don't like ignoring these exceptions, but it's // unclear how likely they are to happen or what damage we // might do by ignoring them. Note that the pieces are separate // so that we can ensure one failing does not affect the others // running. // clean up the key in the selector try { if (key != null) key.cancel(); if (currentSelector != null) currentSelector.selectNow(); } catch (Exception e) { // ignore } // shut down and null out the selector try { if (currentSelector != null) { getRuntime().getSelectorPool().put(currentSelector); } } catch (Exception e) { // ignore } finally { currentSelector = null; } // remove this thread as a blocker against the given IO if (io != null) io.removeBlockingThread(this); // go back to previous blocking state on the selectable try { selectable.configureBlocking(oldBlocking); } catch (Exception e) { // ignore } // clear thread state from blocking call afterBlockingCall(); } } } else { // can't select, just have to do a blocking call return true; } } public void interrupt() { Selector activeSelector = currentSelector; if (activeSelector != null) { activeSelector.wakeup(); } BlockingIO.Condition iowait = blockingIO; if (iowait != null) { iowait.cancel(); } BlockingTask task = currentBlockingTask; if (task != null) { task.wakeup(); } } private volatile BlockingIO.Condition blockingIO = null; public boolean waitForIO(ThreadContext context, RubyIO io, int ops) { Channel channel = io.getChannel(); if (!(channel instanceof SelectableChannel)) { return true; } try { io.addBlockingThread(this); blockingIO = BlockingIO.newCondition(channel, ops); boolean ready = blockingIO.await(); // check for thread events, in case we've been woken up to die pollThreadEvents(); return ready; } catch (IOException ioe) { throw context.runtime.newRuntimeError("Error with selector: " + ioe); } catch (InterruptedException ex) { // FIXME: not correct exception throw context.runtime.newRuntimeError("Interrupted"); } finally { blockingIO = null; io.removeBlockingThread(this); } } public void beforeBlockingCall() { pollThreadEvents(); enterSleep(); } public void afterBlockingCall() { exitSleep(); pollThreadEvents(); } private void receivedAnException(ThreadContext context, IRubyObject exception) { RubyModule kernelModule = getRuntime().getKernel(); debug(this, "before propagating exception"); kernelModule.callMethod(context, "raise", exception); } public boolean wait_timeout(IRubyObject o, Double timeout) throws InterruptedException { if (timeout != null) { long delay_ns = (long) (timeout.doubleValue() * 1000000000.0); long start_ns = System.nanoTime(); if (delay_ns > 0) { long delay_ms = delay_ns / 1000000; int delay_ns_remainder = (int) (delay_ns % 1000000); executeBlockingTask(new SleepTask(o, delay_ms, delay_ns_remainder)); } long end_ns = System.nanoTime(); return (end_ns - start_ns) <= delay_ns; } else { executeBlockingTask(new SleepTask(o, 0, 0)); return true; } } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final RubyThread other = (RubyThread) obj; if (this.threadImpl != other.threadImpl && (this.threadImpl == null || !this.threadImpl.equals(other.threadImpl))) { return false; } return true; } @Override public int hashCode() { int hash = 3; hash = 97 * hash + (this.threadImpl != null ? this.threadImpl.hashCode() : 0); return hash; } public String toString() { return threadImpl.toString(); } /** * Acquire the given lock, holding a reference to it for cleanup on thread termination. * * @param lock the lock to acquire, released on thread termination */ public void lock(Lock lock) { assert Thread.currentThread() == getNativeThread(); lock.lock(); heldLocks.add(lock); } /** * Acquire the given lock interruptibly, holding a reference to it for cleanup on thread * termination. * * @param lock the lock to acquire, released on thread termination * @throws InterruptedException if the lock acquisition is interrupted */ public void lockInterruptibly(Lock lock) throws InterruptedException { assert Thread.currentThread() == getNativeThread(); lock.lockInterruptibly(); heldLocks.add(lock); } /** * Try to acquire the given lock, adding it to a list of held locks for cleanup on thread * termination if it is acquired. Return immediately if the lock cannot be acquired. * * @param lock the lock to acquire, released on thread termination */ public boolean tryLock(Lock lock) { assert Thread.currentThread() == getNativeThread(); boolean locked = lock.tryLock(); if (locked) { heldLocks.add(lock); } return locked; } /** * Release the given lock and remove it from the list of locks to be released on thread * termination. * * @param lock the lock to release and dereferences */ public void unlock(Lock lock) { assert Thread.currentThread() == getNativeThread(); lock.unlock(); heldLocks.remove(lock); } /** Release all locks held. */ public void unlockAll() { assert Thread.currentThread() == getNativeThread(); for (Lock lock : heldLocks) { lock.unlock(); } } private String identityString() { return "0x" + Integer.toHexString(System.identityHashCode(this)); } }
public class InterpretedIRBlockBody extends IRBlockBody implements Compilable<InterpreterContext> { private static final Logger LOG = LoggerFactory.getLogger("InterpretedIRBlockBody"); protected boolean pushScope; protected boolean reuseParentScope; private boolean displayedCFG = false; // FIXME: Remove when we find nicer way of logging CFG private int callCount = 0; private InterpreterContext interpreterContext; private InterpreterContext fullInterpreterContext; public InterpretedIRBlockBody(IRClosure closure, Signature signature) { super(closure, signature); this.pushScope = true; this.reuseParentScope = false; // JIT currently JITs blocks along with their method and no on-demand by themselves. We only // promote to full build here if we are -X-C. if (closure.getManager().getInstanceConfig().getCompileMode().shouldJIT() || Options.JIT_THRESHOLD.load() == -1) { callCount = -1; } } @Override public void setCallCount(int callCount) { this.callCount = callCount; } @Override public void completeBuild(InterpreterContext interpreterContext) { this.fullInterpreterContext = interpreterContext; // This enables IR & CFG to be dumped in debug mode // when this updated code starts executing. this.displayedCFG = false; } @Override public IRScope getIRScope() { return closure; } @Override public ArgumentDescriptor[] getArgumentDescriptors() { return closure.getArgumentDescriptors(); } public InterpreterContext ensureInstrsReady() { if (IRRuntimeHelpers.isDebug() && !displayedCFG) { LOG.info( "Executing '" + closure + "' (pushScope=" + pushScope + ", reuseParentScope=" + reuseParentScope); LOG.info(closure.debugOutput()); displayedCFG = true; } if (interpreterContext == null) { if (Options.IR_PRINT.load()) { ByteArrayOutputStream baos = IRDumper.printIR(closure, false); LOG.info( "Printing simple IR for " + closure.getName(), "\n" + new String(baos.toByteArray())); } interpreterContext = closure.getInterpreterContext(); fullInterpreterContext = interpreterContext; } return interpreterContext; } @Override public String getClassName(ThreadContext context) { return null; } @Override public String getName() { return null; } @Override public boolean canCallDirect() { return interpreterContext != null && interpreterContext.hasExplicitCallProtocol(); } @Override protected IRubyObject callDirect( ThreadContext context, Block block, IRubyObject[] args, Block blockArg) { context.setCurrentBlockType(Block.Type.PROC); InterpreterContext ic = ensureInstrsReady(); // so we get debugging output return Interpreter.INTERPRET_BLOCK( context, block, null, ic, args, block.getBinding().getMethod(), blockArg); } @Override protected IRubyObject yieldDirect( ThreadContext context, Block block, IRubyObject[] args, IRubyObject self) { context.setCurrentBlockType(Block.Type.NORMAL); InterpreterContext ic = ensureInstrsReady(); // so we get debugging output return Interpreter.INTERPRET_BLOCK( context, block, self, ic, args, block.getBinding().getMethod(), Block.NULL_BLOCK); } @Override protected IRubyObject commonYieldPath( ThreadContext context, Block block, Block.Type type, IRubyObject[] args, IRubyObject self, Block blockArg) { if (callCount >= 0) promoteToFullBuild(context); InterpreterContext ic = ensureInstrsReady(); // Update interpreter context for next time this block is executed // This ensures that if we had determined canCallDirect() is false // based on the old IC, we continue to execute with it. interpreterContext = fullInterpreterContext; Binding binding = block.getBinding(); Visibility oldVis = binding.getFrame().getVisibility(); Frame prevFrame = context.preYieldNoScope(binding); // SSS FIXME: Maybe, we should allocate a NoVarsScope/DummyScope for for-loop bodies because the // static-scope here // probably points to the parent scope? To be verified and fixed if necessary. There is no harm // as it is now. It // is just wasteful allocation since the scope is not used at all. DynamicScope actualScope = binding.getDynamicScope(); if (ic.pushNewDynScope()) { context.pushScope(block.allocScope(actualScope)); } else if (ic.reuseParentDynScope()) { // Reuse! We can avoid the push only if surrounding vars aren't referenced! context.pushScope(actualScope); } self = IRRuntimeHelpers.updateBlockState(block, self); try { return Interpreter.INTERPRET_BLOCK( context, block, self, ic, args, binding.getMethod(), blockArg); } finally { // IMPORTANT: Do not clear eval-type in case this is reused in bindings! // Ex: eval("...", foo.instance_eval { binding }) // The dyn-scope used for binding needs to have its eval-type set to INSTANCE_EVAL binding.getFrame().setVisibility(oldVis); if (ic.popDynScope()) { context.postYield(binding, prevFrame); } else { context.postYieldNoScope(prevFrame); } } } // Unlike JIT in MixedMode this will always successfully build but if using executor pool it may // take a while // and replace interpreterContext asynchronously. protected void promoteToFullBuild(ThreadContext context) { if (context.runtime.isBooting()) return; // don't Promote to full build during runtime boot if (callCount++ >= Options.JIT_THRESHOLD.load()) context.runtime.getJITCompiler().buildThresholdReached(context, this); } public RubyModule getImplementationClass() { return null; } }
public class Interpreter { private static class IRCallSite { IRScope s; int v; // scope version CallBase call; long count; InterpretedIRMethod tgtM; public IRCallSite() {} public IRCallSite(IRCallSite cs) { this.s = cs.s; this.v = cs.v; this.call = cs.call; this.count = 0; } public int hashCode() { return (int) this.call.callSiteId; } } private static class CallSiteProfile { IRCallSite cs; HashMap<IRScope, Counter> counters; public CallSiteProfile(IRCallSite cs) { this.cs = new IRCallSite(cs); this.counters = new HashMap<IRScope, Counter>(); } } private static IRCallSite callerSite = new IRCallSite(); private static final Logger LOG = LoggerFactory.getLogger("Interpreter"); private static int versionCount = 1; private static HashMap<IRScope, Integer> scopeVersionMap = new HashMap<IRScope, Integer>(); private static int inlineCount = 0; private static int interpInstrsCount = 0; private static int codeModificationsCount = 0; private static int numCyclesWithNoModifications = 0; private static int globalThreadPollCount = 0; private static HashMap<IRScope, Counter> scopeThreadPollCounts = new HashMap<IRScope, Counter>(); private static HashMap<Long, CallSiteProfile> callProfile = new HashMap<Long, CallSiteProfile>(); private static HashMap<Operation, Counter> opStats = new HashMap<Operation, Counter>(); private static IRScope getEvalContainerScope(Ruby runtime, StaticScope evalScope) { // SSS FIXME: Weirdness here. We cannot get the containing IR scope from evalScope because of // static-scope wrapping // that is going on // 1. In all cases, DynamicScope.getEvalScope wraps the executing static scope in a new local // scope. // 2. For instance-eval (module-eval, class-eval) scenarios, there is an extra scope that is // added to // the stack in ThreadContext.java:preExecuteUnder // I dont know what rule to apply when. However, in both these cases, since there is no // IR-scope associated, // I have used the hack below where I first unwrap once and see if I get a non-null IR scope. // If that doesn't // work, I unwarp once more and I am guaranteed to get the IR scope I want. IRScope containingIRScope = ((IRStaticScope) evalScope.getEnclosingScope()).getIRScope(); if (containingIRScope == null) containingIRScope = ((IRStaticScope) evalScope.getEnclosingScope().getEnclosingScope()).getIRScope(); return containingIRScope; } public static IRubyObject interpretCommonEval( Ruby runtime, String file, int lineNumber, String backtraceName, RootNode rootNode, IRubyObject self, Block block) { // SSS FIXME: Is this required here since the IR version cannot change from eval-to-eval? This // is much more of a global setting. boolean is_1_9 = runtime.is1_9(); if (is_1_9) IRBuilder.setRubyVersion("1.9"); StaticScope ss = rootNode.getStaticScope(); IRScope containingIRScope = getEvalContainerScope(runtime, ss); IREvalScript evalScript = IRBuilder.createIRBuilder(runtime, runtime.getIRManager()) .buildEvalRoot(ss, containingIRScope, file, lineNumber, rootNode); evalScript.prepareForInterpretation(false); // evalScript.runCompilerPass(new CallSplitter()); ThreadContext context = runtime.getCurrentContext(); runBeginEndBlocks( evalScript.getBeginBlocks(), context, self, null); // FIXME: No temp vars yet right? IRubyObject rv = evalScript.call( context, self, evalScript.getStaticScope().getModule(), rootNode.getScope(), block, backtraceName); runBeginEndBlocks(evalScript.getEndBlocks(), context, self, null); // FIXME: No temp vars right? return rv; } public static IRubyObject interpretSimpleEval( Ruby runtime, String file, int lineNumber, String backtraceName, Node node, IRubyObject self) { return interpretCommonEval( runtime, file, lineNumber, backtraceName, (RootNode) node, self, Block.NULL_BLOCK); } public static IRubyObject interpretBindingEval( Ruby runtime, String file, int lineNumber, String backtraceName, Node node, IRubyObject self, Block block) { return interpretCommonEval( runtime, file, lineNumber, backtraceName, (RootNode) node, self, block); } public static void runBeginEndBlocks( List<IRClosure> beBlocks, ThreadContext context, IRubyObject self, Object[] temp) { if (beBlocks == null) return; for (IRClosure b : beBlocks) { // SSS FIXME: Should I piggyback on WrappedIRClosure.retrieve or just copy that code here? b.prepareForInterpretation(false); Block blk = (Block) (new WrappedIRClosure(b)).retrieve(context, self, context.getCurrentScope(), temp); blk.yield(context, null); } } public static IRubyObject interpret(Ruby runtime, Node rootNode, IRubyObject self) { if (runtime.is1_9()) IRBuilder.setRubyVersion("1.9"); IRScriptBody root = (IRScriptBody) IRBuilder.createIRBuilder(runtime, runtime.getIRManager()) .buildRoot((RootNode) rootNode); // We get the live object ball rolling here. This give a valid value for the top // of this lexical tree. All new scope can then retrieve and set based on lexical parent. if (root.getStaticScope().getModule() == null) { // If an eval this may already be setup. root.getStaticScope().setModule(runtime.getObject()); } RubyModule currModule = root.getStaticScope().getModule(); // Scope state for root? IRStaticScopeFactory.newIRLocalScope(null).setModule(currModule); ThreadContext context = runtime.getCurrentContext(); try { runBeginEndBlocks( root.getBeginBlocks(), context, self, null); // FIXME: No temp vars yet...not needed? InterpretedIRMethod method = new InterpretedIRMethod(root, currModule); IRubyObject rv = method.call(context, self, currModule, "(root)", IRubyObject.NULL_ARRAY); runBeginEndBlocks( root.getEndBlocks(), context, self, null); // FIXME: No temp vars yet...not needed? if ((IRRuntimeHelpers.isDebug() || IRRuntimeHelpers.inProfileMode()) && interpInstrsCount > 10000) { LOG.info("-- Interpreted instructions: {}", interpInstrsCount); /* for (Operation o: opStats.keySet()) { System.out.println(o + " = " + opStats.get(o).count); } */ } return rv; } catch (IRBreakJump bj) { throw IRException.BREAK_LocalJumpError.getException(context.runtime); } } private static void analyzeProfile() { versionCount++; // if (inlineCount == 2) return; if (codeModificationsCount == 0) numCyclesWithNoModifications++; else numCyclesWithNoModifications = 0; codeModificationsCount = 0; if (numCyclesWithNoModifications < 3) return; // We are now good to go -- start analyzing the profile // System.out.println("-------------------start analysis-----------------------"); final HashMap<IRScope, Long> scopeCounts = new HashMap<IRScope, Long>(); final ArrayList<IRCallSite> callSites = new ArrayList<IRCallSite>(); HashMap<IRCallSite, Long> callSiteCounts = new HashMap<IRCallSite, Long>(); // System.out.println("# call sites: " + callProfile.keySet().size()); long total = 0; for (Long id : callProfile.keySet()) { Long c; CallSiteProfile csp = callProfile.get(id); IRCallSite cs = csp.cs; if (cs.v != scopeVersionMap.get(cs.s).intValue()) { // System.out.println("Skipping callsite: <" + cs.s + "," + cs.v + "> with compiled version: // " + scopeVersionMap.get(cs.s)); continue; } Set<IRScope> calledScopes = csp.counters.keySet(); cs.count = 0; for (IRScope s : calledScopes) { c = scopeCounts.get(s); if (c == null) { c = new Long(0); scopeCounts.put(s, c); } long x = csp.counters.get(s).count; c += x; cs.count += x; } CallBase call = cs.call; if (calledScopes.size() == 1 && !call.inliningBlocked()) { CallSite runtimeCS = call.getCallSite(); if (runtimeCS != null && (runtimeCS instanceof CachingCallSite)) { CachingCallSite ccs = (CachingCallSite) runtimeCS; CacheEntry ce = ccs.getCache(); if (!(ce.method instanceof InterpretedIRMethod)) { // System.out.println("NOT IR-M!"); continue; } else { callSites.add(cs); cs.tgtM = (InterpretedIRMethod) ce.method; } } } total += cs.count; } Collections.sort( callSites, new java.util.Comparator<IRCallSite>() { @Override public int compare(IRCallSite a, IRCallSite b) { if (a.count == b.count) return 0; return (a.count < b.count) ? 1 : -1; } }); // Find top N call sites double freq = 0.0; int i = 0; boolean noInlining = true; Set<IRScope> inlinedScopes = new HashSet<IRScope>(); for (IRCallSite ircs : callSites) { double contrib = (ircs.count * 100.0) / total; // 1% is arbitrary if (contrib < 1.0) break; i++; freq += contrib; // This check is arbitrary if (i == 100 || freq > 99.0) break; // System.out.println("Considering: " + ircs.call + " with id: " + ircs.call.callSiteId + // " in scope " + ircs.s + " with count " + ircs.count + "; contrib " + contrib + "; freq: " + // freq); // Now inline here! CallBase call = ircs.call; IRScope hs = ircs.s; boolean isHotClosure = hs instanceof IRClosure; IRScope hc = isHotClosure ? hs : null; hs = isHotClosure ? hs.getLexicalParent() : hs; IRScope tgtMethod = ircs.tgtM.getIRMethod(); Instr[] instrs = tgtMethod.getInstrsForInterpretation(); // Dont inline large methods -- 500 is arbitrary // Can be null if a previously inlined method hasn't been rebuilt if ((instrs == null) || instrs.length > 500) { // if (instrs == null) System.out.println("no instrs!"); // else System.out.println("large method with " + instrs.length + " instrs. skipping!"); continue; } RubyModule implClass = ircs.tgtM.getImplementationClass(); int classToken = implClass.getGeneration(); String n = tgtMethod.getName(); boolean inlineCall = true; if (isHotClosure) { Operand clArg = call.getClosureArg(null); inlineCall = (clArg instanceof WrappedIRClosure) && (((WrappedIRClosure) clArg).getClosure() == hc); } if (inlineCall) { noInlining = false; long start = new java.util.Date().getTime(); hs.inlineMethod(tgtMethod, implClass, classToken, null, call); inlinedScopes.add(hs); long end = new java.util.Date().getTime(); // System.out.println("Inlined " + tgtMethod + " in " + hs + // " @ instr " + call + " in time (ms): " // + (end-start) + " # instrs: " + instrs.length); inlineCount++; } else { // System.out.println("--no inlining--"); } } for (IRScope x : inlinedScopes) { // update version count for 'hs' scopeVersionMap.put(x, versionCount); // System.out.println("Updating version of " + x + " to " + versionCount); // System.out.println("--- pre-inline-instrs ---"); // System.out.println(x.getCFG().toStringInstrs()); // System.out.println("--- post-inline-instrs ---"); // System.out.println(x.getCFG().toStringInstrs()); } // reset codeModificationsCount = 0; callProfile = new HashMap<Long, CallSiteProfile>(); // Every 1M thread polls, discard stats by reallocating the thread-poll count map if (globalThreadPollCount % 1000000 == 0) { globalThreadPollCount = 0; } } private static void outputProfileStats() { ArrayList<IRScope> scopes = new ArrayList<IRScope>(scopeThreadPollCounts.keySet()); Collections.sort( scopes, new java.util.Comparator<IRScope>() { @Override public int compare(IRScope a, IRScope b) { // In non-methods and non-closures, we may not have any thread poll instrs. int aden = a.getThreadPollInstrsCount(); if (aden == 0) aden = 1; int bden = b.getThreadPollInstrsCount(); if (bden == 0) bden = 1; // Use estimated instr count to order scopes -- rather than raw thread-poll count float aCount = scopeThreadPollCounts.get(a).count * (1.0f * a.getInstrsForInterpretation().length / aden); float bCount = scopeThreadPollCounts.get(b).count * (1.0f * b.getInstrsForInterpretation().length / bden); if (aCount == bCount) return 0; return (aCount < bCount) ? 1 : -1; } }); /* LOG.info("------------------------"); LOG.info("Stats after " + globalThreadPollCount + " thread polls:"); LOG.info("------------------------"); LOG.info("# instructions: " + interpInstrsCount); LOG.info("# code modifications in this period : " + codeModificationsCount); LOG.info("------------------------"); */ int i = 0; float f1 = 0.0f; for (IRScope s : scopes) { long n = scopeThreadPollCounts.get(s).count; float p1 = ((n * 1000) / globalThreadPollCount) / 10.0f; String msg = i + ". " + s + " [file:" + s.getFileName() + ":" + s.getLineNumber() + "] = " + n + "; (" + p1 + "%)"; if (s instanceof IRClosure) { IRMethod m = s.getNearestMethod(); // if (m != null) LOG.info(msg + " -- nearest enclosing method: " + m); // else LOG.info(msg + " -- no enclosing method --"); } else { // LOG.info(msg); } i++; f1 += p1; // Top 20 or those that account for 95% of thread poll events. if (i == 20 || f1 >= 95.0) break; } // reset code modification counter codeModificationsCount = 0; // Every 1M thread polls, discard stats by reallocating the thread-poll count map if (globalThreadPollCount % 1000000 == 0) { // System.out.println("---- resetting thread-poll counters ----"); scopeThreadPollCounts = new HashMap<IRScope, Counter>(); globalThreadPollCount = 0; } } private static Integer initProfiling(IRScope scope) { /* SSS: Not being used currently tpCount = scopeThreadPollCounts.get(scope); if (tpCount == null) { tpCount = new Counter(); scopeThreadPollCounts.put(scope, tpCount); } */ Integer scopeVersion = scopeVersionMap.get(scope); if (scopeVersion == null) { scopeVersionMap.put(scope, versionCount); scopeVersion = new Integer(versionCount); } if (callerSite.call != null) { Long id = callerSite.call.callSiteId; CallSiteProfile csp = callProfile.get(id); if (csp == null) { csp = new CallSiteProfile(callerSite); callProfile.put(id, csp); } Counter csCount = csp.counters.get(scope); if (csCount == null) { csCount = new Counter(); csp.counters.put(scope, csCount); } csCount.count++; } return scopeVersion; } private static void setResult( Object[] temp, DynamicScope currDynScope, Variable resultVar, Object result) { if (resultVar instanceof TemporaryVariable) { temp[((TemporaryVariable) resultVar).offset] = result; } else { LocalVariable lv = (LocalVariable) resultVar; currDynScope.setValue((IRubyObject) result, lv.getLocation(), lv.getScopeDepth()); } } private static void setResult( Object[] temp, DynamicScope currDynScope, Instr instr, Object result) { if (instr instanceof ResultInstr) { setResult(temp, currDynScope, ((ResultInstr) instr).getResult(), result); } } private static Object retrieveOp( Operand r, ThreadContext context, IRubyObject self, DynamicScope currDynScope, Object[] temp) { Object res; if (r instanceof Self) { return self; } else if (r instanceof TemporaryVariable) { res = temp[((TemporaryVariable) r).offset]; return res == null ? context.nil : res; } else if (r instanceof LocalVariable) { LocalVariable lv = (LocalVariable) r; res = currDynScope.getValue(lv.getLocation(), lv.getScopeDepth()); return res == null ? context.nil : res; } else { return r.retrieve(context, self, currDynScope, temp); } } private static void updateCallSite(Instr instr, IRScope scope, Integer scopeVersion) { if (instr instanceof CallBase) { callerSite.s = scope; callerSite.v = scopeVersion; callerSite.call = (CallBase) instr; } } private static void receiveArg( ThreadContext context, Instr i, Operation operation, IRubyObject[] args, int kwArgHashCount, DynamicScope currDynScope, Object[] temp, Object exception, Block block) { Object result = null; ResultInstr instr = (ResultInstr) i; switch (operation) { case RECV_PRE_REQD_ARG: int argIndex = ((ReceivePreReqdArgInstr) instr).getArgIndex(); result = ((argIndex + kwArgHashCount) < args.length) ? args[argIndex] : context.nil; // SSS FIXME: This check is only required for closures, not methods break; case RECV_CLOSURE: result = (block == Block.NULL_BLOCK) ? context.nil : context.runtime.newProc(Block.Type.PROC, block); break; case RECV_OPT_ARG: result = ((ReceiveOptArgInstr) instr).receiveOptArg(args, kwArgHashCount); break; case RECV_POST_REQD_ARG: result = ((ReceivePostReqdArgInstr) instr).receivePostReqdArg(args, kwArgHashCount); // For blocks, missing arg translates to nil result = result == null ? context.nil : result; break; case RECV_REST_ARG: result = ((ReceiveRestArgInstr) instr).receiveRestArg(context.runtime, args, kwArgHashCount); break; case RECV_KW_ARG: result = ((ReceiveKeywordArgInstr) instr).receiveKWArg(context, kwArgHashCount, args); break; case RECV_KW_REST_ARG: result = ((ReceiveKeywordRestArgInstr) instr).receiveKWArg(context, kwArgHashCount, args); break; case RECV_EXCEPTION: { ReceiveExceptionInstr rei = (ReceiveExceptionInstr) instr; result = (exception instanceof RaiseException && rei.checkType) ? ((RaiseException) exception).getException() : exception; break; } } setResult(temp, currDynScope, instr.getResult(), result); } private static void processCall( ThreadContext context, Instr instr, Operation operation, IRScope scope, DynamicScope currDynScope, Object[] temp, IRubyObject self, Block block, Block.Type blockType) { Object result = null; switch (operation) { case RUNTIME_HELPER: { RuntimeHelperCall rhc = (RuntimeHelperCall) instr; result = rhc.callHelper(context, currDynScope, self, temp, scope, blockType); setResult(temp, currDynScope, rhc.getResult(), result); break; } case CALL_1F: { OneFixnumArgNoBlockCallInstr call = (OneFixnumArgNoBlockCallInstr) instr; IRubyObject r = (IRubyObject) retrieveOp(call.getReceiver(), context, self, currDynScope, temp); result = call.getCallSite().call(context, self, r, call.getFixnumArg()); setResult(temp, currDynScope, call.getResult(), result); break; } case CALL_1O: { OneOperandArgNoBlockCallInstr call = (OneOperandArgNoBlockCallInstr) instr; IRubyObject r = (IRubyObject) retrieveOp(call.getReceiver(), context, self, currDynScope, temp); IRubyObject o = (IRubyObject) call.getArg1().retrieve(context, self, currDynScope, temp); result = call.getCallSite().call(context, self, r, o); setResult(temp, currDynScope, call.getResult(), result); break; } case CALL_0O: { ZeroOperandArgNoBlockCallInstr call = (ZeroOperandArgNoBlockCallInstr) instr; IRubyObject r = (IRubyObject) retrieveOp(call.getReceiver(), context, self, currDynScope, temp); result = call.getCallSite().call(context, self, r); setResult(temp, currDynScope, call.getResult(), result); break; } case NORESULT_CALL_1O: { OneOperandArgNoBlockNoResultCallInstr call = (OneOperandArgNoBlockNoResultCallInstr) instr; IRubyObject r = (IRubyObject) retrieveOp(call.getReceiver(), context, self, currDynScope, temp); IRubyObject o = (IRubyObject) call.getArg1().retrieve(context, self, currDynScope, temp); call.getCallSite().call(context, self, r, o); break; } case NORESULT_CALL: instr.interpret(context, currDynScope, self, temp, block); break; case CALL: default: result = instr.interpret(context, currDynScope, self, temp, block); setResult(temp, currDynScope, instr, result); break; } } private static IRubyObject interpret( ThreadContext context, IRubyObject self, IRScope scope, Visibility visibility, RubyModule implClass, IRubyObject[] args, Block block, Block.Type blockType) { Instr[] instrs = scope.getInstrsForInterpretation(); // The base IR may not have been processed yet if (instrs == null) instrs = scope.prepareForInterpretation(blockType == Block.Type.LAMBDA); int numTempVars = scope.getTemporaryVariableSize(); Object[] temp = numTempVars > 0 ? new Object[numTempVars] : null; int n = instrs.length; int ipc = 0; Instr instr = null; Object exception = null; int kwArgHashCount = (scope.receivesKeywordArgs() && args[args.length - 1] instanceof RubyHash) ? 1 : 0; DynamicScope currDynScope = context.getCurrentScope(); // Counter tpCount = null; // Init profiling this scope boolean debug = IRRuntimeHelpers.isDebug(); boolean profile = IRRuntimeHelpers.inProfileMode(); Integer scopeVersion = profile ? initProfiling(scope) : 0; // Enter the looooop! while (ipc < n) { instr = instrs[ipc]; ipc++; Operation operation = instr.getOperation(); if (debug) { LOG.info("I: {}", instr); interpInstrsCount++; } else if (profile) { if (operation.modifiesCode()) codeModificationsCount++; interpInstrsCount++; /* Counter cnt = opStats.get(operation); if (cnt == null) { cnt = new Counter(); opStats.put(operation, cnt); } cnt.count++; */ } try { switch (operation.opClass) { case ARG_OP: { receiveArg( context, instr, operation, args, kwArgHashCount, currDynScope, temp, exception, block); break; } case BRANCH_OP: { if (operation == Operation.JUMP) { ipc = ((JumpInstr) instr).getJumpTarget().getTargetPC(); } else { ipc = instr.interpretAndGetNewIPC(context, currDynScope, self, temp, ipc); } break; } case CALL_OP: { if (profile) updateCallSite(instr, scope, scopeVersion); processCall( context, instr, operation, scope, currDynScope, temp, self, block, blockType); break; } case BOOK_KEEPING_OP: { switch (operation) { case PUSH_FRAME: { context.preMethodFrameAndClass( implClass, scope.getName(), self, block, scope.getStaticScope()); context.setCurrentVisibility(visibility); break; } case PUSH_BINDING: { // SSS NOTE: Method scopes only! // // Blocks are a headache -- so, these instrs. are only added to IRMethods. // Blocks have more complicated logic for pushing a dynamic scope (see // InterpretedIRBlockBody) // Changed by DPR currDynScope = DynamicScope.newDynamicScope( scope.getStaticScope(), context.getCurrentScope().getDepth()); context.pushScope(currDynScope); break; } case CHECK_ARITY: ((CheckArityInstr) instr).checkArity(context.runtime, args.length); break; case POP_FRAME: context.popFrame(); context.popRubyClass(); break; case POP_BINDING: context.popScope(); break; case THREAD_POLL: if (profile) { // SSS: Not being used currently // tpCount.count++; globalThreadPollCount++; // 20K is arbitrary // Every 20K profile counts, spit out profile stats if (globalThreadPollCount % 20000 == 0) { analyzeProfile(); // outputProfileStats(); } } context.callThreadPoll(); break; case LINE_NUM: context.setLine(((LineNumberInstr) instr).lineNumber); break; case RECORD_END_BLOCK: ((RecordEndBlockInstr) instr).interpret(); break; } break; } case OTHER_OP: { Object result = null; switch (operation) { // --------- Return flavored instructions -------- case BREAK: { BreakInstr bi = (BreakInstr) instr; IRubyObject rv = (IRubyObject) bi.getReturnValue().retrieve(context, self, currDynScope, temp); // This also handles breaks in lambdas -- by converting them to a return return IRRuntimeHelpers.initiateBreak( context, scope, bi.getScopeToReturnTo().getScopeId(), rv, blockType); } case RETURN: { return (IRubyObject) retrieveOp( ((ReturnBase) instr).getReturnValue(), context, self, currDynScope, temp); } case NONLOCAL_RETURN: { NonlocalReturnInstr ri = (NonlocalReturnInstr) instr; IRubyObject rv = (IRubyObject) retrieveOp(ri.getReturnValue(), context, self, currDynScope, temp); ipc = n; // If not in a lambda, check if this was a non-local return if (!IRRuntimeHelpers.inLambda(blockType)) { IRRuntimeHelpers.initiateNonLocalReturn( context, scope, ri.methodToReturnFrom, rv); } return rv; } // ---------- Common instruction --------- case COPY: { CopyInstr c = (CopyInstr) instr; result = retrieveOp(c.getSource(), context, self, currDynScope, temp); setResult(temp, currDynScope, c.getResult(), result); break; } case GET_FIELD: { GetFieldInstr gfi = (GetFieldInstr) instr; IRubyObject object = (IRubyObject) gfi.getSource().retrieve(context, self, currDynScope, temp); VariableAccessor a = gfi.getAccessor(object); result = a == null ? null : (IRubyObject) a.get(object); if (result == null) { result = context.nil; } setResult(temp, currDynScope, gfi.getResult(), result); break; } case SEARCH_CONST: { SearchConstInstr sci = (SearchConstInstr) instr; result = sci.getCachedConst(); if (!sci.isCached(context, result)) result = sci.cache(context, currDynScope, self, temp); setResult(temp, currDynScope, sci.getResult(), result); break; } // ---------- All the rest --------- default: result = instr.interpret(context, currDynScope, self, temp, block); setResult(temp, currDynScope, instr, result); break; } break; } } } catch (Throwable t) { // Unrescuable: // IRReturnJump, ThreadKill, RubyContinuation, MainExitException, etc. // These cannot be rescued -- only run ensure blocks // // Others: // IRBreakJump, Ruby exceptions, errors, and other java exceptions. // These can be rescued -- run rescue blocks if (debug) LOG.info( "in scope: " + scope + ", caught Java throwable: " + t + "; excepting instr: " + instr); ipc = (t instanceof Unrescuable) ? scope.getEnsurerPC(instr) : scope.getRescuerPC(instr); if (debug) LOG.info("ipc for rescuer/ensurer: " + ipc); if (ipc == -1) { Helpers.throwException((Throwable) t); } else { exception = t; } } } // Control should never get here! // SSS FIXME: But looks like BEGIN/END blocks get here -- needs fixing return null; } public static IRubyObject INTERPRET_EVAL( ThreadContext context, IRubyObject self, IRScope scope, RubyModule clazz, IRubyObject[] args, String name, Block block, Block.Type blockType) { try { ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine()); return interpret(context, self, scope, null, clazz, args, block, blockType); } finally { ThreadContext.popBacktrace(context); } } public static IRubyObject INTERPRET_BLOCK( ThreadContext context, IRubyObject self, IRScope scope, IRubyObject[] args, String name, Block block, Block.Type blockType) { try { ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine()); return interpret(context, self, scope, null, null, args, block, blockType); } finally { ThreadContext.popBacktrace(context); } } public static IRubyObject INTERPRET_METHOD( ThreadContext context, InterpretedIRMethod irMethod, IRubyObject self, String name, IRubyObject[] args, Block block, Block.Type blockType, boolean isTraceable) { Ruby runtime = context.runtime; IRScope scope = irMethod.getIRMethod(); RubyModule implClass = irMethod.getImplementationClass(); Visibility viz = irMethod.getVisibility(); boolean syntheticMethod = name == null || name.equals(""); try { if (!syntheticMethod) ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine()); if (isTraceable) methodPreTrace(runtime, context, name, implClass); return interpret(context, self, scope, viz, implClass, args, block, blockType); } finally { if (isTraceable) { try { methodPostTrace(runtime, context, name, implClass); } finally { if (!syntheticMethod) ThreadContext.popBacktrace(context); } } else { if (!syntheticMethod) ThreadContext.popBacktrace(context); } } } private static void methodPreTrace( Ruby runtime, ThreadContext context, String name, RubyModule implClass) { if (runtime.hasEventHooks()) context.trace(RubyEvent.CALL, name, implClass); } private static void methodPostTrace( Ruby runtime, ThreadContext context, String name, RubyModule implClass) { if (runtime.hasEventHooks()) context.trace(RubyEvent.RETURN, name, implClass); } }
// This class represents JVM as the target of compilation // and outputs bytecode public class JVM { private static final Logger LOG = LoggerFactory.getLogger("IRBuilder"); Stack<ClassData> clsStack = new Stack(); ClassWriter writer; public JVM() {} public static final int CMP_EQ = 0; public byte[] code() { return writer.toByteArray(); } public ClassVisitor cls() { return clsData().cls; } public ClassData clsData() { return clsStack.peek(); } public MethodData methodData() { return clsData().methodData(); } public void pushclass(String clsName) { PrintWriter pw = new PrintWriter(System.out); clsStack.push( new ClassData( clsName, new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS))); pw.flush(); } public void pushscript(String clsName, String filename) { PrintWriter pw = new PrintWriter(System.out); writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); clsStack.push(new ClassData(clsName, writer)); cls() .visit( RubyInstanceConfig.JAVA_VERSION, ACC_PUBLIC + ACC_SUPER, clsName, null, p(Object.class), null); cls().visitSource(filename, null); pw.flush(); } public void popclass() { clsStack.pop(); } public IRBytecodeAdapter method() { return clsData().method(); } public void pushmethod(String name, int arity) { clsData().pushmethod(name, arity); method().startMethod(); // locals for ThreadContext and self methodData().local("$context", JVM.THREADCONTEXT_TYPE); methodData().local("$scope", JVM.STATICSCOPE_TYPE); methodData().local("$self"); // , JVM.OBJECT_TYPE); for (int i = 0; i < arity; i++) { // incoming arguments methodData().local("$argument" + i); } methodData().local("$block", Type.getType(Block.class)); // TODO: this should go into the PushBinding instruction methodData().local("$dynamicScope"); } public void popmethod() { clsData().popmethod(); } public static String scriptToClass(String name) { if (name.equals("-e")) { return "DashE"; } else { return JavaNameMangler.mangledFilenameForStartupClasspath(name); } } public void declareField(String field) { if (!clsData().fieldSet.contains(field)) { cls().visitField(ACC_PROTECTED, field, ci(Object.class), null, null); clsData().fieldSet.add(field); } } public static final Class OBJECT = IRubyObject.class; public static final Class BLOCK = Block.class; public static final Class THREADCONTEXT = ThreadContext.class; public static final Class STATICSCOPE = StaticScope.class; public static final Type OBJECT_TYPE = Type.getType(OBJECT); public static final Type BLOCK_TYPE = Type.getType(BLOCK); public static final Type THREADCONTEXT_TYPE = Type.getType(THREADCONTEXT); public static final Type STATICSCOPE_TYPE = Type.getType(STATICSCOPE); }
public class IREvalScript extends IRClosure { private static final Logger LOG = LoggerFactory.getLogger("IREvalScript"); private IRScope nearestNonEvalScope; private int nearestNonEvalScopeDepth; private List<IRClosure> beginBlocks; private List<IRClosure> endBlocks; public IREvalScript( IRManager manager, IRScope lexicalParent, String fileName, int lineNumber, StaticScope staticScope) { super(manager, lexicalParent, fileName, lineNumber, staticScope, "EVAL_"); int n = 0; IRScope s = lexicalParent; while (s instanceof IREvalScript) { n++; s = s.getLexicalParent(); } this.nearestNonEvalScope = s; this.nearestNonEvalScopeDepth = n; this.nearestNonEvalScope.initEvalScopeVariableAllocator(false); } @Override public Label getNewLabel() { return getNewLabel("EV" + closureId + "_LBL"); } @Override public IRScopeType getScopeType() { return IRScopeType.EVAL_SCRIPT; } @Override public Operand[] getBlockArgs() { return new Operand[0]; } /* Record a begin block -- not all scope implementations can handle them */ @Override public void recordBeginBlock(IRClosure beginBlockClosure) { if (beginBlocks == null) beginBlocks = new ArrayList<IRClosure>(); beginBlockClosure.setBeginEndBlock(); beginBlocks.add(beginBlockClosure); } /* Record an end block -- not all scope implementations can handle them */ @Override public void recordEndBlock(IRClosure endBlockClosure) { if (endBlocks == null) endBlocks = new ArrayList<IRClosure>(); endBlockClosure.setBeginEndBlock(); endBlocks.add(endBlockClosure); } public List<IRClosure> getBeginBlocks() { return beginBlocks; } public List<IRClosure> getEndBlocks() { return endBlocks; } public IRubyObject call( ThreadContext context, IRubyObject self, RubyModule clazz, DynamicScope evalScope, Block block, String backtraceName) { if (IRRuntimeHelpers.isDebug()) { LOG.info("Graph:\n" + cfg().toStringGraph()); LOG.info("CFG:\n" + cfg().toStringInstrs()); } // FIXME: Do not push new empty arg array in every time return Interpreter.INTERPRET_EVAL( context, self, this, clazz, new IRubyObject[] {}, backtraceName, block, null); } @Override public LocalVariable lookupExistingLVar(String name) { return nearestNonEvalScope.evalScopeVars.get(name); } @Override public LocalVariable findExistingLocalVariable(String name, int scopeDepth) { // Look in the nearest non-eval scope's shared eval scope vars first. // If you dont find anything there, look in the nearest non-eval scope's regular vars. LocalVariable lvar = lookupExistingLVar(name); if (lvar != null || scopeDepth == 0) return lvar; else return nearestNonEvalScope.findExistingLocalVariable( name, scopeDepth - nearestNonEvalScopeDepth - 1); } @Override public LocalVariable getLocalVariable(String name, int scopeDepth) { LocalVariable lvar = findExistingLocalVariable(name, scopeDepth); if (lvar == null) lvar = getNewLocalVariable(name, scopeDepth); // Create a copy of the variable usable at the right depth if (lvar.getScopeDepth() != scopeDepth) lvar = lvar.cloneForDepth(scopeDepth); return lvar; } @Override public LocalVariable getImplicitBlockArg() { return getLocalVariable(Variable.BLOCK, nearestNonEvalScopeDepth); } @Override public LocalVariable getNewLocalVariable(String name, int depth) { assert depth == nearestNonEvalScopeDepth : "Local variable depth in IREvalScript:getNewLocalVariable must be " + nearestNonEvalScopeDepth + ". Got " + depth; LocalVariable lvar = new ClosureLocalVariable(this, name, 0, nearestNonEvalScope.evalScopeVars.size()); nearestNonEvalScope.evalScopeVars.put(name, lvar); // CON: unsure how to get static scope to reflect this name as in IRClosure and IRMethod return lvar; } @Override public LocalVariable getNewFlipStateVariable() { return getLocalVariable("%flip_" + allocateNextPrefixedName("%flip"), 0); } @Override public int getUsedVariablesCount() { return 1 + nearestNonEvalScope.evalScopeVars.size() + getPrefixCountSize("%flip"); } @Override public boolean isScriptScope() { return true; } @Override public boolean isTopLocalVariableScope() { return false; } @Override public boolean isFlipScope() { return true; } }
/** Created by headius on 10/23/14. */ public abstract class InvokeSite extends MutableCallSite { final Signature signature; final Signature fullSignature; final int arity; protected final String methodName; final MethodHandle fallback; private final Set<Integer> seenTypes = new HashSet<Integer>(); private int clearCount; private static final AtomicLong SITE_ID = new AtomicLong(1); private final long siteID = SITE_ID.getAndIncrement(); private final int argOffset; private boolean boundOnce; CacheEntry cache = CacheEntry.NULL_CACHE; private static final Logger LOG = LoggerFactory.getLogger("InvokeSite"); public String name() { return methodName; } public final CallType callType; public InvokeSite(MethodType type, String name, CallType callType) { super(type); this.methodName = name; this.callType = callType; Signature startSig; if (callType == CallType.SUPER) { // super calls receive current class argument, so offsets and signature are different 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().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)) { return callMethodMissing(entry, callType, context, self, methodName, args, block); } MethodHandle mh = getHandle(selfClass, this, method); updateInvocationTarget(mh, self, selfClass, entry, switchPoint); return method.call(context, self, selfClass, methodName, args, block); } /** Failover version uses a monomorphic cache and DynamicMethod.call, as in non-indy. */ 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, selfClass, name, args, block); } entry = selfClass.searchWithCache(name); if (methodMissing(entry, caller)) { return callMethodMissing(entry, callType, context, self, name, args, block); } cache = entry; return entry.method.call(context, self, selfClass, name, args, block); } public Binder prepareBinder() { SmartBinder binder = SmartBinder.from(signature); // prepare arg[] if (arity == -1) { // do nothing, already have IRubyObject[] in args } else if (arity == 0) { binder = binder.insert(argOffset, "args", IRubyObject.NULL_ARRAY); } else { binder = binder.collect("args", "arg[0-9]+"); } // add block if needed if (signature.lastArgType() != Block.class) { binder = binder.append("block", Block.NULL_BLOCK); } // bind to site binder = binder.insert(0, "site", this); return binder.binder(); } MethodHandle getHandle(RubyClass dispatchClass, InvokeSite site, DynamicMethod method) throws Throwable { boolean blockGiven = signature.lastArgType() == Block.class; MethodHandle mh = Bootstrap.buildNativeHandle(site, method, blockGiven); if (mh == null) mh = Bootstrap.buildIndyHandle(site, method, method.getImplementationClass()); if (mh == null) mh = Bootstrap.buildJittedHandle(site, method, blockGiven); if (mh == null) mh = Bootstrap.buildGenericHandle(site, method, dispatchClass); assert mh != null : "we should have a method handle of some sort by now"; return mh; } /** * Update the given call site using the new target, wrapping with appropriate guard and * argument-juggling logic. Return a handle suitable for invoking with the site's original method * type. */ MethodHandle updateInvocationTarget( MethodHandle target, IRubyObject self, RubyModule testClass, CacheEntry entry, SwitchPoint switchPoint) { if (target == null || clearCount > Options.INVOKEDYNAMIC_MAXFAIL.load() || (!hasSeenType(testClass.id) && seenTypesCount() + 1 > Options.INVOKEDYNAMIC_MAXPOLY.load())) { setTarget(target = prepareBinder().invokeVirtualQuiet(lookup(), "fail")); } else { MethodHandle fallback; MethodHandle gwt; // if we've cached no types, and the site is bound and we haven't seen this new type... if (seenTypesCount() > 0 && getTarget() != null && !hasSeenType(testClass.id)) { // stack it up into a PIC if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info(methodName + "\tadded to PIC " + logMethod(entry.method)); fallback = getTarget(); } else { // wipe out site with this new type and method String bind = boundOnce ? "rebind" : "bind"; if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.info( methodName + "\ttriggered site #" + siteID + " " + bind); // + " (" + file() + ":" + line() + ")"); fallback = this.fallback; clearTypes(); } addType(testClass.id); SmartHandle test; SmartBinder selfTest = SmartBinder.from(signature.asFold(boolean.class)).permute("self"); if (self instanceof RubySymbol || self instanceof RubyFixnum || self instanceof RubyFloat || self instanceof RubyNil || self instanceof RubyBoolean.True || self instanceof RubyBoolean.False) { test = selfTest .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); // wrap in switchpoint for mutation invalidation gwt = switchPoint.guardWithTest(gwt, fallback); setTarget(gwt); } return target; } public RubyClass pollAndGetClass(ThreadContext context, IRubyObject self) { context.callThreadPoll(); RubyClass selfType = ((RubyBasicObject) self).getMetaClass(); return selfType; } @Override public void setTarget(MethodHandle target) { super.setTarget(target); boundOnce = true; } public void setInitialTarget(MethodHandle target) { super.setTarget(target); } public synchronized boolean hasSeenType(int typeCode) { return seenTypes.contains(typeCode); } public synchronized void addType(int typeCode) { seenTypes.add(typeCode); } public synchronized int seenTypesCount() { return seenTypes.size(); } public synchronized void clearTypes() { seenTypes.clear(); clearCount++; } public abstract boolean methodMissing(CacheEntry entry, IRubyObject caller); public IRubyObject callMethodMissing( CacheEntry entry, CallType callType, ThreadContext context, IRubyObject self, String name, IRubyObject[] args, Block block) { return Helpers.selectMethodMissing(context, self, entry.method.getVisibility(), name, callType) .call(context, self, self.getMetaClass(), name, args, block); } private static String logMethod(DynamicMethod method) { return "[#" + method.getSerialNumber() + " " + method.getImplementationClass() + "]"; } @JIT public static boolean testMetaclass(RubyClass metaclass, IRubyObject self) { return metaclass == ((RubyBasicObject) self).getMetaClass(); } @JIT public static boolean testClass(Object object, Class clazz) { return object.getClass() == clazz; } private static final MethodHandle TEST_CLASS = Binder.from(boolean.class, Object.class, Class.class) .invokeStaticQuiet(lookup(), InvokeSite.class, "testClass"); }
/** @author headius */ public class ASTInspector { private static final Logger LOG = LoggerFactory.getLogger("ASTInspector"); private final boolean dump; private final String name; public ASTInspector() { dump = false; name = null; } public ASTInspector(String name, boolean dump) { this.name = name; this.dump = dump; } private static final boolean DEBUG = false; enum Flag { BLOCK_ARG(0x1), // block argument to the method CLOSURE(0x2), // closure present CLASS(0x4), // class present METHOD(0x8), // method table mutations, def, defs, undef, alias EVAL(0x10), // likely call to eval FRAME_AWARE(0x20), // makes calls that are aware of the frame FRAME_SELF(0x40), // makes calls that are aware of the frame's self FRAME_VISIBILITY(0x80), // makes calls that are aware of the frame's visibility FRAME_BLOCK(0x100), // makes calls that are aware of the frame's block FRAME_NAME(0x200), // makes calls that are aware of the frame's name BACKREF(0x400), // makes calls that set or get backref LASTLINE(0x800), // makes calls that set or get lastline FRAME_CLASS(0x1000), // makes calls that are aware of the frame's class OPT_ARGS(0x2000), // optional arguments to the method REST_ARG(0x4000), // rest arg to the method SCOPE_AWARE(0x8000), // makes calls that are aware of the scope ZSUPER(0x10000), // makes a zero-argument super call CONSTANT(0x20000), // accesses or sets constants CLASS_VAR(0x40000), // accesses or sets class variables SUPER(0x80000), // makes normal super call RETRY(0x100000); // contains a retry private Flag(int value) { flag = value; } public final int flag; } public static final Flag BLOCK_ARG = Flag.BLOCK_ARG; public static final Flag CLOSURE = Flag.CLOSURE; public static final Flag CLASS = Flag.CLASS; public static final Flag METHOD = Flag.METHOD; public static final Flag EVAL = Flag.EVAL; public static final Flag FRAME_AWARE = Flag.FRAME_AWARE; public static final Flag FRAME_SELF = Flag.FRAME_SELF; public static final Flag FRAME_VISIBILITY = Flag.FRAME_VISIBILITY; public static final Flag FRAME_BLOCK = Flag.FRAME_BLOCK; public static final Flag FRAME_NAME = Flag.FRAME_NAME; public static final Flag BACKREF = Flag.BACKREF; public static final Flag LASTLINE = Flag.LASTLINE; public static final Flag FRAME_CLASS = Flag.FRAME_CLASS; public static final Flag OPT_ARGS = Flag.OPT_ARGS; public static final Flag REST_ARG = Flag.REST_ARG; public static final Flag SCOPE_AWARE = Flag.SCOPE_AWARE; public static final Flag ZSUPER = Flag.ZSUPER; public static final Flag CONSTANT = Flag.CONSTANT; public static final Flag CLASS_VAR = Flag.CLASS_VAR; public static final Flag SUPER = Flag.SUPER; public static final Flag RETRY = Flag.RETRY; private static final String[] MODIFIER_NAMES = { "BLOCK", "CLOSURE", "CLASS", "METHOD", "EVAL", "FRAME_AWARE", "FRAME_SELF", "FRAME_VISIBILITY", "FRAME_BLOCK", "FRAME_NAME", "BACKREF", "LASTLINE", "FRAME_CLASS", "OPT_ARGS", "REST_ARG", "SCOPE_AWARE", "ZSUPER", "CONSTANT", "CLASS_VAR", "SUPER", "RETRY" }; private int flags; // pragmas private boolean noFrame; public static final Set<String> FRAME_AWARE_METHODS = Collections.synchronizedSet(new HashSet<String>()); public static final Set<String> SCOPE_AWARE_METHODS = Collections.synchronizedSet(new HashSet<String>()); public static void addFrameAwareMethods(String... methods) { if (DEBUG) LOG.debug("Adding frame-aware method names: {}", Arrays.toString(methods)); FRAME_AWARE_METHODS.addAll(Arrays.asList(methods)); } public static void addScopeAwareMethods(String... methods) { if (DEBUG) LOG.debug("Adding scope-aware method names: {}", Arrays.toString(methods)); SCOPE_AWARE_METHODS.addAll(Arrays.asList(methods)); } public static final Set<String> PRAGMAS = Collections.synchronizedSet(new HashSet<String>()); static { FRAME_AWARE_METHODS.add("eval"); FRAME_AWARE_METHODS.add("module_eval"); FRAME_AWARE_METHODS.add("class_eval"); FRAME_AWARE_METHODS.add("instance_eval"); FRAME_AWARE_METHODS.add("binding"); FRAME_AWARE_METHODS.add("public"); FRAME_AWARE_METHODS.add("private"); FRAME_AWARE_METHODS.add("protected"); FRAME_AWARE_METHODS.add("module_function"); FRAME_AWARE_METHODS.add("block_given?"); FRAME_AWARE_METHODS.add("iterator?"); SCOPE_AWARE_METHODS.addAll(RubyModule.SCOPE_CAPTURING_METHODS); PRAGMAS.add("__NOFRAME__"); } public void disable() { if (dump) LOG.debug("[ASTInspector] {} DISABLED", name); flags = 0xFFFFFFFF; } public CallConfiguration getCallConfig() { if (!noFrame() && (hasFrameAwareMethods() || hasClosure() || RubyInstanceConfig.FULL_TRACE_ENABLED)) { // We're doing normal framed compilation or the method needs a frame if (hasClosure() || hasScopeAwareMethods()) { // The method also needs a scope, do both return CallConfiguration.FrameFullScopeFull; } else { // The method doesn't need a scope or static scope; frame only return CallConfiguration.FrameFullScopeNone; } } else { if (hasClosure() || hasScopeAwareMethods()) { return CallConfiguration.FrameNoneScopeFull; } else { return CallConfiguration.FrameNoneScopeNone; } } } /** * Perform an inspection of a subtree or set of subtrees separate from the parent inspection, to * make independent decisions based on that subtree(s). * * @param nodes The child nodes to walk with a new inspector * @return The new inspector resulting from the walk */ public ASTInspector subInspect(Node... nodes) { ASTInspector newInspector = new ASTInspector(name, dump); for (Node node : nodes) { newInspector.inspect(node); } return newInspector; } public boolean getFlag(Flag modifier) { return (flags & modifier.flag) != 0; } public boolean getFlag(Flag... modifiers) { int mask = 0; for (Flag flag : modifiers) { mask |= flag.flag; } return (flags & mask) != 0; } public void setFlag(Flag modifier) { if (dump) { LOG.info("[ASTInspector] " + name + "\n\tset flag " + modifier); } flags |= modifier.flag; } public void setFlag(Node node, Flag modifier) { if (dump) { LOG.info( "[ASTInspector] " + name + "\n\tset flag " + modifier + " because of " + node.getNodeType() + " at " + node.getPosition()); } flags |= modifier.flag; } /** * Integrate the results of a separate inspection into the state of this inspector. * * @param other The other inspector whose state to integrate. */ public void integrate(ASTInspector other) { flags |= other.flags; } public void inspect(Node node) { if (RubyInstanceConfig.FULL_TRACE_ENABLED) { disable(); // we still inspect since some nodes change state as a result (JRUBY-6836) } if (node == null) return; switch (node.getNodeType()) { case ALIASNODE: setFlag(node, METHOD); break; case ANDNODE: AndNode andNode = (AndNode) node; inspect(andNode.getFirstNode()); inspect(andNode.getSecondNode()); break; case ARGSCATNODE: ArgsCatNode argsCatNode = (ArgsCatNode) node; inspect(argsCatNode.getFirstNode()); inspect(argsCatNode.getSecondNode()); break; case ARGSPUSHNODE: ArgsPushNode argsPushNode = (ArgsPushNode) node; inspect(argsPushNode.getFirstNode()); inspect(argsPushNode.getSecondNode()); break; case ARGUMENTNODE: break; case ARRAYNODE: case BLOCKNODE: case DREGEXPNODE: case DSTRNODE: case DSYMBOLNODE: case DXSTRNODE: case LISTNODE: ListNode listNode = (ListNode) node; for (int i = 0; i < listNode.size(); i++) { inspect(listNode.get(i)); } break; case ARGSNODE: ArgsNode argsNode = (ArgsNode) node; if (argsNode.getBlock() != null) setFlag(node, BLOCK_ARG); if (argsNode.getOptArgs() != null) { setFlag(node, OPT_ARGS); inspect(argsNode.getOptArgs()); } if (argsNode.getRestArg() == -2 || argsNode.getRestArg() >= 0) setFlag(node, REST_ARG); break; case ATTRASSIGNNODE: AttrAssignNode attrAssignNode = (AttrAssignNode) node; inspect(attrAssignNode.getArgsNode()); inspect(attrAssignNode.getReceiverNode()); break; case BACKREFNODE: setFlag(node, BACKREF); break; case BEGINNODE: inspect(((BeginNode) node).getBodyNode()); break; case BIGNUMNODE: break; case BINARYOPERATORNODE: BinaryOperatorNode binaryOperatorNode = (BinaryOperatorNode) node; inspect(binaryOperatorNode.getFirstNode()); inspect(binaryOperatorNode.getSecondNode()); break; case BLOCKARGNODE: break; case BLOCKPASSNODE: BlockPassNode blockPassNode = (BlockPassNode) node; inspect(blockPassNode.getArgsNode()); inspect(blockPassNode.getBodyNode()); break; case BREAKNODE: inspect(((BreakNode) node).getValueNode()); break; case CALLNODE: CallNode callNode = (CallNode) node; inspect(callNode.getReceiverNode()); // check for Proc.new, an especially magic method if (callNode.getName() == "new" && callNode.getReceiverNode() instanceof ConstNode && ((ConstNode) callNode.getReceiverNode()).getName() == "Proc") { // Proc.new needs the caller's block to instantiate a proc setFlag(node, FRAME_BLOCK); } if (callNode.getArgsNode() == null && callNode.getIterNode() == null) { switch (callNode.getReceiverNode().getNodeType()) { // no unary methods on literal numbers, symbols, or strings have frame/scope effects case FIXNUMNODE: case FLOATNODE: case BIGNUMNODE: case STRNODE: case SYMBOLNODE: return; } } case FCALLNODE: inspect(((IArgumentNode) node).getArgsNode()); inspect(((BlockAcceptingNode) node).getIterNode()); case VCALLNODE: INameNode nameNode = (INameNode) node; if (FRAME_AWARE_METHODS.contains(nameNode.getName())) { setFlag(node, FRAME_AWARE); if (nameNode.getName().indexOf("eval") != -1) { setFlag(node, EVAL); } } if (SCOPE_AWARE_METHODS.contains(nameNode.getName())) { setFlag(node, SCOPE_AWARE); } break; case CASENODE: CaseNode caseNode = (CaseNode) node; inspect(caseNode.getCaseNode()); if (caseNode.getCases().size() > Options.COMPILE_OUTLINE_CASECOUNT.load()) { // if more than N cases, disable; we'll compile them as separate bodies // see BaseBodyCompiler#compiledSequencedConditional and ASTCompiler#compileCase disable(); return; } else { for (Node when : caseNode.getCases().childNodes()) { inspect(when); } inspect(caseNode.getElseNode()); } break; case CLASSNODE: setFlag(node, CLASS); ClassNode classNode = (ClassNode) node; inspect(classNode.getCPath()); inspect(classNode.getSuperNode()); break; case CLASSVARNODE: setFlag(node, CLASS_VAR); break; case CONSTDECLNODE: inspect(((AssignableNode) node).getValueNode()); setFlag(node, CONSTANT); break; case CLASSVARASGNNODE: inspect(((AssignableNode) node).getValueNode()); setFlag(node, CLASS_VAR); break; case CLASSVARDECLNODE: inspect(((AssignableNode) node).getValueNode()); setFlag(node, CLASS_VAR); break; case COLON2NODE: inspect(((Colon2Node) node).getLeftNode()); break; case COLON3NODE: break; case CONSTNODE: setFlag(node, CONSTANT); break; case DEFNNODE: case DEFSNODE: setFlag(node, METHOD); setFlag(node, FRAME_VISIBILITY); setFlag(node, SCOPE_AWARE); break; case DEFINEDNODE: switch (((DefinedNode) node).getExpressionNode().getNodeType()) { case CLASSVARASGNNODE: case CLASSVARDECLNODE: case CONSTDECLNODE: case DASGNNODE: case GLOBALASGNNODE: case LOCALASGNNODE: case MULTIPLEASGNNODE: case OPASGNNODE: case OPELEMENTASGNNODE: case DVARNODE: case FALSENODE: case TRUENODE: case LOCALVARNODE: case INSTVARNODE: case BACKREFNODE: case SELFNODE: case VCALLNODE: case YIELDNODE: case GLOBALVARNODE: case CONSTNODE: case FCALLNODE: case CLASSVARNODE: // ok, we have fast paths inspect(((DefinedNode) node).getExpressionNode()); break; default: // long, slow way causes disabling // we still inspect because some nodes may change state (JRUBY-6836) inspect(((DefinedNode) node).getExpressionNode()); disable(); } break; case DOTNODE: DotNode dotNode = (DotNode) node; inspect(dotNode.getBeginNode()); inspect(dotNode.getEndNode()); break; case DASGNNODE: inspect(((AssignableNode) node).getValueNode()); break; case DVARNODE: break; case ENSURENODE: inspect(((EnsureNode) node).getBodyNode()); inspect(((EnsureNode) node).getEnsureNode()); disable(); break; case ENCODINGNODE: break; case EVSTRNODE: inspect(((EvStrNode) node).getBody()); break; case FALSENODE: break; case FIXNUMNODE: break; case FLIPNODE: inspect(((FlipNode) node).getBeginNode()); inspect(((FlipNode) node).getEndNode()); break; case FLOATNODE: break; case FORNODE: setFlag(node, CLOSURE); setFlag(node, SCOPE_AWARE); inspect(((ForNode) node).getIterNode()); inspect(((ForNode) node).getBodyNode()); inspect(((ForNode) node).getVarNode()); break; case GLOBALASGNNODE: GlobalAsgnNode globalAsgnNode = (GlobalAsgnNode) node; if (globalAsgnNode.getName().equals("$_")) { setFlag(node, LASTLINE); } else if (globalAsgnNode.getName().equals("$~")) { setFlag(node, BACKREF); } inspect(globalAsgnNode.getValueNode()); break; case GLOBALVARNODE: { String name = ((GlobalVarNode) node).getName(); if (name.equals("$_") || name.equals("$LAST_READ_LINE")) { setFlag(node, LASTLINE); } else if (name.equals("$~") || name.equals("$`") || name.equals("$'") || name.equals("$+") || name.equals("$LAST_MATCH_INFO") || name.equals("$PREMATCH") || name.equals("$POSTMATCH") || name.equals("$LAST_PAREN_MATCH")) { setFlag(node, BACKREF); } break; } case HASHNODE: HashNode hashNode = (HashNode) node; inspect(hashNode.getListNode()); break; case IFNODE: IfNode ifNode = (IfNode) node; inspect(ifNode.getCondition()); inspect(ifNode.getThenBody()); inspect(ifNode.getElseBody()); break; case INSTASGNNODE: inspect(((AssignableNode) node).getValueNode()); break; case INSTVARNODE: break; case ISCOPINGNODE: IScopingNode iscopingNode = (IScopingNode) node; inspect(iscopingNode.getCPath()); break; case ITERNODE: setFlag(node, CLOSURE); break; case LAMBDANODE: setFlag(node, CLOSURE); break; case LOCALASGNNODE: LocalAsgnNode localAsgnNode = (LocalAsgnNode) node; if (PRAGMAS.contains(localAsgnNode.getName())) { if (localAsgnNode.getName().equals("__NOFRAME__")) { noFrame = localAsgnNode.getValueNode() instanceof TrueNode; } break; } inspect(localAsgnNode.getValueNode()); break; case LOCALVARNODE: break; case MATCHNODE: inspect(((MatchNode) node).getRegexpNode()); setFlag(node, BACKREF); break; case MATCH2NODE: Match2Node match2Node = (Match2Node) node; inspect(match2Node.getReceiverNode()); inspect(match2Node.getValueNode()); setFlag(node, BACKREF); if (match2Node instanceof Match2CaptureNode) { // additionally need scope, to set local vars // FIXME: this can be done without heap scope setFlag(node, SCOPE_AWARE); } break; case MATCH3NODE: Match3Node match3Node = (Match3Node) node; inspect(match3Node.getReceiverNode()); inspect(match3Node.getValueNode()); setFlag(node, BACKREF); break; case MODULENODE: setFlag(node, CLASS); inspect(((ModuleNode) node).getCPath()); break; case MULTIPLEASGN19NODE: MultipleAsgn19Node multipleAsgn19Node = (MultipleAsgn19Node) node; inspect(multipleAsgn19Node.getPre()); inspect(multipleAsgn19Node.getPost()); inspect(multipleAsgn19Node.getRest()); inspect(multipleAsgn19Node.getValueNode()); break; case MULTIPLEASGNNODE: MultipleAsgnNode multipleAsgnNode = (MultipleAsgnNode) node; inspect(multipleAsgnNode.getArgsNode()); inspect(multipleAsgnNode.getHeadNode()); inspect(multipleAsgnNode.getValueNode()); break; case NEWLINENODE: inspect(((NewlineNode) node).getNextNode()); break; case NEXTNODE: inspect(((NextNode) node).getValueNode()); break; case NILNODE: break; case NOTNODE: inspect(((NotNode) node).getConditionNode()); break; case NTHREFNODE: break; case OPASGNANDNODE: OpAsgnAndNode opAsgnAndNode = (OpAsgnAndNode) node; inspect(opAsgnAndNode.getFirstNode()); inspect(opAsgnAndNode.getSecondNode()); break; case OPASGNNODE: OpAsgnNode opAsgnNode = (OpAsgnNode) node; inspect(opAsgnNode.getReceiverNode()); inspect(opAsgnNode.getValueNode()); break; case OPASGNORNODE: switch (((OpAsgnOrNode) node).getFirstNode().getNodeType()) { case CLASSVARASGNNODE: case CLASSVARDECLNODE: case CONSTDECLNODE: case DASGNNODE: case GLOBALASGNNODE: case LOCALASGNNODE: case MULTIPLEASGNNODE: case OPASGNNODE: case OPELEMENTASGNNODE: case DVARNODE: case FALSENODE: case TRUENODE: case LOCALVARNODE: case INSTVARNODE: case BACKREFNODE: case SELFNODE: case VCALLNODE: case YIELDNODE: case GLOBALVARNODE: case CONSTNODE: case FCALLNODE: case CLASSVARNODE: // ok, we have fast paths inspect(((OpAsgnOrNode) node).getSecondNode()); break; default: // long, slow way causes disabling for defined inspect(((OpAsgnOrNode) node).getFirstNode()); inspect(((OpAsgnOrNode) node).getSecondNode()); disable(); } break; case OPELEMENTASGNNODE: OpElementAsgnNode opElementAsgnNode = (OpElementAsgnNode) node; inspect(opElementAsgnNode.getArgsNode()); inspect(opElementAsgnNode.getReceiverNode()); inspect(opElementAsgnNode.getValueNode()); break; case OPTARGNODE: inspect(((OptArgNode) node).getValue()); break; case ORNODE: OrNode orNode = (OrNode) node; inspect(orNode.getFirstNode()); inspect(orNode.getSecondNode()); break; case POSTEXENODE: PostExeNode postExeNode = (PostExeNode) node; setFlag(node, CLOSURE); setFlag(node, SCOPE_AWARE); inspect(postExeNode.getBodyNode()); inspect(postExeNode.getVarNode()); break; case PREEXENODE: PreExeNode preExeNode = (PreExeNode) node; setFlag(node, CLOSURE); setFlag(node, SCOPE_AWARE); inspect(preExeNode.getBodyNode()); inspect(preExeNode.getVarNode()); break; case REDONODE: break; case REGEXPNODE: break; case ROOTNODE: inspect(((RootNode) node).getBodyNode()); if (((RootNode) node).getBodyNode() instanceof BlockNode) { BlockNode blockNode = (BlockNode) ((RootNode) node).getBodyNode(); if (blockNode.size() > 500) { // method has more than 500 lines; we'll need to split it // and therefore need to use a heap-based scope setFlag(node, SCOPE_AWARE); } } break; case RESCUEBODYNODE: RescueBodyNode rescueBody = (RescueBodyNode) node; inspect(rescueBody.getExceptionNodes()); inspect(rescueBody.getBodyNode()); inspect(rescueBody.getOptRescueNode()); break; case RESCUENODE: RescueNode rescueNode = (RescueNode) node; inspect(rescueNode.getBodyNode()); inspect(rescueNode.getElseNode()); inspect(rescueNode.getRescueNode()); disable(); break; case RETRYNODE: setFlag(node, RETRY); break; case RETURNNODE: inspect(((ReturnNode) node).getValueNode()); break; case SCLASSNODE: setFlag(node, CLASS); setFlag(node, FRAME_AWARE); SClassNode sclassNode = (SClassNode) node; inspect(sclassNode.getReceiverNode()); break; case SCOPENODE: break; case SELFNODE: break; case SPLATNODE: inspect(((SplatNode) node).getValue()); break; case STARNODE: break; case STRNODE: break; case SUPERNODE: SuperNode superNode = (SuperNode) node; inspect(superNode.getArgsNode()); inspect(superNode.getIterNode()); setFlag(node, SUPER); break; case SVALUENODE: inspect(((SValueNode) node).getValue()); break; case SYMBOLNODE: break; case TOARYNODE: inspect(((ToAryNode) node).getValue()); break; case TRUENODE: break; case UNDEFNODE: setFlag(node, METHOD); break; case UNTILNODE: UntilNode untilNode = (UntilNode) node; ASTInspector untilInspector = subInspect(untilNode.getConditionNode(), untilNode.getBodyNode()); // a while node could receive non-local flow control from any of these: // * a closure within the loop // * an eval within the loop // * a block-arg-based proc called within the loop if (untilInspector.getFlag(CLOSURE) || untilInspector.getFlag(EVAL)) { untilNode.containsNonlocalFlow = true; // we set scope-aware to true to force heap-based locals setFlag(node, SCOPE_AWARE); } integrate(untilInspector); break; case VALIASNODE: break; case WHENNODE: { inspect(((WhenNode) node).getBodyNode()); inspect(((WhenNode) node).getExpressionNodes()); inspect(((WhenNode) node).getNextCase()); // if any elements are not literals or are regexp, set backref Node expr = ((WhenNode) node).getExpressionNodes(); if (!(expr instanceof ILiteralNode) || expr.getNodeType() == NodeType.REGEXPNODE) { setFlag(node, BACKREF); } break; } case WHILENODE: WhileNode whileNode = (WhileNode) node; ASTInspector whileInspector = subInspect(whileNode.getConditionNode(), whileNode.getBodyNode()); // a while node could receive non-local flow control from any of these: // * a closure within the loop // * an eval within the loop // * a block-arg-based proc called within the loop // * any case that disables optimization, like rescues and ensures if (whileInspector.getFlag(CLOSURE) || whileInspector.getFlag(EVAL) || getFlag(BLOCK_ARG)) { whileNode.containsNonlocalFlow = true; // we set scope-aware to true to force heap-based locals setFlag(node, SCOPE_AWARE); } integrate(whileInspector); break; case XSTRNODE: break; case YIELDNODE: inspect(((YieldNode) node).getArgsNode()); break; case ZARRAYNODE: break; case ZEROARGNODE: break; case ZSUPERNODE: setFlag(node, SCOPE_AWARE); setFlag(node, ZSUPER); inspect(((ZSuperNode) node).getIterNode()); break; default: // encountered a node we don't recognize, set everything to true to disable optz assert false : "All nodes should be accounted for in AST inspector: " + node; disable(); } } public boolean hasClass() { return getFlag(CLASS); } public boolean hasClosure() { return getFlag(CLOSURE); } /** * Whether the tree under inspection contains any method-table mutations, including def, defs, * undef, and alias. * * @return True if there are mutations, false otherwise */ public boolean hasMethod() { return getFlag(METHOD); } public boolean hasFrameAwareMethods() { return getFlag( FRAME_AWARE, FRAME_BLOCK, FRAME_CLASS, FRAME_NAME, FRAME_SELF, FRAME_VISIBILITY, CLOSURE, EVAL, ZSUPER, SUPER, BACKREF, LASTLINE); } public boolean hasScopeAwareMethods() { return getFlag(SCOPE_AWARE); } public boolean hasBlockArg() { return getFlag(BLOCK_ARG); } public boolean hasOptArgs() { return getFlag(OPT_ARGS); } public boolean hasRestArg() { return getFlag(REST_ARG); } public boolean hasConstant() { return getFlag(CONSTANT); } public boolean hasClassVar() { return getFlag(CLASS_VAR); } public boolean noFrame() { return noFrame; } }
public class JITCompiler implements JITCompilerMBean { private static final Logger LOG = LoggerFactory.getLogger("JITCompiler"); public static final boolean USE_CACHE = true; public static final String RUBY_JIT_PREFIX = "rubyjit"; public static final String CLASS_METHOD_DELIMITER = "$$"; public static class JITCounts { private final AtomicLong compiledCount = new AtomicLong(0); private final AtomicLong successCount = new AtomicLong(0); private final AtomicLong failCount = new AtomicLong(0); private final AtomicLong abandonCount = new AtomicLong(0); private final AtomicLong compileTime = new AtomicLong(0); private final AtomicLong averageCompileTime = new AtomicLong(0); private final AtomicLong codeSize = new AtomicLong(0); private final AtomicLong averageCodeSize = new AtomicLong(0); private final AtomicLong largestCodeSize = new AtomicLong(0); } private final JITCounts counts = new JITCounts(); private final ExecutorService executor = new ThreadPoolExecutor( 2, // always two threads 2, 0, // never stop TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DaemonThreadFactory("JRubyJIT", Thread.MIN_PRIORITY)); private final Ruby runtime; private final RubyInstanceConfig config; public JITCompiler(Ruby runtime) { this.runtime = runtime; this.config = runtime.getInstanceConfig(); runtime.getBeanManager().register(this); } public long getSuccessCount() { return counts.successCount.get(); } public long getCompileCount() { return counts.compiledCount.get(); } public long getFailCount() { return counts.failCount.get(); } public long getCompileTime() { return counts.compileTime.get() / 1000; } public long getAbandonCount() { return counts.abandonCount.get(); } public long getCodeSize() { return counts.codeSize.get(); } public long getAverageCodeSize() { return counts.averageCodeSize.get(); } public long getAverageCompileTime() { return counts.averageCompileTime.get() / 1000; } public long getLargestCodeSize() { return counts.largestCodeSize.get(); } public void tryJIT( DefaultMethod method, ThreadContext context, String className, String methodName) { if (!config.getCompileMode().shouldJIT()) return; if (method.incrementCallCount() < config.getJitThreshold()) return; jitThresholdReached(method, config, context, className, methodName); } public void tearDown() { if (executor != null) { try { executor.shutdown(); } catch (SecurityException se) { // ignore, can't shut down executor } } } private void jitThresholdReached( final DefaultMethod method, final RubyInstanceConfig config, ThreadContext context, final String className, final String methodName) { // Disable any other jit tasks from entering queue method.setCallCount(-1); final Ruby runtime = context.runtime; Runnable jitTask = new JITTask(className, method, methodName); // if background JIT is enabled and threshold is > 0 and we have an executor... if (config.getJitBackground() && config.getJitThreshold() > 0 && executor != null) { // JIT in background try { executor.submit(jitTask); } catch (RejectedExecutionException ree) { // failed to submit, just run it directly jitTask.run(); } } else { // just run directly jitTask.run(); } } private class JITTask implements Runnable { private final String className; private final DefaultMethod method; private final String methodName; public JITTask(String className, DefaultMethod method, String methodName) { this.className = className; this.method = method; this.methodName = methodName; } public void run() { try { // The cache is full. Abandon JIT for this method and bail out. ClassCache classCache = config.getClassCache(); if (classCache.isFull()) { counts.abandonCount.incrementAndGet(); return; } // Check if the method has been explicitly excluded if (config.getExcludedMethods().size() > 0) { String excludeModuleName = className; if (method.getImplementationClass().isSingleton()) { IRubyObject possibleRealClass = ((MetaClass) method.getImplementationClass()).getAttached(); if (possibleRealClass instanceof RubyModule) { excludeModuleName = "Meta:" + ((RubyModule) possibleRealClass).getName(); } } if ((config.getExcludedMethods().contains(excludeModuleName) || config.getExcludedMethods().contains(excludeModuleName + "#" + methodName) || config.getExcludedMethods().contains(methodName))) { method.setCallCount(-1); log(method, methodName, "skipping method: " + excludeModuleName + "#" + methodName); return; } } String key = SexpMaker.create(methodName, method.getArgsNode(), method.getBodyNode()); JITClassGenerator generator = new JITClassGenerator(className, methodName, key, runtime, method, counts); Class<Script> sourceClass = (Class<Script>) config.getClassCache().cacheClassByKey(generator.digestString, generator); if (sourceClass == null) { // class could not be found nor generated; give up on JIT and bail out counts.failCount.incrementAndGet(); return; } // successfully got back a jitted method counts.successCount.incrementAndGet(); // finally, grab the script Script jitCompiledScript = sourceClass.newInstance(); // set root scope jitCompiledScript.setRootScope(method.getStaticScope()); // add to the jitted methods set Set<Script> jittedMethods = runtime.getJittedMethods(); jittedMethods.add(jitCompiledScript); // logEvery n methods based on configuration if (config.getJitLogEvery() > 0) { int methodCount = jittedMethods.size(); if (methodCount % config.getJitLogEvery() == 0) { log(method, methodName, "live compiled methods: " + methodCount); } } if (config.isJitLogging()) { log(method, className + "." + methodName, "done jitting"); } method.switchToJitted(jitCompiledScript, generator.callConfig()); return; } catch (Throwable t) { if (runtime.getDebug().isTrue()) { t.printStackTrace(); } if (config.isJitLoggingVerbose()) { log(method, className + "." + methodName, "could not compile", t.getMessage()); } counts.failCount.incrementAndGet(); return; } } } public static String getHashForString(String str) { return getHashForBytes(RubyEncoding.encodeUTF8(str)); } public static String getHashForBytes(byte[] bytes) { try { MessageDigest sha1 = MessageDigest.getInstance("SHA1"); sha1.update(bytes); byte[] digest = sha1.digest(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < digest.length; i++) { builder.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1)); } return builder.toString().toUpperCase(Locale.ENGLISH); } catch (NoSuchAlgorithmException nsae) { throw new RuntimeException(nsae); } } public static void saveToCodeCache( Ruby ruby, byte[] bytecode, String packageName, File cachedClassFile) { String codeCache = RubyInstanceConfig.JIT_CODE_CACHE; File codeCacheDir = new File(codeCache); if (!codeCacheDir.exists()) { ruby.getWarnings().warn("jruby.jit.codeCache directory " + codeCacheDir + " does not exist"); } else if (!codeCacheDir.isDirectory()) { ruby.getWarnings() .warn("jruby.jit.codeCache directory " + codeCacheDir + " is not a directory"); } else if (!codeCacheDir.canWrite()) { ruby.getWarnings().warn("jruby.jit.codeCache directory " + codeCacheDir + " is not writable"); } else { if (!new File(codeCache, packageName).isDirectory()) { boolean createdDirs = new File(codeCache, packageName).mkdirs(); if (!createdDirs) { ruby.getWarnings() .warn("could not create JIT cache dir: " + new File(codeCache, packageName)); } } // write to code cache FileOutputStream fos = null; try { if (RubyInstanceConfig.JIT_LOADING_DEBUG) LOG.info("writing jitted code to to " + cachedClassFile); fos = new FileOutputStream(cachedClassFile); fos.write(bytecode); } catch (Exception e) { e.printStackTrace(); // ignore } finally { try { fos.close(); } catch (Exception e) { } } } } public static class JITClassGenerator implements ClassCache.ClassGenerator { public JITClassGenerator( String className, String methodName, String key, Ruby ruby, DefaultMethod method, JITCounts counts) { this.packageName = JITCompiler.RUBY_JIT_PREFIX; if (RubyInstanceConfig.JAVA_VERSION == Opcodes.V1_7) { // recent Java 7 seems to have a bug that leaks definitions across cousin classloaders // so we force the class name to be unique to this runtime digestString = getHashForString(key) + Math.abs(ruby.hashCode()); } else { digestString = getHashForString(key); } this.className = packageName + "/" + className.replace('.', '/') + CLASS_METHOD_DELIMITER + JavaNameMangler.mangleMethodName(methodName) + "_" + digestString; this.name = this.className.replaceAll("/", "."); this.bodyNode = method.getBodyNode(); this.argsNode = method.getArgsNode(); this.methodName = methodName; filename = calculateFilename(argsNode, bodyNode); staticScope = method.getStaticScope(); asmCompiler = new StandardASMCompiler(this.className, filename); this.ruby = ruby; this.counts = counts; } @SuppressWarnings("unchecked") protected void compile() { if (bytecode != null) return; // check if we have a cached compiled version on disk String codeCache = RubyInstanceConfig.JIT_CODE_CACHE; File cachedClassFile = new File(codeCache + "/" + className + ".class"); if (codeCache != null && cachedClassFile.exists()) { FileInputStream fis = null; try { if (RubyInstanceConfig.JIT_LOADING_DEBUG) LOG.info("loading cached code from: " + cachedClassFile); fis = new FileInputStream(cachedClassFile); bytecode = new byte[(int) fis.getChannel().size()]; fis.read(bytecode); name = new ClassReader(bytecode).getClassName(); return; } catch (Exception e) { // ignore and proceed to compile } finally { try { fis.close(); } catch (Exception e) { } } } // Time the compilation long start = System.nanoTime(); asmCompiler.startScript(staticScope); final ASTCompiler compiler = ruby.getInstanceConfig().newCompiler(); CompilerCallback args = new CompilerCallback() { public void call(BodyCompiler context) { compiler.compileArgs(argsNode, context, true); } }; ASTInspector inspector = new ASTInspector(); if (ruby.getInstanceConfig().isJitDumping()) { inspector = new ASTInspector(className, true); } // check args first, since body inspection can depend on args inspector.inspect(argsNode); inspector.inspect(bodyNode); BodyCompiler methodCompiler; if (bodyNode != null) { // we have a body, do a full-on method methodCompiler = asmCompiler.startFileMethod(args, staticScope, inspector); compiler.compileBody(bodyNode, methodCompiler, true); } else { // If we don't have a body, check for required or opt args // if opt args, they could have side effects // if required args, need to raise errors if too few args passed // otherwise, method does nothing, make it a nop if (argsNode != null && (argsNode.getRequiredArgsCount() > 0 || argsNode.getOptionalArgsCount() > 0)) { methodCompiler = asmCompiler.startFileMethod(args, staticScope, inspector); methodCompiler.loadNil(); } else { methodCompiler = asmCompiler.startFileMethod(null, staticScope, inspector); methodCompiler.loadNil(); jitCallConfig = CallConfiguration.FrameNoneScopeNone; } } methodCompiler.endBody(); asmCompiler.endScript(false, false); // if we haven't already decided on a do-nothing call if (jitCallConfig == null) { jitCallConfig = inspector.getCallConfig(); } bytecode = asmCompiler.getClassByteArray(); if (ruby.getInstanceConfig().isJitDumping()) { TraceClassVisitor tcv = new TraceClassVisitor(new PrintWriter(System.out)); new ClassReader(bytecode).accept(tcv, 0); } if (bytecode.length > ruby.getInstanceConfig().getJitMaxSize()) { bytecode = null; throw new NotCompilableException( "JITed method size exceeds configured max of " + ruby.getInstanceConfig().getJitMaxSize()); } if (codeCache != null) { JITCompiler.saveToCodeCache(ruby, bytecode, packageName, cachedClassFile); } counts.compiledCount.incrementAndGet(); counts.compileTime.addAndGet(System.nanoTime() - start); counts.codeSize.addAndGet(bytecode.length); counts.averageCompileTime.set(counts.compileTime.get() / counts.compiledCount.get()); counts.averageCodeSize.set(counts.codeSize.get() / counts.compiledCount.get()); synchronized (counts) { if (counts.largestCodeSize.get() < bytecode.length) { counts.largestCodeSize.set(bytecode.length); } } } public void generate() { compile(); } public byte[] bytecode() { return bytecode; } public String name() { return name; } public CallConfiguration callConfig() { compile(); return jitCallConfig; } @Override public String toString() { return methodName + "() at " + bodyNode.getPosition().getFile() + ":" + bodyNode.getPosition().getLine(); } private final StandardASMCompiler asmCompiler; private final StaticScope staticScope; private final Node bodyNode; private final ArgsNode argsNode; private final Ruby ruby; private final String packageName; private final String className; private final String filename; private final String methodName; private final JITCounts counts; private final String digestString; private CallConfiguration jitCallConfig; private byte[] bytecode; private String name; } public Block newCompiledClosure(ThreadContext context, IterNode iterNode, IRubyObject self) { Binding binding = context.currentBinding(self); NodeType argsNodeId = getArgumentTypeWackyHack(iterNode); boolean hasMultipleArgsHead = false; if (iterNode.getVarNode() instanceof MultipleAsgnNode) { hasMultipleArgsHead = ((MultipleAsgnNode) iterNode.getVarNode()).getHeadNode() != null; } BlockBody body = new CompiledBlock( Arity.procArityOf(iterNode.getVarNode()), iterNode.getScope(), compileBlock( context, new StandardASMCompiler("blahfooblah" + System.currentTimeMillis(), "blahfooblah"), iterNode), hasMultipleArgsHead, BlockBody.asArgumentType(argsNodeId)); return new Block(body, binding); } public BlockBody newCompiledBlockBody( ThreadContext context, IterNode iterNode, Arity arity, int argumentType) { NodeType argsNodeId = getArgumentTypeWackyHack(iterNode); boolean hasMultipleArgsHead = false; if (iterNode.getVarNode() instanceof MultipleAsgnNode) { hasMultipleArgsHead = ((MultipleAsgnNode) iterNode.getVarNode()).getHeadNode() != null; } return new CompiledBlock( Arity.procArityOf(iterNode.getVarNode()), iterNode.getScope(), compileBlock( context, new StandardASMCompiler("blahfooblah" + System.currentTimeMillis(), "blahfooblah"), iterNode), hasMultipleArgsHead, BlockBody.asArgumentType(argsNodeId)); } // ENEBO: Some of this logic should be put back into the Nodes themselves, but the more // esoteric features of 1.9 make this difficult to know how to do this yet. public BlockBody newCompiledBlockBody19(ThreadContext context, IterNode iterNode) { final ArgsNode argsNode = (ArgsNode) iterNode.getVarNode(); boolean hasMultipleArgsHead = false; if (iterNode.getVarNode() instanceof MultipleAsgnNode) { hasMultipleArgsHead = ((MultipleAsgnNode) iterNode.getVarNode()).getHeadNode() != null; } NodeType argsNodeId = BlockBody.getArgumentTypeWackyHack(iterNode); return new CompiledBlock19( ((ArgsNode) iterNode.getVarNode()).getArity(), iterNode.getScope(), compileBlock19( context, new StandardASMCompiler("blahfooblah" + System.currentTimeMillis(), "blahfooblah"), iterNode), hasMultipleArgsHead, BlockBody.asArgumentType(argsNodeId), Helpers.encodeParameterList(argsNode).split(";")); } public CompiledBlockCallback compileBlock( ThreadContext context, StandardASMCompiler asmCompiler, final IterNode iterNode) { final ASTCompiler astCompiler = new ASTCompiler(); final StaticScope scope = iterNode.getScope(); asmCompiler.startScript(scope); // create the closure class and instantiate it final CompilerCallback closureBody = new CompilerCallback() { public void call(BodyCompiler context) { if (iterNode.getBodyNode() != null) { astCompiler.compile(iterNode.getBodyNode(), context, true); } else { context.loadNil(); } } }; // create the closure class and instantiate it final CompilerCallback closureArgs = new CompilerCallback() { public void call(BodyCompiler context) { if (iterNode.getVarNode() != null) { astCompiler.compileAssignment(iterNode.getVarNode(), context); } else { context.consumeCurrentValue(); } if (iterNode.getBlockVarNode() != null) { astCompiler.compileAssignment(iterNode.getBlockVarNode(), context); } else { context.consumeCurrentValue(); } } }; ASTInspector inspector = new ASTInspector(); inspector.inspect(iterNode.getBodyNode()); inspector.inspect(iterNode.getVarNode()); int scopeIndex = asmCompiler.getCacheCompiler().reserveStaticScope(); ChildScopedBodyCompiler closureCompiler = new ChildScopedBodyCompiler( asmCompiler, "__file__", asmCompiler.getClassname(), inspector, scope, scopeIndex); closureCompiler.beginMethod(closureArgs, scope); closureBody.call(closureCompiler); closureCompiler.endBody(); // __file__ method with [] args; no-op SkinnyMethodAdapter method = new SkinnyMethodAdapter( asmCompiler.getClassVisitor(), ACC_PUBLIC, "__file__", getMethodSignature(4), null, null); method.start(); method.aload(SELF_INDEX); method.areturn(); method.end(); // __file__ method to call static version method = new SkinnyMethodAdapter( asmCompiler.getClassVisitor(), ACC_PUBLIC, "__file__", getMethodSignature(1), null, null); method.start(); // invoke static __file__ method.aload(THIS); method.aload(THREADCONTEXT_INDEX); method.aload(SELF_INDEX); method.aload(ARGS_INDEX); method.aload(ARGS_INDEX + 1); // block method.invokestatic( asmCompiler.getClassname(), "__file__", getStaticMethodSignature(asmCompiler.getClassname(), 1)); method.areturn(); method.end(); asmCompiler.endScript(false, false); byte[] bytes = asmCompiler.getClassByteArray(); Class blockClass = new JRubyClassLoader(context.runtime.getJRubyClassLoader()) .defineClass(asmCompiler.getClassname(), bytes); try { final AbstractScript script = (AbstractScript) blockClass.newInstance(); script.setRootScope(scope); return new CompiledBlockCallback() { @Override public IRubyObject call( ThreadContext context, IRubyObject self, IRubyObject args, Block block) { return script.__file__(context, self, args, block); } @Override public String getFile() { return "blah"; } @Override public int getLine() { return -1; } }; } catch (Exception e) { throw new RuntimeException(e); } } public CompiledBlockCallback19 compileBlock19( ThreadContext context, StandardASMCompiler asmCompiler, final IterNode iterNode) { final ASTCompiler19 astCompiler = new ASTCompiler19(); final StaticScope scope = iterNode.getScope(); asmCompiler.startScript(scope); final ArgsNode argsNode = (ArgsNode) iterNode.getVarNode(); // create the closure class and instantiate it final CompilerCallback closureBody = new CompilerCallback() { public void call(BodyCompiler context) { if (iterNode.getBodyNode() != null) { astCompiler.compile(iterNode.getBodyNode(), context, true); } else { context.loadNil(); } } }; // create the closure class and instantiate it final CompilerCallback closureArgs = new CompilerCallback() { public void call(BodyCompiler context) { // FIXME: This is temporary since the variable compilers assume we want // args already on stack for assignment. We just pop and continue with // 1.9 args logic. context.consumeCurrentValue(); // args value context.consumeCurrentValue(); // passed block if (iterNode.getVarNode() != null) { if (iterNode instanceof LambdaNode) { final int required = argsNode.getRequiredArgsCount(); final int opt = argsNode.getOptionalArgsCount(); final int rest = argsNode.getRestArg(); context.getVariableCompiler().checkMethodArity(required, opt, rest); astCompiler.compileMethodArgs(argsNode, context, true); } else { astCompiler.compileMethodArgs(argsNode, context, true); } } } }; ASTInspector inspector = new ASTInspector(); inspector.inspect(iterNode.getBodyNode()); inspector.inspect(iterNode.getVarNode()); NodeType argsNodeId = BlockBody.getArgumentTypeWackyHack(iterNode); int scopeIndex = asmCompiler.getCacheCompiler().reserveStaticScope(); ChildScopedBodyCompiler closureCompiler = new ChildScopedBodyCompiler19( asmCompiler, "__file__", asmCompiler.getClassname(), inspector, scope, scopeIndex); closureCompiler.beginMethod(argsNodeId == null ? null : closureArgs, scope); closureBody.call(closureCompiler); closureCompiler.endBody(); // __file__ method to call static version SkinnyMethodAdapter method = new SkinnyMethodAdapter( asmCompiler.getClassVisitor(), ACC_PUBLIC, "__file__", getMethodSignature(4), null, null); method.start(); // invoke static __file__ method.aload(THIS); method.aload(THREADCONTEXT_INDEX); method.aload(SELF_INDEX); method.aload(ARGS_INDEX); method.aload(ARGS_INDEX + 1); // block method.invokestatic( asmCompiler.getClassname(), "__file__", asmCompiler.getStaticMethodSignature(asmCompiler.getClassname(), 4)); method.areturn(); method.end(); asmCompiler.endScript(false, false); byte[] bytes = asmCompiler.getClassByteArray(); Class blockClass = new JRubyClassLoader(context.runtime.getJRubyClassLoader()) .defineClass(asmCompiler.getClassname(), bytes); try { final AbstractScript script = (AbstractScript) blockClass.newInstance(); script.setRootScope(scope); return new CompiledBlockCallback19() { @Override public IRubyObject call( ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) { return script.__file__(context, self, args, block); } @Override public String getFile() { return iterNode.getPosition().getFile(); } @Override public int getLine() { return iterNode.getPosition().getLine(); } }; } catch (Exception e) { throw new RuntimeException(e); } } static void log(DefaultMethod method, String name, String message, String... reason) { String className = method.getImplementationClass().getBaseName(); if (className == null) className = "<anon class>"; StringBuilder builder = new StringBuilder(message + ":" + className + "." + name + " at " + method.getPosition()); if (reason.length > 0) { builder.append(" because of: \""); for (int i = 0; i < reason.length; i++) { builder.append(reason[i]); } builder.append('"'); } LOG.info(builder.toString()); } private static String calculateFilename(ArgsNode argsNode, Node bodyNode) { if (bodyNode != null) return bodyNode.getPosition().getFile(); if (argsNode != null) return argsNode.getPosition().getFile(); return "__eval__"; } }
/** * ChannelDescriptor provides an abstraction similar to the concept of a "file descriptor" on any * POSIX system. In our case, it's a numbered object (fileno) enclosing a Channel (@see * java.nio.channels.Channel), FileDescriptor (@see java.io.FileDescriptor), and flags under which * the original open occured (@see org.jruby.util.io.ModeFlags). Several operations you would * normally expect to use with a POSIX file descriptor are implemented here and used by higher-level * classes to implement higher-level IO behavior. * * <p>Note that the channel specified when constructing a ChannelDescriptor will be * reference-counted; that is, until all known references to it through this class have gone away, * it will be left open. This is to support operations like "dup" which must produce two independent * ChannelDescriptor instances that can be closed separately without affecting the other. * * <p>At present there's no way to simulate the behavior on some platforms where POSIX dup also * allows independent positioning information. */ public class ChannelDescriptor { private static final Logger LOG = LoggerFactory.getLogger("ChannelDescriptor"); /** Whether to log debugging information */ private static final boolean DEBUG = false; /** The java.nio.channels.Channel this descriptor wraps. */ private Channel channel; /** * The file number (equivalent to the int file descriptor value in POSIX) for this descriptor. * This is generated new for most ChannelDescriptor instances, except when they need to masquerade * as another fileno. */ private int internalFileno; /** The java.io.FileDescriptor object for this descriptor. */ private FileDescriptor fileDescriptor; /** The original org.jruby.util.io.ModeFlags with which the specified channel was opened. */ private ModeFlags originalModes; /** * The reference count for the provided channel. Only counts references through ChannelDescriptor * instances. */ private AtomicInteger refCounter; /** * Used to work-around blocking problems with STDIN. In most cases <code>null</code>. See {@link * ChannelDescriptor#ChannelDescriptor(java.io.InputStream, ModeFlags, java.io.FileDescriptor)} * for more details. You probably should not use it. */ private InputStream baseInputStream; /** * Process streams get Channel.newChannel()ed into FileChannel but are not actually seekable. So * instead of just the isSeekable check doing instanceof FileChannel, we must also add this * boolean to check, which we set to false when it's known that the incoming channel is from a * process. * * <p>FIXME: This is gross, and it's NIO's fault for not providing a nice way to tell if a channel * is "really" seekable. */ private boolean canBeSeekable = true; /** * If the incoming channel is already in append mode (i.e. it will do the requisite seeking), we * don't want to do our own additional seeks. */ private boolean isInAppendMode = false; /** Whether the current channe is writable or not. */ private boolean readableChannel; /** Whether the current channel is readable or not. */ private boolean writableChannel; /** Whether the current channel is seekable or not. */ private boolean seekableChannel; /** * Construct a new ChannelDescriptor with the specified channel, file number, mode flags, file * descriptor object and reference counter. This constructor is only used when constructing a new * copy of an existing ChannelDescriptor with an existing reference count, to allow the two * instances to safely share and appropriately close a given channel. * * @param channel The channel for the new descriptor, which will be shared with another * @param fileno The new file number for the new descriptor * @param originalModes The mode flags to use as the "origina" set for this descriptor * @param fileDescriptor The java.io.FileDescriptor object to associate with this * ChannelDescriptor * @param refCounter The reference counter from another ChannelDescriptor being duped. * @param canBeSeekable If the underlying channel can be considered seekable. * @param isInAppendMode If the underlying channel is already in append mode. */ private ChannelDescriptor( Channel channel, int fileno, ModeFlags originalModes, FileDescriptor fileDescriptor, AtomicInteger refCounter, boolean canBeSeekable, boolean isInAppendMode) { this.refCounter = refCounter; this.channel = channel; this.internalFileno = fileno; this.originalModes = originalModes; this.fileDescriptor = fileDescriptor; this.canBeSeekable = canBeSeekable; this.isInAppendMode = isInAppendMode; this.readableChannel = channel instanceof ReadableByteChannel; this.writableChannel = channel instanceof WritableByteChannel; this.seekableChannel = channel instanceof FileChannel; registerDescriptor(this); } private ChannelDescriptor( Channel channel, int fileno, ModeFlags originalModes, FileDescriptor fileDescriptor) { this(channel, fileno, originalModes, fileDescriptor, new AtomicInteger(1), true, false); } /** * Construct a new ChannelDescriptor with the given channel, file number, mode flags, and file * descriptor object. The channel will be kept open until all ChannelDescriptor references to it * have been closed. * * @param channel The channel for the new descriptor * @param originalModes The mode flags for the new descriptor * @param fileDescriptor The java.io.FileDescriptor object for the new descriptor */ public ChannelDescriptor( Channel channel, ModeFlags originalModes, FileDescriptor fileDescriptor) { this(channel, getNewFileno(), originalModes, fileDescriptor, new AtomicInteger(1), true, false); } /** * Construct a new ChannelDescriptor with the given channel, file number, mode flags, and file * descriptor object. The channel will be kept open until all ChannelDescriptor references to it * have been closed. * * @param channel The channel for the new descriptor * @param originalModes The mode flags for the new descriptor * @param fileDescriptor The java.io.FileDescriptor object for the new descriptor */ public ChannelDescriptor( Channel channel, ModeFlags originalModes, FileDescriptor fileDescriptor, boolean isInAppendMode) { this( channel, getNewFileno(), originalModes, fileDescriptor, new AtomicInteger(1), true, isInAppendMode); } /** * Construct a new ChannelDescriptor with the given channel, file number, mode flags, and file * descriptor object. The channel will be kept open until all ChannelDescriptor references to it * have been closed. * * @param channel The channel for the new descriptor * @param originalModes The mode flags for the new descriptor */ public ChannelDescriptor(Channel channel, ModeFlags originalModes) { this( channel, getNewFileno(), originalModes, getDescriptorFromChannel(channel), new AtomicInteger(1), true, false); } /** * Special constructor to create the ChannelDescriptor out of the stream, file number, mode flags, * and file descriptor object. The channel will be created from the provided stream. The channel * will be kept open until all ChannelDescriptor references to it have been closed. <b>Note:</b> * in most cases, you should not use this constructor, it's reserved mostly for STDIN. * * @param baseInputStream The stream to create the channel for the new descriptor * @param originalModes The mode flags for the new descriptor * @param fileDescriptor The java.io.FileDescriptor object for the new descriptor */ public ChannelDescriptor( InputStream baseInputStream, ModeFlags originalModes, FileDescriptor fileDescriptor) { // The reason why we need the stream is to be able to invoke available() on it. // STDIN in Java is non-interruptible, non-selectable, and attempt to read // on such stream might lead to thread being blocked without *any* way to unblock it. // That's where available() comes it, so at least we could check whether // anything is available to be read without blocking. this( Channels.newChannel(baseInputStream), getNewFileno(), originalModes, fileDescriptor, new AtomicInteger(1), true, false); this.baseInputStream = baseInputStream; } /** * Special constructor to create the ChannelDescriptor out of the stream, file number, mode flags, * and file descriptor object. The channel will be created from the provided stream. The channel * will be kept open until all ChannelDescriptor references to it have been closed. <b>Note:</b> * in most cases, you should not use this constructor, it's reserved mostly for STDIN. * * @param baseInputStream The stream to create the channel for the new descriptor * @param originalModes The mode flags for the new descriptor */ public ChannelDescriptor(InputStream baseInputStream, ModeFlags originalModes) { // The reason why we need the stream is to be able to invoke available() on it. // STDIN in Java is non-interruptible, non-selectable, and attempt to read // on such stream might lead to thread being blocked without *any* way to unblock it. // That's where available() comes it, so at least we could check whether // anything is available to be read without blocking. this( Channels.newChannel(baseInputStream), getNewFileno(), originalModes, new FileDescriptor(), new AtomicInteger(1), true, false); this.baseInputStream = baseInputStream; } /** * Construct a new ChannelDescriptor with the given channel, file number, and file descriptor * object. The channel will be kept open until all ChannelDescriptor references to it have been * closed. The channel's capabilities will be used to determine the "original" set of mode flags. * * @param channel The channel for the new descriptor * @param fileDescriptor The java.io.FileDescriptor object for the new descriptor */ public ChannelDescriptor(Channel channel, FileDescriptor fileDescriptor) throws InvalidValueException { this(channel, getModesFromChannel(channel), fileDescriptor); } @Deprecated public ChannelDescriptor(Channel channel, int fileno, FileDescriptor fileDescriptor) throws InvalidValueException { this(channel, getModesFromChannel(channel), fileDescriptor); } /** * Construct a new ChannelDescriptor with the given channel, file number, and file descriptor * object. The channel will be kept open until all ChannelDescriptor references to it have been * closed. The channel's capabilities will be used to determine the "original" set of mode flags. * This version generates a new fileno. * * @param channel The channel for the new descriptor */ public ChannelDescriptor(Channel channel) throws InvalidValueException { this(channel, getModesFromChannel(channel), getDescriptorFromChannel(channel)); } /** * Get this descriptor's file number. * * @return the fileno for this descriptor */ public int getFileno() { return internalFileno; } /** * Get the FileDescriptor object associated with this descriptor. This is not guaranteed to be a * "valid" descriptor in the terms of the Java implementation, but is provided for completeness * and for cases where it is possible to get a valid FileDescriptor for a given channel. * * @return the java.io.FileDescriptor object associated with this descriptor */ public FileDescriptor getFileDescriptor() { return fileDescriptor; } /** * The channel associated with this descriptor. The channel will be reference counted through * ChannelDescriptor and kept open until all ChannelDescriptor objects have been closed. * References that leave ChannelDescriptor through this method will not be counted. * * @return the java.nio.channels.Channel associated with this descriptor */ public Channel getChannel() { return channel; } /** * This is intentionally non-public, since it should not be really used outside of very limited * use case (handling of STDIN). See {@link * ChannelDescriptor#ChannelDescriptor(java.io.InputStream, ModeFlags, java.io.FileDescriptor)} * for more info. */ /*package-protected*/ InputStream getBaseInputStream() { return baseInputStream; } /** * Whether the channel associated with this descriptor is seekable (i.e. whether it is instanceof * FileChannel). * * @return true if the associated channel is seekable, false otherwise */ public boolean isSeekable() { return canBeSeekable && seekableChannel; } /** * Set the channel to be explicitly seekable or not, for streams that appear to be seekable with * the instanceof FileChannel check. * * @param canBeSeekable Whether the channel is seekable or not. */ public void setCanBeSeekable(boolean canBeSeekable) { this.canBeSeekable = canBeSeekable; } /** * Whether the channel associated with this descriptor is a NullChannel, for which many operations * are simply noops. */ public boolean isNull() { return channel instanceof NullChannel; } /** * Whether the channel associated with this descriptor is writable (i.e. whether it is instanceof * WritableByteChannel). * * @return true if the associated channel is writable, false otherwise */ public boolean isWritable() { return writableChannel; } /** * Whether the channel associated with this descriptor is readable (i.e. whether it is instanceof * ReadableByteChannel). * * @return true if the associated channel is readable, false otherwise */ public boolean isReadable() { return readableChannel; } /** * Whether the channel associated with this descriptor is open. * * @return true if the associated channel is open, false otherwise */ public boolean isOpen() { return channel.isOpen(); } /** * Check whether the isOpen returns true, raising a BadDescriptorException if it returns false. * * @throws org.jruby.util.io.BadDescriptorException if isOpen returns false */ public void checkOpen() throws BadDescriptorException { if (!isOpen()) { throw new BadDescriptorException(); } } /** * Get the original mode flags for the descriptor. * * @return the original mode flags for the descriptor */ public ModeFlags getOriginalModes() { return originalModes; } /** * Check whether a specified set of mode flags is a superset of this descriptor's original set of * mode flags. * * @param newModes The modes to confirm as superset * @throws org.jruby.util.io.InvalidValueException if the modes are not a superset */ public void checkNewModes(ModeFlags newModes) throws InvalidValueException { if (!newModes.isSubsetOf(originalModes)) { throw new InvalidValueException(); } } /** * Mimics the POSIX dup(2) function, returning a new descriptor that references the same open * channel. * * @return A duplicate ChannelDescriptor based on this one */ public ChannelDescriptor dup() { synchronized (refCounter) { refCounter.incrementAndGet(); int newFileno = getNewFileno(); if (DEBUG) LOG.info("Reopen fileno {}, refs now: {}", newFileno, refCounter.get()); return new ChannelDescriptor( channel, newFileno, originalModes, fileDescriptor, refCounter, canBeSeekable, isInAppendMode); } } /** * Mimics the POSIX dup2(2) function, returning a new descriptor that references the same open * channel but with a specified fileno. * * @param fileno The fileno to use for the new descriptor * @return A duplicate ChannelDescriptor based on this one */ public ChannelDescriptor dup2(int fileno) { synchronized (refCounter) { refCounter.incrementAndGet(); if (DEBUG) LOG.info("Reopen fileno {}, refs now: {}", fileno, refCounter.get()); return new ChannelDescriptor( channel, fileno, originalModes, fileDescriptor, refCounter, canBeSeekable, isInAppendMode); } } /** * Mimics the POSIX dup2(2) function, returning a new descriptor that references the same open * channel but with a specified fileno. This differs from the fileno version by making the target * descriptor into a new reference to the current descriptor's channel, closing what it originally * pointed to and preserving its original fileno. * * @param other the descriptor to dup this one into */ public void dup2Into(ChannelDescriptor other) throws BadDescriptorException, IOException { synchronized (refCounter) { refCounter.incrementAndGet(); if (DEBUG) LOG.info("Reopen fileno {}, refs now: {}", internalFileno, refCounter.get()); other.close(); other.channel = channel; other.originalModes = originalModes; other.fileDescriptor = fileDescriptor; other.refCounter = refCounter; other.canBeSeekable = canBeSeekable; other.readableChannel = readableChannel; other.writableChannel = writableChannel; other.seekableChannel = seekableChannel; } } public ChannelDescriptor reopen(Channel channel, ModeFlags modes) { return new ChannelDescriptor(channel, internalFileno, modes, fileDescriptor); } public ChannelDescriptor reopen(RandomAccessFile file, ModeFlags modes) throws IOException { return new ChannelDescriptor(file.getChannel(), internalFileno, modes, file.getFD()); } /** * Perform a low-level seek operation on the associated channel if it is instanceof FileChannel, * or raise PipeException if it is not a FileChannel. Calls checkOpen to confirm the target * channel is open. This is equivalent to the lseek(2) POSIX function, and like that function it * bypasses any buffer flushing or invalidation as in ChannelStream.fseek. * * @param offset the offset value to use * @param whence whence to seek * @throws java.io.IOException If there is an exception while seeking * @throws org.jruby.util.io.InvalidValueException If the value specified for offset or whence is * invalid * @throws org.jruby.util.io.PipeException If the target channel is not seekable * @throws org.jruby.util.io.BadDescriptorException If the target channel is already closed. * @return the new offset into the FileChannel. */ public long lseek(long offset, int whence) throws IOException, InvalidValueException, PipeException, BadDescriptorException { if (seekableChannel) { checkOpen(); FileChannel fileChannel = (FileChannel) channel; try { long pos; switch (whence) { case Stream.SEEK_SET: pos = offset; fileChannel.position(pos); break; case Stream.SEEK_CUR: pos = fileChannel.position() + offset; fileChannel.position(pos); break; case Stream.SEEK_END: pos = fileChannel.size() + offset; fileChannel.position(pos); break; default: throw new InvalidValueException(); } return pos; } catch (IllegalArgumentException e) { throw new InvalidValueException(); } catch (IOException ioe) { // "invalid seek" means it's an ESPIPE, so we rethrow as a PipeException() if (ioe.getMessage().equals("Illegal seek")) { throw new PipeException(); } throw ioe; } } else { throw new PipeException(); } } /** * Perform a low-level read of the specified number of bytes into the specified byte list. The * incoming bytes will be appended to the byte list. This is equivalent to the read(2) POSIX * function, and like that function it ignores read and write buffers defined elsewhere. * * @param number the number of bytes to read * @param byteList the byte list on which to append the incoming bytes * @return the number of bytes actually read * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed. * @see org.jruby.util.ByteList */ public int read(int number, ByteList byteList) throws IOException, BadDescriptorException { checkOpen(); byteList.ensure(byteList.length() + number); int bytesRead = read( ByteBuffer.wrap( byteList.getUnsafeBytes(), byteList.begin() + byteList.length(), number)); if (bytesRead > 0) { byteList.length(byteList.length() + bytesRead); } return bytesRead; } /** * Perform a low-level read of the remaining number of bytes into the specified byte buffer. The * incoming bytes will be used to fill the remaining space in the target byte buffer. This is * equivalent to the read(2) POSIX function, and like that function it ignores read and write * buffers defined elsewhere. * * @param buffer the java.nio.ByteBuffer in which to put the incoming bytes * @return the number of bytes actually read * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed * @see java.nio.ByteBuffer */ public int read(ByteBuffer buffer) throws IOException, BadDescriptorException { checkOpen(); // TODO: It would be nice to throw a better error for this if (!isReadable()) { throw new BadDescriptorException(); } ReadableByteChannel readChannel = (ReadableByteChannel) channel; int bytesRead = 0; bytesRead = readChannel.read(buffer); return bytesRead; } /** * Write the bytes in the specified byte list to the associated channel. * * @param buffer the byte list containing the bytes to be written * @return the number of bytes actually written * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed */ public int internalWrite(ByteBuffer buffer) throws IOException, BadDescriptorException { checkOpen(); // TODO: It would be nice to throw a better error for this if (!isWritable()) { throw new BadDescriptorException(); } WritableByteChannel writeChannel = (WritableByteChannel) channel; // if appendable, we always seek to the end before writing if (isSeekable() && originalModes.isAppendable()) { // if already in append mode, we don't do our own seeking if (!isInAppendMode) { FileChannel fileChannel = (FileChannel) channel; fileChannel.position(fileChannel.size()); } } return writeChannel.write(buffer); } /** * Write the bytes in the specified byte list to the associated channel. * * @param buffer the byte list containing the bytes to be written * @return the number of bytes actually written * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed */ public int write(ByteBuffer buffer) throws IOException, BadDescriptorException { checkOpen(); return internalWrite(buffer); } /** * Write the bytes in the specified byte list to the associated channel. * * @param buf the byte list containing the bytes to be written * @return the number of bytes actually written * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed */ public int write(ByteList buf) throws IOException, BadDescriptorException { checkOpen(); return internalWrite(ByteBuffer.wrap(buf.getUnsafeBytes(), buf.begin(), buf.length())); } /** * Write the bytes in the specified byte list to the associated channel. * * @param buf the byte list containing the bytes to be written * @param offset the offset to start at. this is relative to the begin variable in the but * @param len the amount of bytes to write. this should not be longer than the buffer * @return the number of bytes actually written * @throws java.io.IOException if there is an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed */ public int write(ByteList buf, int offset, int len) throws IOException, BadDescriptorException { checkOpen(); return internalWrite(ByteBuffer.wrap(buf.getUnsafeBytes(), buf.begin() + offset, len)); } /** * Write the byte represented by the specified int to the associated channel. * * @param c The byte to write * @return 1 if the byte was written, 0 if not and -1 if there was an error (@see * java.nio.channels.WritableByteChannel.write(java.nio.ByteBuffer)) * @throws java.io.IOException If there was an exception during IO * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed */ public int write(int c) throws IOException, BadDescriptorException { checkOpen(); ByteBuffer buf = ByteBuffer.allocate(1); buf.put((byte) c); buf.flip(); return internalWrite(buf); } /** * Open a new descriptor using the given working directory, file path, mode flags, and file * permission. This is equivalent to the open(2) POSIX function. See * org.jruby.util.io.ChannelDescriptor.open(String, String, ModeFlags, int, POSIX) for the version * that also sets file permissions. * * @param cwd the "current working directory" to use when opening the file * @param path the file path to open * @param flags the mode flags to use for opening the file * @return a new ChannelDescriptor based on the specified parameters */ public static ChannelDescriptor open(String cwd, String path, ModeFlags flags) throws FileNotFoundException, DirectoryAsFileException, FileExistsException, IOException { return open(cwd, path, flags, 0, null, null); } /** * Open a new descriptor using the given working directory, file path, mode flags, and file * permission. This is equivalent to the open(2) POSIX function. See * org.jruby.util.io.ChannelDescriptor.open(String, String, ModeFlags, int, POSIX) for the version * that also sets file permissions. * * @param cwd the "current working directory" to use when opening the file * @param path the file path to open * @param flags the mode flags to use for opening the file * @param classLoader a ClassLoader to use for classpath: resources * @return a new ChannelDescriptor based on the specified parameters */ public static ChannelDescriptor open( String cwd, String path, ModeFlags flags, ClassLoader classLoader) throws FileNotFoundException, DirectoryAsFileException, FileExistsException, IOException { return open(cwd, path, flags, 0, null, classLoader); } /** * Open a new descriptor using the given working directory, file path, mode flags, and file * permission. This is equivalent to the open(2) POSIX function. * * @param cwd the "current working directory" to use when opening the file * @param path the file path to open * @param flags the mode flags to use for opening the file * @param perm the file permissions to use when creating a new file (currently unobserved) * @param posix a POSIX api implementation, used for setting permissions; if null, permissions are * ignored * @return a new ChannelDescriptor based on the specified parameters */ public static ChannelDescriptor open( String cwd, String path, ModeFlags flags, int perm, POSIX posix) throws FileNotFoundException, DirectoryAsFileException, FileExistsException, IOException { return open(cwd, path, flags, perm, posix, null); } /** * Open a new descriptor using the given working directory, file path, mode flags, and file * permission. This is equivalent to the open(2) POSIX function. * * @param cwd the "current working directory" to use when opening the file * @param path the file path to open * @param flags the mode flags to use for opening the file * @param perm the file permissions to use when creating a new file (currently unobserved) * @param posix a POSIX api implementation, used for setting permissions; if null, permissions are * ignored * @param classLoader a ClassLoader to use for classpath: resources * @return a new ChannelDescriptor based on the specified parameters */ public static ChannelDescriptor open( String cwd, String path, ModeFlags flags, int perm, POSIX posix, ClassLoader classLoader) throws FileNotFoundException, DirectoryAsFileException, FileExistsException, IOException { if (path.equals("/dev/null") || path.equalsIgnoreCase("nul:") || path.equalsIgnoreCase("nul")) { Channel nullChannel = new NullChannel(); // FIXME: don't use RubyIO for this return new ChannelDescriptor(nullChannel, flags); } if (path.startsWith("classpath:/") && classLoader != null) { path = path.substring("classpath:/".length()); InputStream is = classLoader.getResourceAsStream(path); // FIXME: don't use RubyIO for this return new ChannelDescriptor(Channels.newChannel(is), flags); } return JRubyFile.createResource(posix, cwd, path).openDescriptor(flags, perm); } /** * Close this descriptor. If in closing the last ChannelDescriptor reference to the associate * channel is closed, the channel itself will be closed. * * @throws org.jruby.util.io.BadDescriptorException if the associated channel is already closed * @throws java.io.IOException if there is an exception during IO */ public void close() throws BadDescriptorException, IOException { // tidy up finish(true); } void finish(boolean close) throws BadDescriptorException, IOException { synchronized (refCounter) { // if refcount is at or below zero, we're no longer valid if (refCounter.get() <= 0) { throw new BadDescriptorException(); } // if channel is already closed, we're no longer valid if (!channel.isOpen()) { throw new BadDescriptorException(); } // otherwise decrement and possibly close as normal if (close) { int count = refCounter.decrementAndGet(); if (DEBUG) LOG.info("Descriptor for fileno {} refs: {}", internalFileno, count); if (count <= 0) { // if we're the last referrer, close the channel try { channel.close(); } finally { unregisterDescriptor(internalFileno); } } } } } /** * Build a set of mode flags using the specified channel's actual capabilities. * * @param channel the channel to examine for capabilities * @return the mode flags * @throws org.jruby.util.io.InvalidValueException */ private static ModeFlags getModesFromChannel(Channel channel) throws InvalidValueException { ModeFlags modes; if (channel instanceof ReadableByteChannel) { if (channel instanceof WritableByteChannel) { modes = new ModeFlags(RDWR); } else { modes = new ModeFlags(RDONLY); } } else if (channel instanceof WritableByteChannel) { modes = new ModeFlags(WRONLY); } else { // FIXME: I don't like this modes = new ModeFlags(RDWR); } return modes; } // FIXME shouldn't use static; would interfere with other runtimes in the same JVM protected static final AtomicInteger internalFilenoIndex = new AtomicInteger(2); public static int getNewFileno() { return internalFilenoIndex.incrementAndGet(); } private static void registerDescriptor(ChannelDescriptor descriptor) { filenoDescriptorMap.put(descriptor.getFileno(), descriptor); } public static void unregisterDescriptor(int aFileno) { filenoDescriptorMap.remove(aFileno); } public static ChannelDescriptor getDescriptorByFileno(int aFileno) { return filenoDescriptorMap.get(aFileno); } public static Map<Integer, ChannelDescriptor> getFilenoDescriptorMapReadOnly() { return Collections.unmodifiableMap(filenoDescriptorMap); } private static final Map<Integer, ChannelDescriptor> filenoDescriptorMap = new ConcurrentHashMap<Integer, ChannelDescriptor>(); private static final Class SEL_CH_IMPL; private static final Method SEL_CH_IMPL_GET_FD; private static final Class FILE_CHANNEL_IMPL; private static final Field FILE_CHANNEL_IMPL_FD; private static final Field FILE_DESCRIPTOR_FD; static { Method getFD; Class selChImpl; try { selChImpl = Class.forName("sun.nio.ch.SelChImpl"); try { getFD = selChImpl.getMethod("getFD"); getFD.setAccessible(true); } catch (Exception e) { getFD = null; } } catch (Exception e) { selChImpl = null; getFD = null; } SEL_CH_IMPL = selChImpl; SEL_CH_IMPL_GET_FD = getFD; Field fd; Class fileChannelImpl; try { fileChannelImpl = Class.forName("sun.nio.ch.FileChannelImpl"); try { fd = fileChannelImpl.getDeclaredField("fd"); fd.setAccessible(true); } catch (Exception e) { fd = null; } } catch (Exception e) { fileChannelImpl = null; fd = null; } FILE_CHANNEL_IMPL = fileChannelImpl; FILE_CHANNEL_IMPL_FD = fd; Field ffd; try { ffd = FileDescriptor.class.getDeclaredField("fd"); ffd.setAccessible(true); } catch (Exception e) { ffd = null; } FILE_DESCRIPTOR_FD = ffd; } public static FileDescriptor getDescriptorFromChannel(Channel channel) { if (SEL_CH_IMPL_GET_FD != null && SEL_CH_IMPL.isInstance(channel)) { // Pipe Source and Sink, Sockets, and other several other selectable channels try { return (FileDescriptor) SEL_CH_IMPL_GET_FD.invoke(channel); } catch (Exception e) { // return bogus below } } else if (FILE_CHANNEL_IMPL_FD != null && FILE_CHANNEL_IMPL.isInstance(channel)) { // FileChannels try { return (FileDescriptor) FILE_CHANNEL_IMPL_FD.get(channel); } catch (Exception e) { // return bogus below } } else if (FILE_DESCRIPTOR_FD != null) { FileDescriptor unixFD = new FileDescriptor(); // UNIX sockets, from jnr-unixsocket try { if (channel instanceof UnixSocketChannel) { FILE_DESCRIPTOR_FD.set(unixFD, ((UnixSocketChannel) channel).getFD()); return unixFD; } else if (channel instanceof UnixServerSocketChannel) { FILE_DESCRIPTOR_FD.set(unixFD, ((UnixServerSocketChannel) channel).getFD()); return unixFD; } } catch (Exception e) { // return bogus below } } return new FileDescriptor(); } }
/** * Right now, this class abstracts the following execution scopes: Method, Closure, Module, Class, * MetaClass Top-level Script, and Eval Script * * <p>In the compiler-land, IR versions of these scopes encapsulate only as much information as is * required to convert Ruby code into equivalent Java code. * * <p>But, in the non-compiler land, there will be a corresponding java object for some of these * scopes which encapsulates the runtime semantics and data needed for implementing them. In the * case of Module, Class, MetaClass, and Method, they also happen to be instances of the * corresponding Ruby classes -- so, in addition to providing code that help with this specific ruby * implementation, they also have code that let them behave as ruby instances of their corresponding * classes. * * <p>Examples: - the runtime class object might have refs. to the runtime method objects. - the * runtime method object might have a slot for a heap frame (for when it has closures that need * access to the method's local variables), it might have version information, it might have * references to other methods that were optimized with the current version number, etc. - the * runtime closure object will have a slot for a heap frame (for when it has closures within) and * might get reified as a method in the java land (but inaccessible in ruby land). So, passing * closures in Java land might be equivalent to passing around the method handles. * * <p>and so on ... */ public abstract class IRScope { private static final Logger LOG = LoggerFactory.getLogger("IRScope"); private static Integer globalScopeCount = 0; /** Unique global scope id */ private int scopeId; /** Name */ private String name; /** File within which this scope has been defined */ private final String fileName; /** Starting line for this scope's definition */ private final int lineNumber; /** Lexical parent scope */ private IRScope lexicalParent; /** Parser static-scope that this IR scope corresponds to */ private StaticScope staticScope; /** Live version of module within whose context this method executes */ private RubyModule containerModule; /** List of IR instructions for this method */ private List<Instr> instrList; /** Control flow graph representation of this method's instructions */ private CFG cfg; /** List of (nested) closures in this scope */ private List<IRClosure> nestedClosures; /** Local variables defined in this scope */ private Set<Variable> definedLocalVars; /** Local variables used in this scope */ private Set<Variable> usedLocalVars; /** Is %block implicit block arg unused? */ private boolean hasUnusedImplicitBlockArg; /** %current_module and %current_scope variables */ private TemporaryVariable currentModuleVar; private TemporaryVariable currentScopeVar; /** Map of name -> dataflow problem */ private Map<String, DataFlowProblem> dfProbs; private Instr[] linearizedInstrArray; private List<BasicBlock> linearizedBBList; protected int temporaryVariableIndex; /** Keeps track of types of prefix indexes for variables and labels */ private Map<String, Integer> nextVarIndex; // Index values to guarantee we don't assign same internal index twice private int nextClosureIndex; // List of all scopes this scope contains lexically. This is not used // for execution, but is used during dry-runs for debugging. List<IRScope> lexicalChildren; protected static class LocalVariableAllocator { public int nextSlot; public Map<String, LocalVariable> varMap; public LocalVariableAllocator() { varMap = new HashMap<String, LocalVariable>(); nextSlot = 0; } public final LocalVariable getVariable(String name) { return varMap.get(name); } public final void putVariable(String name, LocalVariable var) { varMap.put(name, var); nextSlot++; } } LocalVariableAllocator localVars; LocalVariableAllocator evalScopeVars; /** Have scope flags been computed? */ private boolean flagsComputed; /* ***************************************************************************************************** * Does this execution scope (applicable only to methods) receive a block and use it in such a way that * all of the caller's local variables need to be materialized into a heap binding? * Ex: * def foo(&b) * eval 'puts a', b * end * * def bar * a = 1 * foo {} # prints out '1' * end * * Here, 'foo' can access all of bar's variables because it captures the caller's closure. * * There are 2 scenarios when this can happen (even this is conservative -- but, good enough for now) * 1. This method receives an explicit block argument (in this case, the block can be stored, passed around, * eval'ed against, called, etc.). * CAVEAT: This is conservative ... it may not actually be stored & passed around, evaled, called, ... * 2. This method has a 'super' call (ZSuper AST node -- ZSuperInstr IR instruction) * In this case, the parent (in the inheritance hierarchy) can access the block and store it, etc. So, in reality, * rather than assume that the parent will always do this, we can query the parent, if we can precisely identify * the parent method (which in the face of Ruby's dynamic hierarchy, we cannot). So, be pessimistic. * * This logic was extracted from an email thread on the JRuby mailing list -- Yehuda Katz & Charles Nutter * contributed this analysis above. * ********************************************************************************************************/ private boolean canCaptureCallersBinding; /* **************************************************************************** * Does this scope define code, i.e. does it (or anybody in the downward call chain) * do class_eval, module_eval? In the absence of any other information, we default * to yes -- which basically leads to pessimistic but safe optimizations. But, for * library and internal methods, this might be false. * **************************************************************************** */ private boolean canModifyCode; /* **************************************************************************** * Does this scope require a binding to be materialized? * Yes if any of the following holds true: * - calls 'Proc.new' * - calls 'eval' * - calls 'call' (could be a call on a stored block which could be local!) * - calls 'send' and we cannot resolve the message (method name) that is being sent! * - calls methods that can access the caller's binding * - calls a method which we cannot resolve now! * - has a call whose closure requires a binding * **************************************************************************** */ private boolean bindingHasEscaped; /** Does this scope call any eval */ private boolean usesEval; /** Does this scope receive keyword args? */ private boolean receivesKeywordArgs; /** Does this scope have a break instr? */ protected boolean hasBreakInstrs; /** Can this scope receive breaks */ protected boolean canReceiveBreaks; /** Does this scope have a non-local return instr? */ protected boolean hasNonlocalReturns; /** Can this scope receive a non-local return? */ public boolean canReceiveNonlocalReturns; /** * Since backref ($~) and lastline ($_) vars are allocated space on the dynamic scope, this is an * useful flag to compute. */ private boolean usesBackrefOrLastline; /** Does this scope call any zsuper */ private boolean usesZSuper; /** Does this scope have loops? */ private boolean hasLoops; /** # of thread poll instrs added to this scope */ private int threadPollInstrsCount; /** * Does this scope have explicit call protocol instructions? If yes, there are IR instructions for * managing bindings/frames, etc. If not, this has to be managed implicitly as in the current * runtime For now, only dyn-scopes are managed explicitly. Others will come in time */ private boolean hasExplicitCallProtocol; /** Should we re-run compiler passes -- yes after we've inlined, for example */ private boolean relinearizeCFG; private IRManager manager; // Used by cloning code protected IRScope(IRScope s, IRScope lexicalParent) { this.lexicalParent = lexicalParent; this.manager = s.manager; this.fileName = s.fileName; this.lineNumber = s.lineNumber; this.staticScope = s.staticScope; this.threadPollInstrsCount = s.threadPollInstrsCount; this.nextClosureIndex = s.nextClosureIndex; this.temporaryVariableIndex = s.temporaryVariableIndex; this.hasLoops = s.hasLoops; this.hasUnusedImplicitBlockArg = s.hasUnusedImplicitBlockArg; this.instrList = null; this.nestedClosures = new ArrayList<IRClosure>(); this.dfProbs = new HashMap<String, DataFlowProblem>(); this.nextVarIndex = new HashMap<String, Integer>(); // SSS FIXME: clone! this.cfg = null; this.linearizedInstrArray = null; this.linearizedBBList = null; this.flagsComputed = s.flagsComputed; this.canModifyCode = s.canModifyCode; this.canCaptureCallersBinding = s.canCaptureCallersBinding; this.receivesKeywordArgs = s.receivesKeywordArgs; this.hasBreakInstrs = s.hasBreakInstrs; this.hasNonlocalReturns = s.hasNonlocalReturns; this.canReceiveBreaks = s.canReceiveBreaks; this.canReceiveNonlocalReturns = s.canReceiveNonlocalReturns; this.bindingHasEscaped = s.bindingHasEscaped; this.usesEval = s.usesEval; this.usesBackrefOrLastline = s.usesBackrefOrLastline; this.usesZSuper = s.usesZSuper; this.hasExplicitCallProtocol = s.hasExplicitCallProtocol; this.localVars = new LocalVariableAllocator(); // SSS FIXME: clone! this.localVars.nextSlot = s.localVars.nextSlot; this.relinearizeCFG = false; setupLexicalContainment(); } public IRScope( IRManager manager, IRScope lexicalParent, String name, String fileName, int lineNumber, StaticScope staticScope) { this.manager = manager; this.lexicalParent = lexicalParent; this.name = name; this.fileName = fileName; this.lineNumber = lineNumber; this.staticScope = staticScope; this.threadPollInstrsCount = 0; this.nextClosureIndex = 0; this.temporaryVariableIndex = -1; this.instrList = new ArrayList<Instr>(); this.nestedClosures = new ArrayList<IRClosure>(); this.dfProbs = new HashMap<String, DataFlowProblem>(); this.nextVarIndex = new HashMap<String, Integer>(); this.cfg = null; this.linearizedInstrArray = null; this.linearizedBBList = null; this.hasLoops = false; this.hasUnusedImplicitBlockArg = false; this.flagsComputed = false; this.receivesKeywordArgs = false; this.hasBreakInstrs = false; this.hasNonlocalReturns = false; this.canReceiveBreaks = false; this.canReceiveNonlocalReturns = false; // These flags are true by default! this.canModifyCode = true; this.canCaptureCallersBinding = true; this.bindingHasEscaped = true; this.usesEval = true; this.usesBackrefOrLastline = true; this.usesZSuper = true; this.hasExplicitCallProtocol = false; this.localVars = new LocalVariableAllocator(); synchronized (globalScopeCount) { this.scopeId = globalScopeCount++; } this.relinearizeCFG = false; setupLexicalContainment(); } private final void setupLexicalContainment() { if (manager.isDryRun()) { lexicalChildren = new ArrayList<IRScope>(); if (lexicalParent != null) lexicalParent.addChildScope(this); } } @Override public int hashCode() { return scopeId; } protected void addChildScope(IRScope scope) { lexicalChildren.add(scope); } public List<IRScope> getLexicalScopes() { return lexicalChildren; } public void addClosure(IRClosure c) { nestedClosures.add(c); } public Instr getLastInstr() { return instrList.get(instrList.size() - 1); } public void addInstrAtBeginning(Instr i) { instrList.add(0, i); } public void addInstr(Instr i) { // SSS FIXME: If more instructions set these flags, there may be // a better way to do this by encoding flags in its own object // and letting every instruction update it. if (i instanceof ThreadPollInstr) threadPollInstrsCount++; else if (i instanceof BreakInstr) this.hasBreakInstrs = true; else if (i instanceof NonlocalReturnInstr) this.hasNonlocalReturns = true; else if (i instanceof DefineMetaClassInstr) this.canReceiveNonlocalReturns = true; else if (i instanceof ReceiveKeywordArgInstr || i instanceof ReceiveKeywordRestArgInstr) this.receivesKeywordArgs = true; instrList.add(i); } public LocalVariable getNewFlipStateVariable() { return getLocalVariable("%flip_" + allocateNextPrefixedName("%flip"), 0); } public void initFlipStateVariable(Variable v, Operand initState) { // Add it to the beginning instrList.add(0, new CopyInstr(v, initState)); } public boolean isForLoopBody() { return false; } public Label getNewLabel(String prefix) { return new Label(prefix + "_" + allocateNextPrefixedName(prefix)); } public Label getNewLabel() { return getNewLabel("LBL"); } public List<IRClosure> getClosures() { return nestedClosures; } public IRManager getManager() { return manager; } /** Returns the lexical scope that contains this scope definition */ public IRScope getLexicalParent() { return lexicalParent; } public StaticScope getStaticScope() { return staticScope; } public IRMethod getNearestMethod() { IRScope current = this; while (current != null && !(current instanceof IRMethod)) { current = current.getLexicalParent(); } return (IRMethod) current; } public IRScope getNearestFlipVariableScope() { IRScope current = this; while (current != null && !current.isFlipScope()) { current = current.getLexicalParent(); } return current; } public IRScope getNearestTopLocalVariableScope() { IRScope current = this; while (current != null && !current.isTopLocalVariableScope()) { current = current.getLexicalParent(); } return current; } /** * Returns the nearest scope which we can extract a live module from. If this returns null (like * for evals), then it means it cannot be statically determined. */ public IRScope getNearestModuleReferencingScope() { IRScope current = this; while (!(current instanceof IRModuleBody)) { // When eval'ing, we dont have a lexical view of what module we are nested in // because binding_eval, class_eval, module_eval, instance_eval can switch // around the lexical scope for evaluation to be something else. if (current == null || current instanceof IREvalScript) return null; current = current.getLexicalParent(); } return current; } public String getName() { return name; } public void setName(String name) { // This is for IRClosure ;( this.name = name; } public String getFileName() { return fileName; } public int getLineNumber() { return lineNumber; } /** Returns the top level scope */ public IRScope getTopLevelScope() { IRScope current = this; for (; current != null && !current.isScriptScope(); current = current.getLexicalParent()) {} return current; } public boolean isNestedInClosure(IRClosure closure) { for (IRScope s = this; s != null && !s.isTopLocalVariableScope(); s = s.getLexicalParent()) { if (s == closure) return true; } return false; } public void setHasLoopsFlag(boolean f) { hasLoops = true; } public boolean hasLoops() { return hasLoops; } public boolean hasExplicitCallProtocol() { return hasExplicitCallProtocol; } public void setExplicitCallProtocolFlag(boolean flag) { this.hasExplicitCallProtocol = flag; } public void setCodeModificationFlag(boolean f) { canModifyCode = f; } public boolean receivesKeywordArgs() { return this.receivesKeywordArgs; } public boolean modifiesCode() { return canModifyCode; } public boolean bindingHasEscaped() { return bindingHasEscaped; } public boolean usesBackrefOrLastline() { return usesBackrefOrLastline; } public boolean usesEval() { return usesEval; } public boolean usesZSuper() { return usesZSuper; } public boolean canCaptureCallersBinding() { return canCaptureCallersBinding; } public boolean canReceiveNonlocalReturns() { if (this.canReceiveNonlocalReturns) { return true; } boolean canReceiveNonlocalReturns = false; for (IRClosure cl : getClosures()) { if (cl.hasNonlocalReturns || cl.canReceiveNonlocalReturns()) { canReceiveNonlocalReturns = true; } } return canReceiveNonlocalReturns; } public CFG buildCFG() { cfg = new CFG(this); cfg.build(instrList); // Clear out instruction list after CFG has been built. this.instrList = null; return cfg; } protected void setCFG(CFG cfg) { this.cfg = cfg; } public CFG getCFG() { return cfg; } private void setupLabelPCs(HashMap<Label, Integer> labelIPCMap) { for (BasicBlock b : linearizedBBList) { Label l = b.getLabel(); l.setTargetPC(labelIPCMap.get(l)); } } private Instr[] prepareInstructionsForInterpretation() { checkRelinearization(); if (linearizedInstrArray != null) return linearizedInstrArray; // Already prepared try { buildLinearization(); // FIXME: compiler passes should have done this depends(linearization()); } catch (RuntimeException e) { LOG.error("Error linearizing cfg: ", e); CFG c = cfg(); LOG.error("\nGraph:\n" + c.toStringGraph()); LOG.error("\nInstructions:\n" + c.toStringInstrs()); throw e; } // Set up IPCs HashMap<Label, Integer> labelIPCMap = new HashMap<Label, Integer>(); List<Instr> newInstrs = new ArrayList<Instr>(); int ipc = 0; for (BasicBlock b : linearizedBBList) { labelIPCMap.put(b.getLabel(), ipc); List<Instr> bbInstrs = b.getInstrs(); int bbInstrsLength = bbInstrs.size(); for (int i = 0; i < bbInstrsLength; i++) { Instr instr = bbInstrs.get(i); if (instr instanceof Specializeable) { instr = ((Specializeable) instr).specializeForInterpretation(); bbInstrs.set(i, instr); } if (!(instr instanceof ReceiveSelfInstr)) { newInstrs.add(instr); ipc++; } } } // Set up label PCs setupLabelPCs(labelIPCMap); // Exit BB ipc cfg().getExitBB().getLabel().setTargetPC(ipc + 1); linearizedInstrArray = newInstrs.toArray(new Instr[newInstrs.size()]); return linearizedInstrArray; } private void runCompilerPasses() { // SSS FIXME: Why is this again? Document this weirdness! // Forcibly clear out the shared eval-scope variable allocator each time this method executes initEvalScopeVariableAllocator(true); // SSS FIXME: We should configure different optimization levels // and run different kinds of analysis depending on time budget. Accordingly, we need to set // IR levels/states (basic, optimized, etc.) and the // ENEBO: If we use a MT optimization mechanism we cannot mutate CFG // while another thread is using it. This may need to happen on a clone() // and we may need to update the method to return the new method. Also, // if this scope is held in multiple locations how do we update all references? for (CompilerPass pass : getManager().getCompilerPasses(this)) { pass.run(this); } } /** Run any necessary passes to get the IR ready for interpretation */ public synchronized Instr[] prepareForInterpretation(boolean isLambda) { if (isLambda) { // Add a global ensure block to catch uncaught breaks // and throw a LocalJumpError. if (((IRClosure) this).addGEBForUncaughtBreaks()) { this.relinearizeCFG = true; } } checkRelinearization(); if (linearizedInstrArray != null) return linearizedInstrArray; // Build CFG and run compiler passes, if necessary if (getCFG() == null) runCompilerPasses(); // Linearize CFG, etc. return prepareInstructionsForInterpretation(); } /* SSS FIXME: Do we need to synchronize on this? Cache this info in a scope field? */ /** Run any necessary passes to get the IR ready for compilation */ public Tuple<Instr[], Map<Integer, Label[]>> prepareForCompilation() { // Build CFG and run compiler passes, if necessary if (getCFG() == null) runCompilerPasses(); // Add this always since we dont re-JIT a previously // JIT-ted closure. But, check if there are other // smarts available to us and eliminate adding this // code to every closure there is. // // Add a global ensure block to catch uncaught breaks // and throw a LocalJumpError. if (this instanceof IRClosure && ((IRClosure) this).addGEBForUncaughtBreaks()) { this.relinearizeCFG = true; } try { buildLinearization(); // FIXME: compiler passes should have done this depends(linearization()); } catch (RuntimeException e) { LOG.error("Error linearizing cfg: ", e); CFG c = cfg(); LOG.error("\nGraph:\n" + c.toStringGraph()); LOG.error("\nInstructions:\n" + c.toStringInstrs()); throw e; } // Set up IPCs // FIXME: Would be nice to collapse duplicate labels; for now, using Label[] HashMap<Integer, Label[]> ipcLabelMap = new HashMap<Integer, Label[]>(); List<Instr> newInstrs = new ArrayList<Instr>(); int ipc = 0; for (BasicBlock b : linearizedBBList) { Label l = b.getLabel(); ipcLabelMap.put(ipc, catLabels(ipcLabelMap.get(ipc), l)); for (Instr i : b.getInstrs()) { if (!(i instanceof ReceiveSelfInstr)) { newInstrs.add(i); ipc++; } } } return new Tuple<Instr[], Map<Integer, Label[]>>( newInstrs.toArray(new Instr[newInstrs.size()]), ipcLabelMap); } private List<Object[]> buildJVMExceptionTable() { List<Object[]> etEntries = new ArrayList<Object[]>(); for (BasicBlock b : linearizedBBList) { // We need handlers for: // - Unrescuable (handled by ensures), // - Throwable (handled by rescues) // in that order since Throwable < Unrescuable BasicBlock rBB = cfg().getRescuerBBFor(b); BasicBlock eBB = cfg().getEnsurerBBFor(b); if ((eBB != null) && (rBB == eBB || rBB == null)) { // 1. same rescue and ensure handler ==> just spit out one entry with a Throwable class // 2. only ensure handler ==> just spit out one entry with a Throwable class etEntries.add(new Object[] {b.getLabel(), eBB.getLabel(), Throwable.class}); } else if (rBB != null) { // Unrescuable comes before Throwable if (eBB != null) etEntries.add(new Object[] {b.getLabel(), eBB.getLabel(), Unrescuable.class}); etEntries.add(new Object[] {b.getLabel(), rBB.getLabel(), Throwable.class}); } } // SSS FIXME: This could be optimized by compressing entries for adjacent BBs that have // identical handlers // This could be optimized either during generation or as another pass over the table. But, if // the JVM // does that already, do we need to bother with it? return etEntries; } private static Label[] catLabels(Label[] labels, Label cat) { if (labels == null) return new Label[] {cat}; Label[] newLabels = new Label[labels.length + 1]; System.arraycopy(labels, 0, newLabels, 0, labels.length); newLabels[labels.length] = cat; return newLabels; } private boolean computeScopeFlags(boolean receivesClosureArg, List<Instr> instrs) { for (Instr i : instrs) { Operation op = i.getOperation(); if (op == Operation.RECV_CLOSURE) { receivesClosureArg = true; } else if (op == Operation.ZSUPER) { this.canCaptureCallersBinding = true; this.usesZSuper = true; } else if (i instanceof CallBase) { CallBase call = (CallBase) i; if (call.targetRequiresCallersBinding()) this.bindingHasEscaped = true; if (call.canBeEval()) { this.usesEval = true; // If this method receives a closure arg, and this call is an eval that has more than 1 // argument, // it could be using the closure as a binding -- which means it could be using pretty much // any // variable from the caller's binding! if (receivesClosureArg && (call.getCallArgs().length > 1)) { this.canCaptureCallersBinding = true; } } } else if (op == Operation.GET_GLOBAL_VAR) { GlobalVariable gv = (GlobalVariable) ((GetGlobalVariableInstr) i).getSource(); String gvName = gv.getName(); if (gvName.equals("$_") || gvName.equals("$~") || gvName.equals("$`") || gvName.equals("$'") || gvName.equals("$+") || gvName.equals("$LAST_READ_LINE") || gvName.equals("$LAST_MATCH_INFO") || gvName.equals("$PREMATCH") || gvName.equals("$POSTMATCH") || gvName.equals("$LAST_PAREN_MATCH")) { this.usesBackrefOrLastline = true; } } else if (op == Operation.PUT_GLOBAL_VAR) { GlobalVariable gv = (GlobalVariable) ((PutGlobalVarInstr) i).getTarget(); String gvName = gv.getName(); if (gvName.equals("$_") || gvName.equals("$~")) usesBackrefOrLastline = true; } else if (op == Operation.MATCH || op == Operation.MATCH2 || op == Operation.MATCH3) { this.usesBackrefOrLastline = true; } else if (op == Operation.BREAK) { this.hasBreakInstrs = true; } else if (i instanceof NonlocalReturnInstr) { this.hasNonlocalReturns = true; } else if (i instanceof DefineMetaClassInstr) { // SSS: Inner-classes are defined with closures and // a return in the closure can force a return from this method // For now conservatively assume that a scope with inner-classes // can receive non-local returns. (Alternatively, have to inspect // all lexically nested scopes, not just closures in computeScopeFlags()) this.canReceiveNonlocalReturns = true; } } return receivesClosureArg; } // // This can help use eliminate writes to %block that are not used since this is // a special local-variable, not programmer-defined local-variable public void computeScopeFlags() { if (flagsComputed) { return; } // init canModifyCode = true; canCaptureCallersBinding = false; usesZSuper = false; usesEval = false; usesBackrefOrLastline = false; // NOTE: bindingHasEscaped is the crucial flag and it effectively is // unconditionally true whenever it has a call that receives a closure. // See CallInstr.computeRequiresCallersBindingFlag bindingHasEscaped = (this instanceof IREvalScript); // for eval scopes, bindings are considered escaped ... hasBreakInstrs = false; hasNonlocalReturns = false; canReceiveBreaks = false; canReceiveNonlocalReturns = false; // recompute flags -- we could be calling this method different times // definitely once after ir generation and local optimizations propagates constants locally // but potentially at a later time after doing ssa generation and constant propagation if (cfg == null) { computeScopeFlags(false, getInstrs()); } else { boolean receivesClosureArg = false; for (BasicBlock b : cfg.getBasicBlocks()) { receivesClosureArg = computeScopeFlags(receivesClosureArg, b.getInstrs()); } } // Compute flags for nested closures (recursively) and set derived flags. for (IRClosure cl : getClosures()) { cl.computeScopeFlags(); if (cl.hasBreakInstrs || cl.canReceiveBreaks) { canReceiveBreaks = true; } if (cl.hasNonlocalReturns || cl.canReceiveNonlocalReturns) { canReceiveNonlocalReturns = true; } if (cl.usesZSuper()) { usesZSuper = true; } } flagsComputed = true; } public abstract String getScopeName(); @Override public String toString() { return getScopeName() + " " + getName() + "[" + getFileName() + ":" + getLineNumber() + "]"; } public String toStringInstrs() { StringBuilder b = new StringBuilder(); int i = 0; for (Instr instr : instrList) { if (i > 0) b.append("\n"); b.append(" ").append(i).append('\t').append(instr); i++; } if (!nestedClosures.isEmpty()) { b.append("\n\n------ Closures encountered in this scope ------\n"); for (IRClosure c : nestedClosures) b.append(c.toStringBody()); b.append("------------------------------------------------\n"); } return b.toString(); } public String toPersistableString() { StringBuilder b = new StringBuilder(); b.append("Scope:<"); b.append(name); b.append(">"); for (Instr instr : instrList) { b.append("\n"); b.append(instr); } return b.toString(); } public String toStringVariables() { Map<Variable, Integer> ends = new HashMap<Variable, Integer>(); Map<Variable, Integer> starts = new HashMap<Variable, Integer>(); SortedSet<Variable> variables = new TreeSet<Variable>(); for (int i = instrList.size() - 1; i >= 0; i--) { Instr instr = instrList.get(i); if (instr instanceof ResultInstr) { Variable var = ((ResultInstr) instr).getResult(); variables.add(var); starts.put(var, i); } for (Operand operand : instr.getOperands()) { if (operand != null && operand instanceof Variable && ends.get((Variable) operand) == null) { ends.put((Variable) operand, i); variables.add((Variable) operand); } } } StringBuilder sb = new StringBuilder(); int i = 0; for (Variable var : variables) { Integer end = ends.get(var); if (end != null) { // Variable is actually used somewhere and not dead if (i > 0) sb.append("\n"); i++; sb.append(" ").append(var).append(": ").append(starts.get(var)).append("-").append(end); } } return sb.toString(); } /** * --------------------------------------- SSS FIXME: What is this method for? @Interp public void * calculateParameterCounts() { for (int i = instrList.size() - 1; i >= 0; i--) { Instr instr = * instrList.get(i); } } ------------------------------------------ * */ public LocalVariable getSelf() { return Self.SELF; } public Variable getCurrentModuleVariable() { // SSS: Used in only 3 cases in generated IR: // -> searching a constant in the inheritance hierarchy // -> searching a super-method in the inheritance hierarchy // -> looking up 'StandardError' (which can be eliminated by creating a special operand type for // this) if (currentModuleVar == null) currentModuleVar = getNewTemporaryVariable(Variable.CURRENT_MODULE); return currentModuleVar; } public Variable getCurrentScopeVariable() { // SSS: Used in only 1 case in generated IR: // -> searching a constant in the lexical scope hierarchy if (currentScopeVar == null) currentScopeVar = getNewTemporaryVariable(Variable.CURRENT_SCOPE); return currentScopeVar; } public abstract LocalVariable getImplicitBlockArg(); public void markUnusedImplicitBlockArg() { hasUnusedImplicitBlockArg = true; } public LocalVariable findExistingLocalVariable(String name, int depth) { return localVars.getVariable(name); } /** * Find or create a local variable. By default, scopes are assumed to only check current depth. * Blocks/Closures override this because they have special nesting rules. */ public LocalVariable getLocalVariable(String name, int scopeDepth) { LocalVariable lvar = findExistingLocalVariable(name, scopeDepth); if (lvar == null) { lvar = new LocalVariable(name, scopeDepth, localVars.nextSlot); localVars.putVariable(name, lvar); } return lvar; } public LocalVariable getNewLocalVariable(String name, int depth) { throw new RuntimeException( "getNewLocalVariable should be called for: " + this.getClass().getName()); } protected void initEvalScopeVariableAllocator(boolean reset) { if (reset || evalScopeVars == null) evalScopeVars = new LocalVariableAllocator(); } public TemporaryVariable getNewTemporaryVariable() { temporaryVariableIndex++; return new TemporaryVariable(temporaryVariableIndex); } public TemporaryVariable getNewTemporaryVariable(String name) { temporaryVariableIndex++; return new TemporaryVariable(name, temporaryVariableIndex); } public void resetTemporaryVariables() { temporaryVariableIndex = -1; } public int getTemporaryVariableSize() { return temporaryVariableIndex + 1; } // Generate a new variable for inlined code public Variable getNewInlineVariable(String inlinePrefix, Variable v) { if (v instanceof LocalVariable) { LocalVariable lv = (LocalVariable) v; return getLocalVariable(inlinePrefix + lv.getName(), lv.getScopeDepth()); } else { return getNewTemporaryVariable(); } } public int getThreadPollInstrsCount() { return threadPollInstrsCount; } public int getLocalVariablesCount() { return localVars.nextSlot; } public int getUsedVariablesCount() { // System.out.println("For " + this + ", # lvs: " + nextLocalVariableSlot); // # local vars, # flip vars // // SSS FIXME: When we are opting local var access, // no need to allocate local var space except when we have been asked to! return getLocalVariablesCount() + getPrefixCountSize("%flip"); } public void setUpUseDefLocalVarMaps() { definedLocalVars = new java.util.HashSet<Variable>(); usedLocalVars = new java.util.HashSet<Variable>(); for (BasicBlock bb : cfg().getBasicBlocks()) { for (Instr i : bb.getInstrs()) { for (Variable v : i.getUsedVariables()) { if (v instanceof LocalVariable) usedLocalVars.add(v); } if (i instanceof ResultInstr) { Variable v = ((ResultInstr) i).getResult(); if (v instanceof LocalVariable) definedLocalVars.add(v); } } } for (IRClosure cl : getClosures()) { cl.setUpUseDefLocalVarMaps(); } } public boolean usesLocalVariable(Variable v) { if (usedLocalVars == null) setUpUseDefLocalVarMaps(); if (usedLocalVars.contains(v)) return true; for (IRClosure cl : getClosures()) { if (cl.usesLocalVariable(v)) return true; } return false; } public boolean definesLocalVariable(Variable v) { if (definedLocalVars == null) setUpUseDefLocalVarMaps(); if (definedLocalVars.contains(v)) return true; for (IRClosure cl : getClosures()) { if (cl.definesLocalVariable(v)) return true; } return false; } public void setDataFlowSolution(String name, DataFlowProblem p) { dfProbs.put(name, p); } public DataFlowProblem getDataFlowSolution(String name) { return dfProbs.get(name); } // This should only be used to do pre-cfg opts and to build the CFG. // Everyone else should use the CFG. public List<Instr> getInstrs() { if (cfg != null) throw new RuntimeException("Please use the CFG to access this scope's instructions."); return instrList; } public Instr[] getInstrsForInterpretation() { return linearizedInstrArray; } public void resetLinearizationData() { linearizedBBList = null; relinearizeCFG = false; } public void checkRelinearization() { if (relinearizeCFG) resetLinearizationData(); } public List<BasicBlock> buildLinearization() { checkRelinearization(); if (linearizedBBList != null) return linearizedBBList; // Already linearized linearizedBBList = CFGLinearizer.linearize(cfg); return linearizedBBList; } // SSS FIXME: Extremely inefficient public int getRescuerPC(Instr excInstr) { depends(cfg()); for (BasicBlock b : linearizedBBList) { for (Instr i : b.getInstrs()) { if (i == excInstr) { BasicBlock rescuerBB = cfg().getRescuerBBFor(b); return (rescuerBB == null) ? -1 : rescuerBB.getLabel().getTargetPC(); } } } // SSS FIXME: Cannot happen! Throw runtime exception LOG.error("Fell through looking for rescuer ipc for " + excInstr); return -1; } // SSS FIXME: Extremely inefficient public int getEnsurerPC(Instr excInstr) { depends(cfg()); for (BasicBlock b : linearizedBBList) { for (Instr i : b.getInstrs()) { if (i == excInstr) { BasicBlock ensurerBB = cfg.getEnsurerBBFor(b); return (ensurerBB == null) ? -1 : ensurerBB.getLabel().getTargetPC(); } } } // SSS FIXME: Cannot happen! Throw runtime exception LOG.error("Fell through looking for ensurer ipc for " + excInstr); return -1; } public List<BasicBlock> linearization() { depends(cfg()); assert linearizedBBList != null : "You have not run linearization"; return linearizedBBList; } protected void depends(Object obj) { assert obj != null : "Unsatisfied dependency and this depends() was set " + "up wrong. Use depends(build()) not depends(build)."; } public CFG cfg() { assert cfg != null : "Trying to access build before build started"; return cfg; } public void splitCalls() { // FIXME: (Enebo) We are going to make a SplitCallInstr so this logic can be separate // from unsplit calls. Comment out until new SplitCall is created. // for (BasicBlock b: getNodes()) { // List<Instr> bInstrs = b.getInstrs(); // for (ListIterator<Instr> it = ((ArrayList<Instr>)b.getInstrs()).listIterator(); // it.hasNext(); ) { // Instr i = it.next(); // // Only user calls, not Ruby & JRuby internal calls // if (i.operation == Operation.CALL) { // CallInstr call = (CallInstr)i; // Operand r = call.getReceiver(); // Operand m = call.getMethodAddr(); // Variable mh = _scope.getNewTemporaryVariable(); // MethodLookupInstr mli = new MethodLookupInstr(mh, m, r); // // insert method lookup at the right place // it.previous(); // it.add(mli); // it.next(); // // update call address // call.setMethodAddr(mh); // } // } // } // // List<IRClosure> nestedClosures = _scope.getClosures(); // if (!nestedClosures.isEmpty()) { // for (IRClosure c : nestedClosures) { // c.getCFG().splitCalls(); // } // } } public void resetDFProblemsState() { dfProbs = new HashMap<String, DataFlowProblem>(); for (IRClosure c : nestedClosures) c.resetDFProblemsState(); } public void resetState() { relinearizeCFG = true; linearizedInstrArray = null; cfg.resetState(); // reset flags flagsComputed = false; canModifyCode = true; canCaptureCallersBinding = true; bindingHasEscaped = true; usesEval = true; usesZSuper = true; hasBreakInstrs = false; hasNonlocalReturns = false; canReceiveBreaks = false; canReceiveNonlocalReturns = false; // Reset dataflow problems state resetDFProblemsState(); } public void inlineMethod( IRScope method, RubyModule implClass, int classToken, BasicBlock basicBlock, CallBase call) { // Inline depends(cfg()); new CFGInliner(cfg).inlineMethod(method, implClass, classToken, basicBlock, call); // Reset state resetState(); // Re-run opts for (CompilerPass pass : getManager().getInliningCompilerPasses(this)) { pass.run(this); } } public void buildCFG(List<Instr> instrList) { CFG newBuild = new CFG(this); newBuild.build(instrList); cfg = newBuild; } public void resetCFG() { cfg = null; } /* Record a begin block -- not all scope implementations can handle them */ public void recordBeginBlock(IRClosure beginBlockClosure) { throw new RuntimeException("BEGIN blocks cannot be added to: " + this.getClass().getName()); } /* Record an end block -- not all scope implementations can handle them */ public void recordEndBlock(IRClosure endBlockClosure) { throw new RuntimeException("END blocks cannot be added to: " + this.getClass().getName()); } // Enebo: We should just make n primitive int and not take the hash hit protected int allocateNextPrefixedName(String prefix) { int index = getPrefixCountSize(prefix); nextVarIndex.put(prefix, index + 1); return index; } protected void resetVariableCounter(String prefix) { nextVarIndex.remove(prefix); } protected int getPrefixCountSize(String prefix) { Integer index = nextVarIndex.get(prefix); if (index == null) return 0; return index.intValue(); } public RubyModule getContainerModule() { // System.out.println("GET: container module of " + getName() + " with hc " + hashCode() // + " to " + containerModule.getName()); return containerModule; } public int getNextClosureId() { nextClosureIndex++; return nextClosureIndex; } /** * Does this scope represent a module body? (SSS FIXME: what about script or eval script bodies?) */ public boolean isModuleBody() { return false; } /** Is this IRClassBody but not IRMetaClassBody? */ public boolean isNonSingletonClassBody() { return false; } public boolean isFlipScope() { return true; } public boolean isTopLocalVariableScope() { return true; } /** Is this an eval script or a regular file script? */ public boolean isScriptScope() { return false; } }
/** * Class used to launch the interpreter. This is the main class as defined in the jruby.mf manifest. * It is very basic and does not support yet the same array of switches as the C interpreter. Usage: * java -jar jruby.jar [switches] [rubyfile.rb] [arguments] -e 'command' one line of script. Several * -e's allowed. Omit [programfile] * * @author jpetersen */ public class Main { private static final Logger LOG = LoggerFactory.getLogger("Main"); public Main(RubyInstanceConfig config) { this(config, false); } public Main(final InputStream in, final PrintStream out, final PrintStream err) { this(new RubyInstanceConfig(in, out, err)); } public Main() { this(new RubyInstanceConfig()); } private Main(RubyInstanceConfig config, boolean hardExit) { this.config = config; config.setHardExit(hardExit); } private Main(boolean hardExit) { // used only from main(String[]), so we process dotfile here processDotfile(); this.config = new RubyInstanceConfig(); config.setHardExit(hardExit); } private static List<String> getDotfileDirectories() { ArrayList<String> searchList = new ArrayList<String>(); for (String homeProp : new String[] {"user.dir", "user.home"}) { String home = SafePropertyAccessor.getProperty(homeProp); if (home != null) searchList.add(home); } // JVM sometimes picks odd location for user.home based on a registry entry // (see http://bugs.sun.com/view_bug.do?bug_id=4787931). Add extra check in // case that entry is wrong. Add before user.home in search list. if (Platform.IS_WINDOWS) { String homeDrive = System.getenv("HOMEDRIVE"); String homePath = System.getenv("HOMEPATH"); if (homeDrive != null && homePath != null) { searchList.add(1, (homeDrive + homePath).replace('\\', '/')); } } return searchList; } public static void processDotfile() { for (String home : getDotfileDirectories()) { File dotfile = new File(home + "/.jrubyrc"); if (dotfile.exists()) loadJRubyProperties(dotfile); } } private static void loadJRubyProperties(File dotfile) { FileInputStream fis = null; try { // update system properties with long form jruby properties from .jrubyrc Properties sysProps = System.getProperties(); Properties newProps = new Properties(); // load properties and re-set as jruby.* fis = new FileInputStream(dotfile); newProps.load(fis); for (Map.Entry entry : newProps.entrySet()) { sysProps.put("jruby." + entry.getKey(), entry.getValue()); } } catch (IOException ioe) { LOG.debug("exception loading " + dotfile, ioe); } catch (SecurityException se) { LOG.debug("exception loading " + dotfile, se); } finally { if (fis != null) try { fis.close(); } catch (Exception e) { } } } public static class Status { private boolean isExit = false; private int status = 0; /** * Creates a status object with the specified value and with explicit exit flag. An exit flag * means that Kernel.exit() has been explicitly invoked during the run. * * @param status The status value. */ Status(int status) { this.isExit = true; this.status = status; } /** Creates a status object with 0 value and no explicit exit flag. */ Status() {} public boolean isExit() { return isExit; } public int getStatus() { return status; } } /** * This is the command-line entry point for JRuby, and should ONLY be used by Java when starting * up JRuby from a command-line. Use other mechanisms when embedding JRuby into another * application. * * @param args command-line args, provided by the JVM. */ public static void main(String[] args) { doGCJCheck(); Main main; if (DripMain.DRIP_RUNTIME != null) { main = new Main(DripMain.DRIP_CONFIG, true); } else { main = new Main(true); } try { Status status = main.run(args); if (status.isExit()) { System.exit(status.getStatus()); } } catch (RaiseException rj) { System.exit(handleRaiseException(rj)); } catch (Throwable t) { // print out as a nice Ruby backtrace System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t)); while ((t = t.getCause()) != null) { System.err.println("Caused by:"); System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t)); } System.exit(1); } } public Status run(String[] args) { try { if (Options.TRUFFLE_PRINT_RUNTIME.load()) { config.getError().println("jruby: using " + Truffle.getRuntime().getName()); } config.processArguments(args); return internalRun(); } catch (MainExitException mee) { return handleMainExit(mee); } catch (OutOfMemoryError oome) { return handleOutOfMemory(oome); } catch (StackOverflowError soe) { return handleStackOverflow(soe); } catch (UnsupportedClassVersionError ucve) { return handleUnsupportedClassVersion(ucve); } catch (ThreadKill kill) { return new Status(); } } @Deprecated public Status run() { return internalRun(); } private Status internalRun() { doShowVersion(); doShowCopyright(); if (!config.getShouldRunInterpreter()) { doPrintUsage(false); doPrintProperties(); return new Status(); } InputStream in = config.getScriptSource(); String filename = config.displayedFileName(); doProcessArguments(in); Ruby _runtime; if (DripMain.DRIP_RUNTIME != null) { // use drip's runtime, reinitializing config _runtime = DripMain.DRIP_RUNTIME; _runtime.reinitialize(true); } else { _runtime = Ruby.newInstance(config); } final Ruby runtime = _runtime; final AtomicBoolean didTeardown = new AtomicBoolean(); if (config.isHardExit()) { // we're the command-line JRuby, and should set a shutdown hook for // teardown. Runtime.getRuntime() .addShutdownHook( new Thread() { public void run() { if (didTeardown.compareAndSet(false, true)) { runtime.tearDown(); } } }); } try { doSetContextClassLoader(runtime); if (in == null) { // no script to run, return success return new Status(); } else if (config.isXFlag() && !config.hasShebangLine()) { // no shebang was found and x option is set throw new MainExitException(1, "jruby: no Ruby script found in input (LoadError)"); } else if (config.getShouldCheckSyntax()) { // check syntax only and exit return doCheckSyntax(runtime, in, filename); } else { // proceed to run the script return doRunFromMain(runtime, in, filename); } } finally { if (didTeardown.compareAndSet(false, true)) { runtime.tearDown(); } } } private Status handleUnsupportedClassVersion(UnsupportedClassVersionError ucve) { config .getError() .println("Error: Some library (perhaps JRuby) was built with a later JVM version."); config .getError() .println( "Please use libraries built with the version you intend to use or an earlier one."); if (config.isVerbose()) { config.getError().println("Exception trace follows:"); ucve.printStackTrace(); } else { config.getError().println("Specify -w for full UnsupportedClassVersionError stack trace"); } return new Status(1); } /** Print a nicer stack size error since Rubyists aren't used to seeing this. */ private Status handleStackOverflow(StackOverflowError soe) { String memoryMax = getRuntimeFlagValue("-Xss"); if (memoryMax != null) { config .getError() .println( "Error: Your application used more stack memory than the safety cap of " + memoryMax + "."); } else { config .getError() .println("Error: Your application used more stack memory than the default safety cap."); } config.getError().println("Specify -J-Xss####k to increase it (#### = cap size in KB)."); if (config.isVerbose()) { config.getError().println("Exception trace follows:"); soe.printStackTrace(config.getError()); } else { config.getError().println("Specify -w for full StackOverflowError stack trace"); } return new Status(1); } /** Print a nicer memory error since Rubyists aren't used to seeing this. */ private Status handleOutOfMemory(OutOfMemoryError oome) { System.gc(); // try to clean up a bit of space, hopefully, so we can report this error String oomeMessage = oome.getMessage(); if (oomeMessage.contains("PermGen")) { // report permgen memory error config.getError().println("Error: Your application exhausted PermGen area of the heap."); config .getError() .println("Specify -J-XX:MaxPermSize=###M to increase it (### = PermGen size in MB)."); } else { // report heap memory error String memoryMax = getRuntimeFlagValue("-Xmx"); if (memoryMax != null) { config .getError() .println( "Error: Your application used more memory than the safety cap of " + memoryMax + "."); } else { config .getError() .println("Error: Your application used more memory than the default safety cap."); } config.getError().println("Specify -J-Xmx####m to increase it (#### = cap size in MB)."); } if (config.isVerbose()) { config.getError().println("Exception trace follows:"); oome.printStackTrace(config.getError()); } else { config.getError().println("Specify -w for full OutOfMemoryError stack trace"); } return new Status(1); } private String getRuntimeFlagValue(String prefix) { RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); for (String param : runtime.getInputArguments()) { if (param.startsWith(prefix)) { return param.substring(prefix.length()).toUpperCase(); } } return null; } private Status handleMainExit(MainExitException mee) { if (!mee.isAborted()) { config.getOutput().println(mee.getMessage()); if (mee.isUsageError()) { doPrintUsage(true); } } return new Status(mee.getStatus()); } private Status doRunFromMain(Ruby runtime, InputStream in, String filename) { long now = -1; try { doCheckSecurityManager(); runtime.runFromMain(in, filename); runtime.getTruffleBridge().shutdown(); } catch (RaiseException rj) { return new Status(handleRaiseException(rj)); } return new Status(); } private Status doCheckSyntax(Ruby runtime, InputStream in, String filename) throws RaiseException { // check primary script boolean status = checkStreamSyntax(runtime, in, filename); // check other scripts specified on argv for (String arg : config.getArgv()) { status = status && checkFileSyntax(runtime, arg); } return new Status(status ? 0 : -1); } private boolean checkFileSyntax(Ruby runtime, String filename) { File file = new File(filename); if (file.exists()) { try { return checkStreamSyntax(runtime, new FileInputStream(file), filename); } catch (FileNotFoundException fnfe) { config.getError().println("File not found: " + filename); return false; } } else { return false; } } private boolean checkStreamSyntax(Ruby runtime, InputStream in, String filename) { try { runtime.parseFromMain(in, filename); config.getOutput().println("Syntax OK"); return true; } catch (RaiseException re) { if (re.getException().getMetaClass().getBaseName().equals("SyntaxError")) { config .getError() .println("SyntaxError in " + re.getException().message(runtime.getCurrentContext())); } else { throw re; } return false; } } private void doSetContextClassLoader(Ruby runtime) { // set thread context JRuby classloader here, for the main thread try { Thread.currentThread().setContextClassLoader(runtime.getJRubyClassLoader()); } catch (SecurityException se) { // can't set TC classloader if (runtime.getInstanceConfig().isVerbose()) { config .getError() .println( "WARNING: Security restrictions disallowed setting context classloader for main thread."); } } } private void doProcessArguments(InputStream in) { config.processArguments(config.parseShebangOptions(in)); } private void doPrintProperties() { if (config.getShouldPrintProperties()) { config.getOutput().print(OutputStrings.getPropertyHelp()); } } private void doPrintUsage(boolean force) { if (config.getShouldPrintUsage() || force) { config.getOutput().print(OutputStrings.getBasicUsageHelp()); } } private void doShowCopyright() { if (config.isShowCopyright()) { config.getOutput().println(OutputStrings.getCopyrightString()); } } private void doShowVersion() { if (config.isShowVersion()) { config.getOutput().println(OutputStrings.getVersionString()); } } private static void doGCJCheck() { // Ensure we're not running on GCJ, since it's not supported and leads to weird errors if (Platform.IS_GCJ) { System.err.println("Fatal: GCJ (GNU Compiler for Java) is not supported by JRuby."); System.exit(1); } } private void doCheckSecurityManager() { if (Main.class.getClassLoader() == null && System.getSecurityManager() != null) { System.err.println( "Warning: security manager and JRuby running from boot classpath.\n" + "Run from jruby.jar or set env VERIFY_JRUBY=true to enable security."); } } private static int handleRaiseException(RaiseException rj) { RubyException raisedException = rj.getException(); Ruby runtime = raisedException.getRuntime(); if (runtime.getSystemExit().isInstance(raisedException)) { IRubyObject status = raisedException.callMethod(runtime.getCurrentContext(), "status"); if (status != null && !status.isNil()) { return RubyNumeric.fix2int(status); } else { return 0; } } else { runtime .getErrorStream() .print( runtime .getInstanceConfig() .getTraceType() .printBacktrace(raisedException, runtime.getPosix().isatty(FileDescriptor.err))); return 1; } } private final RubyInstanceConfig config; }