All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.teavm.jso.impl.JSObjectClassTransformer Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2014 Alexey Andreev.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.teavm.jso.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teavm.diagnostics.Diagnostics;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSExport;
import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.model.AccessLevel;
import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationValue;
import org.teavm.model.BasicBlock;
import org.teavm.model.CallLocation;
import org.teavm.model.ClassHierarchy;
import org.teavm.model.ClassHolder;
import org.teavm.model.ClassHolderTransformer;
import org.teavm.model.ClassHolderTransformerContext;
import org.teavm.model.ClassReader;
import org.teavm.model.ElementModifier;
import org.teavm.model.FieldHolder;
import org.teavm.model.Instruction;
import org.teavm.model.MethodDescriptor;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.ValueType;
import org.teavm.model.Variable;
import org.teavm.model.instructions.ExitInstruction;
import org.teavm.model.instructions.IntegerConstantInstruction;
import org.teavm.model.instructions.InvocationType;
import org.teavm.model.instructions.InvokeInstruction;

class JSObjectClassTransformer implements ClassHolderTransformer {
    private JSClassProcessor processor;
    private JSBodyRepository repository;
    private JSTypeHelper typeHelper;
    private ClassHierarchy hierarchy;
    private Map exposedClasses = new HashMap<>();

    JSObjectClassTransformer(JSBodyRepository repository) {
        this.repository = repository;
    }

    @Override
    public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) {
        this.hierarchy = context.getHierarchy();
        if (processor == null || processor.getClassSource() != hierarchy.getClassSource()) {
            typeHelper = new JSTypeHelper(hierarchy.getClassSource());
            processor = new JSClassProcessor(hierarchy.getClassSource(), typeHelper, repository,
                    context.getDiagnostics(), context.getIncrementalCache(), context.isStrict());
        }
        processor.processClass(cls);
        if (typeHelper.isJavaScriptClass(cls.getName())) {
            processor.processMemberMethods(cls);
        }

        for (MethodHolder method : cls.getMethods().toArray(new MethodHolder[0])) {
            if (method.getProgram() != null) {
                processor.processProgram(method);
            }
        }
        processor.createJSMethods(cls);

        if (cls.hasModifier(ElementModifier.ABSTRACT)
                || cls.getAnnotations().get(JSClass.class.getName()) != null && isJavaScriptClass(cls)) {
            return;
        }

        MethodReference functorMethod = processor.isFunctor(cls.getName());
        if (functorMethod != null) {
            if (processor.isFunctor(cls.getParent()) != null) {
                functorMethod = null;
            }
        }

        ClassReader originalClass = hierarchy.getClassSource().get(cls.getName());
        ExposedClass exposedClass;
        if (originalClass != null) {
            exposedClass = getExposedClass(cls.getName());
        } else {
            exposedClass = new ExposedClass();
            createExposedClass(cls, exposedClass);
        }

        exposeMethods(cls, exposedClass, context.getDiagnostics(), functorMethod);
        exportStaticMethods(cls, context.getDiagnostics());
    }

    private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics,
            MethodReference functorMethod) {
        int index = 0;
        for (var entry : classToExpose.methods.entrySet()) {
            var method = entry.getKey();
            var export = entry.getValue();
            MethodReference methodRef = new MethodReference(classHolder.getName(), method);
            CallLocation callLocation = new CallLocation(methodRef);

            var paramCount = method.parameterCount();
            if (export.vararg) {
                --paramCount;
            }
            var exportedMethodSignature = new ValueType[paramCount + 1];
            Arrays.fill(exportedMethodSignature, JSMethods.JS_OBJECT);
            MethodDescriptor exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++,
                    exportedMethodSignature);
            MethodHolder exportedMethod = new MethodHolder(exportedMethodDesc);
            Program program = new Program();
            exportedMethod.setProgram(program);
            program.createVariable();

            BasicBlock basicBlock = program.createBasicBlock();
            List marshallInstructions = new ArrayList<>();
            JSValueMarshaller marshaller = new JSValueMarshaller(diagnostics, typeHelper, hierarchy.getClassSource(),
                    program, marshallInstructions);

            Variable[] variablesToPass = new Variable[method.parameterCount()];
            for (int i = 0; i < method.parameterCount(); ++i) {
                variablesToPass[i] = program.createVariable();
            }
            if (export.vararg) {
                transformVarargParam(variablesToPass, program, marshallInstructions, exportedMethod, 1);
            }

            for (int i = 0; i < method.parameterCount(); ++i) {
                var byRef = i == method.parameterCount() - 1 && export.vararg
                        && typeHelper.isSupportedByRefType(method.parameterType(i));
                variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i],
                        method.parameterType(i), byRef, true);
            }

            basicBlock.addAll(marshallInstructions);
            marshallInstructions.clear();

            InvokeInstruction invocation = new InvokeInstruction();
            invocation.setType(InvocationType.VIRTUAL);
            invocation.setInstance(program.variableAt(0));
            invocation.setMethod(methodRef);
            invocation.setArguments(variablesToPass);
            basicBlock.add(invocation);

            ExitInstruction exit = new ExitInstruction();
            if (method.getResultType() != ValueType.VOID) {
                invocation.setReceiver(program.createVariable());
                exit.setValueToReturn(marshaller.wrapArgument(callLocation, invocation.getReceiver(),
                        method.getResultType(), JSType.MIXED, false));
                basicBlock.addAll(marshallInstructions);
                marshallInstructions.clear();
            }
            basicBlock.add(exit);

            classHolder.addMethod(exportedMethod);
            exportedMethod.getAnnotations().add(createExportAnnotation(export));

            if (methodRef.equals(functorMethod)) {
                addFunctorField(classHolder, exportedMethod.getReference());
            }
        }
    }

    private void exportStaticMethods(ClassHolder classHolder, Diagnostics diagnostics) {
        int index = 0;
        for (var method : classHolder.getMethods().toArray(new MethodHolder[0])) {
            if (!method.hasModifier(ElementModifier.STATIC)
                    || method.getAnnotations().get(JSExport.class.getName()) == null) {
                continue;
            }

            var paramCount = method.parameterCount();
            var vararg = method.hasModifier(ElementModifier.VARARGS);
            if (vararg) {
                --paramCount;
            }
            var callLocation = new CallLocation(method.getReference());
            var exportedMethodSignature = new ValueType[paramCount + 1];
            Arrays.fill(exportedMethodSignature, JSMethods.JS_OBJECT);
            var exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++,
                    exportedMethodSignature);
            var exportedMethod = new MethodHolder(exportedMethodDesc);
            exportedMethod.getModifiers().add(ElementModifier.STATIC);
            var program = new Program();
            program.createVariable();
            exportedMethod.setProgram(program);

            var basicBlock = program.createBasicBlock();
            var marshallInstructions = new ArrayList();
            var marshaller = new JSValueMarshaller(diagnostics, typeHelper, hierarchy.getClassSource(),
                    program, marshallInstructions);

            var variablesToPass = new Variable[method.parameterCount()];
            for (int i = 0; i < method.parameterCount(); ++i) {
                variablesToPass[i] = program.createVariable();
            }
            if (vararg) {
                transformVarargParam(variablesToPass, program, marshallInstructions, exportedMethod, 0);
            }

            for (int i = 0; i < method.parameterCount(); ++i) {
                var byRef = i == method.parameterCount() - 1 && vararg
                        && typeHelper.isSupportedByRefType(method.parameterType(i));
                variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i],
                        method.parameterType(i), byRef, true);
            }

            basicBlock.addAll(marshallInstructions);
            marshallInstructions.clear();

            var invocation = new InvokeInstruction();
            invocation.setType(InvocationType.SPECIAL);
            invocation.setMethod(method.getReference());
            invocation.setArguments(variablesToPass);
            basicBlock.add(invocation);

            var exit = new ExitInstruction();
            if (method.getResultType() != ValueType.VOID) {
                invocation.setReceiver(program.createVariable());
                exit.setValueToReturn(marshaller.wrapArgument(callLocation, invocation.getReceiver(),
                        method.getResultType(), JSType.MIXED, false));
                basicBlock.addAll(marshallInstructions);
                marshallInstructions.clear();
            }
            basicBlock.add(exit);

            classHolder.addMethod(exportedMethod);

            var export = createMethodExport(method);
            exportedMethod.getAnnotations().add(createExportAnnotation(export));
        }
    }

    private void transformVarargParam(Variable[] variablesToPass, Program program,
            List instructions, MethodHolder method, int additionalSkip) {
        var last = variablesToPass.length - 1;

        var lastConstant = new IntegerConstantInstruction();
        lastConstant.setReceiver(program.createVariable());
        lastConstant.setConstant(last + additionalSkip);
        instructions.add(lastConstant);

        var extractVarargs = new InvokeInstruction();
        extractVarargs.setType(InvocationType.SPECIAL);
        extractVarargs.setMethod(JSMethods.ARGUMENTS_BEGINNING_AT);
        extractVarargs.setArguments(lastConstant.getReceiver());
        extractVarargs.setReceiver(variablesToPass[last]);
        instructions.add(extractVarargs);

        method.getAnnotations().add(new AnnotationHolder(JSVararg.class.getName()));
    }

    private AnnotationHolder createExportAnnotation(MethodExport export) {
        String annotationName;
        switch (export.kind) {
            case GETTER:
                annotationName = JSGetterToExpose.class.getName();
                break;
            case SETTER:
                annotationName = JSSetterToExpose.class.getName();
                break;
            case CONSTRUCTOR:
                annotationName = JSConstructorToExpose.class.getName();
                break;
            default:
                annotationName = JSMethodToExpose.class.getName();
                break;
        }
        var annot = new AnnotationHolder(annotationName);
        if (export.kind != MethodKind.CONSTRUCTOR) {
            annot.getValues().put("name", new AnnotationValue(export.alias));
        }
        return annot;
    }

    private ExposedClass getExposedClass(String name) {
        ExposedClass cls = exposedClasses.get(name);
        if (cls == null) {
            cls = createExposedClass(name);
            exposedClasses.put(name, cls);
        }
        return cls;
    }

    private ExposedClass createExposedClass(String name) {
        ClassReader cls = hierarchy.getClassSource().get(name);
        ExposedClass exposedCls = new ExposedClass();
        if (cls != null) {
            createExposedClass(cls, exposedCls);
        }
        return exposedCls;
    }

    private void createExposedClass(ClassReader cls, ExposedClass exposedCls) {
        if (cls.hasModifier(ElementModifier.INTERFACE)) {
            return;
        }
        if (cls.getParent() != null) {
            ExposedClass parent = getExposedClass(cls.getParent());
            exposedCls.inheritedMethods.addAll(parent.inheritedMethods);
            exposedCls.inheritedMethods.addAll(parent.methods.keySet());
            exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces);
        }
        if (!addInterfaces(exposedCls, cls)) {
            addExportedMethods(exposedCls, cls);
        }
    }

    private boolean isJavaScriptClass(ClassReader cls) {
        if (cls.getParent() != null && typeHelper.isJavaScriptClass(cls.getParent())) {
            return true;
        }
        for (var itf : cls.getInterfaces()) {
            if (typeHelper.isJavaScriptClass(itf)) {
                return true;
            }
        }
        return false;
    }

    private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) {
        boolean added = false;
        for (String ifaceName : cls.getInterfaces()) {
            if (exposedCls.implementedInterfaces.contains(ifaceName)) {
                continue;
            }
            ClassReader iface = hierarchy.getClassSource().get(ifaceName);
            if (iface == null) {
                continue;
            }
            if (addInterface(exposedCls, iface)) {
                added = true;
                for (MethodReader method : iface.getMethods()) {
                    if (method.hasModifier(ElementModifier.STATIC)
                            || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) {
                        continue;
                    }
                    addExportedMethod(exposedCls, method);
                }
            } else {
                addExportedMethods(exposedCls, iface);
            }
        }
        return added;
    }

    private boolean addInterface(ExposedClass exposedCls, ClassReader cls) {
        if (cls.getName().equals(JSObject.class.getName())) {
            return true;
        }
        return addInterfaces(exposedCls, cls);
    }

    private void addExportedMethods(ExposedClass exposedCls, ClassReader cls) {
        for (var method : cls.getMethods()) {
            if (method.hasModifier(ElementModifier.STATIC)) {
                continue;
            }
            if (method.getAnnotations().get(JSExport.class.getName()) != null) {
                addExportedMethod(exposedCls, method);
            }
        }
    }

    private void addExportedMethod(ExposedClass exposedCls, MethodReader method) {
        if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) {
            exposedCls.methods.put(method.getDescriptor(), createMethodExport(method));
        }
    }

    private MethodExport createMethodExport(MethodReader method) {
        String name = null;
        MethodKind kind = MethodKind.METHOD;
        if (method.getName().equals("")) {
            kind = MethodKind.CONSTRUCTOR;
        } else {
            var methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
            if (methodAnnot != null) {
                name = method.getName();
                var nameVal = methodAnnot.getValue("value");
                if (nameVal != null) {
                    String nameStr = nameVal.getString();
                    if (!nameStr.isEmpty()) {
                        name = nameStr;
                    }
                }
            } else {
                var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
                if (propertyAnnot != null) {
                    var nameVal = propertyAnnot.getValue("value");
                    if (nameVal != null) {
                        String nameStr = nameVal.getString();
                        if (!nameStr.isEmpty()) {
                            name = nameStr;
                        }
                    }
                    String expectedPrefix;
                    if (method.parameterCount() == 0) {
                        if (method.getResultType() == ValueType.BOOLEAN) {
                            expectedPrefix = "is";
                        } else {
                            expectedPrefix = "get";
                        }
                        kind = MethodKind.GETTER;
                    } else {
                        expectedPrefix = "set";
                        kind = MethodKind.SETTER;
                    }

                    if (name == null) {
                        name = method.getName();
                        if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length()
                                && Character.isUpperCase(name.charAt(expectedPrefix.length()))) {
                            name = Character.toLowerCase(name.charAt(expectedPrefix.length()))
                                    + name.substring(expectedPrefix.length() + 1);
                        }
                    }
                }
            }
            if (name == null) {
                name = method.getName();
            }
        }
        return new MethodExport(name, kind, method.hasModifier(ElementModifier.VARARGS));
    }

    private void addFunctorField(ClassHolder cls, MethodReference method) {
        if (cls.getAnnotations().get(FunctorImpl.class.getName()) != null) {
            return;
        }

        FieldHolder field = new FieldHolder("$$jso_functor$$");
        field.setLevel(AccessLevel.PUBLIC);
        field.setType(ValueType.parse(JSObject.class));
        cls.addField(field);

        AnnotationHolder annot = new AnnotationHolder(FunctorImpl.class.getName());
        annot.getValues().put("value", new AnnotationValue(method.getDescriptor().toString()));
        cls.getAnnotations().add(annot);
    }

    static class ExposedClass {
        Set inheritedMethods = new HashSet<>();
        Map methods = new HashMap<>();
        Set implementedInterfaces = new HashSet<>();
    }

    enum MethodKind {
        METHOD,
        GETTER,
        SETTER,
        CONSTRUCTOR
    }

    static class MethodExport {
        final String alias;
        final MethodKind kind;
        boolean vararg;

        MethodExport(String alias, MethodKind kind, boolean vararg) {
            this.alias = alias;
            this.kind = kind;
            this.vararg = vararg;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy