Invoke trap

# Brandon Benvie (12 years ago)

On 6/7/2013 12:53 PM, David Bruant wrote:

Le 02/06/2013 09:46, Rick Waldron a écrit :

4.4 Proxies

Proxy Invoke Trap and wrong |this|-binding on built-in methods

AWB: with current default behavior of “get”, “Caretaker” will break on built-ins such as Date, because the |this| binding is by default set to the proxy, so the Date built-in method will not find the correct private state. ARB: Same issue with binary methods ... STH: We should add invoke trap but not change the object model MM: Pleasant to have. Separate from private state. AWB: used to think this was an issue with proxies, but convinced that it’s an API issue: we need to provide default handlers that do the right thing, and which users can subclass. In particular, want a handler that, on forwarding, rebinds |this| to the target. STH: If you want to proxy a Date method the underlying this needs to be a non wrapped Date object. Doesn't this break encapsulation?

var d = new Date(); var pd = new Proxy(d, { invoke: function(target, name, thisArg, args){ return target[name].apply(thisArg, args) } })

var pt = pd.getTime(); // calls the invoke trap

// in a mixed-trusted context with access to pd: var ppd = new Proxy(pd, { invoke: function(target, name, thisArg, args){ // does the actual unwrapped date object leak through thisArg? } });

var ppt = ppd.getTime();

In any case, what is the following supposed to do:

Date.prototype.getTime.call( new Proxy(new Date(), handler) )

I imagine we would want this to work too. Is this going through the invoke trap as well? The question stands for all new "class-specific" (Map, Set, etc.) methods that would be dot-called with a proxy as first argument.

If the problem being solved is making Date (and Map, Set, etc) built-in methods works on wrapped objects, I feel the invoke trap is only a partial solution.

I intuit (but have no proof) that for Date.prototype.getTime.call to work on proxies, there is a need for class-specific traps.

  • Object properties have specific traps to intermediate access (has/get/set/defineProperty/keys, etc.),
  • [[Prototype]] has specific traps to intermediate access (get/setPrototypeOf)
  • [[Extensible]] has a specific trap to intermediate access (preventExtensions)
  • [[Call]] has a specific trap to intermediate access (apply)
  • [[Construct]] has a specific trap to intermediate access (construct)
  • Some other things that are somewhat internal (@@iterator, @@class, etc.) are exposed as symbols and are mediated via object properties-related trap

The pattern I see is that a good share of object internal properties (ES5.1 - 8.6.2 - Table 8 and Table 9) have traps dedicated to mediate access.

The case of functions is interesting. There are traps that are only relevant when the target is a function. In that regard, adding traps that are specific to accessing the Date internal [[Time]] value or specific traps to mediate access to [[MapData]] isn't any different than the current specific traps that mediate [[Call]] or [[Construct]]

David


es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss

I believe the purpose of adding multiple types of handlers [1] was to address this and similar issues.

In the case of the |get|, |set| and |invoke| traps, there is a
choice to be made when forwarding the property get/set or the
invoked method: should the value of |this| in a target’s accessor or
method be set to the proxy object or to the target object? Either
choice can be sensible, depending on your application. 

The |DelegatingHandler| implements |get|, |set| and |invoke| in such
a way that |this| inside forwarded accessors or method invocations
remains bound to the |receiver| argument. Proxies using the
|DelegatingHandler| can thus be used as prototypes for other
objects: they leave the |this|-binding intact upon forwarding.

...

|ForwardingHandler| is itself a subclass of |DelegatingHandler| (so
it comes with an appropriate default implementation for all Proxy
traps). It overrides the |get|, |set| and |invoke| traps so that
forwarded accessors or method calls get run with |this| set to the
target object.

[1] harmony:virtual_object_api

# David Bruant (12 years ago)

Le 07/06/2013 13:15, Brandon Benvie a écrit :

On 6/7/2013 12:53 PM, David Bruant wrote:

Le 02/06/2013 09:46, Rick Waldron a écrit :

4.4 Proxies

Proxy Invoke Trap and wrong |this|-binding on built-in methods

AWB: with current default behavior of "get", "Caretaker" will break on built-ins such as Date, because the |this| binding is by default set to the proxy, so the Date built-in method will not find the correct private state. ARB: Same issue with binary methods ... STH: We should add invoke trap but not change the object model MM: Pleasant to have. Separate from private state. AWB: used to think this was an issue with proxies, but convinced that it's an API issue: we need to provide default handlers that do the right thing, and which users can subclass. In particular, want a handler that, on forwarding, rebinds |this| to the target. STH: If you want to proxy a Date method the underlying this needs to be a non wrapped Date object. Doesn't this break encapsulation?

var d = new Date(); var pd = new Proxy(d, { invoke: function(target, name, thisArg, args){ return target[name].apply(thisArg, args) } })

var pt = pd.getTime(); // calls the invoke trap

// in a mixed-trusted context with access to pd: var ppd = new Proxy(pd, { invoke: function(target, name, thisArg, args){ // does the actual unwrapped date object leak through thisArg? } });

var ppt = ppd.getTime();

In any case, what is the following supposed to do:

Date.prototype.getTime.call( new Proxy(new Date(), handler) )

I imagine we would want this to work too. Is this going through the invoke trap as well? The question stands for all new "class-specific" (Map, Set, etc.) methods that would be dot-called with a proxy as first argument.

If the problem being solved is making Date (and Map, Set, etc) built-in methods works on wrapped objects, I feel the invoke trap is only a partial solution.

I intuit (but have no proof) that for Date.prototype.getTime.call to work on proxies, there is a need for class-specific traps.

  • Object properties have specific traps to intermediate access (has/get/set/defineProperty/keys, etc.),
  • [[Prototype]] has specific traps to intermediate access (get/setPrototypeOf)
  • [[Extensible]] has a specific trap to intermediate access (preventExtensions)
  • [[Call]] has a specific trap to intermediate access (apply)
  • [[Construct]] has a specific trap to intermediate access (construct)
  • Some other things that are somewhat internal (@@iterator, @@class, etc.) are exposed as symbols and are mediated via object properties-related trap

The pattern I see is that a good share of object internal properties (ES5.1 - 8.6.2 - Table 8 and Table 9) have traps dedicated to mediate access.

The case of functions is interesting. There are traps that are only relevant when the target is a function. In that regard, adding traps that are specific to accessing the Date internal [[Time]] value or specific traps to mediate access to [[MapData]] isn't any different than the current specific traps that mediate [[Call]] or [[Construct]]

David


es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss

I believe the purpose of adding multiple types of handlers [1] was to address this and similar issues.

I don't see how this solves the issue related to Date.prototype.getTime.call( new Proxy(new Date(), handler) )

# Brandon Benvie (12 years ago)

On 6/7/2013 1:22 PM, David Bruant wrote:

I don't see how this solves the issue related to Date.prototype.getTime.call( new Proxy(new Date(), handler) )

Assuming invoke also works for call and apply, if handler is a ForwardingHandler then that should work with getTime ultimately being called on the real Date object as the receiver.

# Brandon Benvie (12 years ago)

On 6/7/2013 1:24 PM, Brandon Benvie wrote:

On 6/7/2013 1:22 PM, David Bruant wrote:

I don't see how this solves the issue related to Date.prototype.getTime.call( new Proxy(new Date(), handler) )

Assuming invoke also works for call and apply, if handler is a ForwardingHandler then that should work with getTime ultimately being called on the real Date object as the receiver.

Nevermind this, I wasn't thinking. You're right.

# David Bruant (12 years ago)

Le 09/06/2013 11:37, Tom Van Cutsem a écrit :

I think your analysis is mostly right: if we want to make e.g. Date.prototype.getTime.call(proxy) work, then the easiest way to fit that into the current design would be to add Date-specific traps (and Map-specific, Set-specific, Regexp-specific, etc. traps)

The question is whether there's a strong need for intercepting these operations. It implies a pretty strong growth of the handler API. And it's not sufficiently general-purpose to also work for exotics that are defined outside of ES6 (e.g. DOM objects).

I can live with DOM objects not being fully transparent (since they were designed a while ago without proxies in mind). If it were too complicated because they're legacy, Date and RegExp could be given up on too (I think they're doable though), but I feel all other ES6 objects should be fully transparent. They have no good reason not too.

For full transparency across isolated object graphs, I think membranes are still the way to go.

I don't understand that part. A membrane (with shadow targets?) will have the same problem as a proxy alone when it comes to Map.prototype.set.call, no?

The issue for both proxiedMap.set() and Map.prototype.set.call(proxiedMap) is that the internal 'set' algorithm wants to play with an internal field which can't be accessed. This applies to a bound functions too, I believe:

 var boundHas = Set.prototype.has.bind(proxiedSet)
 boundHas(); // throw TypeError
# David Bruant (12 years ago)

Le 09/06/2013 12:50, David Bruant a écrit :

Le 09/06/2013 11:37, Tom Van Cutsem a écrit :

For full transparency across isolated object graphs, I think membranes are still the way to go. I don't understand that part. A membrane (with shadow targets?) will have the same problem as a proxy alone when it comes to Map.prototype.set.call, no?

I think I understand what you meant. I think you meant that the outside of the membrane never interacts with built-ins on membrane objects, but rather on membrane-wrapped versions of the built-ins; the actual built-in acting on the actual Map or actual Date happens inside the membrane.

If that's what you meant, I feel that it equally applies to method invocation. That is, if you have a wrapped map, it has a wrapped Map.prototype and mapProxy.set(x, y)

calls mapProxy.[[handler]].get which returns the wrapped 'set' method. This wrapped 'set' method has its 'apply' trap called. In the apply trap, the target is the actual Map.prototype.set, thisArg is a wrapped map. the apply trap has access to some wrapped->unwrap weakmap and can

apply the built-in set to the built-in map. The equivalent demonstration can be made with Map.prototype.set.call(mapProxy) (same as before without the initial mapProxy.[[handler]].get)

My point is that the invoke trap doesn't add something to the isolated graph use case. For the non-membrane proxy use case... I don't know what to think anymore...

# Tom Van Cutsem (12 years ago)

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

Le 09/06/2013 12:50, David Bruant a écrit :

Le 09/06/2013 11:37, Tom Van Cutsem a écrit :

For full transparency across isolated object graphs, I think membranes are still the way to go.

I don't understand that part. A membrane (with shadow targets?) will have the same problem as a proxy alone when it comes to Map.prototype.set.call, no?

I think I understand what you meant. I think you meant that the outside of the membrane never interacts with built-ins on membrane objects, but rather on membrane-wrapped versions of the built-ins; the actual built-in acting on the actual Map or actual Date happens inside the membrane.

Yes, that's what I meant.

If that's what you meant, I feel that it equally applies to method invocation. That is, if you have a wrapped map, it has a wrapped Map.prototype and mapProxy.set(x, y)

calls mapProxy.[[handler]].get which returns the wrapped 'set' method. This wrapped 'set' method has its 'apply' trap called. In the apply trap, the target is the actual Map.prototype.set, thisArg is a wrapped map. the apply trap has access to some wrapped->unwrap weakmap and can apply the built-in set to the built-in map. The equivalent demonstration can be made with Map.prototype.set.call(**mapProxy) (same as before without the initial mapProxy.[[handler]].get)

Indeed. The point is that membranes wrap the built-in functions so that they do the proper unwrapping.

My point is that the invoke trap doesn't add something to the isolated graph use case. For the non-membrane proxy use case... I don't know what to think anymore...

Yes, and I already agreed to that upstream: the only thing that invoke() adds is that it makes it easy and efficient to get the common case right:

proxiedDate.getTime() // works because "invoke" trap can efficiently rebind |this| from proxiedDate to the real Date instance when forwarding

The invoke() trap does not come into play for dot-called methods, so the following remains non-transparent:

Date.prototype.getTime.call(proxiedDate) // error: not a Date

# Tom Van Cutsem (12 years ago)

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

Le 09/06/2013 11:37, Tom Van Cutsem a écrit :

The question is whether there's a strong need for intercepting these operations. It implies a pretty strong growth of the handler API. And it's not sufficiently general-purpose to also work for exotics that are defined outside of ES6 (e.g. DOM objects).

I can live with DOM objects not being fully transparent (since they were designed a while ago without proxies in mind). If it were too complicated because they're legacy, Date and RegExp could be given up on too (I think they're doable though), but I feel all other ES6 objects should be fully transparent. They have no good reason not too.

To me they are all the same, and a good solution should be applicable to any exotic object, whether defined in ES6 or not. What would be gained by making e.g. ES6 Maps transparent, but not DOM NodeLists?

Frankly I don't think a good generic solution exists. There is an inherent trade-off between transparency (which requires "leaking" the |this|-value) and encapsulation (which wants to protect the |this|-value).

# Jason Orendorff (12 years ago)

On Sun, Jun 9, 2013 at 1:28 PM, David Bruant <bruant.d at gmail.com> wrote:

For the non-membrane proxy use case... I don't know what to think anymore...

I'm not sure what we're trying to do is meaningful in the non-membrane case. The properties we would like to have are:

  • Security - A proxy can wrap an arbitrary object, such that code that has a reference to the proxy can't obtain the wrapped object.

  • Transparency - Given an arbitrary object, a proxy can be created that's indistinguishable from the wrapped object.

But how do we define "indistinguishable"? I can imagine an adversarial game: as the challenger, you give me an object. I create a wrapper around it. Then I hand you either the original object or the wrapper, and your challenge is to figure out which one I've given you. But to make the game nontrivial, you shouldn't be allowed to use object identity (that is , ===), and I don't see how to formalize that rule.

In the membrane case, I think it is possible: instead of handing "you" an object to test, I run test code of your choice in some kind of isolated environment.

In the non-membrane case I don't know that there are any invariants to worry about breaking.