alicorn-systems/v8-adapter

Conversion of Java Maps and List to JS objects and arrays.

Closed this issue · 4 comments

This is one feature of Rhino, which is still missing in current project - ability to access Java maps and lists as JS objects and JS arrays when they are injected in V8 runtime.

Right now, after implementing this pull request, JS objects could be read as Java maps. Vice Versa conversion would be nice as well.

It could be easily done, but there are some issues related to backward compatibility and expected behaviour: currently when map is injected in JS runtime - it provides .get()/.put() api. Thus:

  • Behaviour of providing java api of Map and List objects could be desired/expected behaviour by the JS script.
  • It might be not desired/expected behaviour, but current api is still used by some existing script.

Thus keeping .get()/ .put() api is the needed as well as ability to access data in JS-object way. E.g. following should work:

  • injectedMap.get("myKey")
  • injectedMap.myKey
  • injectedMap["myKey"]

Possible solutions:

  1. Do not inject java map instance, but convert it to js object. Define .get(), .put(), .putAll(), .contains(), etc. method. Must be done per instance.
  2. Keep current approach of injecting objects and for the every key use "defineProperty" to emulate JS object api. Must be done per instance.
  3. Keep current approach of injecting objects and wrap it in the Proxy . Could be done per class.
  4. Do both: inject java map and convert java object to js Object. Set injected object as prototype of js object. Finally return js object.

Options 1 and 2 looks a bit complicated and not the best from performance point of view if map has big amount of entries and JS access only one or two of them.
Option 4 looks the easiest to implement, but it must be read-only JS object at the end, which is not matching all the use-cases.
Thus option 3 looks most easy and reliable. The only complicated part - is compatibility with v8 / j2v8 version. Shortly: we need to use newer version of it.

@crahda what do you think about this issue in general and the preferred approach to implement it?

The only problem with Proxy , that I see now is version of V8 engine and how it's comparable with j2v8 version. Proxy was added in V8 version 4.9.

Current latest version of j2v8 is 4.8.2 and it contains v8 version 5.4.500.45, which more then enough.
The problem, that I see it's available only for Android and Linux (correct me if I am wrong),

The latest version, which is also available for mac is 4.6.0. But the problem, that it for some unknown reason contains different v8 version inside:

  • For android it contains 4.10.253 where Proxy is available.
  • For mac it contains 4.6.85.31 where Proxy is not yet available.

To tell the truth I am not anymore confident with running JUnit test with pure java and mac jar of j2v8 anymore. So I am thinking of moving my tests to Android tests and run them with android aar.

Alternative would be to re-build j2v8 for other supported platforms - I see, that you have profiles for windows, linux and mac.

@crahda, have you ever tried to build j2v8 recently? I have seen pull request from you in the past. If we will be able to build it - what is the good place to store such jars (besides the bundling them in the lib folder of this project)? I was using JitPack for java projects, but not sure what are the options for jars with native code inside.

caer commented

Our discussion in #3 has some impact on this (particularly in reference to the Proxy APIs). I have built J2V8 in the past, and it isn't too bad (we can use containers for cross-platform builds in most cases).

When shipping native libraries (.so, .dll, and .dylib) with Java applications, I typically do one of two things:

  1. Embed the native library in the Jar. Maven can do this very easily with assembly files, and I'm sure Gradle can do it as well. The library is then loaded from the class path at runtime when needed. This is a very common solution.

  2. Expect the native library to be present in some configurable folder on the system running the application. This is usually only appropriate when the application code is being shipped as one installation (e.g., for an app on a users computer or a distributable meant to be installed by a package manager). This is how I address this issue for bigger cross-platform projects. This would be a cleaner solution if we want to decouple libraries from the application source, but it would force users to either build them themselves or for us to provide the libraries as separate downloads (possibly via GitHub releases).

Going back to your previous comment about exposing maps with custom operators: I have an idea. What if we exposed a generic API in the adapter for "overloading" certain operators on injected classes, or making those classes appear as certain data types? For example, we could have the methods:

public void injectClassAsObject(Map.class, (map, key) -> map.get(key), (map, key, value) -> map.put(key, value));

public void injectClassAsArray(ArrayList.class, (list, index) -> list.get(index), (list, index, value) -> list.set(index, value));

Essentially, the relevant methods would allow users to inject classes and have J2V8 represent them as certain JS types. When the appropriate operator (e.g., map[key] or list[index]) are invoked, the appropriate lambda will run. It's just an idea and would need more work, but what do you think?

@crahda thanks for the sharing your experience of distributing native libs. Also I fully agree, that proper solution would be easy to find after implementing the fork (#3).

Concerning your idea of "overloading" certain operators on injected classes: that's sounds indeed nice.
As for me, one of the option also might be: if object has some method itself - use this methods if not - provide them as lambda. E.g. similar how Comparable and Comparator are used in Java collections.
Analogue of Comparator would be the lambda and analogue of Comparable could be Map, etc.

The only this, which is not very clear for me - is how to deal with returned value in the methods of injected objects.In fact it's the issue I am facing with right now.

E.g. Currently it does V8JavaObjectUtils.translateJavaArgumentsToJavascript(), which internally calls V8JavaAdapter.injectObject() for non-primitives.

I was thinking of adding some kind of annotation, but that looks a bit complicated. That's the reason why I though, that using Proxy for every injected map/list is a good and easy solution, which covers this use-case as well.

P.S. I have implemented it now as pull request #12. The api is added conditionally if Proxy is available in the V8 runtime so there should be no issues with backward compatibility.

caer commented

@AlexTrotsenko Please take a look at my response in #13 and tell me what you think!