|
|
Extending Mozilla
Warren Harris
Client Engineering
Netscape Communications Corp.
Contents
Introduction
The Netscape Communicator source release, "Mozilla," opens up a whole new
world of possibilities for feature integration. The entire architecture
and operation of Mozilla is exposed for your use -- everything from making
the simplest bug fix, to complete integration of your application. This
document suggests strategies for how to best go about integrating new code
with Mozilla, in a way that is compatible with the Netscape
Public License.
As a developer, you may choose one of several strategies for packaging
and distributing your Mozilla extensions:
-
Hack on Mozilla sources until you've developed a "frankenbrowser" that
embodies the best of Mozilla and your application. Then distribute the
result to your customers for their enjoyment.
There are several downsides to this approach:
-
you have to maintain and support your hacked version which may be a significant
cost,
-
your customers have to deal with your hacked version of the browser which
may lack other features they require from other similarly hacked browsers,
-
your code may become available to others,
-
you may have to reapply and/or redesign your changes for future Mozilla
versions,
-
you have to have your own distribution channel.
-
Same as the previous approach, but you request that your application be
integrated as part of the baseline Mozilla release.
This approach would have the advantage of getting your changes distributed
widely, and may be best if your primary added value is not in the client,
but in a server product that these changes interact with. But you have
to be convincing that your client code has wide appeal, and that these
changes should become part of all future releases. This may be a hard sell
if your application satisfies a particular niche market, but is not something
needed by all users.
-
Hack on Mozilla to add the minimal set of hooks you need to support the
rest of your application. Then develop and distribute the rest of your
application as a "super plugin."
This approach has numerous advantages:
- you only have to provide hooks, not your whole application, so your
code remains unavailable to competitors,
-
since you've make minimal changes to Mozilla, there's a much greater likelihood
that mozilla.org will understand your changes and be able to make them
part of the baseline for future releases,
-
your hooks make be useful to other "super plugin" developers, so again
there's a much greater likelihood that they will be implemented in the
baseline,
-
you don't have to maintain and support the whole browser, just your plugin,
-
since your application is a plugin, you can distribute it over the Web.
The official Mozilla release (with your hooks) is your distribution channel.
Which of these two methods do we recommend? The last one, of course. If
you agree, continue reading and we'll give you the gory details on how
best to go about adding hooks.
"Hey, wait a minute! I started reading this to find out about how to
extend Mozilla, not write some stupid plugin!" you calmly point out. "What
are you trying to pull...." Well, although this paper focuses primarily
on the Plugin API, you have to see the big picture. The Plugin API is a
model for how all extensions to Mozilla should be designed and implemented.
Although plugins to date have been concerned with extensions controlled
by the OBJECT tag, as you'll see, the techniques described here are generally
applicable. Over time, plugins can take on an increasing range of functionality.
One can easily imagine extending them to include networking protocol plugins,
html extensions, or even replacements entire browser subsystems like bookmarks,
or mail/news handling. So when we say plugins here, think big.
The New Plugin API
Starting with the Mozilla source release, there's a new modernized version
of the Plugin API. We made the effort to restructure the Plugin API to
facilitate extensibility. That way you can add your hooks for your new
super plugin with much less effort than the old Plugin API. Some of the
disadvantages of the old API were:
-
The single table of browser entry points did not lend itself to being extended
in multiple ways. Consider the case where you submit your Mozilla hooks
to Netscape, and so do 50 other developers. What order do all these new
methods get inserted in? What happens if one of them need to be changed
slightly in a future release?
-
Glue files needed to be linked with plugin DLLs. This mechanism is brittle
and makes it hard for us to provide new additions to the Plugin API without
breaking existing plugins.
-
The old Plugin API version numbering system didn't lend itself to multiple
extensions. Consider the case where your plugin needs extension A and B
to run, but someone else's plugin prefers A and C, but can actually run
with just C. How do you build a plugin that maintains backward compatibility
in that sort of environment?
-
The old Plugin API was well overdue for modernization and basic object-oriented
(re)design.
With the new Plugin API all these problems are solved. We've provided a
C++ API to plugins that eliminates the need for plugin-side glue files,
and deals with the multi-dimensional versioning issues by using a cross-platform
component architecture based on a subset of COM (the Component Object Model).
COM -- Isn't that a scary Microsoft thing?
No on both counts, actually. COM has its origins in Apollo's NCS system,
and was later developed by Digital and Microsoft as part of their OLE/ActiveX
architecture. We use a very small subset of COM that we call "XPCOM" ("XP"
stands for cross-platform) for the new Plugin API that is essentially a
method of using abstract base classes, and 3 simple but powerful methods:
-
QueryInterface: Deals with interface versioning by using universally unique
interface IDs.
-
AddRef and Release: Deal with reference counting the object so that the
system knows when it can deallocate it.
That's it. Using XPCOM in the plugin context is really quite simple. We
don't make use of any of the COM factory constructor mechanisms, object
aggregation, OLE interfaces, or the registry to find and load components.
Plugin DLLs are still searched for and loaded by Mozilla from the plugin
directories where they've always been found.
But what about backward compatibility?
For backward compatibility, Mozilla will still continue to load and run
plugins using the old Plugin API. So consider the old Plugin API as deprecated,
but understand that it will continue to be supported by Netscape until
it is no longer needed. All new plugins should be written using the new
Plugin API, and most importantly, all extensions made to Mozilla must be
done by extending the new C++ interfaces.
It's also possible to develop a plugin that uses the new Plugin APIs
but also will run in older versions of Communicator (and Navigator). A
new version of the old plugin glue files will be provided that maps from
the old plugin entry points to their new C++ equivalents, so you can take
advantage of the new C++ architecture for any plugins you develop.
Plugin Architecture
This section assumes that you're somewhat familiar with the old plugin
architecture. If not, see The
Plug-In Guide distributed with the LiveConnect/Plug-In SDK.
The new plugin architecture breaks up the old monolithic interface into
several abstract base classes. All "NPP" functions (those implemented by
the plugin developer) become methods on the NPIPlugin, NPIPluginInstance
and NPIPluginStream classes. All "NPN" functions (those provided by Mozilla)
become methods on the NPIPluginManager, NPIPluginManagerStream, NPIPluginInstancePeer
and NPIPluginStreamPeer classes. (The "NP" prefix stands for "Netscape
Plugin" and the "I" denotes that this class is an abstract interface.)
Functions pertaining to LiveConnect functionality are provided as subclasses
of these basic classes and will be discussed later in the document.
When a plugin is loaded by the browser, an instance of NPIPluginManager
is passed to the plugin's initial entry point, NP_CreatePlugin (this corresponds
to the old Plugin API's NPP_Initialize function). This function is responsible
for instantiating a subclass of NPIPlugin and returning it to the caller,
i.e. Mozilla. Mozilla hangs on to this plugin object and uses it to obtain
general information about the plugin, e.g. the MIME type that the plugin
handles, or its name and description strings to be displayed in the "About
Plugins" page (note that on some platform these strings may come from from
resources associated with the plugin DLL).
When an EMBED or OBJECT tag appears on the page, the NewInstance method
is called on the plugin object. It is the responsibility of the NewInstance
method to instantiate a subclass of NPIPluginInstance. The NewInstance
method is passed an instance of NPIPluginInstancePeer which the newly created
plugin instance may use to query particular aspects of the plugin's relationship
to the page -- the HTML tag attributes, or the size and location on the
page. Typically the plugin instance stores the plugin instance peer in
an instance variable for use by other methods later.
After the plugin instance is created, a data stream may be provided
to it by calling the NewStream method on the plugin instance. This will
happen if the OBJECT tag has a DATA attribute (or the EMBED tag has a SRC
attribute), or if the plugin instance explicitly requests a stream by calling
GetURL or PostURL on the plugin instance peer. When NewStream is called
it is passed an instance of NPIPluginStreamPeer which is used to obtain
subsequent information about the stream, or to control it. NewStream constructs
an appropriate instance of NPIPluginStream and passes it back to the browser.
The browser then calls the WriteReady and Write operations on the plugin
stream to pass data to the plugin.
Suspending plugins
One substantial difference between the old Plugin API and the new C++ version
is the means by which plugin instances are suspended when you leave the
browser page containing them. With the old plugin API, the plugin's NPP_Destroy
operation was called which was responsible for saving the state of the
plugin in a flat buffer. This buffer was saved in the browser's history
mechanism, and if the page containing the plugin was ever revisited, the
buffer would be passed to NPP_NewInstance so that a new instance could
be created echoing the previously saved state. And if the page containing
the plugin was ever discarded from the history, the browser would simply
call free on the buffer. Although this method of saving the plugin's state
worked, it required a lot of work on the part of the plugin to save and
restore itself.
With the new C++ Plugin API, a plugin instance will be sent a Stop message
which informs it that it is being suspended. The plugin can either do a
lot of work to suspend itself as before, or can simply stop operation and
wait for a corresponding Start method to indicate that the plugin's page
has again become active. Both Stop and Start will be followed by a SetWindow
call indicating that the plugin's parent window has been changed. Finally,
when a plugin's page leaves the history, the Release method will be called
on the plugin instance, indicating that it should free up its state and
destroy itself. This new Start/Stop/Release mechanism greatly simplifies
the amount of effort needed to write a plugin.
Extending the Plugin API
Hopefully the Plugin API will allow you to write a wide range of extensions
for an off-the-shelf Mozilla without requiring additional access to browser's
internals. But if you've always wished you could gain access to the XYZ
feature from your plugin, here's your chance -- and some advice on how
to do it.
The Plugin API and libraries
First, it's really important to distinguish between the need to access
the internal state of the browser (like information pertaining to the layout
of the plugin on the page) and the need to access functions that could
very well be packaged as a library or DLL. As you explore the Mozilla source
code, you'll find that some of the things that were exposed through the
Plugin API are there because of the way the code evolved. Things that could
have been packaged as a library (like perhaps networking) are instead calls
into the browser through the Plugin API. Newer features like NSPR (Netscape
Portable Runtime for threads, synchronization and I/O) are packaged as
reusable DLLs (dynamically linked libraries) that can be linked with plugins
directly. As Mozilla continues to evolve we intend to make more and more
things available as DLLs and reusable libraries (for example, access to
our imaging code).
As you're sitting down with the morass of Mozilla code thinking about
how to expose that feature you really need, consider making the effort
to pull subsystems into reusable DLLs. Talk with mozilla.org to hook you
up with the original Netscape developers of a piece of code to determine
whether a piece of code is in the process of being pulled out into a DLL,
or whether doing so would benefit not only yourself, but the community
at large. Packaging things as DLLs is by far the most preferable way to
go, providing a nice package that can be used by not only your plugin and
the browser, but by future applications as well.
Be a good citizen
If you decide that you need access to the running browser, and not just
a shared library of functionality, then there are some rules that we ask
you to observe to make sure your extensions are a good citizen with respect
to the rest of source code and our plans for how we'd like to see Mozilla
evolve. First if you extend or change an API we ask that you observe the
following guidelines:
-
Talk about your ideas and proposal for changes with the Mozilla community.
There's no substitute for feedback before embarking on a lengthy development
process. Many developments begin as hacks just to get things going ("I
don't need to create an API... I just need to grab this private field so
that I can get my demo running..."), but as we all know every little feature
has a way of taking on a life of its own -- needing one more thing, or
a bug fix, or works for this, but isn't general for that, or needs documentation,
etc.
Take the time to talk to the community before just hacking something
together. You may get some good advice, or find out that someone has already
done what you're trying to do. Check out the mozilla.org
newsgroups.
-
Try to find existing APIs that accomplish what you want. If you find something
at almost does what you want, think about how to extend it.
-
Use XPCOM to package any new APIs. C++ virtual methods and QueryInterface
are a powerful combination for packaging functionality for a number of
reasons:
-
Packaging functionality as virtual methods allows clients to be compiled
independently of the implementation of a module. This localizes changes,
and minimizes the things that have to get recompiled.
-
The level of indirection inherent with virtual methods allows modules to
be dynamically loaded and unloaded. This goes well beyond basic DLLs which
need to be loaded at startup or first access, and can greatly reduce the
footprint of an application. A user pays for what they use, instead of
everything that the developers could think of throwing in.
-
QueryInterface allows programs to make runtime decisions about the presence
or absence of modules. If an interface can be accessed, the caller can
go ahead and use it -- if not, alternatives can be tried. QueryInterface
allows applications to degrade gracefully.
-
XPCOM is simple to use, very low overhead, and works on all platforms.
That means that you don't have to resort to different mechanisms on each
platform you intend to support.
-
Be sure to create new interface IDs whenever you change or extend an interface.
In XPCOM, interface IDs are used to denote particular interfaces, and
carry with them assumptions about what methods are present, and what arguments
they take. Programs desiring to use a particular interface use an interface
ID to ask the question "do you support this interface?" If so, know that
they can cast the result to a particular C++ base class and call its virtual
methods.
Since interface IDs are strongly tied to C++ classes, it's important
to change the interface ID whenever the class changes. If a class gets
changed in a backward compatible way, it can continue to support both interface
IDs (or any number of them), and savvy clients can use the newer interface
IDs to know its safe to call the newer methods.
-
Observe the rules about reference counting objects so that you don't introduce
memory leaks.
-
Observe the cross-platform coding conventions described in the C++
Portability Guide.
-
Think hard about the best way to break up your new functionality into separate
pieces. Ask yourself the following questions:
-
Does all the new stuff I'm developing belong together in one package, or
does it really belong in separate modules, or as part of existing modules?
(Don't toss things all together just because you're the one that developed
them.)
-
Is there anything that I'm developing that can be pulled out as a separate
facility, or that is more generally applicable?
-
Is there any simple things I can do to make what I'm developing more general
(without going overboard)? (This is a hard one that separates the novices
from the true architects.)
-
What are the dependencies that I've established, and are they all absolutely
necessary?
-
How can I minimize my interface and still achieve the desired goal? (Or
"if I couldn't have this one complicated thing, how bad would that be?")
-
What are the hardest parts of my interface to explain, and what can I do
to make its operation more obvious? (Writing the documentation really helps
clean up the code sometimes.)
-
Have I chosen the best names for my classes and methods, and have I stuck
to the established style guidelines? (Note that you might like your style
better, but you're introducing yet another hurdle for the next guy.)
-
Be prepared for a couple of iterations of your interface's design before
things are considered final. Don't release the "1.0 final" version of your
plugin, order 10,000 CDs to be pressed, and on the same day send your changes
back to mozilla.org. Most likely there will be some questions, suggestions
and changes to be made before your hooks become part of the next Mozilla
source release.
Talk to us, let us know what you want, what your plans are, and a gross
schedule and we promise to work with you. The last thing we want is someone
going away, angry, saying "mozilla.org screwed my product over in the 11th
hour." Remember that we've got a hard job trying to balance the needs of
hundreds of developers while trying to evolve the Mozilla source base in
a coherent direction. Cut us some slack, and be patient, and we'll do our
best to make you look good!
Now down to the nuts and bolts...
First you should be sure to read Will's excellent document, Modularization
Techniques, on using XPCOM and writing modules. Then you should continue
reading to learn the gory details of extending Mozilla.
Adding a method to an existing interface
Let's say that your application is "heavy into networking," and you've
poured through the netlib code and discovered that "true happiness lies
in exposing the 'server_status' field of the URL_Struct associated with
an outstanding network request of a NPIPluginStreamPeer interface." (Go
look at the Mozilla source and you'll see what I'm talking about.) How
exactly should you go about doing this?
First, congratulations on spending enough time looking at the Mozilla
source code to see that what you need is there beneath the covers, but
not exposed through the Plugin API. Good engineering starts with understanding
the terrain.
Second, since the objective here is simple -- to simply add a method
and not totally rearchitect the world -- we'll be sure add the new method(s)
at the end of the class declaration. That way, the extended interface won't
become incompatible with the old vtable layout, and cause plugins that
expected the old NPIPluginStreamPeer interface to be forced to recompile:
| from nsplugin.h |
class NPIPluginStreamPeer : public nsISupports {
public:
...all the old NPIPluginStreamPeer methods...
//////////////////////////////////////////////
// Extensions to NPIPluginStreamPeer for version 2:
// GetServerStatus: This methods returns the server
// status code for the requested URL.
virtual NS_IMETHOD_(PRInt32)
GetServerStatus(void) = 0;
};
#define NP_IPLUGINSTREAMPEER1_IID \
{ /* 38278eb0-a1bd-11d1-85b1-00805f0e4dfe */ \
0x38278eb0, \
0xa1bd, \
0x11d1, \
{ 0x85, 0xb1, 0x00, 0x80, 0x5f, 0x0e, 0x4d,0xfe } \
}
#define NP_IPLUGINSTREAMPEER2_IID \
{ /* f87f0420-c149-11d1-85c1-00805f034dfe */ \
0xf87f0420, \
0xc149, \
0x11d1, \
{ 0x85, 0xc1, 0x00, 0x80, 0x5f, 0x03, 0x4d,0xfe } \
}
#define NP_IPLUGINSTREAMPEER_IID NP_IPLUGINSTREAMPEER2_IID
|
Some things to observe here:
-
The method was added at the end of the class, just as we said. That way
the vtable of the C++ class is extended in a compatible way. (If some other
developer also wants to extend the NPIPluginStreamPeer class in the same
release of Mozilla, then some negotiation needs between you, the other
developer, and the mozilla.org guys -- that's why you should be sure to
communicate what you're trying to do.)
-
We were careful to add a comment saying that some additions were being
made to the original interface. That way it's clear what's being added.
-
The new function is documented (somewhat). That way people reading the
header have some idea how things work.
-
The new method is declared virtual. All interface methods must be virtual.
-
The NS_IMETHOD_(ResultType) macro is used to declare the calling
convention to be used in a cross-platform way. This is important for example
on Windows where the macro expands to add '__stdcall', a declaration for
standard COM calling conventions.
-
The name of the method is specified with inter-caps ("GetServerStatus")
rather than some other convention (like following the netlib convention
and calling it "server_status" or something like that).
-
We've declared the return type of the method PRInt32 (a 32-bit signed integer).
We do this so that the result is the same size on all platforms (Win16
thinks 'int' is a 16-bit quantity), despite the sloppy definition of 'server_status'
as 'int' in the net.h header file. (NSPR's type declarations should be
used whenever possible for cross-platform portability.)
-
In this particular case, the void argument list is explicitly specified
with "(void)". This was done because g++ compilers (which aren't fully
C++ compliant) tread "()" and "(void)" differently with respect to overloaded
operations. (Even if you're not compiling with g++, do this -- somebody
else will be using it.)
-
The method was declared pure abstract ("= 0") since this is an interface.
All interface methods must be pure abstract.
-
The old NP_IPLUGINSTREAMPEER_IID macro was renamed NP_IPLUGINSTREAMPEER1_IID
to make way for the new methods that are to be added. We don't want to
loose this old IID because older plugins still depend on it.
-
A new interface ID was generated by using 'uuidgen -s' on Windows (sorry,
tools to generate universally unique IDs aren't yet available on other
platforms). The result of this was pasted into the NP_IPLUGINSTREAMPEER2_IID
macro.
-
Finally the general name of the interface ID, NP_IPLUGINSTREAMPEER_IID,
was aliased to the new interface ID, NP_IPLUGINSTREAMPEER2_IID. That way
plugins that were compiled with the old interface ID and old version of
the header, when recompiled, will pick up the new IID (and new methods).
That seems like a lot to consider when adding a new method, but most of
these things apply whether we're adding one method or several, and most
are pretty simple once you get the hang of it.
Next we need to implement this addition to the browser. First note that
we've simply extended the interface, NPIPluginStreamPeer. So we have to
add a corresponding declaration to the implementation header file. Hunting
around in the source you'll find that this lives in npglue.h:
| from npglue.h |
class nsPluginStreamPeer : public NPISeekablePluginStreamPeer {
public:
...all the other nsPluginStreamPeer methods...
// GetServerStatus: This methods returns the
// server status code for the requested URL.
virtual NS_IMETHOD_(PRInt32)
GetServerStatus(void);
protected:
...
};
|
Here we've copied the declaration of the method from NPIPluginStreamPeer,
but made it not be a pure abstract method. It doesn't matter where this
method gets inserted in the class because the compiler will sort it out
based on the interface being inherited, but will put it near the end for
clarity anyway. Finally we can implement our extensions as follows:
| from nsplugin.cpp |
NS_IMETHOD_(PRInt32)
nsPluginStreamPeer::GetServerStatus(void)
{
return urls->server_status;
}
NS_DEFINE_IID(kSeekablePluginStreamPeerIID,
NP_ISEEKABLEPLUGINSTREAMPEER_IID);
NS_DEFINE_IID(kPluginStreamPeer1IID,
NP_IPLUGINSTREAMPEER1_IID); // changed
NS_DEFINE_IID(kPluginStreamPeer2IID,
NP_IPLUGINSTREAMPEER2_IID); // NEW!
NS_IMETHOD_(nsresult)
nsPluginStreamPeer::QueryInterface(const nsIID& aIID,
void**aInstancePtr)
{
if (NULL == aInstancePtr) {
return NS_ERROR_NULL_POINTER;
}
if ((stream->seekable &&
aIID.Equals(kSeekablePluginStreamPeerIID)) ||
aIID.Equals(kPluginStreamPeer1IID)||
aIID.Equals(kPluginStreamPeer2IID)|| // NEW!
aIID.Equals(kISupportsIID)) {
*aInstancePtr = (void*)((nsISupports*)this);
AddRef();
return NS_OK;
}
return NS_NOINTERFACE;
}
|
Here we see our humble method definition (most methods will probably be
more complex than this!), and then a change to QueryInterface to say that
we not only support the old interface ID (with the new name, NP_IPLUGINSTREAMPEER1_IID),
but also the new interface ID with that denotes the current version of
the class NPIPluginStreamPeer with the GetServerStatus extension, NP_IPLUGINSTREAMPEER2_IID.
That's it -- with one small exception -- we blew it! In this particular
case we can add a method to NPIPluginStreamPeer and maintain backward compatibility
because there is already a public subclass of NPIPluginStreamPeer defined,
namely, NPISeekablePluginStreamPeer! If we added a method to this superclass
(interface), we'd affect existing plugins that depend on the previous version
of the NPISeekablePluginStreamPeer class -- its RequestRead method would
get moved down one notch because of the addition of the GetServerStatus
method to its superclass. Back to the drawing board!
(Sorry to take you through this big detour, but the above technique
might be useful for classes that don't otherwise have subclasses. However,
the following technique -- although a bit more complex -- will work in
most all circumstances while allowing backward compatibility to be maintained.)
Defining a new interface
Rather than trying to add a new method to an existing class (and possibly
breaking subclasses), a far better way to go is to just define a new interface.
QueryInterface can be used on an object not only to negotiate some subclass
or extension to that object's class, but also to obtain a completely different
object operating as a logical extension to the first.
Here's a new interface that implements the GetServerStatus method:
class NPIServerStatus : public nsISupports {
public:
// use QueryInterface on an instance of
// NPIPluginStreamPeer to obtain this interface
// GetServerStatus: This methods returns the
// server status code for the requested URL.
virtual NS_IMETHOD_(PRInt32)
GetServerStatus(void);
};
#define NP_ISERVERSTATUS_IID \
{ /* f87f0420-c149-11d1-85c1-00805f034dfe */ \
0xf87f0420, \
0xc149, \
0x11d1, \
{ 0x85, 0xc1, 0x00, 0x80, 0x5f, 0x03, 0x4d,0xfe } \
}
|
Then to obtain an instance of this interface, the plugin code would do
something like this:
NS_DEFINE_IID(kServerStatusIID, NP_ISERVERSTATUS_IID);
void somePluginOperation(NPIPluginStreamPeer* peer) {
NPIServerStatus* ssIntf;
if (peer->QueryInterface(kServerStatusIID,
(void**)&ssIntf)== NS_OK) {
// at this point we've successfully obtained
// an instance supporting NPIServerStatus so
// we can call GetServerStatus:
PRInt32 status = ssIntf->GetServerStatus();
...now use status...
// don't forget to release the thing we
// obtained by QueryInterface
ssIntf->Release();
}
else {
// otherwise, the plugin stream peer
// we were passed doesn't support the
// NPIServerStatus interface-- probably
// because it's an older version of the
// browser.
...do something else...
}
...
}
|
Of course this example is rather simple. Your interfaces will usually
provide more than one accessor routine. But for illustrative purposes,
let's continue and look at how to add the NPIServerStatus interface to
the browser's plugin stream peer implementation.
Making your new interface accessible
The easiest way to add support for NPIServerStatus to the browser's nsPluginStreamPeer
implementation is to just use multiple inheritance:
| from npglue.h |
class nsPluginStreamPeer : public NPISeekablePluginStreamPeer,
public NPIServerStatus {
public:
...all the other nsPluginStreamPeer methods...
// GetServerStatus: This methods returns the
// server status code for the requested URL.
virtual NS_IMETHOD_(PRInt32)
GetServerStatus(void);
protected:
...
};
|
Then we implement the GetServerStatus method as we did above. The QueryInterface
method, however, gets an extra 'if' statement:
| from nsplugin.cpp |
NS_IMETHOD_(PRInt32)
nsPluginStreamPeer::GetServerStatus(void)
{
return urls->server_status;
}
NS_DEFINE_IID(kSeekablePluginStreamPeerIID,
NP_ISEEKABLEPLUGINSTREAMPEER_IID);
NS_DEFINE_IID(kPluginStreamPeerIID,
NP_IPLUGINSTREAMPEER_IID);
NS_DEFINE_IID(kServerStatusIID,
NP_ISERVERSTATUS_IID);
NS_IMETHOD_(nsresult)
nsPluginStreamPeer::QueryInterface(const nsIID& aIID,
void**aInstancePtr)
{
if (NULL == aInstancePtr) {
return NS_ERROR_NULL_POINTER;
}
if ((stream->seekable &&
aIID.Equals(kSeekablePluginStreamPeerIID)) ||
aIID.Equals(kPluginStreamPeerIID)||
aIID.Equals(kISupportsIID)) {
*aInstancePtr = (void*)
((NPISeekablePluginStreamPeer*)this);
AddRef();
return NS_OK;
}
if (aIID.Equals(kServerStatusIID)) { // NEW!
*aInstancePtr = (void*)((NPIServerStatus*)this);
AddRef();
return NS_OK;
}
return NS_NOINTERFACE;
}
|
Here, it's important to have separate interface ID tests for those interfaces
being inherited through the NPISeekablePluginStreamPeer superclass, and
for those interfaces being inherited through the NPIServerStatus superclass
(i.e. NPIServerStatus itself). This is because different vtables are obtained
at the point where the 'this' pointer is cast to one superclass or the
other. Note that we don't use the 'static_cast' operator available in newer
versions of C++ because not all compilers that we use to compile Mozilla
support it. Also, there's a nsISupports interface that could also be obtained
through the NPIServerStatus superclass, but since it ends up being operationally
identical to the one from the NPISeekablePluginStreamPeer superclass, we
don't need to do anything special to allow it to be accessed.
Making your implementation "aggregateable"
There's one last thing you might consider doing with your new interface
to give it maximum flexibility and modularity: make it aggregateable. Aggregation
is the concept of allowing two or more instances to be glued together as
if they were one object implementing multiple interfaces. This is a good
thing if different parts of the aggregated whole live in different logical
subsections of the Mozilla codebase, or if part of the object is loaded
on demand from a DLL.
We've provided the header file nsAgg.h (ns/xpcom/src/nsAgg.h) to make
aggregation a relatively straightforward thing to do. For our example,
we'll make NPIServerStatus an aggregated implementation whose "outer" object
is the plugin stream peer. Again, you probably wouldn't want to build such
an elaborate mechanism for such a simple method (and in this particular
case, the GetServerStatus method probably rightfully belongs in the NPIPluginStreamPeer
interface), but we'll continue with the example for illustrative purposes.
(Note that the JVM Plug-in API for adding Java virtual machine plugins
was implemented this way, as an aggregateable interface that extends the
basic nsPluginManager class in Mozilla.)
class nsServerStatus : public NPIServerStatus {
public:
NS_DECL_AGGREGATED
nsServerStatus(nsISupports* outer, URL_Struct*url);
virtual ~nsServerStatus(void);
// GetServerStatus: This methods returns the
// server status code for the requested URL.
virtual NS_IMETHOD_(PRInt32)
GetServerStatus(void);
protected:
URL_Struct* urls;
};
|
nsServerStatus::nsServerStatus(nsISupports* outer,
URL_Struct*url)
: urls(url)
{
NS_INIT_AGGREGATED(outer);
}
nsServerStatus::~nsServerStatus(void)
{
}
|
The first thing you'll notice here is that nsServerStatus, our new
implementation, only inherits from the NPIServerStatus interface that defines
the abstract GetServerStatus method. This class will work hand in hand
with the nsPluginStreamPeer class already defined in nsplugin.cpp.
The second thing is that we use the NS_DECL_AGGREGATED macro to declare
all the mechanism needed to make this class aggregateable. We'll see more
later as we start implementing this class how this works.
Third, the constructor takes an instance that supports the nsISupports
interface as an "outer" object. The mechanism set up by NS_DECL_AGGREGATED
will test whether the outer object is non-NULL, and if so, will forward
nsISupports operations to the outer which will get first crack at QueryInterface,
and will manage the reference counting for the aggregated objects. Also
the URL structure is passed in the constructor in order to initialize the
instance variable needed by the GetServerStatus method. Here's what the
constructor and destructor look like:
nsServerStatus::nsServerStatus(nsISupports* outer,
URL_Struct* url)
: urls(url)
{
NS_INIT_AGGREGATED(outer);
}
nsServerStatus::~nsServerStatus(void)
{
}
|
Here the constructor initializes the urls instance variable from the
url argument, and then uses the NS_INIT_AGGREGATED macro to initialize
outer. NS_INIT_AGGREGATED also does the work that NS_INIT_ISUPPORTS usually
does when you're just implementing a subclass of nsISupports. The destructor
does nothing special. We're just careful not to declare it in the header
file (inline), because it's virtual, and some compilers can't deal with
that. Also we're careful to use 'void' in the argument list for poor g++.
The only other change we have to make to finish our aggregated implementation
is we have to declare the nsISupports operations by using the macro NS_IMPL_AGGREGATED,
and then write an AggregatedQueryInterface method instead of the usual
QueryInterface. It will be called from the QueryInterface implementation
defined by the NS_IMPL_AGGREGATED macro after it does its aggregation magic:
NS_IMPL_AGGREGATED(nsServerStatus);
NS_IMETHOD_(nsresult)
nsServerStatus::AggregatedQueryInterface(const nsIID& aIID,
void** aInstancePtr)
{
if (NULL == aInstancePtr) {
return NS_ERROR_NULL_POINTER;
}
if (aIID.Equals(kServerStatusIID)) {
*aInstancePtr = (void*) this;
AddRef();
return NS_OK;
}
if (aIID.Equals(kISupportsIID)) {
// special case for when we're called
// from the Create operation
*aInstancePtr = &fAggregated;
return NS_OK;
}
return NS_NOINTERFACE;
}
|
The GetServerStatus method looks just like it did before, so we won't
bother to show you again. So that's it! We've defined an aggregated class
with relatively little pain.
Now, how do we hook it up to our nsPluginStreamPeer class? The first
trick is that we must define an instance variable in nsPluginStreamPeer
that points to the instance of the nsServerStatus class -- but we must
declare it of type nsISupports. That's because we're going to use QueryInterface
on it whenever we need a more specific type:
| from npglue.h |
class nsPluginStreamPeer : public NPISeekablePluginStreamPeer {
public:
...all the other nsPluginStreamPeer methods...
(don't declare GetServerStatus here)
protected:
nsISupports* fServerStatus;
...
};
|
Then in the QueryInterface method of nsPluginStreamPeer, we'll instantiate
our nsServerStatus class, possibly loading it dynamically if we want. Usually
that's done by using the nsFactory mechanism (which we strongly encourage
you to use), but we won't go into it here. See the document Modularization
Techniques for more details.
| from nsplugin.cpp |
NS_IMETHOD_(nsresult)
nsPluginStreamPeer::QueryInterface(const nsIID& aIID,
void**aInstancePtr)
{
if (NULL == aInstancePtr) {
return NS_ERROR_NULL_POINTER;
}
if ((stream->seekable &&
aIID.Equals(kSeekablePluginStreamPeerIID)) ||
aIID.Equals(kPluginStreamPeerIID) ||
aIID.Equals(kISupportsIID)) {
*aInstancePtr = (void*)
((NPISeekablePluginStreamPeer*)this);
AddRef();
return NS_OK;
}
if (fServerStatus == NULL) { // try to create/load it
nsServerStatus* ss = ...create or load it...
if (ss) {
nsresult ok = ss->QueryInterface(kISupportsIID,
(void**)&fServerStatus);
if (ok != NS_OK) {
...release or unload ss...
}
}
}
if (fServerStatus)
return fServerStatus->QueryInterface(aIID,aInstancePtr);
else
return NS_NOINTERFACE;
}
|
What's happening here is that we're doing the usual QueryInterface thing
for the plugin stream peer, but if that fails, we check whether fServerStatus
has been set yet to see if we should load it. Note that this implementation
could suffer from continually trying to load or create the server status
object (for instance if the DLL simply isn't available), but we'll let
you fix that. If we are able to load or create it, we use QueryInterface
to set the fServerStatus instance variable. This call to QueryInterface
is very special. The aggregation boiler-plate code that we used to define
QueryInterface for nsServerStatus knows that when kISupportsIID is passed
to it it must return a special internal version of nsISupports that makes
the aggregation work. Fortunately you don't have to think about this too
much -- just copy the example (ok, if you want, you can stare at nsAgg.h
for a while and figure out how it works).
So, that was only a little complicated, but now that you've done it,
you'll be the proud owner of a completely dynamically loadable and modular
interface that can be aggregated with just about any other class in Mozilla.
We think that's a big plus, and we'll worship you if you go to the trouble
to do it right.
|