From 30f187a63663bbb1fddb506b39ea814f63afe9f7 Mon Sep 17 00:00:00 2001 From: spaceflint <> Date: Wed, 26 May 2021 15:32:56 +0300 Subject: [PATCH] Some fixes --- Baselib/mscorlib.filter | 1 + Baselib/src/System/Array.cs | 43 ++++++++++---------- CilToJava/src/CodeArray.cs | 67 +++++++++++++++++--------------- CilToJava/src/CodeBuilder.cs | 6 +-- CilToJava/src/CodeMisc.cs | 62 ++++++++++++++++++++++++++++- CilToJava/src/CodeNumber.cs | 46 +++++++++++++++++++--- JavaBinary/src/JavaCodeWriter.cs | 44 +++++++++++++++++++++ PruneMerge/PruneMerge.fsproj | 12 +++++- README.md | 6 ++- Tests/src/TestCollection.cs | 17 ++++++++ USAGE.md | 24 ++++++++++++ 11 files changed, 262 insertions(+), 66 deletions(-) diff --git a/Baselib/mscorlib.filter b/Baselib/mscorlib.filter index 588c95f..31994d0 100644 --- a/Baselib/mscorlib.filter +++ b/Baselib/mscorlib.filter @@ -65,6 +65,7 @@ System.Nullable`* System.SystemException System.OverflowException +System.Collections.ArrayList System.Collections.BitArray System.Collections.Comparer System.Collections.DictionaryEntry diff --git a/Baselib/src/System/Array.cs b/Baselib/src/System/Array.cs index cb88eb0..5b94235 100644 --- a/Baselib/src/System/Array.cs +++ b/Baselib/src/System/Array.cs @@ -130,18 +130,14 @@ namespace system destinationArray, destinationIndex, length); - var srcArr = sourceArray.arr; - var dstArr = destinationArray.arr; - var elemType = ((java.lang.Object) srcArr).getClass().getComponentType(); - if (elemType != ((java.lang.Object) dstArr).getClass().getComponentType()) - throw new System.ArrayTypeMismatchException(); + var destinationArray_arr = destinationArray.arr; - object copy = Clone(destinationArray.arr, destinationArray.len); + object copy = Clone(destinationArray_arr, destinationArray.len); try { - CopyInternal(srcArr, sourceIndex, - dstArr, destinationIndex, - elemType, length); + CopyInternal(sourceArray.arr, sourceIndex, + destinationArray_arr, destinationIndex, + length); copy = null; } finally @@ -149,8 +145,8 @@ namespace system if (copy != null) { CopyInternal(copy, sourceIndex, - dstArr, destinationIndex, - elemType, length); + destinationArray_arr, destinationIndex, + length); } } } @@ -200,22 +196,25 @@ namespace system destinationArray, destinationIndex, length); - var srcArr = sourceArray.arr; - var dstArr = destinationArray.arr; - var elemType = ((java.lang.Object) srcArr).getClass().getComponentType(); - if (elemType != ((java.lang.Object) dstArr).getClass().getComponentType()) - throw new System.ArrayTypeMismatchException(); - - CopyInternal(srcArr, sourceIndex, - dstArr, destinationIndex, - elemType, length); + CopyInternal(sourceArray.arr, sourceIndex, + destinationArray.arr, destinationIndex, + length); } private static void CopyInternal(object srcArr, int srcIndex, object dstArr, int dstIndex, - java.lang.Class elemType, int length) + int length) { - if (system.RuntimeType.IsValueClass(elemType)) + var srcElemType = ((java.lang.Object) srcArr).getClass().getComponentType(); + var dstElemType = ((java.lang.Object) dstArr).getClass().getComponentType(); + if (! dstElemType.isAssignableFrom(srcElemType)) + throw new System.ArrayTypeMismatchException(); + + bool isValueType = system.RuntimeType.IsValueClass(srcElemType); + if (isValueType != system.RuntimeType.IsValueClass(dstElemType)) + throw new System.ArrayTypeMismatchException(); + + if (isValueType) { ValueType srcObj, dstObj; if ( object.ReferenceEquals(srcArr, dstArr) diff --git a/CilToJava/src/CodeArray.cs b/CilToJava/src/CodeArray.cs index 99d2c19..af9c163 100644 --- a/CilToJava/src/CodeArray.cs +++ b/CilToJava/src/CodeArray.cs @@ -204,7 +204,7 @@ namespace SpaceFlint.CilToJava var length = stackMap.PopStack(CilMain.Where); stackMap.PushStack(length); if (length.Equals(JavaType.LongType)) - CodeNumber.Conversion(code, Code.Conv_Ovf_I4); + CodeNumber.Conversion(code, Code.Conv_Ovf_I4, null); } code.NewInstruction(0xBC /* newarray */, null, elemType.NewArrayType); @@ -311,10 +311,18 @@ namespace SpaceFlint.CilToJava if (elemType.PrimitiveType == TypeCode.Byte) { // unsigned byte result should be truncated to 8-bits - stackMap.PushStack(JavaType.IntegerType); - code.NewInstruction(0x12 /* ldc */, null, (int) 0xFF); - code.NewInstruction(0x7E /* iand */, null, null); - stackMap.PopStack(CilMain.Where); + // (unless already followed by "ldc.i4 255 ; and") + bool followedByAndWith255 = + CodeBuilder.IsLoadConstant(inst.Next) == 0xFF + && inst.Next.Next?.OpCode.Code == Code.And; + + if (! followedByAndWith255) + { + stackMap.PushStack(JavaType.IntegerType); + code.NewInstruction(0x12 /* ldc */, null, (int) 0xFF); + code.NewInstruction(0x7E /* iand */, null, null); + stackMap.PopStack(CilMain.Where); + } } if (arrayType.IsValueClass || elemType.IsValueClass) @@ -405,8 +413,29 @@ namespace SpaceFlint.CilToJava // stelem.i2 with a char[] array, should be 'castore' not 'sastore' elemType = arrayType.AdjustRank(-arrayType.ArrayRank); } + else + { + // Android AOT crashes the compilation if an immediate value + // is stored into a byte or short array, and the value does + // not fit within the range -128..127 or -32768..32767. + // simply checing if the previous instruction loaded the + // constant is not enough, because due to method inlining + // by the Android ART JIT, the immediate value might actually + // originate in a calling method. + // so we always force the value into range using i2b/i2s. + // see also: CodeNumber::ConvertToInteger - CheckImmediate(arrayType.PrimitiveType, inst); + if ( arrayType.PrimitiveType == TypeCode.Boolean + || arrayType.PrimitiveType == TypeCode.SByte + || arrayType.PrimitiveType == TypeCode.Byte) + { + code.NewInstruction(0x91 /* i2b */, null, null); + } + else if ( arrayType.PrimitiveType == TypeCode.Int16) + { + code.NewInstruction(0x93 /* i2s */, null, null); + } + } if (arrayType.IsValueClass || elemType.IsValueClass) { @@ -415,32 +444,6 @@ namespace SpaceFlint.CilToJava code.NewInstruction(elemType.StoreArrayOpcode, null, null); } - - - void CheckImmediate(TypeCode arrayPrimitiveType, Mono.Cecil.Cil.Instruction inst) - { - // Android AOT aborts the compilation with a crash if storing - // an immediate value that does not fit in the target array. - if ( inst != null && inst.Previous != null - && inst.Previous.OpCode.Code == Code.Ldc_I4) - { - var imm = (int) inst.Previous.Operand; - if ( arrayType.PrimitiveType == TypeCode.Boolean - || arrayType.PrimitiveType == TypeCode.SByte - || arrayType.PrimitiveType == TypeCode.Byte) - { - if (imm < -128 || imm >= 128) - code.NewInstruction(0x91 /* i2b */, null, null); - } - else if ( arrayType.PrimitiveType == TypeCode.Char - || arrayType.PrimitiveType == TypeCode.Int16 - || arrayType.PrimitiveType == TypeCode.UInt16) - { - if (imm < -32768 || imm >= 32768) - code.NewInstruction(0x93 /* i2s */, null, null); - } - } - } } diff --git a/CilToJava/src/CodeBuilder.cs b/CilToJava/src/CodeBuilder.cs index 4f26b7d..3d1bf00 100644 --- a/CilToJava/src/CodeBuilder.cs +++ b/CilToJava/src/CodeBuilder.cs @@ -276,7 +276,7 @@ namespace SpaceFlint.CilToJava case Code.Ldc_I8: case Code.Ldc_R4: case Code.Ldc_R8: case Code.Ldstr: case Code.Ldnull: - LoadConstant(cilOp, cilInst.Operand); + LoadConstant(cilOp, cilInst); break; case Code.Ldfld: case Code.Ldflda: case Code.Stfld: @@ -363,7 +363,7 @@ namespace SpaceFlint.CilToJava case Code.Conv_U: case Code.Conv_Ovf_U: case Code.Conv_Ovf_U_Un: case Code.Conv_R4: case Code.Conv_R8: case Code.Conv_R_Un: - CodeNumber.Conversion(code, cilOp); + CodeNumber.Conversion(code, cilOp, cilInst); break; case Code.Add: case Code.Sub: case Code.Mul: case Code.Neg: @@ -374,7 +374,7 @@ namespace SpaceFlint.CilToJava case Code.Sub_Ovf: case Code.Sub_Ovf_Un: case Code.Mul_Ovf: case Code.Mul_Ovf_Un: - CodeNumber.Calculation(code, cilOp); + CodeNumber.Calculation(code, cilOp, cilInst); break; case Code.Ldind_I1: case Code.Ldind_U1: case Code.Ldind_I2: case Code.Ldind_U2: diff --git a/CilToJava/src/CodeMisc.cs b/CilToJava/src/CodeMisc.cs index 508c547..e6b3e6c 100644 --- a/CilToJava/src/CodeMisc.cs +++ b/CilToJava/src/CodeMisc.cs @@ -36,7 +36,7 @@ namespace SpaceFlint.CilToJava - void LoadConstant(Code op, object data) + void LoadConstant(Code op, Mono.Cecil.Cil.Instruction inst) { JavaType pushType; object pushValue; @@ -50,6 +50,7 @@ namespace SpaceFlint.CilToJava } else { + var data = inst.Operand; pushOpcode = 0x12; // ldc if (data is string stringVal) // Code.Ldstr @@ -72,6 +73,13 @@ namespace SpaceFlint.CilToJava pushValue = longVal; pushType = JavaType.LongType; } + else if (IsAndBeforeShift(inst, code)) + { + // jvm shift instructions mask the shift count, so + // eliminate AND-ing with 31 and 63 prior to a shift + code.NewInstruction(0x00 /* nop */, null, null); + return; + } else { pushType = JavaType.IntegerType; @@ -96,6 +104,58 @@ namespace SpaceFlint.CilToJava + public static int? IsLoadConstant(Mono.Cecil.Cil.Instruction inst) + { + if (inst != null) + { + var op = inst.OpCode.Code; + var data = inst.Operand; + if (op == Code.Ldc_I4 && data is int intVal) + return intVal; + if (op == Code.Ldc_I4_S && data is sbyte sbyteVal) + return sbyteVal; + } + return null; + } + + + + public static bool IsAndBeforeShift(Mono.Cecil.Cil.Instruction inst, JavaCode code) + { + // jvm shift instructions mask the shift count, so + // eliminate AND-ing with 31 and 63 prior to a shift. + + // the input inst should point to the first of three + // instructions, and here we check if the sequence is: + // ldc_i4 31 or 63; and; shift + + // used by LoadConstant (see above), CodeNumber::Calculation + + var next1 = inst.Next; + if (next1 != null && next1.OpCode.Code == Code.And) + { + var next2 = next1.Next; + if (next2 != null && ( next2.OpCode.Code == Code.Shl + || next2.OpCode.Code == Code.Shr + || next2.OpCode.Code == Code.Shr_Un)) + { + var stackArray = code.StackMap.StackArray(); + if ( stackArray.Length >= 2 + && IsLoadConstant(inst) is int shiftMask + && ( ( shiftMask == 31 + && stackArray[0].Equals(JavaType.IntegerType)) + || ( shiftMask == 63 + && stackArray[0].Equals(JavaType.LongType)))) + { + return true; + } + } + } + return false; + } + + + void CastToClass(object data) { var srcType = (CilType) code.StackMap.PopStack(CilMain.Where); diff --git a/CilToJava/src/CodeNumber.cs b/CilToJava/src/CodeNumber.cs index c7139f4..6ebd222 100644 --- a/CilToJava/src/CodeNumber.cs +++ b/CilToJava/src/CodeNumber.cs @@ -10,7 +10,8 @@ namespace SpaceFlint.CilToJava public static class CodeNumber { - public static void Conversion(JavaCode code, Code cilOp) + public static void Conversion(JavaCode code, Code cilOp, + Mono.Cecil.Cil.Instruction cilInst) { var oldType = (CilType) code.StackMap.PopStack(CilMain.Where); var (newType, overflow, unsigned) = ConvertOpCodeToTypeCode(cilOp); @@ -32,7 +33,10 @@ namespace SpaceFlint.CilToJava op = ConvertToLong(code, oldType.PrimitiveType, newType); else - op = ConvertToInteger(code, oldType.PrimitiveType, newType); + { + var opNext = cilInst.Next?.OpCode.Code ?? 0; + op = ConvertToInteger(code, oldType.PrimitiveType, newType, opNext); + } if (op != -1) code.NewInstruction((byte) op, null, null); @@ -82,7 +86,7 @@ namespace SpaceFlint.CilToJava return -1; // no output } - else if (! unsigned) // && (newType == TypeCode.Double) + else if (! unsigned) { // // convert to double @@ -184,7 +188,7 @@ namespace SpaceFlint.CilToJava - static int ConvertToInteger(JavaCode code, TypeCode oldType, TypeCode newType) + static int ConvertToInteger(JavaCode code, TypeCode oldType, TypeCode newType, Code opNext) { if (oldType == TypeCode.Double) { @@ -208,10 +212,23 @@ namespace SpaceFlint.CilToJava } if (newType == TypeCode.SByte) + { + // Stelem_I1 inserts 'i2b' (see CodeArrays::Store) + if (opNext == Code.Stelem_I1) + return 0x00; // nop + return 0x91; // i2b + } if (newType == TypeCode.Byte) { + if (opNext == Code.Stelem_I1) + { + // if the next instruction is Stelem.I1, which inserts 'i2b' + // (see CodeArrays::Store), then skip the masking below + return 0x00; // nop + } + code.StackMap.PushStack(JavaType.IntegerType); code.NewInstruction(0x12 /* ldc */, null, (int) 0xFF); code.StackMap.PushStack(JavaType.IntegerType); @@ -221,7 +238,16 @@ namespace SpaceFlint.CilToJava } if (newType == TypeCode.Int16) + { + if (opNext == Code.Stelem_I2) + { + // the next instruction is Stelem.I2, which inserts 'i2s' + // (see CodeArrays::Store) + return 0x00; // nop + } + return 0x93; // i2s + } if (newType == TypeCode.UInt16) return 0x92; // i2c @@ -333,8 +359,18 @@ namespace SpaceFlint.CilToJava - public static void Calculation(JavaCode code, Code cilOp) + public static void Calculation(JavaCode code, Code cilOp, + Mono.Cecil.Cil.Instruction cilInst) { + if ( cilOp == Code.And + && CodeBuilder.IsAndBeforeShift(cilInst.Previous, code)) + { + // jvm shift instructions mask the shift count, so + // eliminate AND-ing with 31 and 63 prior to a shift + code.NewInstruction(0x00 /* nop */, null, null); + return; + } + var stackTop1 = code.StackMap.PopStack(CilMain.Where); if (cilOp == Code.Sub && CodeSpan.SubOffset(stackTop1, code)) return; diff --git a/JavaBinary/src/JavaCodeWriter.cs b/JavaBinary/src/JavaCodeWriter.cs index b2abf56..93b08d8 100644 --- a/JavaBinary/src/JavaCodeWriter.cs +++ b/JavaBinary/src/JavaCodeWriter.cs @@ -12,6 +12,8 @@ namespace SpaceFlint.JavaBinary { wtr.Where.Push("method body"); + EliminateNops(); + int codeLength = FillInstructions(wtr); if (codeLength > 0xFFFE) throw wtr.Where.Exception("output method is too large"); @@ -434,6 +436,7 @@ namespace SpaceFlint.JavaBinary if (inst.Data is int intOffset) { // int data is a jump offset that can be calculated immediately + // note that this prevents nop elimination; see EliminateNops intOffset -= offset; inst.Bytes[1] = (byte) (offset >> 8); inst.Bytes[2] = (byte) offset; @@ -614,6 +617,47 @@ namespace SpaceFlint.JavaBinary return (index <= 3) ? 1 : ((index <= 255) ? 2 : 4); } + + + void EliminateNops() + { + // remove any nop instructions that are not a branch target, + // and only if there are no exception tables. note that + // removing nops that are a branch target, or nops in a method + // with exception tables, would require updating the stack map, + // branch instructions and exception tables. + + if (Exceptions != null && Exceptions.Count != 0) + return; + var nops = new List(); + + int n = Instructions.Count; + for (int i = 0; i < n; i++) + { + byte op = Instructions[i].Opcode; + if (op == 0x00 /* nop */) + { + // collect this nop only if it is not a branch target + if (! StackMap.HasBranchFrame(Instructions[i].Label)) + { + nops.Add(i); + } + } + else if ((instOperandType[op] & 0x40) == 0x40) // jump inst + { + if (Instructions[i].Data is int) + { + // if jump instruction has an explicit byte offset, + // we can't do nop elimination; see FillJumpTargets + return; + } + } + } + + for (int i = nops.Count; i-- > 0; ) + Instructions.RemoveAt(nops[i]); + } + } } diff --git a/PruneMerge/PruneMerge.fsproj b/PruneMerge/PruneMerge.fsproj index a70187d..5361119 100644 --- a/PruneMerge/PruneMerge.fsproj +++ b/PruneMerge/PruneMerge.fsproj @@ -10,6 +10,10 @@ OnOutputUpdated + + + None + @@ -24,5 +28,11 @@ ..\packages\FSharp.Core.4.7.2\lib\net45\FSharp.Core.dll + - \ No newline at end of file + + + + + + diff --git a/README.md b/README.md index 2dad064..12bbf33 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This is an initial release of a partial implementation of the .NET platform on top of the Java Virtual Machine, and compatible with Android runtime. The **Bluebonnet** bytecode compiler translates .NET [CIL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) into [Java bytecode](https://en.wikipedia.org/wiki/Java_bytecode) in Java classes, and additional run-time support is provided by the **Baselib** library. +https://www.spaceflint.com/bluebonnet + ## Highlights - 100% Java 8 bytecode with no native code. @@ -58,8 +60,8 @@ There are some additional demos: - Note that the Android demos require the `ANDROID_HOME` environment directory, and the project is hard-coded to use Android platform version 28, and build-tools 30.0.2 - Note also that the Android demos build an APK file, but do not install it. -See the [BNA](https://github.com/spaceflint7/bna) repository for another demo for Android. +See the [BNA](https://github.com/spaceflint7/bna) and [Unjum](https://github.com/spaceflint7/unjum) repositories for more demos for Android. ## Usage -For more information about using Bluebonnet, please see the [USAGE.md](USAGE.md) file. That document also records any known differences and deficiencies, compared to a proper .NET implementation. \ No newline at end of file +For more information about using Bluebonnet, please see the [USAGE.md](USAGE.md) file. That document also records any known differences and deficiencies, compared to a proper .NET implementation. diff --git a/Tests/src/TestCollection.cs b/Tests/src/TestCollection.cs index 088ef94..4c2f35d 100644 --- a/Tests/src/TestCollection.cs +++ b/Tests/src/TestCollection.cs @@ -25,6 +25,7 @@ namespace Tests TestSet(); TestStack(); TestHash(); + TestArrayList(); } @@ -140,5 +141,21 @@ namespace Tests Console.WriteLine(sum); } + + private class ListElement + { + [java.attr.RetainType] public int v1; + [java.attr.RetainType] public int v2; + } + + public void TestArrayList() + { + var list = new System.Collections.ArrayList( + new ListElement[] { + new ListElement { v1 = 1, v2 = 2 } + }); + foreach (var e in list) System.Console.WriteLine(e); + } + } } diff --git a/USAGE.md b/USAGE.md index 740f56c..55f3fa9 100644 --- a/USAGE.md +++ b/USAGE.md @@ -46,6 +46,30 @@ Java functional interfaces are supported via an artificial delegate, for example In this example, `java.lang.Thread.UncaughtExceptionHandler` is the functional interface, which gets an artificial delegate named `Delegate` as a nested type. The C# lambda is cast to this delegate, and then the `AsInterface` method is invoked, to convert the delegate to a Java interface. +# Attributes + +Bluebonnet recognizes the following attributes: + +- `[java.attr.DiscardAttribute]` on a top-level type (class/struct/interface/delegate) to exclude the type from output. For example, [Baselib/`Object.cs`](https://github.com/spaceflint7/bluebonnet/blob/master/Baselib/src/System/Object.cs) declares a `java.lang.Object` type with a `getClass` method, but there is no need to actually emit a Java class for `java.lang.Object`. For an example of this outside of Baselib, see [BNA/`Import.cs`](https://github.com/spaceflint7/bna/blob/master/BNA/src/Import.cs). + +- `[java.attr.RetainTypeAttribute]` on a field data member indicates not to box the field. This is useful for fields that participate in a code hot path, as it eliminates double-references when accessing the field. It should not be used with fields which may be referenced directly from code outside their containing assembly. + +- `[java.attr.AsInterfaceAttribute]` on a class causes it to be written in output as an interface. This allows the creation of default method implementations even with versions of C# that do not support this feature. See for example [Baselib/`IDisposable.cs`](https://github.com/spaceflint7/bluebonnet/blob/master/Baselib/src/System/IDisposable.cs). + +- `[java.attr.RetainNameAttribute]` on a type or method indicates that renaming should be inhibited. For example, an interface method would be renamed to "", to allow a class to implement multiple interfaces. However, this is not appropriate for implementing a Java interface whose methods already have a pre-set method name. See for example [Baselib/`IDisposable.cs`](https://github.com/spaceflint7/bluebonnet/blob/master/Baselib/src/System/IDisposable.cs). + +These attributes are emitted when Bluebonnet exports Java declarations to a .NET assembly, so they are available when such an assembly is referenced during compilation. + +# Performance Considerations + +For code that runs in a hot path, consider looking at the generated Java byte code to make sure there are no performance issues. Things to consider: + +- Fields of a reference type are generally held in a double-reference, to permit taking their reference. If such a field is used in a hot path, consider applying the `RetainType` attribute (see above). + +- Boxing a primitive or a reference type (as either a field or an array element) is a relatively expensive operation which allocates a wrapper object. For example, `a[i] += 2;` generates code that takes the address of an array element. To generate code which is less expensive when translated to Java, it could be re-written as `var v = a[i] + 2; a[i] = v;` + +Note that running Bluebonnet with a single argument that points to a Java archive will print a disassembly of the classes and methods within the archive. + # Inexact Implementation Here are some known differences, deficiencies and incompatibilities of the Bluebonnet .NET implementation, compared to a proper .NET implementation, in no particular order.