Some changes for BNA

This commit is contained in:
spaceflint 2020-11-15 11:12:37 +02:00
parent 01a98c2f3f
commit d3426fc442
18 changed files with 487 additions and 16 deletions

View File

@ -142,14 +142,18 @@ System.Runtime.Serialization.StreamingContext
System.Runtime.Serialization.SerializationException System.Runtime.Serialization.SerializationException
System.IO.BinaryReader System.IO.BinaryReader
System.IO.BinaryWriter
System.IO.DirectoryNotFoundException System.IO.DirectoryNotFoundException
System.IO.EndOfStreamException System.IO.EndOfStreamException
System.IO.FileAccess System.IO.FileAccess
System.IO.FileAttributes
System.IO.FileMode System.IO.FileMode
System.IO.FileNotFoundException System.IO.FileNotFoundException
System.IO.FileShare
System.IO.MemoryStream System.IO.MemoryStream
System.IO.TextReader System.IO.TextReader
System.IO.TextWriter System.IO.TextWriter
System.IO.SeekOrigin
System.IO.StringReader System.IO.StringReader
System.IO.Stream System.IO.Stream
System.IO.StreamReader System.IO.StreamReader

View File

@ -0,0 +1,149 @@
namespace system
{
public static class BitConverter
{
[java.attr.RetainType] private static readonly bool _IsLittleEndian =
java.nio.ByteOrder.nativeOrder() == java.nio.ByteOrder.LITTLE_ENDIAN;
public static readonly bool IsLittleEndian = _IsLittleEndian;
public static int SingleToInt32Bits(float value) => java.lang.Float.floatToRawIntBits(value);
public static float Int32BitsToSingle(int value) => java.lang.Float.intBitsToFloat(value);
public static long DoubleToInt64Bits(double value) => java.lang.Double.doubleToRawLongBits(value);
public static double Int64BitsToDouble(long value) => java.lang.Double.longBitsToDouble(value);
public static byte[] GetBytes(bool value) => new byte[] { value ? (byte) 1 : (byte) 0 };
public static byte[] GetBytes(short value)
=> (byte[]) (object) java.util.Arrays.copyOf(
GetByteBuffer(2).putShort(0, value).array(), 2);
public static byte[] GetBytes(ushort value) => GetBytes((short) value);
public static byte[] GetBytes(char value) => GetBytes((short) value);
public static byte[] GetBytes(int value)
=> (byte[]) (object) java.util.Arrays.copyOf(
GetByteBuffer(4).putInt(0, value).array(), 4);
public static byte[] GetBytes(uint value) => GetBytes((int) value);
public static byte[] GetBytes(long value)
=> (byte[]) (object) java.util.Arrays.copyOf(
GetByteBuffer(8).putLong(0, value).array(), 8);
public static byte[] GetBytes(ulong value) => GetBytes((long) value);
public static byte[] GetBytes(float value)
=> (byte[]) (object) java.util.Arrays.copyOf(
GetByteBuffer(4).putFloat(0, value).array(), 4);
public static byte[] GetBytes(double value)
=> (byte[]) (object) java.util.Arrays.copyOf(
GetByteBuffer(8).putDouble(0, value).array(), 8);
public static short ToInt16(byte[] value, int startIndex)
{
ThrowHelper.ThrowIfNull(value);
if ((uint) startIndex >= value.Length || startIndex > value.Length - 2)
ThrowHelper.ThrowArgumentOutOfRangeException();
byte b0 = value[startIndex];
byte b1 = value[++startIndex];
if (! _IsLittleEndian)
(b0, b1) = (b1, b0);
return (short) (b0 | (b1 << 8));
}
public static ushort ToUInt16(byte[] value, int startIndex)
=> (ushort) ToInt16(value, startIndex);
public static char ToChar(byte[] value, int startIndex)
=> (char) ToInt16(value, startIndex);
public static int ToInt32(byte[] value, int startIndex)
{
ThrowHelper.ThrowIfNull(value);
if ((uint) startIndex >= value.Length || startIndex > value.Length - 4)
ThrowHelper.ThrowArgumentOutOfRangeException();
byte b0 = value[startIndex];
byte b1 = value[++startIndex];
byte b2 = value[++startIndex];
byte b3 = value[++startIndex];
if (! _IsLittleEndian)
(b0, b1, b2, b3) = (b3, b2, b1, b0);
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
}
public static uint ToUInt32(byte[] value, int startIndex)
=> (uint) ToInt32(value, startIndex);
public static float ToSingle(byte[] value, int startIndex)
=> java.lang.Float.intBitsToFloat(ToInt32(value, startIndex));
public static long ToInt64(byte[] value, int startIndex)
{
ThrowHelper.ThrowIfNull(value);
if ((uint) startIndex >= value.Length || startIndex > value.Length - 8)
ThrowHelper.ThrowArgumentOutOfRangeException();
byte b0 = value[startIndex];
byte b1 = value[++startIndex];
byte b2 = value[++startIndex];
byte b3 = value[++startIndex];
byte b4 = value[++startIndex];
byte b5 = value[++startIndex];
byte b6 = value[++startIndex];
byte b7 = value[++startIndex];
if (! _IsLittleEndian)
(b0, b1, b2, b3, b4, b5, b6, b7) = (b7, b6, b5, b4, b3, b2, b1, b0);
int i1 = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
int i2 = (b4 | (b5 << 8) | (b6 << 16) | (b7 << 24));
return (uint) i1 | ((long) i2 << 32);
}
public static ulong ToUInt64(byte[] value, int startIndex)
=> (ulong) ToInt64(value, startIndex);
public static double ToDouble(byte[] value, int startIndex)
=> java.lang.Double.longBitsToDouble(ToInt64(value, startIndex));
public static bool ToBoolean(byte[] value, int startIndex)
{
ThrowHelper.ThrowIfNull(value);
if (startIndex < 0 || startIndex > value.Length - 1)
ThrowHelper.ThrowArgumentOutOfRangeException();
return (value[startIndex] != 0);
}
public static string ToString(byte[] value, int startIndex, int length)
{
ThrowHelper.ThrowIfNull(value);
int valueLength = value.Length;
if ( startIndex < 0 || length < 0
|| (startIndex >= valueLength && startIndex > 0)
|| (startIndex > valueLength - length))
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
if (length == 0)
return "";
var output = new char[valueLength * 2];
int outputIndex = 0;
while (valueLength --> 0)
{
byte v = value[startIndex++];
output[outputIndex++] = HexChars[v >> 4];
output[outputIndex++] = HexChars[v & 0x0F];
}
return new string(output);
}
private static java.nio.ByteBuffer GetByteBuffer(int len)
{
var buffer = (java.nio.ByteBuffer) TlsByteBuffer.get();
if (buffer == null || buffer.limit() < len)
{
buffer = java.nio.ByteBuffer.allocate(len)
.order(java.nio.ByteOrder.nativeOrder());
TlsByteBuffer.set(buffer);
}
return buffer;
}
[java.attr.RetainType] static java.lang.ThreadLocal TlsByteBuffer =
new java.lang.ThreadLocal();
[java.attr.RetainType] private static readonly char[] HexChars =
"0123456789ABCDEF".ToCharArray();
}
}

View File

@ -8,6 +8,13 @@ namespace system
public static void InternalBlockCopy(Array src, int srcOffsetBytes, public static void InternalBlockCopy(Array src, int srcOffsetBytes,
Array dst, int dstOffsetBytes, int byteCount) Array dst, int dstOffsetBytes, int byteCount)
{ {
if (src.SyncRoot is sbyte[] srcBytes && dst.SyncRoot is sbyte[] dstBytes)
{
java.lang.System.arraycopy(srcBytes, srcOffsetBytes,
dstBytes, dstOffsetBytes, byteCount);
return;
}
if (src.SyncRoot is char[] srcChars && dst.SyncRoot is char[] dstChars) if (src.SyncRoot is char[] srcChars && dst.SyncRoot is char[] dstChars)
{ {
int srcIndex = srcOffsetBytes >> 1; int srcIndex = srcOffsetBytes >> 1;
@ -21,6 +28,7 @@ namespace system
return; return;
} }
} }
throw new System.PlatformNotSupportedException( throw new System.PlatformNotSupportedException(
"InternalBlockCopy/" + src.GetType() + "/" + dst.GetType()); "InternalBlockCopy/" + src.GetType() + "/" + dst.GetType());
} }

View File

@ -82,7 +82,7 @@ namespace system
// ToString // ToString
// //
public override string ToString() => JavaCalendar.ToString(); public override string ToString() => ToString(null, null);
public string ToString(IFormatProvider provider) => ToString(null, provider); public string ToString(IFormatProvider provider) => ToString(null, provider);
@ -207,6 +207,23 @@ namespace system
public static bool operator >= (DateTime d1, DateTime d2) public static bool operator >= (DateTime d1, DateTime d2)
=> d1.Ticks >= d2.Ticks; => d1.Ticks >= d2.Ticks;
//
// SpecifyKind, ToUniversalTime, ToLocalTime
//
public static DateTime SpecifyKind(DateTime value, DateTimeKind kind)
{
var javaCalendar = java.util.Calendar.getInstance();
if (kind == DateTimeKind.Utc)
javaCalendar.setTimeZone(TimeZoneUTC);
javaCalendar.setTimeInMillis(value.JavaCalendar.getTimeInMillis());
return new DateTime(javaCalendar, kind);
}
public DateTime ToUniversalTime() => SpecifyKind(this, DateTimeKind.Utc);
public DateTime ToLocalTime() => SpecifyKind(this, DateTimeKind.Local);
// //
// ISerializable // ISerializable
// //

View File

@ -157,6 +157,29 @@ namespace system
//
// CodeNumber.Indirection methods
//
public int Get_U8() => throw new System.NotSupportedException();
public int Get_I8() => throw new System.NotSupportedException();
public void Set_I8(int v) => throw new System.NotSupportedException();
public int Get_U16() => throw new System.NotSupportedException();
public int Get_I16() => throw new System.NotSupportedException();
public void Set_I16(int v) => throw new System.NotSupportedException();
public int Get_I32() => throw new System.NotSupportedException();
public void Set_I32(int v) => throw new System.NotSupportedException();
public long Get_I64() => java.lang.Double.doubleToRawLongBits(v);
public void Set_I64(long v) => Set(java.lang.Double.longBitsToDouble(v));
public double Get_F64() => throw new System.NotSupportedException();
public void Set_F64(double v) => throw new System.NotSupportedException();
// //
// IConvertible // IConvertible
// //

View File

@ -0,0 +1,32 @@
namespace system.io
{
public static class Directory
{
public static bool Exists(string path)
{
bool exists = false;
if (path != null && path.Length != 0)
{
var file = new java.io.File(path);
exists = file.exists() && file.isDirectory();
}
return exists;
}
public static DirectoryInfo CreateDirectory(string path)
{
ThrowHelper.ThrowIfNull(path);
path = path.Trim();
if (path.Length == 0)
throw new System.ArgumentException();
var file = new java.io.File(path);
file.mkdirs();
return new DirectoryInfo(file);
}
}
}

View File

@ -0,0 +1,12 @@
namespace system.io
{
public sealed class DirectoryInfo : FileSystemInfo
{
public DirectoryInfo(java.io.File javaFile) : base(javaFile) { }
}
}

View File

@ -0,0 +1,22 @@
using FileMode = System.IO.FileMode;
using FileAccess = System.IO.FileAccess;
using FileShare = System.IO.FileShare;
namespace system.io
{
public static class File
{
public static FileStream Open(string path, FileMode mode)
=> new FileStream(path, mode);
public static FileStream Open(string path, FileMode mode, FileAccess access)
=> new FileStream(path, mode, access);
public static FileStream Open(string path, FileMode mode, FileAccess access, FileShare share)
=> new FileStream(path, mode, access);
}
}

View File

@ -27,6 +27,9 @@ namespace system.io
: this(path, mode, (mode == System.IO.FileMode.Append : this(path, mode, (mode == System.IO.FileMode.Append
? System.IO.FileAccess.Write : System.IO.FileAccess.ReadWrite)) { } ? System.IO.FileAccess.Write : System.IO.FileAccess.ReadWrite)) { }
public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share)
: this(path, mode, access) { }
public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access) public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access)
{ {
ThrowHelper.ThrowIfNull(path); ThrowHelper.ThrowIfNull(path);
@ -131,8 +134,7 @@ namespace system.io
public override bool CanWrite => (Flags & CAN_WRITE) != 0; public override bool CanWrite => (Flags & CAN_WRITE) != 0;
public override bool CanSeek => (Flags & CAN_SEEK) != 0; public override bool CanSeek => (Flags & CAN_SEEK) != 0;
public override long Length public override long Length => JavaChannel.size();
=> throw new System.PlatformNotSupportedException();
public override long Position public override long Position
{ {
@ -184,10 +186,23 @@ namespace system.io
} }
public override void SetLength(long value) public override void SetLength(long value)
=> throw new System.PlatformNotSupportedException(); {
if (value < 0)
throw new System.ArgumentOutOfRangeException();
JavaChannel.truncate(value);
}
public override long Seek(long offset, System.IO.SeekOrigin origin) public override long Seek(long offset, System.IO.SeekOrigin origin)
=> throw new System.PlatformNotSupportedException(); {
if (origin == System.IO.SeekOrigin.Current)
offset += JavaChannel.position();
else if (origin == System.IO.SeekOrigin.End)
offset = JavaChannel.size() - offset;
else if (origin != System.IO.SeekOrigin.Begin)
throw new System.ArgumentException();
JavaChannel.position(offset);
return JavaChannel.position();
}
// //
// static constructor // static constructor
@ -199,6 +214,17 @@ namespace system.io
(java.lang.Class) typeof(java.io.FileNotFoundException), (java.lang.Class) typeof(java.io.FileNotFoundException),
(exc) => new System.IO.FileNotFoundException(exc.getMessage()) (exc) => new System.IO.FileNotFoundException(exc.getMessage())
); );
system.Util.DefineException(
(java.lang.Class) typeof(java.nio.channels.NonWritableChannelException),
(exc) => new System.NotSupportedException(exc.getMessage())
);
system.Util.DefineException(
(java.lang.Class) typeof(java.nio.channels.ClosedChannelException),
(exc) => new System.ObjectDisposedException(exc.getMessage())
);
} }
} }

View File

@ -0,0 +1,101 @@
using FileAttributes = System.IO.FileAttributes;
namespace system.io
{
public abstract class FileSystemInfo
{
[java.attr.RetainType] protected java.io.File JavaFile;
protected FileSystemInfo(java.io.File javaFile)
{
JavaFile = javaFile;
}
public virtual string FullName => JavaFile.getAbsolutePath();
public virtual string Name => JavaFile.getName();
public string Extension
{
get
{
var name = Name;
int dot = Name.LastIndexOf('.');
if (dot != -1)
{
int slash = Name.LastIndexOf('/');
if (dot > slash)
{
return ((java.lang.String) (object) name).substring(dot);
}
}
return "";
}
}
public FileAttributes Attributes
{
get
{
FileAttributes attr = (FileAttributes) 0;
if (JavaFile.isDirectory())
attr |= FileAttributes.Directory;
if (JavaFile.isHidden())
attr |= FileAttributes.Hidden;
if (! JavaFile.canWrite())
attr |= FileAttributes.ReadOnly;
if (attr == (FileAttributes) 0)
attr = FileAttributes.Normal;
return attr;
}
set => throw new System.PlatformNotSupportedException();
}
public virtual void Delete() => JavaFile.delete();
public virtual bool Exists => JavaFile.exists();
public DateTime CreationTimeUtc
{
get => throw new System.PlatformNotSupportedException();
set => throw new System.PlatformNotSupportedException();
}
public DateTime LastAccessTimeUtc
{
get => throw new System.PlatformNotSupportedException();
set => throw new System.PlatformNotSupportedException();
}
public DateTime LastWriteTimeUtc
{
// 10,000 DateTime ticks in a millisecond
get => new DateTime(JavaFile.lastModified() * 10000);
set => JavaFile.setLastModified(value.Ticks / 10000);
}
public DateTime CreationTime
{
get => CreationTimeUtc.ToLocalTime();
set => CreationTimeUtc = value.ToUniversalTime();
}
public DateTime LastAccessTime
{
get => LastAccessTimeUtc.ToLocalTime();
set => LastAccessTimeUtc = value.ToUniversalTime();
}
public DateTime LastWriteTime
{
get => LastWriteTimeUtc.ToLocalTime();
set => LastWriteTimeUtc = value.ToUniversalTime();
}
public void Refresh() { }
}
}

View File

@ -13,7 +13,7 @@ namespace system.io
public static bool IsPathRooted(string path) => false; public static bool IsPathRooted(string path) => false;
public static string Combine (string path1, string path2) public static string Combine(string path1, string path2)
{ {
path1 = CheckPath(path1); path1 = CheckPath(path1);
path2 = CheckPath(path2); path2 = CheckPath(path2);
@ -26,6 +26,12 @@ namespace system.io
return (ch != '/') ? (path1 + "/" + path2) : (path1 + path2); return (ch != '/') ? (path1 + "/" + path2) : (path1 + path2);
} }
public static string Combine(string path1, string path2, string path3)
=> Combine(Combine(path1, path2), path3);
}
public static class PathInternal
{
} }
} }

View File

@ -122,6 +122,29 @@ namespace system
//
// CodeNumber.Indirection methods
//
public int Get_U8() => throw new System.NotSupportedException();
public int Get_I8() => throw new System.NotSupportedException();
public void Set_I8(int v) => throw new System.NotSupportedException();
public int Get_U16() => throw new System.NotSupportedException();
public int Get_I16() => throw new System.NotSupportedException();
public void Set_I16(int v) => throw new System.NotSupportedException();
public int Get_I32() => java.lang.Float.floatToRawIntBits(v);
public void Set_I32(int v) => Set(java.lang.Float.intBitsToFloat(v));
public long Get_I64() => throw new System.NotSupportedException();
public void Set_I64(long v) => throw new System.NotSupportedException();
public double Get_F64() => throw new System.NotSupportedException();
public void Set_F64(double v) => throw new System.NotSupportedException();
// //
// IConvertible // IConvertible
// //

View File

@ -255,6 +255,13 @@ namespace system.text
return bytes; return bytes;
} }
public virtual int GetBytes(string s, int charIndex, int charCount,
byte[] bytes, int byteIndex)
{
ThrowHelper.ThrowIfNull(s);
return GetBytes(s.ToCharArray(), charIndex, charCount, bytes, byteIndex);
}
public virtual string GetString(byte[] bytes) public virtual string GetString(byte[] bytes)
{ {
ThrowHelper.ThrowIfNull(bytes); ThrowHelper.ThrowIfNull(bytes);

View File

@ -561,10 +561,10 @@ namespace SpaceFlint.CilToJava
// if we detect such a conflict at a branch target, we assume this // if we detect such a conflict at a branch target, we assume this
// is the cause, and set the stack elements to a common denominator // is the cause, and set the stack elements to a common denominator
// or to the lowest common denominator, java.lang.Object // or to the lowest common denominator, java.lang.Object
if ( IsReference && other.IsReference && other is CilType other2 if (IsReference && other.IsReference && other is CilType other2)
&& (! this.IsValueClass) && (! other2.IsValueClass))
{ {
return FindCommonSuperType(this, other2) ?? CilType.From(JavaType.ObjectType); return FindCommonSuperType(this, other2)
?? CilType.From(JavaType.ObjectType);
} }
return null; return null;

View File

@ -330,7 +330,7 @@ namespace SpaceFlint.CilToJava
public void Store(Code op) public void Store(Code op, Mono.Cecil.Cil.Instruction inst)
{ {
TypeCode elemType; TypeCode elemType;
@ -338,7 +338,7 @@ namespace SpaceFlint.CilToJava
{ {
case Code.Stelem_Ref: case Code.Stelem_Any: case Code.Stelem_Ref: case Code.Stelem_Any:
Store(null); Store(null, null);
return; return;
case Code.Stelem_I1: elemType = TypeCode.Byte; break; case Code.Stelem_I1: elemType = TypeCode.Byte; break;
@ -351,12 +351,12 @@ namespace SpaceFlint.CilToJava
default: throw new InvalidProgramException(); default: throw new InvalidProgramException();
} }
Store(CilType.From(new JavaType(elemType, 0, null))); Store(CilType.From(new JavaType(elemType, 0, null)), inst);
} }
void Store(CilType elemType) void Store(CilType elemType, Mono.Cecil.Cil.Instruction inst)
{ {
stackMap.PopStack(CilMain.Where); // value stackMap.PopStack(CilMain.Where); // value
stackMap.PopStack(CilMain.Where); // index stackMap.PopStack(CilMain.Where); // index
@ -406,6 +406,8 @@ namespace SpaceFlint.CilToJava
elemType = arrayType.AdjustRank(-arrayType.ArrayRank); elemType = arrayType.AdjustRank(-arrayType.ArrayRank);
} }
CheckImmediate(arrayType.PrimitiveType, inst);
if (arrayType.IsValueClass || elemType.IsValueClass) if (arrayType.IsValueClass || elemType.IsValueClass)
{ {
CilMethod.ValueMethod(CilMethod.ValueClone, code); CilMethod.ValueMethod(CilMethod.ValueClone, code);
@ -413,6 +415,32 @@ namespace SpaceFlint.CilToJava
code.NewInstruction(elemType.StoreArrayOpcode, null, null); 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);
}
}
}
} }
@ -505,7 +533,7 @@ namespace SpaceFlint.CilToJava
stackMap.PushStack(valueType); stackMap.PushStack(valueType);
} }
Store(elemType); Store(elemType, null);
} }
else if (method.Name == "Address") else if (method.Name == "Address")

View File

@ -418,7 +418,7 @@ namespace SpaceFlint.CilToJava
case Code.Stelem_I: case Code.Stelem_R4:case Code.Stelem_R8:case Code.Stelem_Any: case Code.Stelem_I: case Code.Stelem_R4:case Code.Stelem_R8:case Code.Stelem_Any:
case Code.Stelem_Ref: case Code.Stelem_Ref:
arrays.Store(cilOp); arrays.Store(cilOp, cilInst);
break; break;
case Code.Ldftn: case Code.Ldvirtftn: case Code.Ldftn: case Code.Ldvirtftn:

View File

@ -121,7 +121,18 @@ namespace SpaceFlint.CilToJava
else if (dstType.IsReference) else if (dstType.IsReference)
{ {
CodeArrays.CheckCast(dstType, true, code); CodeArrays.CheckCast(dstType, true, code);
if (dstType.IsGenericParameter)
if ( srcType.ArrayRank != 0
&& srcType.ArrayRank == dstType.ArrayRank
&& srcType.PrimitiveType != 0
&& dstType.PrimitiveType != 0
&& srcType.AdjustRank(-srcType.ArrayRank).NewArrayType
== dstType.AdjustRank(-dstType.ArrayRank).NewArrayType)
{
// casting to same java array type, e.g. byte[] to sbyte[]
op = 0x00; // nop
}
else if (dstType.IsGenericParameter)
op = 0x00; // nop op = 0x00; // nop
else else
op = 0xC0; // checkcast op = 0xC0; // checkcast

View File

@ -58,6 +58,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 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. - 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.
## Usage ## 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.