What is a membrane?
A membrane is a wrapper around an entire object graph, as opposed to a wrapper for just a single object. Typically, the creator of a membrane starts out wrapping just a single object in a membrane. The key idea is that any object reference that crosses the membrane is itself transitively wrapped in the same membrane. Object references typically cross the membrane in one direction (out->in) by passing parameters to an object’s method, and in the opposite direction (in->out) by being returned as a value from a method or by being thrown as an exception.
To ease speaking about membranes, I will use the term “wet objects” for objects that live inside of the membrane, and the term “dry objects” to refer to objects that live outside of the membrane. The key thing about membranes is that they convert any outbound reference to a wet object that crosses them into a proxy, which is a dry object representing the corresponding wet object on the other side of the membrane. Similarly, the membrane converts any inbound dry object reference into a proxy which is a wet object that represents the corresponding dry object on the other side of the membrane.
Here’s a simple example of how a membrane can be constructed:
So now, dryA is a proxy to the corresponding wetA object. Once a dry wrapper like dryA is created, it is possible for clients to navigate transparently through the wrapped object graph via the dry wrapper, yet never getting a direct reference to the wet objects:
Now, dryB is a wrapper for the corresponding wetB object. Typically, primitives are passed through the membrane unwrapped:
After executing all this code, we end up with the following object graph:
If the outside (dry) world now passes in a dry object to m, the membrane will receive a wrapped version instead:
And we end up with the following object graph:
This is all fine, but why is it useful? A membrane provides its creator with the power to intervene whenever a reference crosses the membrane (either in->out or out->in). One simple use case is for instance to install a kill-switch on the membrane: while the switch has not yet been triggered, all the wrappers belonging to the same membrane do nothing but transparently and dutifully forwarding all operations to their corresponding targets on the other side of the membrane. However, once the kill-switch is triggered, these wrappers instantaneously stop forwarding, drop the reference to their target, and will complain whenever clients still try to access their target. In effect, the membrane has hermetically sealed off its wrapped object graph from the world outside it: clients that only have membrane-wrapped references will hold on to nothing but useless pointers. After revocation, we are left with the following object graph:
Such membranes are called “revocable” membranes, and triggering the kill-switch is also called “revoking” the membrane. Revocable membranes are useful to exercise the principle of least authority in application design: module A may only want to provide module B with access to its objects for a limited amount of time. A can thus wrap its objects in a membrane, pass the wrapped version to B, and when A (the rightful owner of the objects) decides that B no longer has the right to access its objects, it revokes the membrane.
Revocable membranes require a slightly updated API: when we create a new revocable membrane, we don’t get back just the wrapped object, but also a revoke() function that we can call to trigger the kill-switch:
Again, a client object receiving only dryA can make use of the object as if it was the wetA object: the membrane will dutifully mimic the corresponding object inside of the membrane:
But now for the interesting part: the moment the membrane creator calls revoke, clients with access to dryA or dryB only lose all ability to continue using these objects:
Of course, if some code has access to both the original wetA and the wrapped dryA, then that code can still continue using the wetA object (but not via the revoked dryA reference). Often when using membranes, the code will arrange for the original wrapped objects to be well encapsulated (perhaps hidden within the scope of a closure) so that clients can only ever get wrapped references to it.
How are membranes implemented?
I will not go into full detail in this post, but let me just mention a couple of points.
Naturally, membranes require Harmony proxies in order to implement the transparent wrappers. Until the new direct proxies API lands, I have been experimenting with membranes using my DirectProxies.js shim, which implements the new direct proxies API in terms of the older one.
Perhaps surprisingly, proper membranes also require WeakMaps, another Harmony feature (the one-line summary is that WeakMaps are like object-identity-keyed hashtables that hold on to their key weakly, ideal for building caches). Membranes need WeakMaps for two reasons: first, to preserve object identity across the membrane. Consider the following:
We would like the answer to this question to be yes. In other words: a membrane should cache whatever wrapper it creates for wetB and consistently return the same wrapper whenever wetB crosses the membrane (in this case, by being accessed via wetA’s x property).
The second reason is more subtle and has to do with preserving object identity on either side of the membrane. Consider the following:
Here, again, we would expect true to be returned. For this to happen, however, when dryB (which is an in->out wrapper for wetB) crosses the membrane (in the out->in direction), we want the x parameter to be bound to the original wetB object, i.e. we want the membrane wrapper to be automatically unwrapped. Again, the membrane implementation needs WeakMaps to be able to distinguish and unwrap its own wrappers. The same kind of unwrapping is needed in the other direction (in->out):
My current experimental version of membranes doesn’t uphold identity yet, but a sketch of how it would work using the old proxy API is still available here. Update: a membrane implementation using the latest direct proxies API, which upholds object identity across both sides of the membrane, is available here.
Only scratching the surface…