Wrapping native JavaScript libraries in ActionScript

This post is about the second SWC of my “dreamed up” Flex SDK for JavaScript, browser.swc. In a nutshell, I want to put the main framework class, core JavaScript library class wrapper, and browser DOM API wrapper into browser.swc.

 

Framework class

When you start building a Flex SDK in ActionScript you will quickly realize that there are a bunch of utility functions that you will use over and over again. I have already shown you a few in this series about cross-compiling ActionScript to JavaScript:

  • as3.getProperty/setProperty/callProperty
  • as3.isProxy
  • as3.instanceOf
  • as3.addToUint
  • as3.vectorFilter
  • as3.implements

You might now recall that I put all of those utility functions into the as3 namespace. Instead of “as3″ I am using “adobe” as my main framework class, which also contains those low-level utility functions. I have only one rule for the main framework class: all members and methods have to be static. But you can tell FalconJS to tell which framework class should be used.

My adobe framework class is not tied to a specific core JavaScript library like jQuery or Google Closure Library. Instead it uses a static member that points to an IFramework interface:

public static var m_framework : IFramework = null;

IFramework is my attempt to create an abstraction layer that works for most core JavaScript libraries.

 

IFramework

This is roughly in IFramework:

package browser
{
    import org.w3c.dom.Element;
    public interface IFramework
    {
       function isFunction( obj:Object ) : Boolean;
       function isArray( val: * ) : Boolean;
       function bind( fn:Function, selfObj:Object ) : Function;
       function exportSymbol(publicPath:String, 
            object : *, opt_objectToExportTo : Object = null ) : void;
       // Style interface
       function setStyle( element : Element, name : String, value : String ) : void;
       function getStyle( element : Element, name : String) : String;
       // Event interface
       function createEvent( type : String ) : Object;
       function listenToObject(src:Element, type:String, listener:Function,
               opt_capt:Boolean = false,
               opt_handler : Object = null ) : uint;
       function unlistenToObject(src:Element, type:String, listener:Function,
               opt_capt:Boolean = false,
               opt_handler : Object = null ) : Boolean;
       function dispatchEvent(eventTarget:Element, e:IEvent) : Boolean;
   }
}

Most core JavaScript libraries come with a bunch of utility functions for compensating browser quirks, binding functions to instances, and event handling. For browser.swc I implemented JQueryFramework and ClosureFramework based on the IFramework interface above and that has worked fine for me.

I will explain later what org.w3c.dom.Element is all about.

 

Core JavaScript Library Classes

JQueryFramework implements IFramework but it uses jQuery functions. This is all done in ActionScript:

import com.jquery.jQuery;
public class JQueryFramework implements IFramework
{
   public function isFunction(obj:Object):Boolean
   {
      return jQuery.isFunction(obj);
   }
   ...
}

Okay, the next question is: What’s in com.jquery.jQuery?

package com.jquery
{
   public class jQuery implements IExtern
   {
      // http://api.jquery.com/jQuery.isFunction
      public static native function isFunction( obj:Object ) : Boolean;
   }
}

IExtern is something I came up in order to let FalconJS know that an interface, or class is representing a JavaScript implementation. I could have used metadata tags but at that time when I wrote those wrappers FalconJS was not able to support metadata tags. Below I will show you a cleaner declaration of external classes.

As you can see I use the same trick as Tamarin does for declaring atomic classes and functions. Declaring isFunction() as a native function makes sure that FalconJS’s MXMLC (the JavaScript application compiler) will assume that the host environment (in our case the browser) will provide those “native” implementations. That’s exactly what we want to do with all native JavaScript functions!

Let’s have a quick look at the emitted JavaScript for JQueryFramework.isFunction:

JQueryFramework.prototype.isFunction = function(obj)
{
   return jQuery.isFunction(obj); // not com.jquery.jQuery.isFunction(obj);
}

Wait a second, shouldn’t that be “return com.jquery.jQuery.isFunction(obj);”, because isFunction is a static method?

This is a little bit iffy, but because jQuery implements IExtern FalconJS knows that there is no real com.jquery package. This assumption works for most of the JavaScript libraries I know. A much cleaner solution would use metadata tags, perhaps like this:

package com.jquery
{
 [Extern(name="jQuery")]
   public class jQuery
   {
      // http://api.jquery.com/jQuery.isFunction
      public static native function isFunction( obj:Object ) : Boolean;
      ...
   }
}

Switching over from IExtern to Extern metadata tag is one of the clean up tasks I have on my list for FalconJS.

Alternatively I could throw everything into the default package namespace just like the browser does. But I like using ActionScript’s package names. That way I can keep my projects better organized.

 

Browser DOM API

The jQuery example above used the “native” keyword for declaring functions that are part of external code that we expect the host environment to provide at runtime. There is a second way of declaring external functions that I prefer. In FlashRT most of the browser DOM APIs are defined as interfaces. This works pretty well, because in the browser you can get to all APIs through one root object called DOMWindow. For example from DOMWindow you can get the Document and from Document you can get or create other DOM elements etc.

In my adobe framework class I added a static variable pointing to a DOMWindow:

import org.w3c.dom.DOMWindow;
... 
public static var globals : DOMWindow = null;

The startup code will set DOMWindow, which you can use to retrieve other browser DOM interfaces:

// ActionScript
package org.w3c.dom
{
    // map org.w3c.dom.DOMWindow to DOMWindow
    [Extern(name="DOMWindow")]
    public interface DOMWindow
    {
        ...
        function get document() : Document;
        function get console() : Console;
        function get XMLHttpRequest() : Class;
        function get navigator() : Navigator;
        function setTimeout(closure:Function, delay:uint, ...args) : uint;
        ...
    }
}

In the code snippet above I am only showing a small section of what is in DOMWindow. For a more complete list please see Mozilla’s documentation, or this neat DOM Reference Manual.  I don’t know why, but the W3C specs don’t seem to define DOMWindow. Other interfaces like Document are described in their own interface description language (IDL):

interface Document : Node {
  readonly attribute DocumentType     doctype;
  readonly attribute DOMImplementation  implementation;
  readonly attribute Element          documentElement;
  Element            createElement(in DOMString tagName)
                                        raises(DOMException);
  DocumentFragment   createDocumentFragment();
  Text               createTextNode(in DOMString data);
  Comment            createComment(in DOMString data);
  CDATASection       createCDATASection(in DOMString data)
                                        raises(DOMException);
  ProcessingInstruction createProcessingInstruction(in DOMString target,
                                                    in DOMString data)
                                        raises(DOMException);
  Attr               createAttribute(in DOMString name)
                                        raises(DOMException);
  EntityReference    createEntityReference(in DOMString name)
                                        raises(DOMException);
  NodeList           getElementsByTagName(in DOMString tagname);
};

It’s tedious but pretty easy to create corresponding ActionScript interfaces for those IDL snippets manually. Alternatively you could write a custom IDL compiler (like Google’s Dart team seems to use). But I don’t find it necessary to develop an IDL compiler for W3C specs. I would change my mind if W3C started pumping out 300 important specs a year that I have to write wrappers for.

 

Without a trace

Let me show you how all those pieces come together in this implementation of trace(), which is also in browser.swc:

     /**
     * Displays expressions, or writes to log files, while debugging. A single trace
     * statement can support multiple arguments. If any argument in a trace statement
     * includes a data type other than a String, the trace function invokes the associated
     * <code>toString()</code> method for that data type. For example, if the argument is
     * a Boolean value the trace function invokes
     * <code>Boolean.toString()</code> and displays the return value.
     * @param arguments One or more (comma separated) expressions to evaluate.
     *                  For multiple expressions, a space is inserted between each
     *                  expression in the output.
     * @playerversion Flash 9
     * @langversion 3.0
     * @includeExample examples\TraceExample.as -noswf
     * @playerversion Lite 4
     */
package
{
    import adobe;
    import org.w3c.dom.DOMWindow;
    import org.w3c.dom.Console;
     [DebugOnly]
     public function trace(...arguments) : void
     {
         const domWindow : DOMWindow = adobe.globals;
         const console : Console = domWindow.console;
         if( console != null )
         {
             var s : String = "";
             for(var i:uint =0; i < arguments.length; i++)
                 s += arguments[i];
             if( s.length > 0 )
                 console.info( s );
         }
     }
}

The [DebugOnly] metadata tag tells your cross-compiler that this function and any calls to it can be stripped out in release builds.

 

Who is in and who is not?

Here are a few files you would find in my browser.swc:

  • adobe.as – main framework class.
  • goog.as – root class with static native methods wrapping base.js functions.
  • trace.as – package function that calls adobe.globals.console.info() .
  • browser/IFramework.as – interface used by adobe framework.
  • browser/JQueryFramework.as – implements IFramework in terms of jQuery.
  • browser/ClosureFramework.as – implements IFramework in terms of Google’s base.js.
  • com/jquery/$.as – wrapper for jQuery’s $ function.
  • com/jquery/Event.as – wrapper for jQuery’s Event object.
  • com/jquery/fn.as – wrapper for jQuery.fn.
  • com/jquery/jQuery.as – wrapper for jQuery’s root object.
  • goog/* – wrapper for Google’s Closure Library.
  • org/w3c/dom/* – wrapper for the browser DOM API.

You could argue that the wrappers for jQuery, Google Closure Library, and perhaps even org.w3c.dom should go into their own separate SWCs. I wouldn’t disagree with you. The separation into browserglobal.swc, browser.swc, and flash.swc I have been describing so far just turned out to be most practical one for me. I did try to merge browserglobal.swc with browser.swc. But as mentioned in my previous post I have not been successful so far.

 

Wrapping everything up

browser.swc contains the main framework class, core JavaScript library class wrappers, and browser DOM API wrappers.

When creating wrapper classes and interfaces for JavaScript libraries you can either use the “native” keyword or interfaces. I prefer interfaces over classes with “native” functions. But sometimes “native” functions are unavoidable, because ActionScript interfaces don’t support static functions.

I also recommend taking advantage of ActionScript’s package namespace feature. This requires you to do a little extra work, because browsers usually declare all classes in the default package (i.e. Document instead of org.w3c.dom.Document).

It turns out that it is beneficial to add extra information to wrapper classes and interfaces for JavaScript libraries that indicate that those are implemented by external code. I propose adding Extern metadata tags to those wrapper classes and interfaces, which could also be used to map package names to names used by the host environment (i.e. [Extern(name="jQuery")] would map the native class “com.jquery.jQuery” to “jQuery”).