Bug 133531 - REGRESSION: Can't change the prototype of the lexical global object (changes the proxy prototype instead)
Summary: REGRESSION: Can't change the prototype of the lexical global object (changes ...
Status: RESOLVED WONTFIX
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: 528+ (Nightly build)
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Nobody
URL: http://test.saurik.com/apple/protocha...
Keywords:
Depends on:
Blocks:
 
Reported: 2014-06-04 19:23 PDT by Jay Freeman (saurik)
Modified: 2014-06-09 13:45 PDT (History)
4 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Jay Freeman (saurik) 2014-06-04 19:23:32 PDT
To put this upfront: this issue affects JavaScriptCore on iOS 8.0 and Mac OS X 10.10. The functionality in question works on iOS 2-7 and OS X 10.5-9. I was told by an Apple engineer at WWDC to file a bug here and start the summary with "REGRESSION". I attempted to replicate this issue in a browser, and it seems my attempt "fails" on older versions (iOS 6/7, OS X 10.8/9), but I could see many (maybe silly/wrong) reasons why this might be different.

So, here's an example interaction (using a JavaScript REPL that runs everything through JavaScriptCore). I am going to assign the prototype of the global object and then walk to a variable. My REPL prints objects using JSObjectCopyPropertyNames, but I have also provided code using a for/in loop to get the same data into an array (note the REPL shows the array as an object). Note that I am using the global object: the scenario works for other objects.

I have filed two separate bugs because I was asked to do so by Oliver. This bug is regarding a ReferenceError that results attempting to walk prototypes at global scope.

Here is the working behavior from the old version of JavaScriptCore:

cy# a = {}; this.__proto__ = a; a.f = 5; this
{a:{f:5},f:5}
cy# this.f
5
cy# f
5

Here is the broken behavior from the new version of JavaScriptCore:

cy# a = {}; this.__proto__ = a; a.f = 5; this
{a:{f:5}}
cy# this.f
5
cy# f
ReferenceError: Can't find variable: f

FWIW, if there is some different way of doing this, if this was never supposed to have worked, etc. I would be more than happy to be told "do something different". I don't see myself why this shouldn't work, however, and I've been doing this without issue now on JavaScriptCore for over five years.
Comment 1 Geoffrey Garen 2014-06-05 12:33:08 PDT
I wrote this test page to try to replicate what you described in runnable code:

<script>
a = {};
this.__proto__ = a;
a.f = 5;
console.log(f);
</script>

What I found was that older versions of Safari, and the latest version of Safari, both throw a ReferenceError on the last line, because "f" is not in lexical scope.

The reason this happens is that "this" is a forwarding proxy for the lexical global object, and when you change the prototype of "this", rather than changing the prototype of the lexical global object, you disconnect the forwarding proxy from the lexical global object's prototype.

The reason you didn't notice this behavior using the JavaScriptCore API outside the WebKit context is that the forwarding proxy behavior used to be built into WebKit, and now it is built into JavaScriptCore.

Another way to install a bag of properties onto the lexical global object is this:

function installProperties(bag)
{
    for (var p in bag)
        this[p] = bag[p];
}
Comment 2 Jay Freeman (saurik) 2014-06-05 23:06:23 PDT
Geoffrey,

Thanks. That code is very similar to the html file I attached as the URL for this bug (I had mentioned I tried this behavior in Safari and it seems to have always done this).

What I am actually doing in my production code is using JSContextGetGlobalObject to get the global object, and then using JSObjectGetPrototype in a loop to find the top-most prototype, and finally using JSObjectSetPrototype on the final not-NULL result. I can appreciate the idea that "this" is not the same as the lexical global object, but JSContextGetGlobalObject should be the actual global object, no?

Is there some other way I can affect the prototype of the global object? Just copying properties is not viable... the prototype that I install is a custom JS class that implements getProperty to do lookups against an external property source. (Also, one of the main use cases of my code is to be "installed" onto an existing JSGlobalContextRef, which is part of the reason I haven't just changed the global object.)
Comment 3 Jay Freeman (saurik) 2014-06-06 00:47:05 PDT
I guess a different question I could be asking: how am I able to get an actual legitimate reference to the global object, at the JavaScriptCore API level, if JSContextGetGlobalObject returns a JSProxy? It at least used to be the case that I could, even in the context of WebKit, call JSContextGetGlobalObject on the JSGlobalContextRef for a WebFrame (retrieved from a web page being rendered in a view), modify its prototype using JSObjectSetPrototype, and then have my extended features available to the executing JavaScript context. Why is JSContextGetGlobalObject, not code running "this" but the low-level C API, returning a proxy rather than the actual global object? :(
Comment 4 Geoffrey Garen 2014-06-09 13:45:03 PDT
> What I am actually doing in my production code is using JSContextGetGlobalObject to get the global object, and then using JSObjectGetPrototype in a loop to find the top-most prototype, and finally using JSObjectSetPrototype on the final not-NULL result.

Can you post a test case that does this, and demonstrates the problem? It looks like the html test case you posted demonstrates some other behavior, which is intentional and not harmful to your app.