XTech 2005: XML, the Web and beyond.

Extending Gecko through XTF and XBL

What is Gecko?

Sitting at the core of applications such as Firefox and Thunderbird is the Gecko layout engine. Gecko provides:

Extending Gecko

There is constant pressure to add new functionality to Gecko. This can be in the form of new standards support, or adding new functionality to an existing language (Canvas, for example). Since it is not desirable to have the layout engine grow immensely large, we have tried to adopt a compromise. New functionality is added to Gecko if it could not be efficiently implemented as an add-on, and there is a clear benefit to developers or end-users.

In practice, this means that there is a class of "fringe" technologies that are of interest to a very specialized audience, or are too new to establish themselves as ready to be part of the core engine. This does not apply only to working group standards; it could also include types of markup specific to a company or industry, or just new types of widgets that a site or extension author wants to use.

To address this need, we have two ways of extending the Gecko engine dynamically. XBL (XML Binding Language) is a mature technology which has been used to implement many features in the XUL language and application UI. It is particularly well-suited to implementing user-interface widgets. The second interface for extending the engine is called XTF, which stands for Extensible Tag Framework. This is a somewhat experimental technology that has been used quite successfully to implement XForms as a plug-in for Gecko. Unlike XBL, it is well-suited to C++-backed implementations, and can also deal with implementing non-UI XML elements.

Both of these mechanisms employ a concept known as "anonymous content". The idea is to implement the appearance of a new element by using the building blocks present in the layout engine. Because of this, elements implemented as add-ons must conform to some existing type of layout -- currently block, inline, box, or SVG. We believe that by providing a rich set of primitives in the layout engine, complex widgets can be designed without adding additional specialization to the core code.

XBL

XBL has a mostly-declarative syntax, and is attached to elements using CSS. An individual element implementation is called a binding, and resides in an XML bindings file that may contain a number of other bindings. The element that a binding is attached to is called the bound element.

Example of a bindings file:

<?xml version="1.0"?>

<bindings xmlns="http://www.mozilla.org/xbl"

         xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<binding id="mybinding">

<implementation>

     <constructor>...</constructor>

     <destructor>...</destructor>

 

    <method name="showProgressDetails">

       <body>

        ...

       </body>

     </method>

 

    <property name="active">

       <setter>...</setter>

       <getter>...</getter>

     </property>

   </implementation>

  <content>

    <xul:label inherits="value=labeltext"/>

     <xul:progressmeter/>

   </content>

   <handlers>

    <handler event="click" action="this.showProgressDetails();"/>

</handlers>

</binding>

</bindings>

 

There are three main sections to a binding. The first section is called the implementation. This defines all of the methods that can be called on the element. It also defines fields, which are simple "member variables", and properties, which are like properties but with author-defined getter and setter functions. The implementation also defines the constructor and destructor, which are special methods that are called when the binding is attached to or detached from an element.

The second section is called the content. The content section declares all of the anonymous content for the binding. When the binding is attached, the anonymous content is constructed and inserted into the DOM as anonymous children of the bound element. Because the children are anonymous, they do not appear in the childNodes of the bound element. XBL provides some special attributes and elements that may appear inside of <content>. The "inherits" attribute can be used to map attributes from the bound element onto attributes on anonymous children. Also, the special "children" element creates an "insertion point". This means that any non-anonymous children of the bound element are inserted in place of the <children> element.

The third section is the <handlers> section, which defines all of the events that the binding is interested in listening for. This is equivalent to calling addEventListener() for each of the event types listed. A <handler> can give an event type and a phase (capturing or bubbling), and script to execute when the event is triggered.

XBL is attached to an element using the CSS property -moz-binding:

myprogress { -moz-binding: url(mybindings.xml#mybinding); }

Because the binding file can be loaded from an arbitrary URI, web site authors can use XBL to implement new functionality without requiring a separate download. Scripts in the XBL binding will have the same security privileges as Javascript on the page.

XTF

Whereas XBL is a declarative approach to creating an element implementation, XTF is a more traditional API based on C++ abstract interfaces. To utilize XTF, a developer registers for the namespace(s) for which they want to handle element creation. Registering for a namespace causes the new "element factory" to be queried when an element in the given namespace is encountered. The element factory then returns an object implementing nsIXTFElement, if the element tag is recognized.

The object implementing nsIXTFElement receives notifications for DOM changes that affect the element, such as child insertion or removal and setting attributes. If the XTFElement has a "visual" type, it is also asked to provide a root element for the anonymous content tree. The anonymous content tree works as described for XBL, except that "inherits" and "children" are not available. There is a separate method that provides the insertion point element for the anonymous content tree. Unlike XBL, the XTF insertion point should generally be an element with no other children, since the ordering of explicit and insertion-point children is undefined.

XTF Example -- XForms

In order to demonstrate how to develop layout engine extensions, we will now take a closer look at the architecture of the XForms extension. Three options were considered for implementing XForms support in Gecko:

The first option brought about concerns over code size increases for little benefit to most users. Since it was also desirable to have XForms available in stock versions of Firefox (that is, not a compile-time option), we examined options for implementing it as an extension. Due to the relative comfort of the coders with C++ over Javascript, and the need to implement non-UI elements such as <model>, the decision was made to use XTF.

In addition, we found that nearly all of the XForms controls have analogs in HTML, and it would be very worthwhile to reuse the HTML versions of those controls. So, we proceeded using XTF to wrap HTML anonymous content.

Example: XTF IDL interfaces (abbreviated)

nsIXTFElement -- this is what you implement

interface nsIXTFElement : nsISupports

{

// Constants for attribute elementType:

 

// Elements of type GENERIC_ELEMENT are required to implement the

// nsIXTFGenericElement interface in addition to nsIXTFElement:

const unsigned long ELEMENT_TYPE_GENERIC_ELEMENT = 0;

 

.....

 

readonly attribute unsigned long elementType;

 

readonly attribute boolean isAttributeHandler;

 

void getScriptingInterfaces(out unsigned long count,

                          [array, size_is(count), retval] out nsIIDPtr array);

 

 

// Notification masks

const unsigned long NOTIFY_WILL_CHANGE_DOCUMENT = 0x00000001;

const unsigned long NOTIFY_DOCUMENT_CHANGED    = 0x00000002;

 

const unsigned long NOTIFY_WILL_CHANGE_PARENT = 0x00000004;

const unsigned long NOTIFY_PARENT_CHANGED    = 0x00000008;

 

....

 

// Event notifications:

 

void willChangeDocument(in nsIDOMDocument newDoc);

void documentChanged(in nsIDOMDocument newDoc);

 

void willChangeParent(in nsIDOMElement newParent);

void parentChanged(in nsIDOMElement newParent);

 

....

 

boolean handleDefault(in nsIDOMEvent aEvent);

 

....

};

 

interface nsIXTFGenericElement : nsIXTFElement

{

void onCreated(in nsIXTFGenericElmeentWrapper wrapper);

};

 

interface nsIXTFElementWrapper : nsISupports

{

readonly attribute nsIDOMElement elementNode;

readonly attribute nsIDOMElement documentFrameElement;

 

attribute unsigned long notificationMask;

};

 

interface nsIXTFGenericElementWrapper : nsIXTFElementWrapper

{

};

 

 

Example: XForms Model Element C++ (simplified)

 

class nsXFormsModelElement : public nsIXTFGenericElement, // xtf interface

                           public nsIXFormsModelElement // xforms scripting interface

{

public:

NS_DECL_ISUPPORTS

NS_DECL_NSIXTFELEMENT

NS_DECL_NSIXTFGENERICELEMENT

NS_DECL_NSIXFORMSMODELELEMENT

 

private:

nsIDOMElement *mElement;

};

 

NS_IMPL_ISUPPORTS3(nsXFormsModelElement, nsIXTFElement, nsIXTFGenericElement, nsIXFormsModelElement)

 

NS_IMETHODIMP

nsXFormsModelElement::OnCreated(nsIXTFGenericElementWrapper *aWrapper)

{

aWrapper->SetNotificationMask(nsIXTFElement::NOTIFY_WILL_CHANGE_DOCUMENT |

                      nsIXTFElement::NOTIFY_DOCUMENT_CHANGED |

                               nsIXTFElement::NOTIFY_DONE_ADDING_CHILDREN |

                               nsIXTFElement::NOTIFY_HANDLE_DEFAULT);

 

nsCOMPtr<nsIDOMElement> node;

aWrapper->GetElementNode(getter_AddRefs(node));

mElement = node;

 

return NS_OK;

}

 

NS_IMETHODIMP

nsXFormsModelElement::GetElementType(PRUint32 *aElementType)

{

*aElementType = nsIXTFElement::ELEMENT_TYPE_GENERIC_ELEMENT;

return NS_OK;

}

 

NS_IMETHODIMP

nsXFormsModelElement::WillChangeDocument(nsIDOMDocument *aNewDocument)

{

// disconnect model from the document and dispatch ModelDestruct event

return NS_OK;

}

 

NS_IMETHODMIP

nsXFormsModelElement::DocumentChanged(nsIDOMDocument *aNewDocument)

{

// add load listener to document

return NS_OK;

}

 

NS_IMETHODIMP

nsXFormsModelElement::HandleDefault(nsIDOMEvent *aEvent, PRBool *aHandled)

{

*aHandled = PR_TRUE;

 

nsAutoString type;

aEvent->GetType(type);

 

if (type.Equals("refresh")) {

  Refresh();

} else if (type.Equals("revalidate")) {

  .....

}

 

return NS_OK;

}

 

NS_IMETHODIMP

nsXFormsModelElement::GetScriptingInterfaces(PRUint32 *aCount, nsIID ***aArray)

{

// Set *aArray to a new array of nsIID pointers for each IID that should be

// accessible from script.

return NS_OK;

}

 

Example: XForms input element (simplified)

 

NS_IMETHODIMP

nsXFormsInputElement::OnCreated(nsIXTFXMLVisualWrapper *aWrapper)

{

...

 

nsCOMPtr<nsIDOMDocument> domDoc;

mElement->GetOwnerDocument(getter_AddRefs(domDoc));

 

domDoc->CreateElementNS(NS_LITERAL_STRING(NS_NAMESPACE_XHTML),

                         NS_LITERAL_STRING("input"),

                         getter_AddRefs(mInput));

 

...

 

return NS_OK;

}

 

NS_IMETHODIMP

nsXFormsInputElement::GetVisualContent(nsIDOMElement **aElement)

{

NS_ADDREF(*aElement = mInput);

return NS_OK;

}

Limitations

While XTF or XBL implementations can achieve impressive results using the built-in layout primitives, there are still cases where they are not sufficient. For example, SVG or MathML could not be implemented as extensions because they affect the flow of the document at a basic level that cannot be expressed using existing rendering.

Because XBL bindings are attached asynchronously, it is possible for the bound elements to be referenced by script before the binding is attached. This makes widget initialization harder than it should be. Also, because the bindings are attached via CSS, it is possible for a binding to change dynamically when the style changes. This is especially problematic for bindings that implement scripting interfaces, since the element may no longer support the interface that it previously did.

There is currently no way to implement an XTF extension without requiring the user to download and install it. This restricts its usefulness to plug-ins such as XForms, as opposed to per-site extensions to Gecko's XML capabilities.

Future plans

Moving forward, we are planning to provide a single API which can do everything that XBL and XTF can do. "XBL 2" will allow developers to provide C++ implementations instead of Javascript for some or all of the methods declared. In addition, it will have a mechanism for binding based on namespace and tag, instead of through CSS.