Multiprocess on Windows

Overview

A high-level overview of the ideas behind the a11y+e10s design is available on the wiki.

Prerequisite reading

Since so much of this design resolves around Microsoft COM and its concept of the apartment, readers of this document should have a solid understanding of what apartments are. Unfortunately this topic is often poorly explained. One of the better pieces of documentation is a two-part series written by Jeff Prosise:

For the purposes of this document, "COM" will refer to Microsoft COM (as opposed to XPCOM).

Gecko and apartments

Most code that runs on Gecko's main thread is not thread safe. Since Gecko's main thread uses COM, and COM requires threads to declare their threading model, the main thread must initialize itself to live inside its own single threaded apartment (STA). This is true for both chrome and content processes.

As you should already know from the prerequisite reading, single threaded apartments receive remote procedure calls (RPCs) from COM, via the Windows message queue. From a performance standpoint, this is less than ideal. The Windows message queue carries a lot of baggage for the purposes of maintaining backward compatibility. On the other hand, COM's multithreaded apartment (MTA) uses a much faster IPC mechanism that does not suffer from the same problems as the message queue. Ideally, we would like to receive RPCs via threads in the MTA, and then forward them in a thread-safe way to Gecko's main thread. Unfortunately, crossing apartment boundaries using COM incurs the exact same problem as crossing process boundaries: if we directly use COM's built in marshaling capabilities to forward an RPC from the MTA to the main thread STA, COM will still use the STA's message queue, thus defeating the purpose of using the MTA in the first place!

Enter the interceptor

To achieve the best of both worlds, we wrote our own code to facilitate the safe handing off of an inbound RPC from the content process's MTA to the content main thread's STA. This uses a COM technology called the interceptor. Interceptors are, essentially, wrapper objects which implement the same interfaces as the object that they are wrapping. When an incoming RPC invokes a method on an interceptor, the interceptor dispatches a callback that allows us to do whatever we want with that request. Our callback implementation forwards the method call to the main thread in order to invoke the method on the wrapped object. The interceptor is also aware that any outparams, which contain interfaces, must also be wrapped with interceptors of their own.

This code lives in the mscom library, located in the tree at /ipc/mscom. Its headers are exported to mozilla/mscom.

Ensuring that the interceptor supports your interfaces

This information is current, but is likely to change when bug 1346957 is landed.

Interceptors cannot replicate interfaces without access to the metadata that describes those interfaces. Since we are using COM, we already generate metadata by declaring interfaces using midl. This does not (yet) work as transparently as we would like.

COM metadata

midl outputs two different types of metadata: "fast format strings" (also known as OICF) and (optionally, if a library statement is included in the IDL) type libraries (also known as typelib). OICF metadata is much richer than typelib metadata, however COM requires typelibs whenever it is being used with interpreted languages. In particular, typelibs were originally designed to work with 1990s-era Visual Basic. Typelib metadata is limited to supporting the same language features which were supported by VB at that time.

Given those two options, it may seem that OICF is the preferred format. However, for various reasons, our COM interceptor currently only supports typelib metadata. In order for the interceptor to be able to work with our interfaces, we must do some additional work to compensate for typelib's weaknesses. The required steps are as follows:

  1. Ensure that you generate typelibs for all of your COM interfaces;
  2. Ensure that those interfaces are registered;
  3. Register any outparams that consist of arrays of interfaces.

We will now further detail each of these items:

Generating typelibs for all COM interfaces

You first need to include a library statement in your IDL. When midl processes your IDL, it generates C code for building a proxy DLL, containing the OICF metadata. It also will output a file with the .tlb extension. This file contains the typelib metadata. Both metadata types must be shipped with Firefox. The OICF metadata is shipped via the proxy DLL. To include the typelib metadata, embed it in the proxy DLL's resources. Both types of metadata are then made available from within the same DLL. This is achieved by specifying RCINCLUDE in the DLL's moz.build, and then specifying the resources in an .rc file. You should always embed your typelib with its resource ID set to 1.

We embed typelib metadata in AccessibleMarshal.dll and IA2Marshal.dll. Use their moz.build and .rc files as examples.

Ensuring typelib registration

For interceptors to be able to use the typelibs, they must be registered. Any typelibs that are already registered in the system registry will automatically be available for use by the interceptor. This is great for system interfaces, but for interfaces that are specific to Gecko, it is better to register them at runtime. The mscom library provides an API for doing this: the RegisterProxy and RegisterTypelib functions in mozilla/mscom/Registration.h. Both of these functions return unique pointers and should be treated as opaque by the caller. Save them to a location where they will be able to exist for the lifetime of the process.

If your typelib is embedded in a proxy's resources, then use RegisterProxy(). This is the preferred mechanism, as it will register both your OICF and typelib metadata. If you have a standalone .tlb file that you need to register, then use RegisterTypelib().

Note: you should register your typelibs and proxies in both the chrome and content processes. If you only register your interfaces in one process, COM won't be able to understand the interface in the other process.

Registering outparams that consist of arrays of interfaces

Recall that one of the limitations of typelibs is that their metadata isn't as rich as OICF metadata. Something typelibs do not include in their metadata, is IDL annotations, such as length_is and size_is. Remember, for interceptors to work correctly, they must be able to wrap any outparams that are interfaces with their own interceptors. Without the support for length_is and size_is annotations, the interceptor cannot tell the difference between a scalar outparam and an array outparam. If your interface outputs an array of interfaces, the interceptor would only wrap the first element in the array, because it cannot distinguish between scalars and arrays.

Note: If your new interface does not contain either of these annotations, then you do not need to worry about this step.

For those interfaces that do contain length_is or size_is annotations, we need to use another API declared in mozilla/mscom/Registration.h: RegisterArrayData().

RegisterArrayData() accepts an array of ArrayData structs. You should statically define this array in your code, as const. For example:

static const mozilla::mscom::ArrayData kMyArrayData[] {
  { // First ArrayData definition },
  { // Second ArrayData definition }
};

mozilla::mscom::RegisterArrayData(kMyArrayData);

Each ArrayData struct corresponds to one function; with length_is and/or size_is annotations. If your interface contains two functions with those annotations, then you will need to add two ArrayData entries.

ArrayData contains seven fields:

IID     mIid;
ULONG   mMethodIndex;
ULONG   mArrayParamIndex;
VARTYPE mArrayParamType;
IID     mArrayParamIid;
ULONG   mLengthParamIndex;
Flag    mFlag;
  • mIid is the UUID of the interface owning the function.
  • mMethodIndex is the vtable index of the function within its interface. This index must take into account the vtable(s) of parent interfaces. For example, if interface IFoo inherits from IUnknown, the lowest possible mMethodIndex for IFoo could be 3, because indices 0, 1, and 2 are occupied by the functions that IFoo inherited from IUnknown.
  • mArrayParamIndex is the index of the parameter that contains the array (excluding the this pointer).
  • mArrayParamType is the expected VARIANT type of the outparam. This should usually be set to VT_UNKNOWN | VT_BYREF, which is telling the interceptor that the outparam is an IUnknown**.
  • mArrayParamIid is the UUID of the interface that the outparam array uses. This should be the IID that we want the interceptor to actually use when wrapping the array element.
  • mLengthParamIndex is the index of the out parameter that contains the length of the array (excluding the this pointer). The interceptor needs to be able to read this parameter to know how many interface pointers exist in the array.
  • mFlag may currently be set to one of two values: ArrayData::Flag::eNone or ArrayData::Flag::eAllocatedByServer. Setting eAllocatedByServer signals to the interceptor that the array is allocated within the method's implementation by calling CoTaskMemAlloc().

Using the interceptor in a11y code

Unlike the built in COM marshaling schemes, we must explicitly wrap any COM objects that we want to expose, through the MTA with interceptors. The function call to wrap a COM object is mozilla::mscom::MainThreadHandoff::WrapInterface(). This receives the COM interface which you want to wrap as its first parameter, and outputs the wrapped object (with the same interface) as its second parameter.

A word about smart pointers

As long as you're manipulating an object in its home apartment, it is okay to just use RefPtr. On the other hand, some APIs in the mscom library require you to use smart pointers that are able to cross apartment boundaries. As you should already know, you can't directly touch a COM object's reference count unless you're already inside the apartment which contains that object. This is a problem for smart pointers that are not apartment aware; they will try to AddRef() and Release() on whichever thread they happen to be running. The mscom library provides a set of smart pointers that are aware of COM apartments:

mscom smart pointer types
Pointer type Release semantics
STAUniquePtr<T> Forces reference to be released on the main thread.
MTAUniquePtr<T> Forces reference to be released on an MTA thread.
ProxyUniquePtr<T>

In the chrome process, forces reference to be released on the main thread.

In the content process, forces reference to be released on an MTA thread.

InterceptorTargetPtr<T> No-op deleter: used to annotate pointers whose reference counts must never be touched.

These pointers all use mozilla::UniquePtr under the hood. Use the mozilla::mscom::To*UniquePtr functions in ipc/mscom/Ptr.h to create new instances of these. There is also a mozilla::mscom::getter_AddRefs() function that allows these pointers to receive outparams.

Simple Example

Now you know about MainThreadHandoff::WrapInterface and about the smart pointers, you're ready to wrap an interface:

Accessible* myAccessible = ....;

mozilla::mscom::STAUniquePtr<IAccessible> accToWrap;
myAccessible->GetNativeInterface(mozilla::mscom::getter_AddRefs(accToWrap));

mozilla::mscom::ProxyUniquePtr<IAccessible> wrapped;
HRESULT hr = mozilla::mscom::MainThreadHandoff::WrapInterface(mozilla::Move(accToWrap),
                                                              mozilla::mscom::getter_AddRefs(wrapped));
if (FAILED(hr)) {
  // Handle your error here
}

// The wrapped interface should be given to the AT from within the MTA.

Given the generic accessible myAccessible, we first obtain the native (i.e. COM) interface for that accessible, and store it in accToWrap. We use STAUniquePtr, because MainThreadHandoff::WrapInterface requires one in its input.

Integrating interceptors into the a11y tree

Now that we have solved the apartment problem, and know how to wrap a COM interface with an interceptor, we need to discuss how these things fit into the a11y tree.

In general, the only COM objects which should need to be explicitly wrapped are the IAccessibles for top-level documents in the content process. As mentioned previously, the interceptor will automatically wrap outparams. When a wrapped COM object is queried for its children, those children are provided as outparams, and will themselves be wrapped.