Saturday 23 November 2013

How to see the Java that your CFML compiles down to

G'day:
This is in response to a question Upendra asked on my earlier article "CFCamp: <cfprocessingdirective> and how not to use it". Within that article I said this:

This results in being compiled to:


this.MSG.set("??, ?????, ????? ???? ??? ???? (????) – ??????!");

Upendra's question is: "how do I know what it compiles to?". Good question.

First some history. Back when ColdFusion ported onto Java in CFMX 6.0, it went from being an interpreted language to being a compiled one. Not compiled to a native executable like C code would be, but compiled into Java bytecode that is then executed by the JVM. Your CFML itself is not executed, it's just an intermediary language between you and the JVM. Note: OpebBD is still interpreted, but ColdFusion and Railo CFML are both compiled.

Initially on CFMX 6.0 the CFML was translated into Java source code, then that was compiled. If one looked in the cfclasses dir (C:\apps\adobe\ColdFusion\10\cfusion\wwwroot\WEB-INF\cfclasses for me), one could see the .java files appearing as you browsed to a .cfm URL, and then being replaced by .class files shortly afterwards. This process was as slow as a dog, and by the time 6.1 (or perhaps even update 3 of 6.0?), the intermediary step to Java source had been scrapped, and the CFML was compiled straight to bytecode. Much quicker.

The compilation process is such that the resultant class file is loaded into memory (which is where it needs to be to be executed), but if one has a setting set in CFAdmin, then the .class file is also written out into thar cfclasses dir still. The idea is that the compilation process is quite an overhead, and if the results only go into RAM, they're lost when the JVM restarts. So to expedite things next time the class is needed, if it's there to load from disk it's much faster than to recompile from CFML. I discuss my thoughts on this in an earlier article: "Saving class files in ColdFusion (and Railo): anecdote should not take precedence over analysis".

I've just set my CFAdmin to save class files:


And have run this code:

<!--- gdayWorld.cfm --->
<cfset msg = "G'day world @ #now()#">
<cfoutput>#msg#</cfoutput>

It outputs this:

G'day world @ {ts '2013-11-23 09:20:12'}

If I look in my cfclasses dir, I see a new file (indeed two new files: I forgot about my Application.cfc):


Note the filename cfgdayWorld2ecfm576519084.class is the name of the CFM file (with the dot being encoded to 2E which is the fullstop's ASCII code in hex), followed by some sort of hash of the file name (I'm guessing). If I alter the file, the code doesn't change, so it's not based on the contents or the timestamp. It's possibly based on the file's path? Dunno.

A class file is binary, so no use to us. The first five lines of that file are as follows:

Êþº¾   - ­  
SourceFile  ]C:\apps\adobe\ColdFusion\10\cfusion\wwwroot\shared\git\blogExamples\compilation\gdayWorld.cfm   cfgdayWorld2ecfm576519084      coldfusion/runtime/CFPage      <init>   ()V     
          bindPageVariables  D(Lcoldfusion/runtime/VariableScope;Lcoldfusion/runtime/LocalScope;)V        coldfusion/runtime/CfJspPage   
   
   MSG   Lcoldfusion/runtime/Variable;      bindPageVariable  r(Ljava/lang/String;Lcoldfusion/runtime/VariableScope;Lcoldfusion/runtime/LocalScope;)Lcoldfusion/runtime/Variable;     

Barely comprehensible.

Fortunately there are Java decompilers out there, and Google can help you find one: "java decompiler". The decompiler converts a class file back into a .java source code file. The source code for gdayWorld.cfm in Java is:

import coldfusion.runtime.AttributeCollection;
import coldfusion.runtime.CFPage;
import coldfusion.runtime.Cast;
import coldfusion.runtime.CfJspPage;
import coldfusion.runtime.LocalScope;
import coldfusion.runtime.Variable;
import coldfusion.runtime.VariableScope;
import coldfusion.tagext.GenericTag;
import coldfusion.tagext.QueryLoop;
import coldfusion.tagext.io.OutputTag;
import java.io.Writer;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.Tag;

public final class cfgdayWorld2ecfm576519084 extends CFPage
{
  private Variable MSG;
  static final Class class$coldfusion$tagext$io$OutputTag;
  public static final Object metaData;

  static
  {
    class$coldfusion$tagext$io$OutputTag = Class.forName("coldfusion.tagext.io.OutputTag");
    metaData = new AttributeCollection(new Object[0]);
  }

  protected final void bindPageVariables(VariableScope varscope, LocalScope locscope)
  {
    super.bindPageVariables(varscope, locscope);
    this.MSG = bindPageVariable("MSG", varscope, locscope);
  }

  public final Object getMetadata()
  {
    return metaData;
  }

  protected final Object runPage()
  {
    Throwable t8;
    Throwable t7;
    Object t6;
    int mode0;
    Object value;
    JspWriter out = this.pageContext.getOut(); Tag parent = this.parent; bindImportPath("com.adobe.coldfusion.*"); _whitespace(out, "\r\n"); _setCurrentLineNo(2); this.MSG.set("G'day world @ ".concat(Cast._String(Now()))); _whitespace(out, "\r\n"); OutputTag output0 = (OutputTag)_initTag(class$coldfusion$tagext$io$OutputTag, 0, parent); _setCurrentLineNo(3); output0.hasEndTag(true);
    try { if ((mode0 = output0.doStartTag()) != 0) do out.write(Cast._String(_autoscalarize(this.MSG))); while (output0.doAfterBody() != 0); if (output0.doEndTag() == 5) return null;  } catch (Throwable localThrowable1) { output0.doCatch(localThrowable1); } catch (Throwable localThrowable2) { jsr 6; throw localThrowable2; } Object t9 = returnAddress; output0.doFinally(); ret; return null;
  }
}

Or on Railo:

package shared.git.blogexamples.compilation;

import railo.runtime.PageContext;
import railo.runtime.PagePlus;
import railo.runtime.PageSource;
import railo.runtime.component.ImportDefintion;
import railo.runtime.exp.PageException;
import railo.runtime.functions.dateTime.Now;
import railo.runtime.op.Caster;
import railo.runtime.type.Collection.Key;
import railo.runtime.type.KeyImpl;
import railo.runtime.type.UDF;
import railo.runtime.type.UDFProperties;
import railo.runtime.type.scope.Undefined;

public final class gdayworld_cfm$cf extends PagePlus
{
  private final ImportDefintion[] imports;
  private Collection.Key[] keys;

  public gdayworld_cfm$cf(PageSource paramPageSource)
  {
    initKeys();
    this.imports = new ImportDefintion[0];
    this.udfs = new UDFProperties[0];
    setPageSource(paramPageSource);
  }

  public final int getVersion()
  {
    return 4010201;
  }

  public final ImportDefintion[] getImportDefintions()
  {
    return new ImportDefintion[0];
  }

  public final long getSourceLastModified()
  {
    return 1385198948206L;
  }

  public final long getCompileTime()
  {
    return 1385199236171L;
  }

  public final void call(PageContext paramPageContext)
    throws Throwable
  {
    paramPageContext.write("\r\n"); paramPageContext.us().set(this.keys[0], "G'day world @ ".concat(Caster.toString(Now.call(paramPageContext)))); paramPageContext.write("\r\n");
    paramPageContext.outputStart();
    try { paramPageContext.write(Caster.toString(paramPageContext.us().get(this.keys[0]))); } finally { paramPageContext.outputEnd(); }

  }

  public final Object udfCall(PageContext paramPageContext, UDF paramUDF, int paramInt)
    throws Throwable
  {
    return null;
  }

  public final void threadCall(PageContext paramPageContext, int paramInt)
    throws Throwable
  {
  }

  public final Object udfDefaultValue(PageContext paramPageContext, int paramInt1, int paramInt2, Object paramObject)
    throws PageException
  {
    return paramObject;
  }

  private final void initKeys()
  {
    this.keys = new Collection.Key[] { KeyImpl.intern("MSG") };
  }
}

There's a lot of overhead in there, so when I post some decompiled code, I usually snip out just the relevant bit and re-indent it. In this case (CF):

JspWriter out = this.pageContext.getOut();
Tag parent = this.parent; 
bindImportPath("com.adobe.coldfusion.*");
_whitespace(out, "\r\n");
_setCurrentLineNo(2);
this.MSG.set("G'day world @ ".concat(Cast._String(Now())));
_whitespace(out, "\r\n");
OutputTag output0 = (OutputTag)_initTag(class$coldfusion$tagext$io$OutputTag, 0, parent);
_setCurrentLineNo(3);
output0.hasEndTag(true);
try {
    if ((mode0 = output0.doStartTag()) != 0)
        do
            out.write(Cast._String(_autoscalarize(this.MSG)));
        while (output0.doAfterBody() != 0);
    if (output0.doEndTag() == 5)
        return null; 
} catch (Throwable localThrowable1) {
    output0.doCatch(localThrowable1);
} catch (Throwable localThrowable2) {
    jsr 6;
    throw localThrowable2;
}
Object t9 = returnAddress;
output0.doFinally();
ret;
return null;

And Railo:

public final void call(PageContext paramPageContext) throws Throwable {
    paramPageContext.write("\r\n");
    paramPageContext.us().set(
        this.keys[0],
        "G'day world @ ".concat(Caster.toString(Now.call(paramPageContext)))
    );
    paramPageContext.write("\r\n");
    paramPageContext.outputStart();
    try {
        paramPageContext.write(Caster.toString(paramPageContext.us().get(this.keys[0])));
    } finally {
        paramPageContext.outputEnd();
    }

}

Railo's version is certainly easier to follow and has far less generic clutter in it (and hard-compiled debug stuff, like all those _setCurrentLineNo() calls in the ColdFusion version!).

The last thing I'd say is that obviously ColdFusion itself is all Java code, so it is possible to decompile its code too. The only thing I will say about that is... check the laws in your jurisdiction before you do so. Well: before you share any info you might glean from doing so, anyhow. All I'm saying is... it's possible. I will only ever post the decompilation of my own code on this blog.

Anyway... there you go. All this before breakfast time! (And even before coffee! Blimey!). I better actually get out of bed soon, I s'pose. I'll leave you with that imagery.

--
Adam