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:
- Ensure that you generate typelibs for all of your COM interfaces;
- Ensure that those interfaces are registered;
- 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 interfaceIFoo
inherits fromIUnknown
, the lowest possiblemMethodIndex
forIFoo
could be 3, because indices 0, 1, and 2 are occupied by the functions thatIFoo
inherited fromIUnknown
.mArrayParamIndex
is the index of the parameter that contains the array (excluding thethis
pointer).mArrayParamType
is the expectedVARIANT
type of the outparam. This should usually be set toVT_UNKNOWN | VT_BYREF
, which is telling the interceptor that the outparam is anIUnknown**
.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 thethis
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
orArrayData::Flag::eAllocatedByServer
. SettingeAllocatedByServer
signals to the interceptor that the array is allocated within the method's implementation by callingCoTaskMemAlloc()
.
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:
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 IAccessible
s 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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论