graaljs
graaljs copied to clipboard
"this" value of function member of ProxyObject is incorrect depending on the way the function is called
For both myObject.myFunction() and myObject["myFunction"]() the value of this is myObject.
This is true both in Chrome and in GraalJS, when using JS objects.
However, when myObject is a ProxyObject only the latter works as intended while the former gets the global object as this.
The expected result would be that myObject.myFunction() also passes myObject as this, whether or not myObject is a ProxyObject.
Class to demonstrate the issue:
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyObject;
public class TestRep {
public static void main(String[] args) {
Context c = Context.newBuilder("js").build();
c.getBindings("js").putMember("jo", new ProxyObject() {
private Object m;
public void putMember(String key, Value value) { if ("mem".equals(key)) { m = value; } }
public boolean hasMember(String key) { return m != null && "mem".equals(key); }
public Object getMemberKeys() { return m != null ? new String[] { "mem" } : null; }
public Object getMember(String key) { return "mem".equals(key) ? m : null; }
});
c.eval("js",
"show = function(o, n) {" +
" console.log(``);" +
" console.log(n + `:`);" +
" o.mem = function() { console.log(this, ` / `, this.mem) };" +
" console.log(o, ` / `, o.mem); /* the expected object */" +
" o[`mem`](); /* expected: object */" +
" o.mem(); /* expected: object, incorrect for ProxyObject */" +
" x = o.mem;" +
" x(); /* expected: window/global */" +
" x = o[`mem`];" +
" x(); /* expected: window/global */" +
"};" +
"console.log(`expected: 3 x object, 2 x window/global`);" +
"show({}, `JS Object`);" +
"if (this.jo) show(jo, `ProxyObject`);");
}
}
The script separately to compare with e.g. Chrome:
show = function(o, n) {
console.log(``);
console.log(n + `:`);
o.mem = function() { console.log(this, ` / `, this.mem) };
console.log(o, ` / `, o.mem); /* the expected object */
o[`mem`](); /* expected: object */
o.mem(); /* expected: object, incorrect for ProxyObject */
x = o.mem;
x(); /* expected: window/global */
x = o[`mem`];
x(); /* expected: window/global */
};
console.log(`expected: 3 x object, 2 x window/global`);
show({}, `JS Object`);
if (this.jo) show(jo, `ProxyObject`);
Your test-case is a bit tricky despite it looks relatively simple at the first sight. I will try to explain what (I believe) is going on here.
I will start with repeating two sentences that I wrote into your last issue: The ability to pass this as an additional argument during function invocation is specific to JavaScript. On the other hand, the Polyglot API (org.graalvm.polyglot) is supposed to be language agnostic.
When JavaScript code evaluates o.mem(), it realizes that o is not a JavaScript object. So, it does not know what exactly the invocation of its mem method should do. Hence, it delegates the invocation to o. o being ProxyObject (that comes from Polyglot API) knows nothing about JavaScript-specific this argument of function invocation. So, it executes the function without passing this argument (= as if this was undefined).
When JavaScript code evaluates o['mem'](), it reads mem member of o. Accidentaly, o['mem'] is a JavaScript function (despite o is not a JavaScript object). Realizing this fact, JavaScript invokes this function with full JavaScript semantics (i.e. including this being set to o).
While you may like the behaviour of o['mem'](), one may argue that it should behave in the same way as o.mem(), i.e., that o['mem']() should delegate the invocation of mem to o (because it is its member; so o should the one who decides how the invocation is performed).
In other words, I am afraid that if the inconsistency (between o.mem() and o['mem']()) is going to be fixed, it will be in the opposite direction than the one you expected, i.e., this will be "broken" for o['mem']() as well.
Generally, I suggest you not to rely on language-specific stuff (like this in JavaScript calls) when doing interoperability with other objects/languages (if possible). If you really need this in such JavaScript function call then I suggest you to "export" (out of JavaScript code) a function that has this bound.
Thank you for getting back so quickly and in so much Detail. However, neither I, nor colleagues of mine that also looked into it, are sure we understand why the behavior should be that way. I have collected our thoughts on it, with some additional information:
Theory: So, do we understand correctly that "full JavaScript semantics" are intentionally not applied when the object containing the function is a ProxyObject? If so, we don't understand the reasoning behind it. We expected ProxyObjects to be treated just like normal/native JS objects, at least from the view of the script. Or is there a technical reason not to retrieve the function from the object and then call it with that object as "this"? If not, we see no advantage in not having the object as "this", while the disadvantage seems to be a loss of information.
Background: In PDFreactor we have implemented a JS environment that mimics browser behavior, except for interactive functionalities. This is currently based on a modified version of Rhino. We primarily expose our Java DOM to JS, which works well as we use W3C interfaces and only have to map those properly. We want to migrate this system to GraalJS.
Use-Case:
We're working on ProxyObjects as wrappers for Java objects to handle various mappings (setter/getter to member, add ProxyArray where applicable). The functionality related to this issue is as follows: We're implementing functionality that maps the Interfaces of the Java Objects to proto objects in JS. The goal is to put ProxyExecutables that wrap Java Methods into those prototype objects. So these ProxyExecutables require the "this" value to have the instance of the Java object to invoke the Method on. (This is similar to Chrome behavior. Placing the "append" function from a DOM Element into a variable and calling that causes an exception. But when put into an Element, or given one as "this" via "call" or "apply", it works correctly, on that Element.) When using o['mem']() this works fine. So we hope o.mem() can be adapted to do the same (maybe as an option).
So, do we understand correctly that "full JavaScript semantics" are intentionally not applied when the object containing the function is a ProxyObject?
You lose "full JavaScript semantics" because you leave the realm of JavaScript by using non-JavaScript objects like ProxyObject (provided by Polyglot API). While we do our best to handle non-JavaScript objects as close to JavaScript semantics as possible, there are inevitable deviations caused by the fact that Polyglot API is an API that is not JavaScript specific (it is a general API covering all kinds of languages) and as such does not support all JavaScript-specific subtleties.
Or is there a technical reason not to retrieve the function from the object and then call it with that object as "this"?
This suggestion is focused too much on your particular test-case. In general, the semantics of an invocation of a member of a non-JavaScript object may be completely different (for example, the corresponding member may not be readable). Hence, the invocation should be delegated to the receiver/target (as this object has the most information about the meaning/semantics of the invocation of its members).
Anyway, I don't want this issue to turn into a lesson on our Polyglot API and/or our design decisions (changes in this are too slow anyway). So, let's focus on what you really need to solve. As I mentioned, the root of the problem is your usage of JavaScript functions as members of ProxyObject. It is not clear what you are getting by not using pure JavaScript object (or JavaScript Proxy) instead. I believe that you can refactor your code a bit to achieve what you need.
For example, one simple change that may or may not be suitable for you is to wrap your ProxyObject by JavaScript Proxy (i.e. use show(new Proxy(jo, {}), "ProxyObject")). You will have to modify the debugging output of your test-case a bit (as new Proxy(jo, {}) cannot be converted to string because it is a JavaScript object that has neither toString() nor valueOf()) but you will get the right this for both o.mem() and o['mem'](), I believe. Of course, the direct usage of JavaScript Proxy (and the complete avoidance of ProxyObject) may be a more appropriate change in your case.