Revision: 8413
Author: [email protected]
Date: Fri Jul 23 11:50:52 2010
Log: Rolling back the cross-site linker using an iframe. It's not clear
this is a safe change for all apps.
Review by: [email protected]
http://code.google.com/p/google-web-toolkit/source/detail?r=8413
Added:
/trunk/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
Modified:
/trunk/dev/core/src/com/google/gwt/core/linker/XSLinker.java
/trunk/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
/trunk/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
/trunk/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
/trunk/user/src/com/google/gwt/core/XSLinker.gwt.xml
=======================================
--- /dev/null
+++
/trunk/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
Fri Jul 23 11:50:52 2010
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.js.ast.JsBinaryOperation;
+import com.google.gwt.dev.js.ast.JsBinaryOperator;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsName;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVars;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.js.ast.JsVars.JsVar;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Rewrite JavaScript to better handle references from one code fragment to
+ * another. For any function defined off the initial download and accessed
from
+ * a different island than the one it's defined on, predefine a variable
in the
+ * initial download to hold its definition.
+ */
+public class HandleCrossFragmentReferences {
+ /**
+ * Find out which islands define and use each named function or variable.
+ * This visitor is not smart about which definitions and uses matter. It
+ * blindly records all of them.
+ */
+ private class FindNameReferences extends JsVisitor {
+ Map<JsName, Set<Integer>> islandsDefining = new LinkedHashMap<JsName,
Set<Integer>>();
+ Map<JsName, Set<Integer>> islandsUsing = new LinkedHashMap<JsName,
Set<Integer>>();
+ private int currentIsland;
+
+ @Override
+ public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+ JsName name = x.getName();
+ if (name != null) {
+ definitionSeen(name);
+ }
+ }
+
+ @Override
+ public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+ if (x.getQualifier() == null) {
+ JsName name = x.getName();
+ if (name != null) {
+ referenceSeen(name);
+ }
+ }
+ }
+
+ @Override
+ public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+ for (JsVar var : x) {
+ JsName name = var.getName();
+ if (name != null) {
+ definitionSeen(name);
+ }
+ }
+ }
+
+ @Override
+ public boolean visit(JsProgram x, JsContext<JsProgram> ctx) {
+ for (int i = 0; i < x.getFragmentCount(); i++) {
+ currentIsland = i;
+ accept(x.getFragmentBlock(i));
+ }
+
+ return false;
+ }
+
+ private void definitionSeen(JsName name) {
+ /*
+ * Support multiple definitions, because local variables can reuse
the
+ * same name.
+ */
+ Set<Integer> defs = islandsDefining.get(name);
+ if (defs == null) {
+ defs = new LinkedHashSet<Integer>();
+ islandsDefining.put(name, defs);
+ }
+ defs.add(currentIsland);
+ }
+
+ private void referenceSeen(JsName name) {
+ Set<Integer> refs = islandsUsing.get(name);
+ if (refs == null) {
+ refs = new HashSet<Integer>();
+ islandsUsing.put(name, refs);
+ }
+ refs.add(currentIsland);
+ }
+ }
+
+ /**
+ * Rewrite var and function declarations as assignments, if their name is
+ * accessed cross-island. Rewrite refs to such names correspondingly.
+ */
+ private class RewriteDeclsAndRefs extends JsModVisitor {
+ @Override
+ public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+ if (namesToPredefine.contains(x.getName())) {
+ JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(),
+ JsBinaryOperator.ASG, makeRefViaJslink(x.getName(),
+ x.getSourceInfo()), x);
+ x.setName(null);
+ ctx.replaceMe(asg);
+ }
+ }
+
+ @Override
+ public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+ if (namesToPredefine.contains(x.getName())) {
+ ctx.replaceMe(makeRefViaJslink(x.getName(), x.getSourceInfo()));
+ }
+ }
+
+ @Override
+ public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+ if (!ctx.canInsert()) {
+ return;
+ }
+
+ /*
+ * Loop through each var and see if it was predefined. If so, then
remove
+ * the var. If the var has an initializer, then add back an
assignment
+ * statement to initialize it. If there is no initializer, then
don't add
+ * anything back; the var will still have undefined as its initial
value,
+ * just like before.
+ *
+ * A complication is that the variables that are predefined might be
+ * interspersed with variables that are not. That means the general
result
+ * of this transformation has alternating var lists and assignment
+ * statements. The currentVar variable holds the most recently
inserted
+ * statement, if that statement was a JsVars; otherwise it holds
null.
+ */
+
+ JsVars currentVar = null;
+ Iterator<JsVar> varsIterator = x.iterator();
+ while (varsIterator.hasNext()) {
+ JsVar var = varsIterator.next();
+ if (namesToPredefine.contains(var.getName())) {
+ // The var was predefined
+ if (var.getInitExpr() != null) {
+ // If it has an initializer, add an assignment statement
+ JsBinaryOperation asg = new
JsBinaryOperation(var.getSourceInfo(),
+ JsBinaryOperator.ASG, makeRefViaJslink(var.getName(),
+ var.getSourceInfo()), var.getInitExpr());
+ ctx.insertBefore(asg.makeStmt());
+ currentVar = null;
+ }
+ } else {
+ // The var was not predefined; add it to a var list
+ if (currentVar == null) {
+ currentVar = new JsVars(x.getSourceInfo());
+ ctx.insertBefore(currentVar);
+ }
+ currentVar.add(var);
+ }
+ }
+
+ ctx.removeMe();
+ }
+
+ private JsNameRef makeRefViaJslink(JsName name, SourceInfo sourceInfo)
{
+ JsNameRef ref = name.makeRef(sourceInfo);
+ ref.setQualifier(jslink.makeRef(sourceInfo));
+ return ref;
+ }
+ }
+
+ public static String PROP_PREDECLARE_VARS
= "compiler.predeclare.cross.fragment.references";
+
+ public static void exec(TreeLogger logger, JsProgram jsProgram,
+ PropertyOracle[] propertyOracles) {
+ new HandleCrossFragmentReferences(logger, jsProgram,
propertyOracles).execImpl();
+ }
+
+ private static boolean containsOtherThan(Set<Integer> set, int allowed) {
+ for (int elem : set) {
+ if (elem != allowed) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private JsName jslink;
+ private final JsProgram jsProgram;
+ private final Set<JsName> namesToPredefine = new LinkedHashSet<JsName>();
+ private final PropertyOracle[] propertyOracles;
+ private final TreeLogger logger;
+
+ private HandleCrossFragmentReferences(TreeLogger logger, JsProgram
jsProgram,
+ PropertyOracle[] propertyOracles) {
+ this.logger = logger;
+ this.jsProgram = jsProgram;
+ this.propertyOracles = propertyOracles;
+ }
+
+ private void chooseNamesToPredefine(Map<JsName, Set<Integer>> map,
+ Map<JsName, Set<Integer>> islandsUsing) {
+ for (Entry<JsName, Set<Integer>> entry : map.entrySet()) {
+ JsName name = entry.getKey();
+ Set<Integer> defIslands = entry.getValue();
+ if (defIslands.size() != 1) {
+ // Only rewrite global variables, which should have exactly one
+ // definition
+ continue;
+ }
+ int defIsland = defIslands.iterator().next();
+ if (defIsland == 0) {
+ // Variables defined on the base island can be accessed directly
from
+ // other islands
+ continue;
+ }
+ Set<Integer> useIslands = islandsUsing.get(name);
+ if (useIslands == null) {
+ // The variable is never used. Leave it alone.
+ continue;
+ }
+
+ if (containsOtherThan(islandsUsing.get(name), defIsland)) {
+ namesToPredefine.add(name);
+ }
+ }
+ }
+
+ /**
+ * Define the jslink object that will be used to fix up cross-island
+ * references.
+ */
+ private void defineJsLink() {
+ SourceInfo info = jsProgram.createSourceInfoSynthetic(
+ HandleCrossFragmentReferences.class, "defining jslink");
+ jslink = jsProgram.getScope().declareName("jslink");
+ JsVars vars = new JsVars(info);
+ JsVar var = new JsVar(info, jslink);
+ var.setInitExpr(new JsObjectLiteral(info));
+ vars.add(var);
+ jsProgram.getFragmentBlock(0).getStatements().add(0, vars);
+ }
+
+ private void execImpl() {
+ if (jsProgram.getFragmentCount() == 1) {
+ return;
+ }
+ if (!shouldPredeclareReferences()) {
+ return;
+ }
+ defineJsLink();
+ FindNameReferences findNameReferences = new FindNameReferences();
+ findNameReferences.accept(jsProgram);
+ chooseNamesToPredefine(findNameReferences.islandsDefining,
+ findNameReferences.islandsUsing);
+ new RewriteDeclsAndRefs().accept(jsProgram);
+ }
+
+ /**
+ * Check the property oracles for whether references should be
predeclared or
+ * not. If any of them say yes, then do the rewrite.
+ */
+ private boolean shouldPredeclareReferences() {
+ for (PropertyOracle props : propertyOracles) {
+ try {
+ String propValue = props.getSelectionProperty(logger,
+ PROP_PREDECLARE_VARS).getCurrentValue();
+ if (Boolean.parseBoolean(propValue)) {
+ return true;
+ }
+ } catch (BadPropertyValueException e) {
+ // Property not defined; don't rewrite
+ }
+ }
+
+ return false;
+ }
+}
=======================================
--- /trunk/dev/core/src/com/google/gwt/core/linker/XSLinker.java Fri Jul 23
09:44:56 2010
+++ /trunk/dev/core/src/com/google/gwt/core/linker/XSLinker.java Fri Jul 23
11:50:52 2010
@@ -17,7 +17,6 @@
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.linker.CompilationResult;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.Shardable;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
@@ -25,8 +24,6 @@
import com.google.gwt.dev.About;
import com.google.gwt.dev.js.JsToStringGenerationVisitor;
import com.google.gwt.dev.util.DefaultTextOutput;
-import com.google.gwt.dev.util.TextOutput;
-import com.google.gwt.dev.util.Util;
/**
* Generates a cross-site compatible bootstrap sequence.
@@ -43,7 +40,6 @@
protected String generateDeferredFragment(TreeLogger logger,
LinkerContext context, int fragment, String js) {
StringBuilder sb = new StringBuilder();
- sb.append("$wnd.");
sb.append(context.getModuleFunctionName());
sb.append(".runAsyncCallback");
sb.append(fragment);
@@ -52,27 +48,6 @@
sb.append(");\n");
return sb.toString();
}
-
- @Override
- protected byte[] generatePrimaryFragment(TreeLogger logger,
- LinkerContext context, CompilationResult result, String[] js) {
- // Wrap the script code with its prefix and suffix
- TextOutput script = new DefaultTextOutput(context.isOutputCompact());
- script.print(getModulePrefix(context, result.getStrongName(),
js.length > 1));
- script.print(js[0]);
- script.print(getModuleSuffix(logger, context));
-
- // Rewrite the code so it can be installed with
- // __MODULE_FUNC__.onScriptDownloaded
-
- StringBuffer out = new StringBuffer();
- out.append(context.getModuleFunctionName());
- out.append(".onScriptDownloaded(");
-
out.append(JsToStringGenerationVisitor.javaScriptString(script.toString()));
- out.append(")");
-
- return Util.getBytes(out.toString());
- }
@Override
protected String getCompilationExtension(TreeLogger logger,
@@ -83,13 +58,13 @@
@Override
protected String getModulePrefix(TreeLogger logger, LinkerContext
context,
String strongName) {
- throw new UnsupportedOperationException("Should not be called");
+ return getModulePrefix(context, strongName, true);
}
@Override
protected String getModulePrefix(TreeLogger logger, LinkerContext
context,
String strongName, int numFragments) {
- throw new UnsupportedOperationException("Should not be called");
+ return getModulePrefix(context, strongName, numFragments > 1);
}
@Override
@@ -103,10 +78,12 @@
// Generate the call to tell the bootstrap code that we're ready to go.
out.newlineOpt();
- out.print("if ($wnd." + context.getModuleFunctionName() + " && $wnd."
- + context.getModuleFunctionName() + ".onScriptInstalled) $wnd."
- + context.getModuleFunctionName()
+ ".onScriptInstalled(gwtOnLoad);");
+ out.print("if (" + context.getModuleFunctionName() + " && "
+ + context.getModuleFunctionName() + ".onScriptLoad)"
+ + context.getModuleFunctionName() + ".onScriptLoad(gwtOnLoad);");
out.newlineOpt();
+ out.print("})();");
+ out.newlineOpt();
return out.toString();
}
@@ -119,11 +96,16 @@
private String getModulePrefix(LinkerContext context, String strongName,
boolean supportRunAsync) {
- TextOutput out = new DefaultTextOutput(context.isOutputCompact());
-
+ DefaultTextOutput out = new
DefaultTextOutput(context.isOutputCompact());
+
+ out.print("(function(){");
+ out.newlineOpt();
+
+ // Setup the well-known variables.
+ //
out.print("var $gwt_version = \"" + About.getGwtVersionNum() + "\";");
out.newlineOpt();
- out.print("var $wnd = window.parent;");
+ out.print("var $wnd = window;");
out.newlineOpt();
out.print("var $doc = $wnd.document;");
out.newlineOpt();
@@ -143,10 +125,13 @@
out.newlineOpt();
if (supportRunAsync) {
- out.print("var __gwtModuleFunction = $wnd.");
out.print(context.getModuleFunctionName());
- out.print(";");
+ out.print(".installCode = function(code) { eval(code) };");
out.newlineOpt();
+ out.print("var __gwtModuleFunction = ");
+ out.print(context.getModuleFunctionName());
+ out.print(";");
+ out.newline();
}
return out.toString();
=======================================
--- /trunk/dev/core/src/com/google/gwt/core/linker/XSTemplate.js Fri Jul 23
09:44:56 2010
+++ /trunk/dev/core/src/com/google/gwt/core/linker/XSTemplate.js Fri Jul 23
11:50:52 2010
@@ -23,17 +23,8 @@
,$stats = $wnd.__gwtStatsEvent ? function(a) {return
$wnd.__gwtStatsEvent(a);} : null
,$sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null
- // Whether the document body element has finished loading
- ,bodyDone
-
- // The downloaded script code, once it arrives
- ,downloadedScript
-
- // Whether the main script has been injected yet
- ,scriptInjected
-
- // The iframe holding the app's code
- ,scriptFrame
+ // These variables gate calling gwtOnLoad; all must be true to start
+ ,gwtOnLoad, bodyDone
// If non-empty, an alternate base url for this module
,base = ''
@@ -94,55 +85,25 @@
return result;
}
- // Called when the body has been finished and when the script
- // to install is available. These can happen in either order.
- // When both have happened, create the code-holding iframe.
+ // Called by onScriptLoad() and onload(). It causes
+ // the specified module to be cranked up.
//
- function maybeCreateFrame() {
- if (bodyDone && downloadedScript && !scriptInjected) {
- scriptInjected = true;
-
- // Create the script frame, making sure it's invisible, but not
- // "display:none", which keeps some browsers from running code in it.
- scriptFrame = $doc.createElement('iframe');
- scriptFrame.src = 'javascript:""';
- scriptFrame.id = '__MODULE_NAME__';
- scriptFrame.style.cssText = 'position:absolute; width:0; height:0;
border:none';
- scriptFrame.tabIndex = -1;
- document.body.appendChild(scriptFrame);
-
- // For some reason, adding this setTimeout makes code installation
- // more reliable.
- setTimeout(function() {
- installCode(downloadedScript);
- })
+ function maybeStartModule() {
+ // TODO: it may not be necessary to check gwtOnLoad here.
+ if (gwtOnLoad && bodyDone) {
+ gwtOnLoad(onLoadErrorFunc, '__MODULE_NAME__', base,
softPermutationId);
+ // Record when the module EntryPoints return.
+ $stats && $stats({
+ moduleName: '__MODULE_NAME__',
+ sessionId: $sessionId,
+ subSystem: 'startup',
+ evtGroup: 'moduleStartup',
+ millis:(new Date()).getTime(),
+ type: 'end',
+ });
}
}
- // Install code into scriptFrame
- //
- function installCode(code) {
- var doc = scriptFrame.contentDocument;
- if (!doc) {
- doc = scriptFrame.contentWindow.document;
- }
-
- var dochead = doc.getElementsByTagName('head')[0];
-
- // Inject the fetched script into the script frame.
- // The script will call onScriptLoad.
- var script = doc.createElement('script');
- script.language='javascript';
- script.text = code;
-
- dochead.appendChild(script);
-
- // Remove the tag to shrink the DOM a little.
- // It should have installed its code immediately after being added.
- dochead.removeChild(script);
- }
-
-
__COMPUTE_SCRIPT_BASE__
__PROCESS_METAS__
@@ -205,32 +166,14 @@
// --------------- EXPOSED FUNCTIONS ----------------
- // Called when the initial script code has been downloaded
- __MODULE_FUNC__.onScriptDownloaded = function(code) {
- downloadedScript = code;
- maybeCreateFrame();
- }
-
- // Called when the compiled script has been installed
+ // Called when the compiled script identified by moduleName is done
loading.
//
- __MODULE_FUNC__.onScriptInstalled = function(gwtOnLoadFunc) {
+ __MODULE_FUNC__.onScriptLoad = function(gwtOnLoadFunc) {
// remove the callback to prevent it being called twice
- __MODULE_FUNC__.onScriptInstalled = null;
- gwtOnLoadFunc(onLoadErrorFunc, '__MODULE_NAME__', base,
softPermutationId);
- // Record when the module EntryPoints return.
- $stats && $stats({
- moduleName: '__MODULE_NAME__',
- sessionId: $sessionId,
- subSystem: 'startup',
- evtGroup: 'moduleStartup',
- millis:(new Date()).getTime(),
- type: 'end',
- });
- }
-
- // Install code pulled in via runAsync
- //
- __MODULE_FUNC__.installCode = installCode;
+ __MODULE_FUNC__.onScriptLoad = null;
+ gwtOnLoad = gwtOnLoadFunc;
+ maybeStartModule();
+ }
// --------------- STRAIGHT-LINE CODE ---------------
@@ -277,7 +220,7 @@
// __MODULE_STYLES_BEGIN__
// Style resources are injected here to prevent operation aborted
errors on ie
// __MODULE_STYLES_END__
- maybeCreateFrame();
+ maybeStartModule();
if ($doc.removeEventListener) {
$doc.removeEventListener("DOMContentLoaded", onBodyDone, false);
=======================================
---
/trunk/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
Fri Jul 23 09:44:56 2010
+++
/trunk/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
Fri Jul 23 11:50:52 2010
@@ -69,6 +69,7 @@
import com.google.gwt.dev.jjs.impl.FragmentLoaderCreator;
import com.google.gwt.dev.jjs.impl.GenerateJavaAST;
import com.google.gwt.dev.jjs.impl.GenerateJavaScriptAST;
+import com.google.gwt.dev.jjs.impl.HandleCrossFragmentReferences;
import com.google.gwt.dev.jjs.impl.JavaScriptObjectNormalizer;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.jjs.impl.JsFunctionClusterer;
@@ -355,6 +356,11 @@
default:
throw new InternalCompilerException("Unknown output mode");
}
+
+ // (10.8) Handle cross-island references.
+ // No new JsNames or references to JSNames can be introduced after
this
+ // point.
+ HandleCrossFragmentReferences.exec(logger, jsProgram,
propertyOracles);
// (11) Perform any post-obfuscation normalizations.
=======================================
--- /trunk/user/src/com/google/gwt/core/CompilerParameters.gwt.xml Fri Jul
23 09:44:56 2010
+++ /trunk/user/src/com/google/gwt/core/CompilerParameters.gwt.xml Fri Jul
23 11:50:52 2010
@@ -22,6 +22,17 @@
<define-configuration-property
name='compiler.splitpoint.initial.sequence'
is-multi-valued='true' />
+ <!--
+ Whether or not the compiler should predeclare variables that are
defined
+ outside the initial download and are referenced from a different code
+ fragment than the one defining them. This is usually determined by
which
+ linker is used and is not directly meaningful to users.
+ -->
+ <define-property name="compiler.predeclare.cross.fragment.references"
+ values="true,false" />
+ <set-property name="compiler.predeclare.cross.fragment.references"
+ value="false" />
+
<!-- From here down, the properties are unsupported and are only
available for test cases -->
<!--
=======================================
--- /trunk/user/src/com/google/gwt/core/XSLinker.gwt.xml Fri Jul 23
09:44:56 2010
+++ /trunk/user/src/com/google/gwt/core/XSLinker.gwt.xml Fri Jul 23
11:50:52 2010
@@ -21,4 +21,8 @@
class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy"
/>
<when-linker-added name="xs" />
</replace-with>
+
+ <set-property name="compiler.predeclare.cross.fragment.references"
value="true">
+ <when-linker-added name="xs" />
+ </set-property>
</module>
--
http://groups.google.com/group/Google-Web-Toolkit-Contributors