konsoletyper / teavm

Compiles Java bytecode to JavaScript, WebAssembly and C
https://teavm.org
Apache License 2.0
2.61k stars 263 forks source link

How to return Object[] from Metaprogramming.proxy? #430

Open fluorumlabs opened 4 years ago

fluorumlabs commented 4 years ago

What is the proper way of returning Object[] from Metaprogramming.proxy()?

konsoletyper commented 4 years ago

Hello. Not sure what do you mean. proxy does not return values, it generates anonymous classes, similarly to Proxy.newProxyInstance. You may return whatever you want from one of the generated methods of a proxy, as if it was normal method, e.g.

Metaprogramming.exit(() -> new Object[0]);

The best way to lear metaprogramming deeper is examining sources, for example this one. Also, you can download sources of Flavour and search for usages of metaprogramming API there.

fluorumlabs commented 4 years ago

I'm building a proxy around javax.validation annotation to pass "Annotation" object to the client side. My naive approach was to do the following:

    private static <T extends Annotation> Value<T> makeAnnotationProxy(T annotation) {
        Class<T> aClass = (Class<T>)annotation.annotationType();

        return proxy(aClass, (proxy, method, args) -> {
            String name = method.getName();
            try {
                Object result = aClass.getMethod(name).invoke(annotation);
                exit(() -> result);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        });
    }

, but that failed on Class<?>[] groups() default { }; method. By looking at the source code, it seems that arrays simply cannot be returned that way. Hence I'm looking for a proper way :)

konsoletyper commented 4 years ago

You can't deal with Class in metaprogramming API, instead you need ReflectClass. Please, read documentation. From your code it's unclear what is the entry point for MP and where you get annotation.

fluorumlabs commented 4 years ago

Ok, let's generalize a bit. Let's say I have an interface with a method like String[] getSomeStrings(). How can I implement it via proxy?

konsoletyper commented 4 years ago

Still does not help to answer your question. The best way to implement this interface is to implement it manually, i.e.:

class MyClass implements MyInterface {
  @Override String[] getSomeStrings() { return new String[0]; }
}

Futher, if you want to use proxy (I don't know why you need it for just implementing an interface), you might take a look at tests sources. For example, this one:

    @Test
    public void createsProxy() {
        A proxy = createProxy(A.class, "!");
        assertEquals("foo!", proxy.foo());
        assertEquals("bar!", proxy.bar());
    }

    @Meta
    private static native <T> T createProxy(Class<T> proxyType, String add);
    private static <T> void createProxy(ReflectClass<T> proxyType, Value<String> add) {
        if (proxyType.getAnnotation(MetaprogrammingClass.class) == null) {
            unsupportedCase();
            return;
        }
        Value<T> proxy = proxy(proxyType, (instance, method, args) -> {
            String name = method.getName();
            exit(() -> name + add.get());
        });
        exit(() -> proxy.get());
    }
fluorumlabs commented 4 years ago

Yes, this I know :)

I'm working on javax.validation (read: hibernate validation) integration for my app. And in order to initialize constraint validator on the client side, I need to provide Annotation instance. I discover those during the compile time, and pass them as a proxies (as, obviously, one cannot subclass annotation). I can ignore Class<?>[] methods in those annotations, as they are not used by my validation library, but, unfortunately, there is org.hibernate.validator.constraints.Currency with

    /**
     * The {@link CurrencyUnit} codes (e.g. USD, EUR...) being accepted.
     */
    String[] value();
fluorumlabs commented 4 years ago

Of cause, I can make a custom implementation just for that particular validation, but I'd like to have more generalized approach.

konsoletyper commented 4 years ago

Sorry, still don't understand what you are trying to do. What is the client? Where does it get annotation instance? How it's supposed to use these annotation instances?

fluorumlabs commented 4 years ago

Client == output of Teavm. Here's a quick and dirty prototype I'm playing with ATM.

...
    @Meta
    private static native <T> void validate_(Class<T> cls, T instance, String fieldName) throws ValidationException;

    private static <T> void validate_(ReflectClass<T> cls, Value<T> instance, Value<String> fieldName) throws ClassNotFoundException {
        Map<String, List<Annotation>> annotations = new HashMap<>();

        Class<T> tClass = (Class<T>) Metaprogramming.getClassLoader().loadClass(cls.getName());

        for (Field field : tClass.getDeclaredFields()) {
            List<Annotation> annotationList = new ArrayList<>();
            for (Annotation annotation : field.getAnnotations()) {
                collectAnnotations(annotation, annotationList::add);
            }
            if (annotationList.stream().anyMatch(a -> a.annotationType() == Constraint.class)) {
                Collections.reverse(annotationList);
                annotations.put(field.getName(), annotationList);
            }
        }

        for (ReflectField field : cls.getDeclaredFields()) {
            String currentFieldName = field.getName();
            List<Annotation> annotationList = annotations.get(field.getName());
            if (annotationList != null) {
                Value<Boolean> shouldEmit = emit(() -> fieldName.get() == null || currentFieldName.equals(fieldName.get()));
                for (Annotation annotation : annotationList) {
                    emitValidator(cls, field, emit(() -> field.get(instance.get())), shouldEmit, annotation);
                }
            }
        }
    }

    private static <T> void emitValidator(ReflectClass<T> cls, ReflectField field, Value<Object> o, Value<Boolean> shouldEmit, Annotation annotation) {
        tryAnnotation(annotation, Min.class, min -> {
            emitValidator(min, MinValidatorForNumber.class, o);
        });
    }

    private static <T, A extends Annotation> void emitValidator(A annotation, Class<?> validatorClass, Value<T> o) {
        Value<A> annotationProxy = makeAnnotationProxy(annotation);
        ReflectClass<?> validatorReflectClass = findClass(validatorClass);
        ReflectMethod method = validatorReflectClass.getMethod("<init>");

        MessageInterpolator defaultMessageInterpolator = javax.validation.Validation.byDefaultProvider().configure().getDefaultMessageInterpolator();
        InternalContext internalContext = new InternalContext(annotation, (Class<? extends ConstraintValidator<A, ?>>) validatorClass);
        String message = defaultMessageInterpolator.interpolate(internalContext.getConstraintDescriptor().getMessageTemplate(), internalContext);

        emit(() -> {
            ConstraintValidator<A, T> validator = (ConstraintValidator<A, T>) method.construct();
            validator.initialize(annotationProxy.get());
            if (!validator.isValid((T) o.get(), null)) {
                throw new ValidationException(message);
            }
        });

    }

    private static <T extends Annotation> Value<T> makeAnnotationProxy(T annotation) {
        Class<T> aClass = (Class<T>) annotation.annotationType();

        return proxy(aClass, (proxy, method, args) -> {
            String name = method.getName();
            try {
                if (method.getReturnType().isArray() && method.getReturnType().getComponentType().isAssignableFrom(Class.class)) {
                    exit(() -> null);
                    // Class<?>[] methods are not used
                } else {
                    Object result = aClass.getMethod(name).invoke(annotation);
                    exit(() -> result);
                    // This will probably fail for String[] return type
                }
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        });
    }

    private static <T extends Annotation> void tryAnnotation(Annotation a, Class<T> target, Consumer<T> emitter) {
        if (a.annotationType() == target) {
            emitter.accept((T) a);
        }
    }
...
konsoletyper commented 4 years ago

Ok, I see. The error is here:

Object result = aClass.getMethod(name).invoke(annotation);
exit(() -> result);

This code is not expected to work properly, since you generate value at compile-time and try to propagate it into run time. TeaVM only supports this for primitives and strings. You better to change approach to the way you try to implement validation. For example, you can write specific code for each annotation, much like it's implemented in JSON binding support in Flavour.

fluorumlabs commented 4 years ago

TeaVM only supports this for primitives and strings

And this is fine, as one can't really have normal objects in annotatins. Of cause, enums are missing in the list, and this is ok, it should be relatively straightforward to implement. However, arrays of primitives/string are completely missing, meaning that it's impossible to make a proxy for a method returning String[] or int[].

fluorumlabs commented 4 years ago

And... Even though it's not expected to work properly, it works for primitives and Strings :)