Friday, 12 July 2013

CFML: Trying to be sneaky and failing

G'day:
As I said in my previous article, it's a bit of a slow day in the office today, so I'm just sitting here hitting ColdFusion interfaces with a sledgehammer to see what happens. I was trying to be clever, and have failed miserably. Oh well.

When I was fiddling around trying to work out "WTF" in that previous article, I ended up with this code.

A vanilla interface:

// TestInterface.cfc
interface {

    public numeric function length(required string s);

}

A CFC which implements it:

// Impl.cfc
component implements="TestInterface" {

    public numeric function length(required string s) {
        return len(s);
    }

}

And some code that mungs around with it:

writeOutput("Initial state<br>");
impl = new Impl();
writeOutput('isInstanceOf(impl, "TestInterface"): ' & isInstanceOf(impl, "TestInterface") & "<br>");
writeOutput("<hr>");


writeOutput("After clearance<br>");
impl = new Impl();
structClear(impl);
writeOutput('isInstanceOf(impl, "TestInterface"): ' & isInstanceOf(impl, "TestInterface") & "<br>");
writeOutput("<hr>");


writeOutput("Restored to working order<br>");
impl = new Impl();

public numeric function length(required string s) {
    return len(s);
}
impl.length = length;

writeOutput('isInstanceOf(impl, "TestInterface"): ' & isInstanceOf(impl, "TestInterface") & "<br>");
writeOutput("<hr>");

All I'm doing here is:
  • creating an Impl object, checking to see if it passes isInstanceOf() for a TestInterface;
  • deleting the length() method from the Impl instance, and checking if it still passes the isInstanceOf() test;
  • pops an appropriate function back into the Impl instance, and checking again if it passes isInstanceOf() for a TestInterface.
 And the output:

Initial state
isInstanceOf(impl, "TestInterface"): YES

After clearance
isInstanceOf(impl, "TestInterface"): NO

Restored to working order
isInstanceOf(impl, "TestInterface"): YES

So - as I kinda expected - I can "break" and "fix" the object when taking methods in and out at runtime. I demonstrated last time that this is not 100% reliable, but still: worth playing with.

And this got me thinking... if I can remove and add methods and it'll change whether it adheres to the interface... can I actually change a whole object to be implementing an interface at runtime? Basically I'll create an instance of a CFC which doesn't implement an interface, and then alter its metadata and tell it it does implement the interface. And see how it behaves.

Here's my test code. First I get the metadata for TestInterface.cfc and Imp.cfc:

metaDataI        = getComponentMetadata("TestInterface");
metaDataImpl    = getComponentMetadata("Impl");

writeDump({
    metaDataI        = metaDataI,
    metaDataImpl    = metaDataImpl
});

Which is as follows:

struct
METADATAI
struct
EXTENDS
struct
WEB-INF.cftags.interface
struct
FULLNAMEWEB-INF.cftags.interface
NAMEWEB-INF.cftags.interface
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\interface.cfc
TYPEinterface
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
FUNCTIONS
array
1
struct
ACCESSpublic
NAMElength
PARAMETERS
array
1
struct
NAMEs
REQUIREDtrue
TYPEstring
RETURNTYPEnumeric
NAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\TestInterface.cfc
TYPEinterface
METADATAIMPL
struct
EXTENDS
struct
FULLNAMEWEB-INF.cftags.component
NAMEWEB-INF.cftags.component
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\component.cfc
TYPEcomponent
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.Impl
FUNCTIONS
array
1
struct
ACCESSpublic
NAMElength
PARAMETERS
array
1
struct
NAMEs
REQUIREDtrue
TYPEstring
RETURNTYPEnumeric
IMPLEMENTS
struct
TestInterface
struct
EXTENDS
struct
WEB-INF.cftags.interface
struct
FULLNAMEWEB-INF.cftags.interface
NAMEWEB-INF.cftags.interface
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\interface.cfc
TYPEinterface
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
FUNCTIONS
array
1
struct
ACCESSpublic
NAMElength
PARAMETERS
array
1
struct
NAMEs
REQUIREDtrue
TYPEstring
RETURNTYPEnumeric
NAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\TestInterface.cfc
TYPEinterface
NAMEshared.git.blogExamples.interfaces.buildfromscratch.Impl
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\Impl.cfc
TYPEcomponent

Note how basically Impl.cfc has a copy of TestInterface.cfc's metadata in there. And if we create an instance of Impl.cfc and get its metadata, it's the same (to save space I'll leave it out... take my word for it).

So I figure, OK, what if that is what makes an object implement an interface: having the relevant metadata in their to say so.

So I toy with this. I have a completely empty CFC:

// Empty.cfc
component {
}

And continuing my code from above, I stick TestInterface.cfc's metadata into Empty.cfc's metadata in the appropriate place:

metaDataEmpty    = getComponentMetadata("Empty");
writeDump({metaDataEmpty=metaDataEmpty});

metaDataEmpty.implements = {
    "TestInterface" = metaDataI
};
writeOutput("<hr>");

empty = new Empty();
public numeric function length(required string s) {
    return len(s);
}
empty.length = length;

metadataEmpty = getMetadata(empty);
writeDump({metadataEmpty=metadataEmpty});

This yields:

struct
METADATAEMPTY
struct
EXTENDS
struct
FULLNAMEWEB-INF.cftags.component
NAMEWEB-INF.cftags.component
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\component.cfc
TYPEcomponent
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.Empty
IMPLEMENTS
struct
TestInterface
struct
EXTENDS
struct
WEB-INF.cftags.interface
struct
FULLNAMEWEB-INF.cftags.interface
NAMEWEB-INF.cftags.interface
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\interface.cfc
TYPEinterface
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
FUNCTIONS
array
1
struct
ACCESSpublic
NAMElength
PARAMETERS
array
1
struct
NAMEs
REQUIREDtrue
TYPEstring
RETURNTYPEnumeric
NAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\TestInterface.cfc
TYPEinterface
NAMEshared.git.blogExamples.interfaces.buildfromscratch.Empty
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\Empty.cfc
TYPEcomponent

struct
METADATAEMPTY
struct
EXTENDS
struct
FULLNAMEWEB-INF.cftags.component
NAMEWEB-INF.cftags.component
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\component.cfc
TYPEcomponent
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.Empty
IMPLEMENTS
struct
TestInterface
struct
EXTENDS
struct
WEB-INF.cftags.interface
struct
FULLNAMEWEB-INF.cftags.interface
NAMEWEB-INF.cftags.interface
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\interface.cfc
TYPEinterface
FULLNAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
FUNCTIONS
array
1
struct
ACCESSpublic
NAMElength
PARAMETERS
array
1
struct
NAMEs
REQUIREDtrue
TYPEstring
RETURNTYPEnumeric
NAMEshared.git.blogExamples.interfaces.buildfromscratch.TestInterface
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\TestInterface.cfc
TYPEinterface
NAMEshared.git.blogExamples.interfaces.buildfromscratch.Empty
PATHD:\websites\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\Empty.cfc
TYPEcomponent

So the metadata for the object instance of Empty.cfc looks the part! So... does it work? (I've kinda already given away the answer to this).

writeOutput('isInstanceOf(empty, "TestInterface"): ' & isInstanceOf(empty, "TestInterface") & "<br>");
writeOutput("<hr>");
try {
    boolean function inboundTester(required TestInterface o){
        return true;
    }
    writeOutput("Test passing into function<br>");
    inboundTester(empty);
    writeOutput("inboundTester(empty): OK<br>");
}
catch (any e){
    writeDump(var=e, label="e");
}

try {
    TestInterface function outboundTester(required any o){
        return o;
    }
    writeOutput("Test passing back from function<br>");
    outboundTester(empty);
    writeOutput("outboundTester(empty): OK<br>");
}
catch (any e){
    writeDump(var=e, label="e");
}

Here I just do the isInstanceOf() test again, and then go ahead and try to see if my Empty instance will work where a TestInterface is needed, both for an argument being passed into a function, or as a value being returned from a function. Results:

isInstanceOf(empty, "TestInterface"): NO

Test passing into function
e - struct
Messagecoldfusion.runtime.Struct cannot be cast to coldfusion.runtime.AttributeCollection
StackTrace
TagContext
Typejava.lang.ClassCastException
Test passing back from function
e - struct
Messagecoldfusion.runtime.Struct cannot be cast to coldfusion.runtime.AttributeCollection
StackTrace
TagContext
Typejava.lang.ClassCastException

So... err... no. That didn't work. And I seemed to have really confused ColdFusion, as it's not saying "that is not a TestInterface", as one would expect, it's bleating about struct not being attributesCollections. Shrug.

On Railo it doesn't work either, but at least I get a sensible error message:

invalid call of the function inboundTester (C:\Apps\railo-express-jre-win64\webapps\railo\www.scribble.local\shared\git\blogExamples\interfaces\buildfromscratch\blankslate.cfm), first Argument (o) is of invalid type, can't cast Object type [Component www.scribble.local.shared.git.blogExamples.interfaces.buildfromscratch.Empty] to a value of type [testinterface]

And:
the function outboundTester has an invalid return value , can't cast Object type [Component www.scribble.local.shared.git.blogExamples.interfaces.buildfromscratch.Empty] to a value of type [TestInterface]

Oh well. Wishful thinking.

I think given CF is quite happy with things being poked in and prodded about the place, then - even if not via this rather simplistic mechanism - one should be able to do what I set out to do. If I can construct something at runtime that would pass the interface contract if it was written out in code, then that should be cool.

But we're not. OK.



--
Adam