graal icon indicating copy to clipboard operation
graal copied to clipboard

Provide a sourceTypeMapping() method that provides the equivalent of HostAccess.Builder.targetTypeMapping() for mapping of Java types to guest types

Open mikethayes opened this issue 2 years ago • 8 comments

Feature request

Please include the following information:

Is your feature request related to a problem? Please describe.

When consuming a Java API in a guest context (for this request, I'm going to assume JavaScript is the guest) you can often be forced in writing a lot of glue code to to convert some Java objects to guest Value types.

Assume that you have a Java class with methods that return objects of a given type. You know that you want to treat that Java type as a specific guest type when called from the guest context.

Right now you have to write a wrapper class that intercepts the java call to convert the object to the known guest value type. For Java classes that have a large number of known methods

For my scenario, this arose when I was working with a class that had multiple methods that returned a CompletableFuture. I needed to convert the CompletableFuture to a Thenable object along the lines described in Asynchronous polyglot programming with Java and JavaScript on GraalVM) So I had to provide a bridge class to convert each CompletableFuture returning call to return a Value/Promise, while all other methods were just pass-through calls.

While this is a specific use case, you can see how the issue would arise when consuming any java framework API, particularly if it wasn't written with embedded polyglot programming in mind

For scenarios where you know the exact type of mapping that should occur when passing over the Java to guest threshold, it would make a lot of sense to have this conversion happen via a custom source type mapping, in much the same way as you get a conversion going in the opposite direction via the target type mapping feature

Describe the solution you'd like. In HostAccess.Builder, provide a sourceTypeMapping() method that provides the equivalent of HostAccess.Builder.targetTypeMapping(), except that it provides a mapping of Java types to guest types

Describe who do you think will benefit the most. I think this feature would be of most benefit to GraalVM users who are working with existing framework and who need to use these frameworks from a guest context. Without some form of source type mapping they potentially face having to write verbose wrappers around the existing framework classes purely to modify the return types of certain methods and these wrapper classes will be fragile, repetiive and hard to maintain.

A source type mapping makes a lot of sense when you know the conversion that should happen for a given Java type ahead of time.

Describe alternatives you've considered.

  • I attempted to achieve this implicit mapping using a Java dynamic proxy but it wasn't possible - ultimately it was not workable.
  • There is an alternative way (for ES6) by implmenting traps for get and apply on the object, but it involves a lot of reflection on both the Java and JavaScript code and is not always applicable
  • The only reliable way to achieve this is to write a bridge that does the type conversion for the types you want to intercept and pass through the rest of the calls. This approach is repetiiv

Additional context. Add any other context about the feature request here. For example, link to the relevant projects, documentation, standards.

Express whether you'd like to help contributing this feature I would like to assist in implementing this feature if possible in whatever capacity I can

mikethayes avatar May 03 '22 08:05 mikethayes

For reference, here is the original discussion around the targetTypeMapping feature: https://github.com/oracle/graaljs/issues/94

mikethayes avatar May 03 '22 08:05 mikethayes

Thank you for your suggestion, we will take a look into it and get back to you

oubidar-Abderrahim avatar May 06 '22 15:05 oubidar-Abderrahim

If you need more context on this @chumer and I discussed this at length before filing, so he might be able to explain the use case in more detail

mikethayes avatar May 06 '22 15:05 mikethayes

Any updates on this matter? @mikethayes any suggestions on dealing with this sort of necessity? At our project we had full control over method output convertion using Rhino and as a result we have quite a few classes that needs modification to work with Graal. We successfuly managed to implement via reflection a proxy to handle method calls but couldn't use the constructors as it were possible in rhino, using Java.type instead solved the constructor issue but now we're not able to intercept methods output conversion.

gabrielmattar avatar Oct 04 '23 19:10 gabrielmattar

Unfortunately, we didn't get to work on this yet.

@gabrielmattar can you share the pattern you were using here? Maybe a rough sketch of the proxy pattern you are using?

chumer avatar Oct 05 '23 10:10 chumer

Sure! @chumer

We used some internal rhino classes as a base to our implementation, such as this one to emulate a java class and this one to emulate java objects

So mapping to Graal classes we ended up with:

public class JavaClassProxy extends JavaObjectProxy implements ProxyExecutable {

    @Override
    public Object getMember(final String name) {
        final Methods result = this.staticMethods.get(name);
        if (result != null) {
            return result;
        }

        throw this.members.reportMemberNotFound(name);
    }

    @Override
    public Object execute(final Value... args) {
        if (this.constructor != null) {
            // This Methods class trys to find the correct method to invoke, 
            // performs type convertions and invoke the method. 
            // The class also maps the return value using a centralized class 
            // with the purpuse of converting back to a Value for Graal.
            return this.constructor.invoke(args);
        }
        // Throws erros...
    }
}

The major problem we faced using this approach in Graal is that we were not able to use the "new" operator in guest language javascript. Suppose a ProxyObject that returns a instance of JavaClassProxy to js such as:

let fileUtil = ... // proxyObject that provides instances of JavaClassProxy;
let zipFile = fileUtil.ZipFile; // ZipFile is as instance of JavaClassProxy;

let instance = zipFile(); // Currently this is how we invoke the constructor
let instance2 = new zipFile(); // In rhino we were able to use like this

Question: is it possible to enable the use of the "new" operator? Maybe by returning something in the hasMember method or getMember method?

If you want more details I can share with you the full code.

gabrielmattar avatar Oct 05 '23 14:10 gabrielmattar

Actually I managed to enable the use of the new operator with the following signature:

public class JavaClassProxy extends JavaObjectProxy implements ProxyInstantiable {

For now we're problably sticking with this approach instead of using Java.type..

gabrielmattar avatar Oct 06 '23 13:10 gabrielmattar

My teams could make great use of this feature. Similar to OP all of our Java methods return an asynchronous object - RxJava objects in our case (Single/Maybe/Completable). So for every Java class meant to be called from the guest language we need a wrapper class to convert from RxJava to Promise. Being able to consolidate this all in once place would be wonderful.

zikifer avatar May 13 '24 13:05 zikifer