Bug: Endless recursion (Stack overflow) when using 'this(…)' statement in certain class constructors

44 views
Skip to first unread message

Udo

unread,
Mar 5, 2014, 5:55:58 AM3/5/14
to java2...@googlegroups.com
Using a "this(...)" statement in a super class constructor when a sub class has a constructor matching that this' parameters results in an stack overflow.

Details

Assume the following two classes:

--- BaseClass.java ---

public class BaseClass {

    public BaseClass() {
        this("");
    }

    public BaseClass(String prefix) {
        System.out.println(prefix+"Hello from BaseClass");
    }
}

--- SubClass.java ---

public class SubClass extends BaseClass {

    SubClass(String message) {
        super();
        System.out.println(message);
    }

    public static void main(String[] args) {
        SubClass obj = new SubClass("Hello from SubClass");
        System.out.println("Done.");
    }
}

----------------------


Running SubClass as a Java application results in the expected output:

----
Hello from BaseClass.
Hello from SubClass.
Done.
----

However running SubClass as a Java2Script application end with a

RangeError: Maximum call stack size exceeded.

I.e. it results in a stack overflow in the browser.


Analysis

The problem is caused by a bug in the "this(...)" runtime implementation. 

When executing the "this" statement the runtime looks for a method in "this" object matching the given actual parameters. In this example the 'this("");' statement in BaseClass.construct() looks for a function "construct(String)". Wrongly the complete object function chain of "this" object is checked to find this function, starting at the SubClass level. This way the SubClass.construct(String) function is found first. SubClass.construct(String)  calls the "super" constructor, i.e. BaseClass.construct(), and we are in the endless recursion.

Fix

The implementation of this(...) needs to be more similar to a "super" call function lookup than a normal method call function lookup. 

The "super" call implementation correctly takes into consideration, what class is calling it and only checks for constructors in super classes of that class. Similarly the "this" call should consider the class it is called from and only lookup constructors in this class and this class' super classes.





Zhou Renjian

unread,
Mar 5, 2014, 7:24:53 AM3/5/14
to Java2Script
You are right, calling this.construct() directly is not a correct implementation.

To fixed it, I just add a new Class.thisConstructor method in Class.js, similar to Class.superCall/superConstructor
/* public */
Clazz.superConstructor = function (objThis, clazzThis, funParams) {
	Clazz.superCall (objThis, clazzThis, "construct", funParams);
	/* If there are members which are initialized out of the constructor */
	if (clazzThis.con$truct != null) {
		clazzThis.con$truct.apply (objThis, []);
	}
};

/**
 * Call this constructor of the class. 
 * The same effect as Java's expression:
 * <code> this (*) </code>
 * 
 * @param objThis host object
 * @param clazzThis class of declaring method scope.
 * @param funParams Array of method parameters
 */
/* public */
Clazz.thisConstructor = function (objThis, clazzThis, funParams) {
	var funName = "construct";
	var fx = null;
	var i = -1;
	var clazzFun = objThis[funName];
	if (clazzFun != null) {
		if (clazzFun.claxxOwner != null) { 
			// claxxOwner is a mark for methods that is single.
			if (clazzFun.claxxOwner !== clazzThis) {
				// This is a single method, call directly!
				fx = clazzFun;
			}
		} else if (clazzFun.stacks == null && !(clazzFun.lastClaxxRef != null
					&& clazzFun.lastClaxxRef.prototype[funName] != null
					&& clazzFun.lastClaxxRef.prototype[funName].stacks != null)) { // super.toString
			fx = clazzFun;
		} else { // normal wrapped method
			var stacks = clazzFun.stacks;
			if (stacks == null) {
				stacks = clazzFun.lastClaxxRef.prototype[funName].stacks;
			}
			var length = stacks.length;
			for (i = length - 1; i >= 0; i--) {
				/*
				 * Once super call is computed precisely, there are no need 
				 * to calculate the inherited level but just an equals
				 * comparision
				 */
				//var level = Clazz.getInheritedLevel (clazzThis, stacks[i]);
				if (clazzThis === stacks[i]) { // level == 0
					fx = stacks[i].prototype[funName];
					break;
				}
			} // end of for loop
		} // end of normal wrapped method
	} // end of clazzFun != null
	if (fx != null) {
		/* there are members which are initialized out of the constructor */
		if (i == 0) {
			var ss = clazzFun.stacks;
			if (ss != null && ss[0].superClazz == null
					&& ss[0].con$truct != null) {
				ss[0].con$truct.apply (objThis, []);
			}
		}
		/*# {$no.debug.support} >>x #*/
		if (Clazz.tracingCalling) {
			var caller = arguments.callee.caller;
			if (caller === Clazz.thisConstructor) {
				caller = caller.arguments.callee.caller;
			}
			Clazz.pu$hCalling (new Clazz.callingStack (caller, clazzThis));
			var ret = fx.apply (objThis, (funParams == null) ? [] : funParams);
			Clazz.p0pCalling ();
			return ret;
		}
		/*# x<< #*/
		return fx.apply (objThis, (funParams == null) ? [] : funParams);
	} else {
		/* there are members which are initialized out of the constructor */
		/*
		if (i == -1) {
			// should be ignore as there are codes calling con$truct in 
			// #superConstructor
		} else {
			// unreachable
			//var ss = clazzFun.stacks;
			//if (ss != null && ss[0].superClazz == null 
			//		&& ss[0].con$truct != null) {
			//	ss[0].con$truct.apply (objThis, []);
			//}
		}
		*/
		/* No this constructor! */
		return ;
	}
	throw new Clazz.MethodNotFoundException (objThis, clazzThis, funName, 
			Clazz.getParamsType (funParams).typeString);
};

and then modify ASTScriptVisitor#visit(ConstructorInvocation) to generate Class.thisConstructor instead:

	public boolean visit(ConstructorInvocation node) {
		IMethodBinding constructorBinding = node.resolveConstructorBinding();
		if (constructorBinding == null) {
			return false;
		}
		buffer.append("Clazz.thisConstructor (this, ");
		buffer.append(assureQualifiedName(shortenQualifiedName(getFullClassName())));
		IMethodBinding methodDeclaration = null;
		if (constructorBinding != null) {
			methodDeclaration = constructorBinding.getMethodDeclaration();
		}
		visitMethodParameterList(node.arguments(), methodDeclaration, true, ", [", "]");
		buffer.append(");\r\n");
		/*
		buffer.append("this.construct (");
		IMethodBinding methodDeclaration = null;
		IMethodBinding constructorBinding = node.resolveConstructorBinding();
		if (constructorBinding != null) {
			methodDeclaration = constructorBinding.getMethodDeclaration();
		}
		visitMethodParameterList(node.arguments(), methodDeclaration, true, null, null);
		buffer.append(");\r\n");
		// */
		return false;
	}

You may need to rebuild java2script to fix this problem. New build with this fixed is not coming soon.

Regards,
Zhou Renjian


--
You received this message because you are subscribed to the Google Groups "Java2Script" group.
To unsubscribe from this group and stop receiving emails from it, send an email to java2script...@googlegroups.com.
To post to this group, send email to java2...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/java2script/25d9d7b1-4e52-46dc-997c-a146478b6f47%40googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Zhou Renjian

unread,
Jun 23, 2016, 5:32:55 PM6/23/16
to Java2Script
Fixed in latest Java2Script build for Eclipse 4.5 and 4.6.

Robert Hanson

unread,
Nov 9, 2016, 12:04:07 AM11/9/16
to Java2Script
Zhou Renjian,

Q: Sounds like I missed this one and will now need to implement thisConstructor if I upgrade. Is that correct?
Q: Is this also updated for 3.8?

Bob
Reply all
Reply to author
Forward
0 new messages