Implementing membranes using proxies, and [[GetInheritance]]

# Boris Zbarsky (11 years ago)

I was looking at implementing a membrane using ES6 proxies and ran into a snag. Consider a situation where object A has prototype B. A' is a proxy implementing the membrane, whose target is A.

But now if Object.getPrototypeOf(A') is invoked the return value will be B (unless it just throws). There's no way for A' to return a new proxy B' whose target is B in this situation.

Is the intent here that the membrane should not be using A as its target but some third object A''? Or am I just missing something?

# Brendan Eich (11 years ago)

Cc'ing Tom to make sure I tell no lies.

Boris Zbarsky <mailto:bzbarsky at MIT.EDU> September 10, 2013 9:10 PM

Is the intent here that the membrane should not be using A as its target but some third object A''?

This. The target is for nonconfigurable properties. A membrane wants to hide the real target. See harmony:direct_proxies (look for membrane and shadow)

# David Bruant (11 years ago)

Le 11/09/2013 06:10, Boris Zbarsky a écrit :

But now if Object.getPrototypeOf(A') is invoked the return value will be B (unless it just throws). There's no way for A' to return a new proxy B' whose target is B in this situation.

In essence yes. In practice, you can do:

 // trap:
 getPrototypeOf: function(target){
     target.__proto__ = B';
     return B';
 }

But of course, it changes A [[Prototype]], which is probably not desirable. And of course, although to-be-standard, __proto__ is bad taste...

I think it was discussed at some point to get rid of the restriction on the getPrototypeOf trap and enforce it only for non-extensible objects (but I can't find the info anymore, I might just be inventing this...). It would allow you to return a different object assuming the target is and remains extensible (more on that below).

Is the intent here that the membrane should not be using A as its target but some third object A''?

It depends, but in some cases yes and A'' is then called a "shadow target". It depends on what sort of membrane you want. See Tom's May 2013 TC39 meeting slides (starting slide 22 for membranes).

You need a shadow target if code running against the membrane wants to perform actions that require eternal invariants enforcement 1, so typically call Object.preventExtensions, or make some properties non-configurable. And also that you don't want these invariants to be enforced on the actual proxy targets. If you're cool with code running against the membrane to enforce these invariants on objects under your control, you don't need shadow targets.

Ps: btw, wasn't "GetInheritance" supposed to be renamed "GetPrototype"?

# David Bruant (11 years ago)

Le 11/09/2013 06:10, Boris Zbarsky a écrit :

I was looking at implementing a membrane using ES6 proxies

May I ask why you've been working on that? Is it related to the work on WebIDL binding of DOM/browser objects? One design goal of proxies was getting closer to self-hostability of the DOM/browser APIs, so if you're working on something that validates (or not) the proxy design and its practicality for this use case, it would be valuable feedback.

# Tom Van Cutsem (11 years ago)

2013/9/11 David Bruant <bruant.d at gmail.com>

In essence yes. In practice, you can do:

// trap:
getPrototypeOf: function(target){
    target.__proto__ = B';
    return B';
}

But of course, it changes A [[Prototype]], which is probably not desirable. And of course, although to-be-standard, __proto__ is bad taste...

Indeed, this is also the pattern I used, except it doesn't set the __proto__ of the real target, but of a shadow target: tvcutsem/harmony-reflect/blob/master/examples/membrane.js#L246.

Setting __proto__ may be bad taste in general, but this is a case where using this capability is necessary.

Ps: btw, wasn't "GetInheritance" supposed to be renamed "GetPrototype"?

I think we had agreement on that. Allen?

# Tom Van Cutsem (11 years ago)

2013/9/11 David Bruant <bruant.d at gmail.com>

I think it was discussed at some point to get rid of the restriction on the getPrototypeOf trap and enforce it only for non-extensible objects (but I can't find the info anymore, I might just be inventing this...). It would allow you to return a different object assuming the target is and remains extensible (more on that below).

No, I think you're confusing with an invariant on [[SetInheritance]]. Basically [[SetInheritance]] has no invariants as long as the object is extensible. If it is non-extensible, then proxies enforce that the prototype of proxy and target are the same, see < people.mozilla.org/~jorendorff/es6-draft.html#sec-9.3.2>.

[[GetInheritance]] always checks whether the proxy and target's prototype are the same, but as you pointed out, if the target is extensible, you can set its prototype to some other object before returning a value from the getPrototypeOf trap.

# Boris Zbarsky (11 years ago)

On 9/11/13 5:14 AM, David Bruant wrote:

May I ask why you've been working on that?

It came up in the context of a discussion about how to handle __proto__ sets on WebIDL objects in Gecko that are implemented by proxy-like objects due to having named or indexed getters. That led to [[Get/SetInheritance]], which led me to try to understand this part of the spec. It seemed like [[GetInheritance]] was on the one hand providing a lot of room to hang yourself (e.g. traversing the proto chain is no longer idempotent, nor guaranteed to terminate!) while not allowing the basic membrane case to work in the obvious way...

One design goal of proxies was getting closer to self-hostability of the DOM/browser APIs

Yeah, I was basically thinking about how I'd do Gecko's cross-global wrappers with ES proxies (not that that's a reasonable thing to do, since it requires APIs that simply can't be exposed to script).

so if you're working on something that validates (or not) the proxy design and its practicality for this use case, it would be valuable feedback.

All that WebIDL requires is the ability to write a custom implementation of [[GetOwnProperty]], [[Delete]], and [[DefineProperty]]. What I haven't checked is whether the WebIDL algorithms for those can be implemented within the constraints that are imposed on proxy handlers at the moment.

The question of how WindowProxy works is still open; it's not defined in terms of the MOP... yet.

# David Bruant (11 years ago)

Le 11/09/2013 16:22, Tom Van Cutsem a écrit :

No, I think you're confusing with an invariant on [[SetInheritance]]. Basically [[SetInheritance]] has no invariants as long as the object is extensible. If it is non-extensible, then proxies enforce that the prototype of proxy and target are the same, see people.mozilla.org/~jorendorff/es6-draft.html#sec-9.3.2.

oh ok.

[[GetInheritance]] always checks whether the proxy and target's prototype are the same, but as you pointed out, if the target is extensible, you can set its prototype to some other object before returning a value from the getPrototypeOf trap.

It's annoying to cleanup post-trap though (to restore the target initial prototype). Is the invariant on getPrototypeOf that important on extensible objects? I think it is the only trap that enforces something without a related eternal invariant.

# David Bruant (11 years ago)

Le 11/09/2013 16:52, Boris Zbarsky a écrit :

One design goal of proxies was getting closer to self-hostability of the DOM/browser APIs

Yeah, I was basically thinking about how I'd do Gecko's cross-global wrappers with ES proxies (not that that's a reasonable thing to do, since it requires APIs that simply can't be exposed to script).

Can you elaborate on these APIs? (to get an idea of how far away we are and whether your use cases can eventually be achieved in conjunction with other features (symbols?))

# Boris Zbarsky (11 years ago)

On 9/11/13 11:23 AM, David Bruant wrote:

Can you elaborate on these APIs?

In particular, the API that changes the "current global and effective script origin". That clearly can't be done from a script, since the global and effective script origin while a script is running have to be the global and effective script origin of that script.

# Allen Wirfs-Brock (11 years ago)

On Sep 11, 2013, at 7:17 AM, Tom Van Cutsem wrote:

Setting __proto__ may be bad taste in general, but this is a case where using this capability is necessary.

At least if the ES6 spec. is fully implemented, Object.setPrototypeOf would be preferable to assigning to __proto__. EG,

// trap:
getPrototypeOf: function(target){
    Object.setPrototypeOf(target, B');
    return B';
}

There's nothing magic about __proto__=. Both dunder proto and Object.setPrototypeOf are defined in terms of [[SetInheritance]]. The only difference is that the functionality of dunder proto is dependent upon the current [[Prototype]] value of target while setPrototypeOf doesn't have that dependency.

Ps: btw, wasn't "GetInheritance" supposed to be renamed "GetPrototype"?

I think we had agreement on that. Allen?

I'm willing to call them [[GetPrototypeOf]] and [[SetPrototypeOf]] to match the trap names. I prefer avoiding a direct connotation with the [[Prototype]] internal data property as an exotic object is not required to have one.

# David Bruant (11 years ago)

Le 11/09/2013 17:51, Allen Wirfs-Brock a écrit :

I'm willing to call them [[GetPrototypeOf]] and [[SetPrototypeOf]] to match the trap names. I prefer avoiding a direct connotation with the [[Prototype]] internal data property as an exotic object is not required to have one.

Is there a precedent of such an object? Why can't such objects have a [[Prototype]] to null (as is the practice in userland JS)?

# David Bruant (11 years ago)

Le 11/09/2013 17:45, Boris Zbarsky a écrit :

On 9/11/13 11:23 AM, David Bruant wrote:

Can you elaborate on these APIs?

In particular, the API that changes the "current global and effective script origin". That clearly can't be done from a script, since the global and effective script origin while a script is running have to be the global and effective script origin of that script.

Ok. Just so I'm sure I understand. You're saying that Gecko's cross-global wrappers could be implemented with ES proxies (with chrome privileges), but some capabilities can't be exposed to user-land scripts? This seems normal. In a self-hosted implementation, one would have to re-implement the notion of "current global" and "effective script origin" for the content it runs. Of course, the content wouldn't be exposed all capabilities the privileged content has access to.

What I'm trying to understand if whether there is a design limitation of the proxies API. It seems that there is none in your case.

# Boris Zbarsky (11 years ago)

On 9/11/13 12:05 PM, David Bruant wrote:

You're saying that Gecko's cross-global wrappers could be implemented with ES proxies (with chrome privileges), but some capabilities can't be exposed to user-land scripts?

I'm saying that in the current architecture they can't be implemented as privileged script either; the guts of the "we're now in a new global" code have to live in the VM, not in script.

# Boris Zbarsky (11 years ago)

On 9/11/13 12:05 PM, David Bruant wrote:

In a self-hosted implementation, one would have to re-implement the notion of "current global" and "effective script origin" for the content it runs.

Ah, I see the issue.

I'm interested in self-hosting parts of the runtime, not in emulating an entire different runtime. You're describing the latter.

# Allen Wirfs-Brock (11 years ago)

On Sep 11, 2013, at 8:53 AM, David Bruant wrote:

Le 11/09/2013 17:51, Allen Wirfs-Brock a écrit :

I'm willing to call them [[GetPrototypeOf]] and [[SetPrototypeOf]] to match the trap names. I prefer avoiding a direct connotation with the [[Prototype]] internal data property as an exotic object is not required to have one.

Is there a precedent of such an object?

For example, Proxy instances... Also, I may be mistaken but I think that prior to IE9, its COM based host objects did not have a [[Prototype]].

More generally, ES objectness is defined behaviorally via the MOP, not by any required representation of internal states. We don't impose any representational requirements upon exotic objects, just that they expose, in an implementation appropriate manner, the MOP interface.

Why can't such objects have a [[Prototype]] to null (as is the practice in userland JS)?

They don't need to because nothing in the ES6 spec. except for the the ordinary definition of [[GetInheritance]]/[[SetInheritance]] accesses [[Prototype]] directly. All access go through those MOP calls. So, it is totally up to the implementation of those MOP operations to decide on any backing store.

# Tom Van Cutsem (11 years ago)

[+markm, allenwb]

2013/9/11 David Bruant <bruant.d at gmail.com>

It's annoying to cleanup post-trap though (to restore the target initial prototype). Is the invariant on getPrototypeOf that important on extensible objects? I think it is the only trap that enforces something without a related eternal invariant.

For membranes, when using a shadow target, there's no need to clean-up/restore anything.

But more generally, you're right that it's odd [[GetInheritance]] is doing an invariant check on an otherwise extensible/configurable object. I think it's simply a remnant of the time before we fully embraced setPrototypeOf.

Now that Object.setPrototypeOf is part of ES6, there doesn't seem to be a point in guaranteeing the stability of Object.getPrototypeOf for extensible objects.

The important invariant is that getPrototypeOf remain stable for non-extensible objects.

Hence, it seems we could replace steps 8-10 of Proxy.[[GetInheritance]] with:

8. Let extensibleTarget be the result of IsExtensible(target).
9. ReturnIfAbrupt(extensibleTarget).
10. If extensibleTarget is true, then return handlerProto.
// steps below identical to the old steps 8-10:
11. Let targetProto be the result of calling the [[GetInheritance]]
internal method of target.
12. ReturnIfAbrupt(targetProto).
13. If SameValue(handlerProto, targetProto) is false, then throw a
TypeError exception.

Mark, Allen, does that seem right?

# Tom Van Cutsem (11 years ago)

I'm willing to call them [[GetPrototypeOf]] and [[SetPrototypeOf]] to match the trap names. I prefer avoiding a direct connotation with the [[Prototype]] internal data property as an exotic object is not required to have one.

I'm all for matching the internal method names with the trap and corresponding Object/Reflect method names, so +1 for [[Get/SetPrototypeOf]].

# Mark S. Miller (11 years ago)

On Thu, Sep 12, 2013 at 4:14 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

But more generally, you're right that it's odd [[GetInheritance]] is doing an invariant check on an otherwise extensible/configurable object. I think it's simply a remnant of the time before we fully embraced setPrototypeOf.

agreed

Mark, Allen, does that seem right?

yes.

# David Bruant (11 years ago)

Based on this new fresh agreement and assuming it sticks, the answer to Boris initial question changes a bit then: if B.isPrototypeOf(A) and A' = new Proxy(A, handler), then the handler.getPrototypeOf can return any object (including B', a membrane-proxy to B) as long as A is extensible. If the code that runs against your membrane is not expected to be allowed to change extensiveness, you don't need a shadow target.

# Mark S. Miller (11 years ago)

Membranes need shadow targets, because of non-extensibility of objects and non-configurability of properties. This special case of no-invariants-anywhere is not JavaScript. Trying to do membranes without shadow targets is a useless exercise.

# Allen Wirfs-Brock (11 years ago)

On Sep 12, 2013, at 6:35 AM, Mark S. Miller wrote:

Mark, Allen, does that seem right?

yes.

Also, seems right to me.

That sounds like sufficient consensus. I'll make the change to the spec.

Thanks, Boris

# Boris Zbarsky (11 years ago)

On 9/12/13 11:03 AM, David Bruant wrote:

If the code that runs against your membrane is not expected to be allowed to change extensiveness

It's absolutely expected to be allowed to do that. You should be able to freeze objects coming from another global.

# Tom Van Cutsem (11 years ago)

2013/9/12 Mark S. Miller <erights at google.com>

Membranes need shadow targets, because of non-extensibility of objects and non-configurability of properties. This special case of no-invariants-anywhere is not JavaScript. Trying to do membranes without shadow targets is a useless exercise.

True, but by removing the invariant check on getPrototypeOf, a membrane proxy can now avoid the cost of actually storing the wrapped prototype on the shadow target until the real target becomes observably non-extensible (which may never occur).

More generally, membranes always need to be set-up with shadow targets, but they don't actually need to use them until the real target has some invariants.

# David Bruant (11 years ago)

Le 13/09/2013 09:19, Tom Van Cutsem a écrit :

True, but by removing the invariant check on getPrototypeOf, a membrane proxy can now avoid the cost of actually storing the wrapped prototype on the shadow target until the real target becomes observably non-extensible (which may never occur).

More generally, membranes always need to be set-up with shadow targets, but they don't actually need to use them until the real target has some invariants.

If one wants Object.getPrototypeOf to return the same object twice, the membrane has to associate a new prototype with a given object. If there is a shadow target, the place of where to store this object is pretty obvious ([[Prototype]]). That said, the good property that remains is that if a prototype setting capability has been kept around is that prototype chains can be built lazily.

# Tom Van Cutsem (11 years ago)

2013/9/13 David Bruant <bruant.d at gmail.com>

If one wants Object.getPrototypeOf to return the same object twice, the membrane has to associate a new prototype with a given object. If there is a shadow target, the place of where to store this object is pretty obvious ([[Prototype]]). That said, the good property that remains is that if a prototype setting capability has been kept around is that prototype chains can be built lazily.

Returning the same object twice is taken care of by the use of WeakMaps, separately.

I'm thinking of an implementation as follows:

// in a wet->dry membrane proxy's handler:

getPrototypeOf: function(dryShadow) {
  if (!Object.isExtensible(wetTarget)) {
    // no need to use the shadow
    return wet2dry(Object.getPrototypeOf(wetTarget)); // wet2dry consults a WeakMap to avoid creating multiple wrappers
  }
  var dryProto = wet2dry(Object.getPrototypeOf(wetTarget));
  Object.setPrototypeOf(dryShadow, dryProto);
  return dryProto;
}