My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Tuesday, January 12, 2010

ActionScript 2.0 to JavaScript Bridge

As discussed in my new year Ajaxian post, the Flash Player could provide some truly good info about the current user.
I have tried before to create a sort of "perfect bridge" to actually drive ActionScript directly via JavaScript.
Unfortunately, the layer added by the ExternalInterface is not light at all and things would move so slowly that it won't be worth it.
While in my post comments I have showed an example about how it is possible to export info from ActionScript to JavaScript, what I am describing right now is a sort of "general purpose bridge" to bring, statically and stateless, whatever info we need/want from whatever ActionScript 2.0 file/library we like, starting from the root.

Basic Example



<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>AS2Bridge by WebReflection</title>
<script type="text/javascript" src="AS2Bridge.js"></script>
</head>
<body>
<script type="text/javascript">

// override defaults showing the SWF
new AS2Bridge({width:320, height:200, style:""}).onload = function(){
var System = this.retrieve("System");
window.console && console.log(System);
setTimeout(function () {
System.showSettings();
}, 1000);
};

// another movie, an hidden one, just to retrieve some global AS 2.0 property
new AS2Bridge().onload = function(){
var _quality = this.retrieve("_quality");
window.console && console.log(_quality);
};

</script>
</body>
</html>

The AS2Bridge constructor creates each time, and if necessary, an instance related to the current ActionScript 2.0 global scope.

Single Method API: retrieve

Each instance will have a couple of public static properties, as is for example its related object id, plus a single public method:
self.retrieve(namespace:String):Object

Above method accepts a generic namespace string, which is basically the whole path, starting from the root, that will be exported.
This string can point to a property, a generic object, or even a function or method ... does it sound interesting?
As example, it is possible to export the whole System public static object:

var System = this.retrieve("System");

or just one part of it:

var playMusic = this.retrieve("System.capabilities.hasAudio");


The ActionScript 2.0 Code


(function ():Void {
/*! (C) Andrea Giammarchi - Mit Style License */

/** dependencies */
import flash.external.ExternalInterface;

/**
* executes a function and returns its exported value
* @param Number the registerd callback to exec
* @param Array arguments to send
* @return String exported result via callback
*/
function exec(i:Number, arguments:Array):String {
return callback[i].apply(null, arguments);
};

/**
* evaluates a namespace and exports it
* @param String a generic namespace to retrieve
* @return String exported namespace or one of its properties/methods
*/
function retrieve(key:String):String {
return export(eval(key), self);
};

/**
* Exports a generic object injecting its parent scope.
* @param Object generic property, value, method, or namespace to export
* @param Object parent to inject if a method is encountered ( e.g. (o = showSettings).call((p = System)) )
* @return String exported Object, the first argument, JavaScript compatible
*/
function export(o:Object, p:Object):String {
var type:String = typeof o;
switch (true) {
case o === null:
case type === "number":
case o instanceof Number:
if (isNaN(o)) {
return "null";
};
case type === "boolean":
case o instanceof Boolean:
return "" + o;
case type === "function":
case o instanceof Function:
return "function(){return " + bridge + ".__eval__('return '+" + bridge + ".__swf__.AS2Bridge__exec__(" + (callback.push(
function ():String {
return export(o.apply(p, arguments), p);
}
) - 1) + ",Array.prototype.slice.call(arguments)))()}";
case type === "string":
case o instanceof String:
return 'decodeURIComponent("' + escape("" + o) + '")';
case o instanceof Array:
for (var a:Array = new Array(), i:Number = 0, length:Number = o.length; i < length; ++i) {
a[i] = export(o[i], a);
};
return "[" + a.join(",") + "]";
case o instanceof Date:
return "new Date(" + o.getTime() + ")";
case o instanceof Object:
var a:Array = new Array();
var i:Number = 0;
for (var key:String in o) {
a[i++] = key + ":" + export(o[key], o);
};
return "{" + a.join(",") + "}";
default:
return "undefined";
};
};

/**
* Stack of stored callbacks encountered during exports.
* @private
*/
var callback:Array = new Array();

/**
* _root alias
* @private
*/
var self:Object = this;

/**
* String shortcut to use to point to the current bridge object (passed via query string or FlashVar)
* @private
*/
var bridge:String = "AS2Bridge.__register__[" + self.AS2Bridge__index__ + "]";

/**
* unobtrusive exposed callbacks
*/
ExternalInterface.addCallback("AS2Bridge__exec__", this, exec);
ExternalInterface.addCallback("AS2Bridge__retrieve__", this, retrieve);

/**
* JavaScript initialization
*/
ExternalInterface.call("eval", bridge + ".__init__&&" + bridge + ".__init__()");

}).call(this);

// optional stop to avoid loops if/when necessary
stop();

Thanks to a single closure, this snippet is desgned to work as stand alone, or as "general purpose no conflict plugin". In few words, whatever happens in the SWF it is always possible to retrieve a namespace/property/method and it is always possible from the SWF to call JavaScript:

// it is possible to call any public function without problems, e.g.
ExternalInterface.call("eval", 'alert("Hello from SWF")');


The ASBridge Constructor


if (typeof AS2Bridge !== "function") {
/*! (C) Andrea Giammarchi - Mit Style License */
var AS2Bridge = function (options) {
var index = AS2Bridge.__register__.push(this) - 1;
if (options) {
for (var key in options) {
this[key] = options[key];
};
};
this.id += index;
this.src += "?AS2Bridge__index__=" + index;
document.write(document.body ? this.__swf__() : "<body>" + this.__swf__() + "</body>");
};
AS2Bridge.__register__ = [];
AS2Bridge.prototype = {
constructor: AS2Bridge,
id: "__AS2Bridge__",
src: "AS2Bridge.swf",
style: "position:absolute;top:-10000px;left:-10000px;",
width: 1,
height: 1,
retrieve:function (key) {
return this.__eval__("return " + this.__swf__.AS2Bridge__retrieve__(key))();
},
__eval__:Function,
__init__:function(){
this.__swf__ = window[this.id] || document[this.id];
this.__init__ = null;
if (this.onload) {
this.onload();
};
},
__swf__:function () {
return ''.concat(
'<object type="application/x-shockwave-flash" ',
'style="', this.style, '" ',
'id="', this.id, '" ',
'width="', this.width, '" ',
'height="', this.height, '" ',
'data="', this.src, '">',
'<param name="movie" value="', this.src, '" />',
'</object>'
);
}
};
};

I am sorry I had no time to put proper comments, but I think it is simple enough to understand what's going on there.
There are defaults options, plus an horrible document write which aim is simply the one to put the SWF in place avoiding body problems.
Anyway, AS2Bridge would like to be after the head element, possibly inside the body ... the important thing is that it works, so why bother? ;)

The Whole Demo

With 908 bytes of cacheable SWF, and a ridiculous sice for the minified and gzipped JavaScript file, it is possible to test all I have said directly here.

Integration

ActionScript 3 a part, and would be nice to have time to create a bridge for it as well, it is possible to copy and paste in the root of whatever project my AS2.0 code in order to bring ActionScript to JavaScript export capability everywhere.
... told'ya it was something interesting, or maybe not ...

8 comments:

abozhilov said...

You can just shot the man, which teach you to software design.

Mixing your own interface with `options' passed from user is definitely bad idea, and mix two complete independent things.


`__eval__:Function`

Nonsense.

`this.onload.call(this);`

Nonsense.

Andrea Giammarchi said...

what's wrong with the single public method, the only one you should be interested about?

__eval__ is to evaluate
Function is to trap the global Function avoiding overwrites.

Function rather than eval is just to avoid Firebug delay during debug or code execution.

this.onload.call(this) was actually an old copy and paste twitted ages ago.

You can just shot the man, which teach you to talk randomly ;)

You'll never change, will 'ya

Andrea Giammarchi said...

... best part ever, I screwed up the title, but you were too concentrated in blamings.

The JS code is an old one just copied and pasted, I don't mind, I can improve it, it was an experiment, not an official library (no devpro.it or other repos indeed)

The ActionScript is the only thing I was taking care about, since I have a Flash CS4 trial which is gonna expire in few days and no intention on earth to buy Flash CS4 after this try.

Andrea Giammarchi said...

dude, in any case I am still wondering why you read this blog ... you must know that my old company gave me my favorite CUP when I left, where there is my face in a side, and part of our conversations in the other ... you are such a star :D

Andrea Giammarchi said...

OK, I won't let you write in this blog anymore, you passed the limit.

In million of lines of code you got a single old copy and paste of something I know better and before you, but you have to insist.

That is not my code, is some silly test while I was reading something else and you have the tweet, you are not teaching me anything, as you have never done.

The certification is from Adobe, as ActionScript 2.0 Certified Developer (read carefully, not Designer, developer) which covered both AS 1.0, ECMAScript 3rd Edition, and AS 2.0, 4th draft.

You are acting ridiculously and you obviously do not need to read this blog, just stop it, since you offend, you have rarely idea about what you are talking about, but you decided you can behave like this.

Learn more, respect more, and forget this blog, seriously.

Best Regards

abozhilov said...

Again and again, you cannot give answer of my questions. However, i'll stop to write here. If you can say code presented here is good code. You can see on white, black.

Regards, and try to understand the general concept of the language!

Andrea Giammarchi said...

You simply read this blog because you find stuff you have no idea how to do but since you feel skilled, and you did your silly figure in JavaScript ML as well blaming YOUR misunderstanding, I wonder why your life is concentrated to me, my skills you keep following, and this blog.
I have replied to everything you asked, I do not tolerate anymore your arrogance, your useless, pointless arguments, the way you behave everywhere, here or in the ml, and while I've kept posting your comments trying to hope one day you would have grown up, you found the most silly thing about an API that is a single public method.
Again, if you read carefully, I have explained I don't give a shit about JS code in this case, it was an old code used to test you don't even know how many things and a copy and paste typo, after 7 years of good ActionScript first and JavaScript after, cannot justify all this blaming for free.
This blog is not payed, I am not payed to write here, I share stuff, and in this case the stuff was the ActionScript bridge.
Everything you commented about the JavaScript is not relevant for this post topic.
You acting in this way simply ruin the spirit of WebReflection, which is my personal, WHATEVER I WANT POST place, and surely I won't write in my tomb: Damn It I Had To Close WebReflection Because Of Troll Abozhilov.

Last, but not least, you are even printed in my personal Cup good old colleagues gave me when I left the company, since the level of your conversation is so ridiculous that me, and many other people, keep having fun reading you.

That was your last post here, I will configure notification to put you directly into spam so unless you won't change your behavior, this blog has nothing else for you to say.

Have a nice week end.

P.S. in the meanwhile Flex and AS related people added me on twitter, that's how much you got about this post ... congratulations!

Unknown said...

Hello and gratulations :)

Helped me a lot to understand the ExternalInterface. I was working on this some days ago.

I build a function in AS that loads automatically the JS-part of the bridge to have my code a little easier to read.

May I ask how you have made the .swf cachable?

Thanks, Kristian (from germany, excuse my bad english ;) )