Adobe, MAKE SOME NOISE

⇠ Unfucking the ExternalInterface (1 of 2)

Step 2. Validate and Dispatch.
As much as I hate to admit it, JS tends to run quite a bit faster than AS3; So when the two are communicating, the real lag happens on the Flash side. It’s not such a problem if you just need to twiddle a boolean flag or set a variable, but generally when JS fires off some data, thats your cue to run a pile of code on it.

Let's take a look at that last sentance:

…when JS fires…data…your cue…pile of code

Sounds a lot like the event model, no? Well Adobe™ didn't think so, and the ExternalInterface’s sphincter-tight coupling and lack of validation are a slap in the face to anyone who disagrees.

Before we get ahead of ourselves, it’s worth bringing up my second gripe: Data validation.

Anyone who’s written AS3 for more than five minutes has seen a swf lock up as the result of a function receiving too much, too little, or unexpected data. Luckily the compiler catches these bugs and won’t allow you to publish until they’re resolved. When that safety-net is removed, most methods dealing with external data will wrap the incoming content inside a wildcard object like LoaderInfo or URLLoader. However, the ExternalInterface breaks from the rest of the API again and removes that last line of defense. By adding an EI callback, you expose that function to anything JS can send without filter.

Rest and relay.
Since there’s nothing to safeguard our inbound data, we need a small demilitarized zone where we can figure out what parameters we’re dealing with and cast them to a data type that Flash won’t choke on. It’s an ideal job for the rest statement:

  1. ExternalInterface.addCallback('pieceOfAS', incomingJS)
  2.  
  3. private function incomingJS(...args):Boolean{
  4.  
  5. // lets say the real function requires an int and a string
  6. if(args.length < 2 || args.length > 2){ return false; }
  7.  
  8. try{
  9. // cast and validate the integer
  10. var i:int = int(args[0]);
  11. if(i != args[0]){
  12. trace('float received, expecting int');
  13. return false;
  14. }
  15.  
  16. // cast the string
  17. var s:String = String(args[1]);
  18. } catch (e:Error) {
  19. trace(e);
  20. return false;
  21. }
  22.  
  23. trace('no errors encountered');
  24. return true;
  25.  
  26. }

This is mainly a big system of rejecting any data that isn’t perfect. We start by counting the arguments passed (if there aren’t 2, we eject), then we hard-cast the values to the types we’re expecting to get and if all goes well, only then do we alert JS that we got the message and we’re going to start work on it. Unfortunately, this method of validation is pretty specific to whatever function you’re calling, but it’s still a lot better than getting an incorrect value and having your swf flip out without letting you know.

So we’ve got the right data and we’re ready to process it. The last hurdle is solved with a custom event to wrap that data and throw it outside of the currently-running function to be processed at the start of the next frame. Let’s throw that line in the bottom of the previous code:

  1. // ...
  2. trace('no errors encountered');
  3. dispatchEvent(new JSEvent('pieceOfAS', i, s));
  4. return true;
  5. // ...

And of course outside the incomingJS function we’ll need to attach a listener for that event.

  1. addEventListener('pieceOfAS', processJS);
  2.  
  3. private function processJS(e:JSEvent):void{
  4. // ...
  5. }

Done! To recap, JavaScript target’s our swf and calls pieceOfAS(123, "werd"), that translates directly into a call on incomingJS(), which makes sure we’ve gotten exactly two variables, one integer and one string. The incomingJS function then puts those two arguments into an event (here's a tutorial if you’ve never made custom events) and dispatches it, and then returns ‘true’ to JS – we now have both AS and JS running in tandem again. Lastly our eventListener catches the custom event and routes it into the processJS() function, who can now run all the heavy code it wants without bogging anything else down.

⇠ Unfucking the ExternalInterface (1 of 2)

or – Asynchronous Communication and the AS3 ExternalInterface

I just deleted four paragraphs of this post, since they could be summed up as “I hate ExternalInterface because it's slow,” which isn’t even an accurate grievance. ExternalInterface is pretty quick. It's the function’s being called by it that are slow.

My real frustration is that communication between Flash and JS is synchronous. That is, whenever Flash calls out to run a javascript function, all action inside Flash halts until the JS function is completed. This makes sense if you’re using getters and setters, but try anything bigger than that and you’ll start to see problems. At the very shallowest level, you’ll see your animation stutter every time you call out to a JS function that takes more than a couple milliseconds, at deeper levels you’ll enjoy long unexplained freezes and dropped calls.

Before the good stuff, a couple caveats: I’m not a JavaScript guy. I rely on two devs who work wonders with JS – whereas I curl up and stop functioning, the second I lose strict data-typing and custom classes. A lot of the credit for this method goes to them.

Second, the point of this workaround is to decouple AS and JS and let them run at the same time. Doing this sacrifices the return value you get if you wait for a function to complete before jumping back to the other side of the EI.

Step 1. Fix the outbound calls.
The good news first: JS is multithreaded. So if we can get our JS function to run in a different thread than the one targeted by Flash, we can get back to running ActionScript while JS worries about the real work. The secret (thanks to my JS guys!) is JavaScript's setTimeout() function. Here's how it works:

Normally to call a JS function you'd write something like this:

  1. const JS_FUNCTION:String = 'myJSFunction';
  2. const ARG:String = 'foo';
  3.  
  4. // myJSFunction('foo');
  5. ExternalInterface.call(JS_FUNCTION, ARG);

All we need to do is manually construct our JS function signature, and use that as setTimeout’s target:

  1. const JS_FUNCTION:String = 'myJSFunction';
  2. const ARG:String = 'foo';
  3. const JS_CALL:String = JS_FUNCTION + '("' + ARG + '")';
  4.  
  5. // setTimeout(myJSFunction('foo'), 0);
  6. ExternalInterface.call('setTimeout', JSCALL, 0);

What does this accomplish? In the first example Flash stops running until foo runs its course. In the second example, Flash is waiting on setTimeout to complete instead. JavaScript will automatically call foo in another thread and return control to Flash, which allows AS to start running again while the foo function can run slowly on the JS side without gumming things up. To see this in action try calling a large empty loop or an alert in JS while you've got some animation going inside Flash.

Next post: Inbound calls, custom events, and data validation, oh my!

continued: Unfucking the ExternalInterface (2 of 2) ⇢