ExternalInterface is unreliable
“What are you supposed to do if you're the chef at Les Halles
and your fishmonger is giving you smelly fish?”
So while playing with ExternalInterface I found quite a big problem with it. It's serious enough to call it “unreliable” because it mangles your data in strange and undocumented ways. Here's a quick Flash applet to demonstrate that:
Now, if you write some text in the first textarea (from the yellow area, which is Flash) and press “Send to JS”, the second textarea should fill with that data. If you type into the second textarea and press “Send to Flash”, the Flash textarea should fill with data coming from JS.
The problem
Now try to send the following text: “&”. From JS to Flash it works as expected; if you try from Flash to JS, however, you'll only get “&”. Why in the world does it decode HTML entities is beyond me.
Next problem: try to send one backslash (“\”) character from Flash to JS and enjoy this fabulous JS error in your JS console:
Error: syntax error
Source Code:
try { __flash__toXML(getFromFlash("\")) ; } catch (e) { "<undefined/>"; }Note that all problems only happen one way (that is, when you try to send from Flash to JS). The other way around all works as expected.
I spent a few hours investigating this. There's no elegant solution (but though I imagined one, see below). The root of the problem seems to be the fact that, ExternalInterface is actually cheating. :) From the official documentation:
ExternalInterface is similar in functionality to the fscommand(), CallFrame() and CallLabel() methods, but is more flexible and more generally applicable. Use of ExternalInterface is recommended for JavaScript-ActionScript communication.
From ActionScript, you can call any JavaScript function on the HTML page, passing any number of arguments of any data type, and receive a return value from the call.
From JavaScript on the HTML page, you can call an ActionScript function in Flash Player. The ActionScript function can return a value, and JavaScript receives it immediately as the return value of the call.
What my research concluded is that things doesn't stand quite so. In fact, Flash-JS communication is done via pure strings. Therefore, if you try to send an object, Flash will actually serialize it to a string, and make sure to deserialize it on the other side, so it's all transparent to the developer. For this reason you can't pass true object references (let alone functions). An object might contain methods, but as you can guess, they are lost on the other side. (In fact I think that if you try to pass an object that contains functions, you'll get an error right away).
This serialization is handled by a few functions that Flash injects into the browser's global object (window). They are prefixed with __flash__. Google for __flash__toXML to find more information. The problem is that serialization is done poorly (and for some reason it converts objects to XML! how dumb is that?!).
One other thing I don't understand is why does it try to serialize a string to, umm, a string?! I mean, a string containing one backslash is properly encoded in memory—it could have been sent “as is”. But no, what it does in fact is something like this:
eval('"' + string + '"') // which turns out to eval('"\"') // which errors out as the string is not terminated
All this over engineering is beyond me...
The code
Here's the code in my Flash applet, though I doubt it has any part of the blame:
import flash.external.ExternalInterface; function onBtnClicked() { ExternalInterface.call("getFromFlash", txtArea.text); }; function getFromJS(data) { txtArea.text = data; }; sendBtn.addEventListener("click", onBtnClicked); if (ExternalInterface.addCallback("send", null, getFromJS)) { txtArea.text = "ExternalInterface initialized"; } else { txtArea.text = "ExternalInterface unavailable!"; }
The solution
Beware, this is very ugly. :-) The solution is to take the problem in our own hands and therefore encode dangerous characters ourselves, before sending them to JS. I determined the following characters to be problematic: backslash (\), quote (") and ampersand (&). Here's my method of dealing with it:
data = data.split("%").join("%25") .split("\\").join("%5c") .split("\"").join("%22") .split("&").join("%26"); // safe to send data now ExternalInterface.call("alert", data);
So I chosen to encode special chars using the “%XX” notation. I'm encoding “%” first, so we can safely use it for other characters. Then backslash, then quotes, then ampersand.
Note how complex it has to be because Flash (8 at least) doesn't support RegExp-s, nor does it have a string.replace function.
On the JS side, when I receive data I'm doing the reverse transformation:
data = data.replace(/%22/g, "\"") .replace(/%5c/g, "\\") .replace(/%26/g, "&") .replace(/%25/g, "%");
A different solution I found on some sites was to use escape() in ActionScript, and unescape() in JavaScript. This should be theoretically faster since it's one function call (and there's a good chance that this function is implemented in low level C); however, Unicode characters get mangled in translation (curiously, this is a documented bug).
FABridge
My friends over InterAKT pointed out a new solution that seems to give us The Right Thing: FABridge. Using it you can even pass functions and object references back and forth, which is definitely cool. It requires Flex and Flash 9 player, which for me at this point are pretty heavy requirements; but it's cool nonetheless and I'll probably switch to it in the future.
No comments yet — add your comment