Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve NativeJavaList to behave more like NativeArray #830

Closed
wants to merge 16 commits into from

Conversation

rPraml
Copy link
Contributor

@rPraml rPraml commented Jan 25, 2021

This PR extends the NativeJavaList to behave more like NativeArray.

Some ideas are extracted from #827

@tuchida maybe you can take a look at this.

listD.add(1.0);
listD.add(2.0);
listD.add(3.0);
assertEquals("1", runScriptAsString("value.indexOf(2)", listD));
Copy link
Contributor

@tuchida tuchida Jan 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this calling Java's indexOf method?

https://docs.oracle.com/javase/8/docs/api/java/util/List.html#indexOf-java.lang.Object-
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf

They differ in the following ways.

  • JS can receive fromIndex as the second argument.
var list = new java.util.ArrayList();
list.indexOf(1, 2);
// js: Can't find method java.util.ArrayList.indexOf(number,number).
  • The equivalence check is different.
list.add(NaN);
list.indexOf(NaN); // 0

[NaN].indexOf(NaN); // -1

https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#equals-java.lang.Object-

There may be compatibility issues. Sorry, mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, value.indexOf will call the Java's indexOf method.
Array.indexOf(value,...) will call the JS indexOf method.

The question is, what to do, if a method is defined by Java and also by the JS-Array-prototype. Which one should we prefer?
Logically, the method in NativeJavaList will override the method of the prototype.

The next question is, what shoud happen if you wrap an instance, that declares other methods:

public class MyList extends ArrayList {
    public String join() {
       ...
    }
}
list1 = new ArrayList();
list2 = new MyList();
runScriptAsString("value.join('-')", list1); // call js_join
runScriptAsString("value.join('-')", list2); // will call MyList.join - but arguments will not match

@tuchida
Copy link
Contributor

tuchida commented Jan 25, 2021

In Rhino, all methods of an instance can be accessed.

// java
public static List getList() {
    return new ArrayList();
}
// js
var list = Foo.getList();
// ArrayList method, not a List method.
list.forEach;
// function forEach() {/*
// void forEach(java.util.function.Consumer)
// */}

When calling Java's methods against the return value of getList, it is safe to use only the List's methods. However, Java methods have priority, so if you use JS's methods, you need to avoid ArrayList's methods. If getList is changed to return a List implementation that is not an ArrayList, then you may not be able to call the JS's methods.

@tuchida
Copy link
Contributor

tuchida commented Jan 25, 2021

I think it would be better to implement Symbol.iterator and convert it with Array.from to avoid problems. Like NodeList.
https://developer.mozilla.org/en-US/docs/Web/API/NodeList

@rPraml
Copy link
Contributor Author

rPraml commented Jan 25, 2021

When calling Java's methods against the return value of getList, it is safe to use only the List's methods.
However, Java methods have priority, so if you use JS's methods, you need to avoid ArrayList's methods.
If getList is changed to return a List implementation that is not an ArrayList, then you may not be able to call the JS's methods.

On the one hand I think, that JS methods should have always priority, maybe we should not inherit from NativeJavaObject at all??
On the other hand, implementing a method in a Java class (e.g. join()) is like overloading in JS.

I think it would be better to implement Symbol.iterator like NodeList and convert it with Array.from to avoid problems.
https://developer.mozilla.org/en-US/docs/Web/API/NodeList

Hmm, you mean:

var javaList = new java.util.ArrayList()
var jsList = Array.from(javaList)

This means, that changes on jsList are not reflected to javaList
I can/will implement iterator in a separate PR

@tuchida
Copy link
Contributor

tuchida commented Jan 25, 2021

If it don't prioritize Java methods, it may run into problems with code written in the past.
However, whichever it prioritize, it will behave strangely in corner cases as long as the names conflict.

private List<Object> list;

@SuppressWarnings("unchecked")
public NativeJavaList(Scriptable scope, Object list) {
super(scope, list, list.getClass());
assert list instanceof List;
this.list = (List<Object>) list;
setPrototype(ScriptableObject.getClassPrototype(scope, "Array"));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it possible to invoke all/most NativeArray mehtods also an NativeJavaList
Is this a good/acceptable change?

@@ -39,6 +42,13 @@ public boolean has(int index, Scriptable start) {
}
return super.has(index, start);
}

public void delete(int index) {
if (isWithValidIndex(index)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete value[0] does work, but will return false as has(0,start) will still return true

Should we maintain a boolean[] array to store all undefined values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is a tradeOff, I would not add this complexity to NativeArray to simulate "empty slots"

}
if (y instanceof Wrapper) {
y = ((Wrapper) y).unwrap();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unwrap here was neccessary to make 'includes' work.
I can place the unwrap also here: https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/NativeArray.java#L1766

@@ -3372,6 +3378,12 @@ private static boolean eqString(CharSequence x, Object y)
}
public static boolean shallowEq(Object x, Object y)
{
if (x instanceof Wrapper) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// FIXME: This will invoke the java.util.List.indexOf method, which
// is not yet type aware!
// assertEquals("1", runScriptAsString("value.lastIndexOf(2)", listI));
// assertEquals("-1", runScriptAsString("value.lastIndexOf(4)", listD));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to write a test for each method defined by the Array prototype.
There are some methods defined by the wrapped object (java.util.List). These methods have precedence.

listD.add(1.0);
listD.add(2.0);
listD.add(3.0);
assertEquals("1", runScriptAsString("value.indexOf(2)", listD));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, value.indexOf will call the Java's indexOf method.
Array.indexOf(value,...) will call the JS indexOf method.

The question is, what to do, if a method is defined by Java and also by the JS-Array-prototype. Which one should we prefer?
Logically, the method in NativeJavaList will override the method of the prototype.

The next question is, what shoud happen if you wrap an instance, that declares other methods:

public class MyList extends ArrayList {
    public String join() {
       ...
    }
}
list1 = new ArrayList();
list2 = new MyList();
runScriptAsString("value.join('-')", list1); // call js_join
runScriptAsString("value.join('-')", list2); // will call MyList.join - but arguments will not match

@rPraml
Copy link
Contributor Author

rPraml commented Jan 25, 2021

If it don't prioritize Java methods, it may run into problems with code written in the past.

My bad. Changes in this way should not be done.

However, whichever it prioritize, it will behave strangely in corner cases as long as the names conflict.

In most cases, there should be no conflicts, as in most cases, ArrayLists are used. And if there are conflicts, it "works as designed", respectively, this should be specified as "design"

BTW: Sry. I assume most of my "review" comments were not yet visible for you, because they were "pending"

@gbrail
Copy link
Collaborator

gbrail commented Apr 15, 2021

I'm sorry -- I've lost track of what we collectively want to do with this collection of this PR. @rPraml I am hoping that we can still resolve this but I'm not sure where you left it on the review comments. Do we still want to pursue this?

@p-bakker p-bakker added the Java Interop Issues related to the interaction between Java and JavaScript label Jun 30, 2021
@rPraml
Copy link
Contributor Author

rPraml commented Sep 7, 2021

@gbrail I updated this PR (I had not much time in the past)
Not much is left, because many smaller changes are already implemented (Thanks to @tuchida and others)
So what's left:

  • Fixed ScriptRuntime.sameZero und ScriptRuntime.shallowEq when a Wrapper is passed (in my case a wrapped integer was compared with a primitive in Array.indexOf)
  • NativeJavaList does a setPrototype(ScriptableObject.getClassPrototype(scope, "Array")), so all JavaScript Array methods works also on a java.util.List.
    Note: If there is a name clash between Array prototype and java.util.List (e.g. Array.indexOf() and java.util.List.indexOf()) the java method is called. See here
    This is required to be backward compatible.
  • NativeJavaList has methods to set length and to delete entries (delete javaArr[0])

@@ -17,6 +18,7 @@ public NativeJavaList(Scriptable scope, Object list) {
super(scope, list, list.getClass());
assert list instanceof List;
this.list = (List<Object>) list;
setPrototype(ScriptableObject.getClassPrototype(scope, "Array"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you are using instanceof to determine if it is an array?

if (value instanceof Array) {
  // some code to operate `value`
}

How about Array.isArray?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Tuchida, value instanceof Array will return true, while Array.isArray(value) will return false.

I get different results in firefox

class inheritance

class Foo extends Array {};
var y = new Foo();
y instanceof Array; // returns true
Array.isArray(y); // returns true

vs. prototype

var Bar = function(){};
Bar.prototype = Array.prototype;
var x = new Bar();
x instanceof Array; // returns true
Array.isArray(x); // returns false

What do you think, is ok, when we return true for instanceof and false for isArray?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think javaList instanceof Array should be false.

Java's List and JavaScript's Array are different, so their detailed behaviors are different.(For example, .indexOf)
If the existing code is written as follows, a List will be passed to the code that is expecting an Array.

if (value instanceof Array) {
  // some code to operate `value`
}

I think it may cause compatibility issues.

@rPraml
Copy link
Contributor Author

rPraml commented Sep 10, 2021

@tuchida I made JavaList/JavaObject available as prototypes and made some fixes in class inheritance, so all of them is true:

var x= new java.util.ArrayList();
x instanceof JavaList; 
x instanceof JavaObject; 
x instanceof Object;
x instanceof java.util.List

To add the array functions, I can define them now on the JavaList

Object.getOwnPropertyNames(Array.prototype).forEach(function(x) { JavaList.prototype[x] = Array.prototype[x]})

I think this would be the nicest solutuion, when someone wants to handle JavaList as an array.

What do you think about that change?

@p-bakker
Copy link
Collaborator

Haven't followed along here, but you might want to look at how GraalJS handles Java List interoperability, if only to see how they solved things

Copy link
Contributor

@tuchida tuchida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To what extent do you want Java's List and JS's Array to be the same?
If we make such a change, it would be better to discuss the specification before implementing it.
For example, as p-bakker commented, I think there are issues that should be considered for the specification, such as investigating how GraalJS and Nashorn are doing.


public Object call(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
throw new UnsupportedOperationException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an EcmaError and will cause the process to terminate.

js> new JavaMap()
Exception in thread "main" java.lang.UnsupportedOperationException: Function not constructable
	at org.mozilla.javascript.NativeJavaObject$1.call(NativeJavaObject.java:79)
	at org.mozilla.javascript.BaseFunction.construct(BaseFunction.java:385)
	at org.mozilla.javascript.ScriptRuntime.newObject(ScriptRuntime.java:2678)
	at org.mozilla.javascript.gen._stdin__8._c_script_0(Unknown Source)
	at org.mozilla.javascript.gen._stdin__8.call(Unknown Source)
	at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:380)
	at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3872)
	at org.mozilla.javascript.gen._stdin__8.call(Unknown Source)
	at org.mozilla.javascript.gen._stdin__8.exec(Unknown Source)
	at org.mozilla.javascript.tools.shell.Main.processSource(Main.java:497)
	at org.mozilla.javascript.tools.shell.Main.processFiles(Main.java:181)
	at org.mozilla.javascript.tools.shell.Main$IProxy.run(Main.java:101)
	at org.mozilla.javascript.Context.call(Context.java:535)
	at org.mozilla.javascript.ContextFactory.call(ContextFactory.java:472)
	at org.mozilla.javascript.tools.shell.Main.exec(Main.java:163)
	at org.mozilla.javascript.tools.shell.Main.main(Main.java:138)


private List<Object> list;

static void init(ScriptableObject scope, boolean sealed) {
NativeJavaList obj = new NativeJavaList();
obj.exportAsJSClass(scope, "JavaObject", sealed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct to say that new JavaObject, JavaList, and JavaMap will be added to the global variables?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these three? There are many interfaces and classes in Java. For example, if you want to extend a Set or Stream with a prototype, do you have to add more JavaXXX each time? Why List instead of Collection?

If you have a problem you want to solve, it is better to fully discuss what specifications you want to use before implementing them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original problem was, that the Wrappers (JavaList / JavaMap) should provide the same functionality as the equivalent JS objects (Array/Object), so that I can use existing javaScript code that can work on the wrappers without modification. This goal has already been achieved very well by other PRs that improve List/Map handling.

I also took a look at GraalJS as suggested by @p-bakker
You can add objects with list[x] = value, but only if x points to the end of the list (x == list.length)

In this PR I think I've lost the focus.
I will investigate in that direction and will add only neccessary features to rhino

@@ -34,6 +34,8 @@

static void init(ScriptableObject scope, boolean sealed) {
JavaIterableIterator.init(scope, sealed);
NativeJavaObject obj = new NativeJavaObject();
obj.exportAsJSClass(scope, "Object", sealed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that JavaObject inherits from JS Object?
JS Object prototype defines valueOf, toLocaleString, etc. Are there likely to be compatibility issues with these?

@tuchida
Copy link
Contributor

tuchida commented Sep 11, 2021

I consider Java's List and JS's Array to be different things, so I think mutual conversion is sufficient.

var javaList = java.util.ArrayList([1, 2, 3]);
var jsArray = Array.from(javaList);

In JavaScript, there are Array-Like objects that look like Arrays but are not.
For example, arguments and NodeList.
If you want to use Array methods by these, it is customary to convert them using Array.from or Spread syntax.

Copy link
Contributor Author

@rPraml rPraml left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't followed along here, but you might want to look at how GraalJS handles Java List interoperability, if only to see how they solved things

I made some tests with GraalVM - I think this is the way we should also go in

> var ArrayList = Java.type("java.util.ArrayList");
> var List = Java.type("java.util.List")
> var Map = Java.type("java.util.Map")
> typeof ArrayList
function
> var list = new ArrayList();
> typeof list
object
> var arr = []
> typeof arr
object
> arr instanceof Object
true
> list instanceof Object
false
> list instanceof List
true
> list instanceof Map
false
> arr instanceof List
false

> Object.getPrototypeOf(list)
null

> Object.getOwnPropertyNames(arr)
["length"]
> Object.getOwnPropertyNames(list)
(31)["add", "remove", "get", "clone", "indexOf", "clear", "isEmpty", ...

> list[0] = 'a'
> list[1] = 'b'
> list[4] = 'c' // can only add element to the end
> list
(2)["a", "b"]
Array.prototype.includes.call(list, 'b')
true
Array.prototype.includes.call(list, 'c')
false

I will update the PR and remove unneccesary things


private List<Object> list;

static void init(ScriptableObject scope, boolean sealed) {
NativeJavaList obj = new NativeJavaList();
obj.exportAsJSClass(scope, "JavaObject", sealed);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original problem was, that the Wrappers (JavaList / JavaMap) should provide the same functionality as the equivalent JS objects (Array/Object), so that I can use existing javaScript code that can work on the wrappers without modification. This goal has already been achieved very well by other PRs that improve List/Map handling.

I also took a look at GraalJS as suggested by @p-bakker
You can add objects with list[x] = value, but only if x points to the end of the list (x == list.length)

In this PR I think I've lost the focus.
I will investigate in that direction and will add only neccessary features to rhino

@@ -33,7 +37,7 @@ public String getClassName() {
@Override
public boolean has(String name, Scriptable start) {
Context cx = Context.getCurrentContext();
if (cx != null && cx.hasFeature(Context.FEATURE_ENABLE_JAVA_MAP_ACCESS)) {
if (map != null && cx != null && cx.hasFeature(Context.FEATURE_ENABLE_JAVA_MAP_ACCESS)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be introducing the possibility of map being null.

@rPraml
Copy link
Contributor Author

rPraml commented Sep 17, 2021

I would close this PR and create a new one

…ist-length-support

# Conflicts:
#	src/org/mozilla/javascript/NativeJavaList.java
# Conflicts:
#	testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java
@rPraml
Copy link
Contributor Author

rPraml commented Nov 8, 2021

closing this PR - all changes are in other PRs

@rPraml rPraml closed this Nov 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Java Interop Issues related to the interaction between Java and JavaScript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants