caoccao/Javet

JavetProxyConverter and instanceof not working

Closed this issue · 8 comments

When using the JavetProxyConverter, any instanceof call of proxied Java classes fails.
Simple example that throws an error:

final var javetProxyConverter = new JavetProxyConverter();
v8Runtime.setConverter(javetProxyConverter);

final var globalObject = v8Runtime.getGlobalObject();
globalObject.set("URL", URI.class);

v8Runtime.getExecutor(
  // language=javascript
  "let url = new URL('https://example.com');\n"
    + "if (!(url instanceof URL)) {\n"
    + "    throw new Error('url is not instance of URL');\n"
    + "}"
).executeVoid();

Thank you for raising this issue. It is not supported, so far. You may use Class.isAssignableFrom() instead.

But, that is technically doable and could be a feature request.

Update

I just reviewed the source code. It's technically impossible to make instanceof work because the right hand is a Proxy and the left hand cannot be a Proxy. So, the result is always false.

Why cannot the left hand be a Proxy? V8 is designed to recursively traverse the proxied object for its original prototype. If the underlying API always returns a Proxy, V8 will hang because of OOM (not stack overflow).

So, please try the workaround in my previous comment. Please let me know if you have any questions.

The instanceof is part of a third-party library. I cannot modify the source.

I'm a bit confused about the proxy thing.
The script runs in V8 Mode (I have missed this info in my initial post), so URL does not exist.
I tried to "polyfill" the URL with globalObject.set("URL", URI.class);.
Now I can do something like new URL(..) in JS. and this creates a java URI object?!
In my expectation, the left side is a proxy to Javas URI class?

Currently, I have worked around by implementing a JS based polyfill URL class.

I suggest you review this slide for how things work behind the scene in Javet. Actually, the one you create in V8 is a Proxy. The pseudo logic in V8 is as follows:

function getPrototypeOf(obj) {
  while (obj is proxy) {
    obj = obj.target;
  } 
  return obj.prototype;
}

As the object created by Javet is a proxy, the prototype is also a proxy. In your case, that is Proxy(URL.class). That while loop will never end. That causes OOM in V8.

I suggest you review this slide for how things work behind the scene in Javet. Actually, the one you create in V8 is a Proxy. The pseudo logic in V8 is as follows:

function getPrototypeOf(obj) {
  while (obj is proxy) {
    obj = obj.target;
  } 
  return obj.prototype;
}

As the object created by Javet is a proxy, the prototype is also a proxy. In your case, that is Proxy(URL.class). That while loop will never end. That causes OOM in V8.

instanceof运算符右侧是proxy似乎也能正常工作,只需将右侧的proxy对象设置一个prototype属性,然后将此属性加入到实例对象的原型链中

let U = function(){}
let p = {
  constructor: U
}
U.prototype = p
let c = new Proxy(U, {});

let r = Object.create(p);

console.log(r instanceof c);//true

Hi @tristanlins, good news! I was inspired by @aiselp 's comment and spent a week investigating this issue. It's now supported at the dev branch. This new feature passes the following assertions.

v8Runtime.getGlobalObject().set("StringBuilder", StringBuilder.class);
assertTrue(v8Runtime.getExecutor("StringBuilder instanceof StringBuilder;").executeBoolean());
assertTrue(v8Runtime.getExecutor("new StringBuilder() instanceof StringBuilder;").executeBoolean());
assertTrue(v8Runtime.getExecutor("Object.getPrototypeOf(new StringBuilder()) === Object.getPrototypeOf(StringBuilder);").executeBoolean());
v8Runtime.getGlobalObject().delete("StringBuilder");

That's a really tough issue. You may try the latest snapshot build and let me know if you have any questions. If my works pleases you, please consider Buy Me a Cup of Coffee at the donation link. Thank you.

aiselp commented

@caoccao 感谢作者辛苦研究,上面的test包含以下内容或许更符合js标准

v8Runtime.getGlobalObject().set("StringBuilder", StringBuilder.class);
assertTrue(v8Runtime.getExecutor("new StringBuilder() instanceof StringBuilder;").executeBoolean());
assertTrue(v8Runtime.getExecutor("Object.getPrototypeOf(new StringBuilder()) === StringBuilder.prototype;").executeBoolean());
assertTrue(v8Runtime.getExecutor(
"let a = new StringBuilder() \n"+
"StringBuilder.prototype.abc= function(){
retun this === a
};\n"+
"a.abc()"
).executeBoolean());
v8Runtime.getGlobalObject().delete("StringBuilder");

Object.getPrototypeOf(StringBuilder)
should retun null or Function.prototype

Nice catch. There's a flaw in the implementation. It's now corrected as follows.

v8Runtime.getGlobalObject().set("StringBuilder", StringBuilder.class);
assertTrue(v8Runtime.getExecutor("new StringBuilder() instanceof StringBuilder;").executeBoolean());
assertTrue(v8Runtime.getExecutor("Object.getPrototypeOf(new StringBuilder()) === StringBuilder.prototype;").executeBoolean());
assertFalse(v8Runtime.getExecutor("Object.getPrototypeOf(new StringBuilder()) === Object.getPrototypeOf(StringBuilder);").executeBoolean());
assertFalse(v8Runtime.getExecutor("StringBuilder instanceof StringBuilder;").executeBoolean());
v8Runtime.getGlobalObject().delete("StringBuilder");