Some fixes

This commit is contained in:
spaceflint 2021-05-26 15:32:56 +03:00
parent 67ad9b0ccc
commit 30f187a636
11 changed files with 262 additions and 66 deletions

View File

@ -65,6 +65,7 @@ System.Nullable`*
System.SystemException
System.OverflowException
System.Collections.ArrayList
System.Collections.BitArray
System.Collections.Comparer
System.Collections.DictionaryEntry

View File

@ -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)

View File

@ -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);
}
}
}
}

View File

@ -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:

View File

@ -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);

View File

@ -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;

View File

@ -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>();
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]);
}
}
}

View File

@ -10,6 +10,10 @@
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
</PropertyGroup>
<Import Project="..\Solution.project" />
<PropertyGroup>
<!-- keep the following line below the import of Solution.project -->
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Cmdline.fs" />
<Compile Include="Program.fs" />
@ -24,5 +28,11 @@
<HintPath>..\packages\FSharp.Core.4.7.2\lib\net45\FSharp.Core.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression" />
<PackageReference Include="ILMerge" Version="3.0.29" />
</ItemGroup>
</Project>
<!-- use ILMerge to combine everything into PruneMerge.exe in the main output directory -->
<Import Project="..\packages\ILMerge.3.0.29\build\ILMerge.props" />
<Target Name="ILMerge" AfterTargets="AfterBuild" Condition="'$(Configuration)' == 'Release'">
<Exec Command="$(ILMergeConsolePath) /ndebug /out:$(ObjDir)$(AssemblyName).exe $(OutputPath)$(AssemblyName).exe $(OutputPath)JavaBinary.dll $(OutputPath)FSharp.Core.dll" />
</Target>
</Project>

View File

@ -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.
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.

View File

@ -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);
}
}
}

View File

@ -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 "<interfaceName><methodName>", 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.