diff --git a/CodenameOne/src/com/codename1/annotations/Concrete.java b/CodenameOne/src/com/codename1/annotations/Concrete.java new file mode 100644 index 0000000000..c1e5765d7e --- /dev/null +++ b/CodenameOne/src/com/codename1/annotations/Concrete.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Indicates that a class has a known concrete implementation that ParparVM can +/// target directly in native (C/Objective-C) pipelines. +/// +/// When present, the translator may bypass virtual lookup when invoking methods +/// on this type by preferring the concrete class provided in {@link #name()}, +/// and falling back to the annotated type implementation if the concrete class +/// doesn't implement the method. +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Concrete { + /// The fully-qualified class name of the concrete implementation to prefer + /// during ParparVM native translation. + String name(); +} diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 291b7750d0..4777cde002 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -23,6 +23,7 @@ */ package com.codename1.impl; +import com.codename1.annotations.Concrete; import com.codename1.capture.VideoCaptureConstraints; import com.codename1.codescan.CodeScanner; import com.codename1.components.AudioRecorderComponent; @@ -109,6 +110,7 @@ /// Display specifically for key, pointer events and screen resolution. /// /// @author Shai Almog +@Concrete(name = "com.codename1.impl.ios.IOSImplementation") public abstract class CodenameOneImplementation { /// Indicates the range of "hard" RTL bidi characters in unicode private static final int RTL_RANGE_BEGIN = 0x590; diff --git a/docs/developer-guide/performance.asciidoc b/docs/developer-guide/performance.asciidoc index 5697a64eb8..ead89dfd64 100644 --- a/docs/developer-guide/performance.asciidoc +++ b/docs/developer-guide/performance.asciidoc @@ -61,6 +61,28 @@ For ParparVM-generated native code, we now support method-level optimization hin TIP: Use these on methods that are both performance-critical and well-covered by tests. These annotations intentionally trade runtime safety diagnostics for speed. +===== Class-level concrete implementation hints + +For native ParparVM output (C/Objective-C), you can also provide a class-level hint that a base type always maps to a known concrete subclass at runtime: + +* `@Concrete(name="fully.qualified.ConcreteClassName")` + + Allows the translator to bypass virtual table lookup for `invokevirtual` calls on the annotated base class. + The translator first attempts a direct call on the concrete class; if the method is not implemented there, it falls back to the annotated base class implementation. + +This is useful for platform abstraction classes where one implementation is guaranteed in the native pipeline. + +For example, Codename One annotates: + +[source,java] +---- +@Concrete(name = "com.codename1.impl.ios.IOSImplementation") +public abstract class CodenameOneImplementation { + // ... +} +---- + +NOTE: This hint is intended for ParparVM native translation and does not apply to the JavaScript backend. + ===== Fast method-stack path The translator can emit a fast method-stack prologue/epilogue (`DEFINE_METHOD_STACK_FAST_*` and `CN1_FAST_RETURN_RELEASE`) for methods that meet strict safety criteria. diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index 46fbebdad6..6932bd77f2 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -66,6 +66,7 @@ public void setIsAnnotation(boolean isAnnotation) { private String clsName; private String originalClassName; private String baseClass; + private String concreteClass; private List baseInterfaces; private boolean isInterface; private boolean isAbstract; @@ -342,6 +343,11 @@ private BytecodeMethod findDeclaredMethod(String name, String desc) { return null; } + public boolean hasDeclaredNonAbstractMethod(String name, String desc) { + BytecodeMethod declaredMethod = findDeclaredMethod(name, desc); + return declaredMethod != null && !declaredMethod.isAbstract(); + } + public void unmark() { marked = false; } @@ -1891,6 +1897,14 @@ public String getBaseClass() { return baseClass; } + public String getConcreteClass() { + return concreteClass; + } + + public void setConcreteClass(String concreteClass) { + this.concreteClass = concreteClass; + } + public void setSourceFile(String sourceFile) { this.sourceFile = sourceFile; } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index d05501af9c..c1b6413988 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -1473,6 +1473,7 @@ public void setMaxes(int maxStack, int maxLocals) { private void addInstruction(Instruction i) { instructions.add(i); + i.setMethod(this); i.addDependencies(dependentClasses); if (dependencyGraph != null) { String methodUsed = i.getMethodUsed(); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index 2b888883d9..f176e37c6a 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -51,6 +51,7 @@ public class Parser extends ClassVisitor { private static final String DISABLE_DEBUG_INFO_ANNOTATION = "Lcom/codename1/annotations/DisableDebugInfo;"; private static final String DISABLE_NULL_AND_ARRAY_BOUNDS_CHECKS_ANNOTATION = "Lcom/codename1/annotations/DisableNullChecksAndArrayBoundsChecks;"; + private static final String CONCRETE_ANNOTATION = "Lcom/codename1/annotations/Concrete;"; private ByteCodeClass cls; private String clsName; private static String[] nativeSources; @@ -699,6 +700,17 @@ public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, Str @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (CONCRETE_ANNOTATION.equals(desc)) { + return new AnnotationVisitorWrapper(super.visitAnnotation(desc, visible)) { + @Override + public void visit(String name, Object value) { + if ("name".equals(name) && value instanceof String) { + cls.setConcreteClass(((String)value).replace('.', '/')); + } + super.visit(name, value); + } + }; + } return new AnnotationVisitorWrapper(super.visitAnnotation(desc, visible)); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/CustomInvoke.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/CustomInvoke.java index 7abd68daee..2f3f74d296 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/CustomInvoke.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/CustomInvoke.java @@ -91,11 +91,26 @@ public String getMethodUsed() { @Override public void addDependencies(List dependencyList) { + String dependencyOwner = owner; + if (origOpcode == Opcodes.INVOKEVIRTUAL) { + ByteCodeClass bc = Parser.getClassObject(owner.replace('/', '_').replace('$', '_')); + String resolvedConcreteOwner = resolveConcreteInvokeOwner(bc); + if (resolvedConcreteOwner != null) { + dependencyOwner = resolvedConcreteOwner; + } + } String t = owner.replace('.', '_').replace('/', '_').replace('$', '_'); t = unarray(t); if(t != null && !dependencyList.contains(t)) { dependencyList.add(t); } + if (!owner.equals(dependencyOwner)) { + String concreteDependency = dependencyOwner.replace('.', '_').replace('/', '_').replace('$', '_'); + concreteDependency = unarray(concreteDependency); + if (concreteDependency != null && !dependencyList.contains(concreteDependency)) { + dependencyList.add(concreteDependency); + } + } StringBuilder bld = new StringBuilder(); if(origOpcode != Opcodes.INVOKEINTERFACE && origOpcode != Opcodes.INVOKEVIRTUAL) { @@ -134,7 +149,23 @@ private String findActualOwner(ByteCodeClass bc) { } return findActualOwner(bc.getBaseClassObject()); } - + + private String resolveConcreteInvokeOwner(ByteCodeClass ownerClass) { + if (ownerClass == null || ownerClass.getConcreteClass() == null || getMethod() == null) { + return null; + } + String currentClass = getMethod().getClsName(); + String ownerName = ownerClass.getClsName(); + if (ownerName.equals(currentClass) || currentClass.startsWith(ownerName + "_")) { + return null; + } + ByteCodeClass concreteClass = Parser.getClassObject(ownerClass.getConcreteClass().replace('/', '_').replace('$', '_')); + if (concreteClass != null && concreteClass.hasDeclaredNonAbstractMethod(name, desc)) { + return concreteClass.getClsName(); + } + return null; + } + public boolean methodHasReturnValue() { return BytecodeMethod.appendMethodSignatureSuffixFromDesc(desc, new StringBuilder(), new ArrayList<>()) != null; } @@ -160,6 +191,7 @@ public boolean appendExpression(StringBuilder b) { owner = Util.resolveInvokeSpecialOwner(owner, name, desc); } + String invokeOwner = owner; StringBuilder bld = new StringBuilder(); boolean isVirtualCall = false; if(origOpcode == Opcodes.INVOKEINTERFACE || origOpcode == Opcodes.INVOKEVIRTUAL) { @@ -176,6 +208,12 @@ public boolean appendExpression(StringBuilder b) { } else { if (bc.isMethodPrivate(name, desc)) { isVirtual = false; + } else { + String resolvedConcreteOwner = resolveConcreteInvokeOwner(bc); + if (resolvedConcreteOwner != null) { + invokeOwner = resolvedConcreteOwner; + isVirtual = false; + } } } @@ -191,12 +229,12 @@ public boolean appendExpression(StringBuilder b) { if(origOpcode == Opcodes.INVOKESTATIC) { // find the actual class of the static method to work around javac not defining it correctly ByteCodeClass bc = Parser.getClassObject(owner.replace('/', '_').replace('$', '_')); - owner = findActualOwner(bc); + invokeOwner = findActualOwner(bc); } - if (owner.startsWith("[")) { + if (invokeOwner.startsWith("[")) { bld.append("java_lang_Object"); } else{ - bld.append(owner.replace('/', '_').replace('$', '_')); + bld.append(invokeOwner.replace('/', '_').replace('$', '_')); } bld.append("_"); if(name.equals("")) { @@ -264,7 +302,9 @@ public void appendInstruction(StringBuilder b) { owner = Util.resolveInvokeSpecialOwner(owner, name, desc); } + String invokeOwner = owner; StringBuilder bld = new StringBuilder(); + boolean isVirtualCall = false; if(origOpcode == Opcodes.INVOKEINTERFACE || origOpcode == Opcodes.INVOKEVIRTUAL) { b.append(" "); @@ -279,12 +319,19 @@ public void appendInstruction(StringBuilder b) { } else { if (bc.isMethodPrivate(name, desc)) { isVirtual = false; + } else { + String resolvedConcreteOwner = resolveConcreteInvokeOwner(bc); + if (resolvedConcreteOwner != null) { + invokeOwner = resolvedConcreteOwner; + isVirtual = false; + } } } } if (isVirtual) { bld.append("virtual_"); + isVirtualCall = true; } } else { b.append(" "); @@ -293,12 +340,12 @@ public void appendInstruction(StringBuilder b) { if(origOpcode == Opcodes.INVOKESTATIC) { // find the actual class of the static method to work around javac not defining it correctly ByteCodeClass bc = Parser.getClassObject(owner.replace('/', '_').replace('$', '_')); - owner = findActualOwner(bc); + invokeOwner = findActualOwner(bc); } - if (owner.startsWith("[")) { + if (invokeOwner.startsWith("[")) { bld.append("java_lang_Object"); } else{ - bld.append(owner.replace('/', '_').replace('$', '_')); + bld.append(invokeOwner.replace('/', '_').replace('$', '_')); } bld.append("_"); if(name.equals("")) { @@ -313,6 +360,9 @@ public void appendInstruction(StringBuilder b) { bld.append("__"); ArrayList args = new ArrayList<>(); String returnVal = BytecodeMethod.appendMethodSignatureSuffixFromDesc(desc, bld, args); + if (isVirtualCall) { + BytecodeMethod.addVirtualMethodsInvoked(bld.substring("virtual_".length())); + } int numLiteralArgs = this.getNumLiteralArgs(); if (numLiteralArgs > 0) { b.append("/* CustomInvoke */"); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java index 5e10c18876..d3d8dcf251 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java @@ -89,11 +89,26 @@ private String getCMethodName() { @Override public void addDependencies(List dependencyList) { + String dependencyOwner = owner; + if (opcode == Opcodes.INVOKEVIRTUAL) { + ByteCodeClass bc = Parser.getClassObject(owner.replace('/', '_').replace('$', '_')); + String resolvedConcreteOwner = resolveConcreteInvokeOwner(bc); + if (resolvedConcreteOwner != null) { + dependencyOwner = resolvedConcreteOwner; + } + } String t = owner.replace('.', '_').replace('/', '_').replace('$', '_'); t = unarray(t); if(t != null && !dependencyList.contains(t)) { dependencyList.add(t); } + if (!owner.equals(dependencyOwner)) { + String concreteDependency = dependencyOwner.replace('.', '_').replace('/', '_').replace('$', '_'); + concreteDependency = unarray(concreteDependency); + if (concreteDependency != null && !dependencyList.contains(concreteDependency)) { + dependencyList.add(concreteDependency); + } + } StringBuilder bld = new StringBuilder(); if(opcode != Opcodes.INVOKEINTERFACE && opcode != Opcodes.INVOKEVIRTUAL) { @@ -132,7 +147,23 @@ private String findActualOwner(ByteCodeClass bc) { } return findActualOwner(bc.getBaseClassObject()); } - + + private String resolveConcreteInvokeOwner(ByteCodeClass ownerClass) { + if (ownerClass == null || ownerClass.getConcreteClass() == null || getMethod() == null) { + return null; + } + String currentClass = getMethod().getClsName(); + String ownerName = ownerClass.getClsName(); + if (ownerName.equals(currentClass) || currentClass.startsWith(ownerName + "_")) { + return null; + } + ByteCodeClass concreteClass = Parser.getClassObject(ownerClass.getConcreteClass().replace('/', '_').replace('$', '_')); + if (concreteClass != null && concreteClass.hasDeclaredNonAbstractMethod(name, desc)) { + return concreteClass.getClsName(); + } + return null; + } + @Override public void appendInstruction(StringBuilder b) { // special case for clone on an array which isn't a real method invocation @@ -144,6 +175,7 @@ public void appendInstruction(StringBuilder b) { owner = Util.resolveInvokeSpecialOwner(owner, name, desc); } + String invokeOwner = owner; StringBuilder bld = new StringBuilder(); boolean isVirtualCall = false; if(opcode == Opcodes.INVOKEINTERFACE || opcode == Opcodes.INVOKEVIRTUAL) { @@ -161,6 +193,12 @@ public void appendInstruction(StringBuilder b) { } else { if (bc.isMethodPrivate(name, desc)) { isVirtual = false; + } else { + String resolvedConcreteOwner = resolveConcreteInvokeOwner(bc); + if (resolvedConcreteOwner != null) { + invokeOwner = resolvedConcreteOwner; + isVirtual = false; + } } } } @@ -175,14 +213,14 @@ public void appendInstruction(StringBuilder b) { if(opcode == Opcodes.INVOKESTATIC) { // find the actual class of the static method to work around javac not defining it correctly ByteCodeClass bc = Parser.getClassObject(owner.replace('/', '_').replace('$', '_')); - owner = findActualOwner(bc); + invokeOwner = findActualOwner(bc); } - if (owner.startsWith("[")) { + if (invokeOwner.startsWith("[")) { // Kotlin seems to generate calls to toString() on arrays using the array class // as an owner. We'll just change this to java_lang_Object instead. bld.append("java_lang_Object"); } else{ - bld.append(owner.replace('/', '_').replace('$', '_')); + bld.append(invokeOwner.replace('/', '_').replace('$', '_')); } bld.append("_"); if(name.equals("")) {