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

Can't find a way to use Value.as with generic types #8803

Open
OrkValerian opened this issue Apr 18, 2024 · 1 comment
Open

Can't find a way to use Value.as with generic types #8803

OrkValerian opened this issue Apr 18, 2024 · 1 comment
Assignees
Labels

Comments

@OrkValerian
Copy link

Describe GraalVM and your environment :

  • GraalVM version or commit id if built from source:
    On PopOs: 22.0.1-graalce and GraalJS 24.0.1
    On Debian: graalvm-ce-java17-22.3.0 with JS module
  • CE or EE: CE
  • JDK version:
    On Pop OS: openjdk 22.0.1 2024-04-16
    On Debian: java 17.0.9 2023-10-17 LTS
  • OS and OS Version: Tested on Debian GNU/Linux 11 (bullseye) server and Pop!_OS 22.04 LTS local machine
  • Architecture: x86_64
  • The output of java -Xinternalversion:
    on Pop OS: OpenJDK 64-Bit Server VM (22.0.1+8-jvmci-b01) for linux-amd64 JRE (22.0.1+8-jvmci-b01), built on 2024-03-14T14:46:36Z by "buildslave" with gcc 13.2.0
    on Debian: Java HotSpot(TM) 64-Bit Server VM (17.0.9+11-LTS-jvmci-23.0-b21) for linux-amd64 JRE (17.0.9+11-LTS-jvmci-23.0-b21), built on Oct 11 2023 14:34:18 by "buildslave" with gcc 11.2.0

Have you verified this issue still happens when using the latest snapshot?
Latest downloadable version on main site

Describe the issue
I want to be able to call from javascript java methods that take Records as arguments.
I don't think GraalJS/VM handles this natively so I wrote a generic function to do it that I use for each Record type I want to use. I uses reflection to convert each record component into its target type thanks to Value.as().
It is working fine except for generic types, most common case is when a record has a property which type is a List of an other record.
I couldn't find a way to use Value.as in this case, it has two signatures, one with a Class, but I'm loosing the type parameter so I end up with a List of wrong typed elements, the other signature is with a TypeLiteral, but I can't dynamically create an instance of this class (or at least I did not figure how).
You'll find hereunder a snippet that shows what is my issue, along with the output of its execution.
At the end it fails as it tries to instanciate a record instance with a wrongly typed argument.
I'm no reflection expert, so maybe there's something that I missed somewhere.
Or maybe there's a simpler way to achieve what I'm trying to do?
If so let me know!
Thanks

Additional question:
From the output we can see that my sub record is converted not when I call Value.as() on the parent list, but when I first access the converted list element to print it. Is there any lazy evaluation mecanism involved here?

Code snippet or code repository that reproduces the issue

package org.example;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.TypeLiteral;
import org.graalvm.polyglot.Value;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.util.List;

public class ValueAsWithGenericCollectionTest {

    public record MainRecord(List<SubRecord> subRecords) {}

    public record SubRecord(String someValue) {}

    private static final List<Class<? extends Record>> API_RECORDS = List.of(
            MainRecord.class,
            SubRecord.class
    );

    public static void main(String[] args) {
        HostAccess.Builder hostAccessBuilder = HostAccess.newBuilder().
                allowPublicAccess(true).
                allowAllImplementations(true).
                allowAllClassImplementations(true).
                allowArrayAccess(true).allowListAccess(true).allowBufferAccess(true).
                allowIterableAccess(true).allowIteratorAccess(true).allowMapAccess(true).
                allowAccessInheritance(true);

        API_RECORDS.forEach(recordClass -> addRecordTargetTypeMapping(recordClass, hostAccessBuilder));

        try (Context jsContext = Context.newBuilder("js")
                .allowIO(true)
                .allowExperimentalOptions(true).option("js.nashorn-compat", "true") // only to access java getters and setters with just the property name
                .option("js.ecmascript-version", "2021")
                .option("js.intl-402", "true") // before GraalVM 23.1, was set by default to false
                .allowHostAccess(hostAccessBuilder.build())
                .build()) {
            ValueAsWithGenericCollectionTest javaApi = new ValueAsWithGenericCollectionTest();

            jsContext.getBindings("js").putMember("javaApi", javaApi);

            jsContext.eval("js", """
                    const mainData = {
                        subRecords: [
                            { someValue: "val1" },
                            { someValue: "val2" }
                        ]
                    };
                    
                    const firstValue = javaApi.getFirstSubRecordValue(mainData);
                    console.log(firstValue);
                    """);
        }
    }

    public String getFirstSubRecordValue(MainRecord mainRecord) {
        return mainRecord.subRecords.get(0).someValue();
    }

    private static <R extends Record> void addRecordTargetTypeMapping(Class<R> recordClass, HostAccess.Builder builder) {
        builder.targetTypeMapping(Value.class, recordClass, null, value -> convertToRecord(value, recordClass));
    }

    private static <R extends Record> R convertToRecord(Value value, Class<R> recordClass) {
        if (value == null || value.isNull()) return null;

        RecordComponent[] recordComponents = recordClass.getRecordComponents();

        Object[] params = new Object[recordComponents.length];

        for (int i = 0; i < recordComponents.length; i++) {
            RecordComponent component = recordComponents[i];
            String componentName = component.getName();

            // this is demo code, not handling case where there is no value
            Value pValue = value.getMember(componentName);

            System.out.println("[" + componentName + "] Source value class: " + pValue.getClass());

            // I guess that due to type erasure, we loose the actual parameter type
            Class<?> recordCompType = component.getType();
            System.out.println("[" + componentName + "] Record component class: " + recordCompType);

            // I can get actual type arguments through this, but this is not a Class instance, and I see no way to build an instance of TypeLiteral from it
            if (MainRecord.class.equals(recordClass)) {
                ParameterizedType genericType = (ParameterizedType) component.getGenericType();
                System.out.println("[" + componentName + "] Record component generic type: " + genericType);
            }

            // this would work, but I want a generic behavior, not single class one:
            if (MainRecord.class.equals(recordClass)) {
                List<SubRecord> targetValueFromStaticTypeLiteral = pValue.as(new TypeLiteral<List<SubRecord>>() {});
                System.out.println("[" + componentName + "] Static converted class: " + targetValueFromStaticTypeLiteral.getClass());
                System.out.println("[" + componentName + "] Static converted sub element class: " + targetValueFromStaticTypeLiteral.get(0).getClass());
            }

            // I'm getting List<Truc> instead of List<SubRecord>
            Object targetValue = pValue.as(recordCompType);
            System.out.println("[" + componentName + "] Generic converted class: " + targetValue.getClass());
            if (MainRecord.class.equals(recordClass)) {
                System.out.println("[" + componentName + "] Generic converted sub element class: " + ((List<?>) targetValue).get(0).getClass());
            }

            params[i] = targetValue;
        }
        try {
            Constructor<?> constructor = recordClass.getConstructors()[0];

            @SuppressWarnings("unchecked")
            R recordInstance = (R) constructor.newInstance(params);
            System.out.println("Record instance: " + recordInstance);
            return recordInstance;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException("could not deserialize record of class " + recordClass.getSimpleName(), e);
        }
    }
}

Execution output

[subRecords] Source value class: class org.graalvm.polyglot.Value
[subRecords] Record component class: interface java.util.List
[subRecords] Record component generic type: java.util.List<org.example.ValueAsWithGenericCollectionTest$SubRecord>
[subRecords] Static converted class: class com.oracle.truffle.polyglot.PolyglotList
[someValue] Source value class: class org.graalvm.polyglot.Value
[someValue] Record component class: class java.lang.String
[someValue] Generic converted class: class java.lang.String
Record instance: SubRecord[someValue=val1]
[subRecords] Static converted sub element class: class org.example.ValueAsWithGenericCollectionTest$SubRecord
[subRecords] Generic converted class: class com.oracle.truffle.polyglot.PolyglotList
[subRecords] Generic converted sub element class: class com.oracle.truffle.polyglot.PolyglotMap
Record instance: MainRecord[subRecords=[object Array]]
Exception in thread "main" class com.oracle.truffle.polyglot.PolyglotMap cannot be cast to class org.example.ValueAsWithGenericCollectionTest$SubRecord (com.oracle.truffle.polyglot.PolyglotMap and org.example.ValueAsWithGenericCollectionTest$SubRecord are in unnamed module of loader 'app')
	at org.example.ValueAsWithGenericCollectionTest.getFirstSubRecordValue(ValueAsWithGenericCollectionTest.java:64)
	at <js> :program(Unnamed:8:127-166)
	at org.graalvm.polyglot.Context.eval(Context.java:428)
	at org.example.ValueAsWithGenericCollectionTest.main(ValueAsWithGenericCollectionTest.java:49)
Caused by host exception: java.lang.ClassCastException: class com.oracle.truffle.polyglot.PolyglotMap cannot be cast to class org.example.ValueAsWithGenericCollectionTest$SubRecord (com.oracle.truffle.polyglot.PolyglotMap and org.example.ValueAsWithGenericCollectionTest$SubRecord are in unnamed module of loader 'app')
@brahimhaddou brahimhaddou self-assigned this Apr 19, 2024
@brahimhaddou
Copy link
Member

Hi @OrkValerian, thank you for reporting this issue, we will take a look at this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants