Back to Basics: Read-only Data

(I’m posting two articles today – and for those of you who correctly pointed out my XPCOM Services article had bugs in the sample code, I’ve updated it to point to live code with a couple adjustments. I continue to welcome your insight.)

XPCOM provides a lot of basic data types – nsIVariant
is one of the more complex ones from a C++ standpoint. In particular, they
provide arrays through the nsIArray
interface, object containers through nsIPropertyBag
and nsIPropertyBag2,
and a bunch of primitive values such as strings and floating-point numbers
through nsISupportsPrimitives.
XPCOM also provides components
and contracts
for these data types in C++, so you don’t have to
reimplement them. (You can if you want to, but it’s usually not necessary, as
I’ll explain in another article.)

There’s only one downside to these basic components: they’re eternally
changeable. What that means is I can pass a
nsISupportsString to your component, and it can change that
value before sending it onto another component. From my perspective, I don’t
have any way of “sealing” the data, of making it read-only. Even if I pass
you one of these components in an interface that has no change methods
(nsIArray), you could easily QueryInterface it for
an interface that has change methods (nsIMutableArray).
You’ll find an exception to the rule in nsIWritableVariant, but
that’s about it.

In this article, I’ll talk about two approaches to this problem – and the
choice I made.

First approach – nsILockable

The first idea I had was a new interface to lock an individual
component:

[scriptable, uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
interface nsILockable : nsISupports {
readonly attribute PRBool isLocked;
void lock();
};

At first, I thought, “This is the way to go.” I could simply implement
this new interface on every data type I wanted to. That implied one of two
options:

  • Modifying the original XPCOM data components.
  • Creating – and registering – wrapper components to forward the
    appropriate calls.

In the first case, I’d be introducing new code into very stable,
well-tested components. I considered that risky and probably unnecessary. In
the second case, it would mean a lot of new code, and in particular, a whole
bunch of new component classes to implement. Finally, neither solution
defeated the QueryInterface problem – the original
change-the-data interfaces would still be available, indicating the component
might be changeable… when in reality if it’s locked, these methods wouldn’t
work. Better to not expose the write-data interfaces.

There’s one other flaw: Suppose I change my mind and want to edit the
data. I don’t have any access to the methods in question anymore. Whoops.

Second approach – nsIInterfaceWrapper

[scriptable, uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
interface nsIInterfaceWrapper : nsISupports
{
void getWrapper(in nsIIDRef uuid,
in nsISupports wrappee,
[retval] out nsISupports wrapper);
};

In this approach, I would implement a service for wrapping the data
component in a read-only component. Getter methods we would forward, but
setters would throw NS_ERROR_NOT_AVAILABLE. The wrapper
components would not implement the change-the-data interfaces. Most
importantly, the original data component would still be available to my
function, and I could alter it at will.

Here’s an example in JavaScript:

var data = Components.classes["@mozilla.org/hash-property-bag;1"]
.createInstance(Components.interfaces.nsIWritablePropertyBag);
data.setProperty("foo", true);
var wrapperRO = Components.classes["@mozilla.org/readonly-wrapper;1"]
.getService(Components.interfaces.nsIInterfaceWrapper);
var dataRO = wrapperRO.getWrapper(Components.interfaces.nsIPropertyBag, data);
data.setProperty("bar", false);
otherObj.doSomething(dataRO);

Source code

I decided on the second course, above. You’ll find my implementation of this nsReadonlyWrapper
service
. The various read-only forwarding classes are starting
to take shape
as well. Thanks for reading!