From 3d84879ed845777a1a224fae81acfd97c950bd5d Mon Sep 17 00:00:00 2001 From: spaceflint <> Date: Mon, 5 Jul 2021 19:32:34 +0300 Subject: [PATCH] Optimizations --- CilToJava/src/CilInterface.cs | 39 ++-- CilToJava/src/CilMethod.cs | 12 +- CilToJava/src/CilType.cs | 72 +++++-- CilToJava/src/CodeCall.cs | 18 ++ CilToJava/src/CodeCompare.cs | 48 +++-- CilToJava/src/CodeLocals.cs | 4 +- JavaBinary/src/JavaCodeWriter.cs | 30 +++ README.md | 14 +- USAGE-ANDROID.md | 311 +++++++++++++++++++++++++++++++ USAGE.md | 4 +- 10 files changed, 504 insertions(+), 48 deletions(-) create mode 100644 USAGE-ANDROID.md diff --git a/CilToJava/src/CilInterface.cs b/CilToJava/src/CilInterface.cs index 11b268d..65c12ce 100644 --- a/CilToJava/src/CilInterface.cs +++ b/CilToJava/src/CilInterface.cs @@ -320,11 +320,15 @@ namespace SpaceFlint.CilToJava public static List CollectAll(TypeDefinition fromType) { - var list = new List(); - Process(fromType, list); - return list; + var map = new Dictionary>(); + Process(fromType, map); - void Process(TypeDefinition fromType, List list) + var newList = new List(); + foreach (var oldList in map.Values) + newList.AddRange(oldList); + return newList; + + void Process(TypeDefinition fromType, Dictionary> map) { foreach (var fromMethod in fromType.Methods) { @@ -343,19 +347,26 @@ namespace SpaceFlint.CilToJava var outputMethod = new CilInterfaceMethod(inputMethod); - bool dup = false; - foreach (var oldMethod in list) + if (map.TryGetValue(inputMethod.Name, out var list)) { - if ( oldMethod.Method.Name == outputMethod.Method.Name - && oldMethod.EqualParameters(outputMethod)) + bool dup = false; + foreach (var oldMethod in list) { - dup = true; - break; + if (oldMethod.EqualParameters(outputMethod)) + { + dup = true; + break; + } } + if (! dup) + list.Add(outputMethod); + } + else + { + var list2 = new List(); + list2.Add(outputMethod); + map.Add(inputMethod.Name, list2); } - - if (! dup) - list.Add(outputMethod); CilMain.GenericStack.Release(genericMark); } @@ -369,7 +380,7 @@ namespace SpaceFlint.CilToJava var genericMark = CilMain.GenericStack.Mark(); CilMain.GenericStack.EnterType(fromBaseTypeDef); - Process(fromBaseTypeDef, list); + Process(fromBaseTypeDef, map); CilMain.GenericStack.Release(genericMark); } diff --git a/CilToJava/src/CilMethod.cs b/CilToJava/src/CilMethod.cs index c3d4eea..253ee3a 100644 --- a/CilToJava/src/CilMethod.cs +++ b/CilToJava/src/CilMethod.cs @@ -708,13 +708,21 @@ namespace SpaceFlint.CilToJava + static Dictionary _MethodDefs = + new Dictionary(); + internal static MethodDefinition AsDefinition(MethodReference _ref) { if (_ref.IsDefinition) return _ref as MethodDefinition; - var def = _ref.Resolve(); - if (def != null) + if (_MethodDefs.TryGetValue(_ref, out var def)) return def; + def = _ref.Resolve(); + if (def != null) + { + _MethodDefs.Add(_ref, def); + return def; + } throw CilMain.Where.Exception( $"could not resolve method '{_ref.Name}' from assembly '{_ref.DeclaringType.Scope}'"); } diff --git a/CilToJava/src/CilType.cs b/CilToJava/src/CilType.cs index 695289a..5067de3 100644 --- a/CilToJava/src/CilType.cs +++ b/CilToJava/src/CilType.cs @@ -25,6 +25,10 @@ namespace SpaceFlint.CilToJava { if (! _Types.TryGetValue(fromType, out var converted)) { + converted = GetCachedPrimitive(fromType); + if (converted != null) + return converted; + CilMain.Where.Push($"type '{fromType.FullName}'"); converted = new CilType(); @@ -89,11 +93,12 @@ namespace SpaceFlint.CilToJava } else { - var metadataType = AsDefinition(fromType).MetadataType; + var defType = AsDefinition(fromType); + var metadataType = defType.MetadataType; bool isValueType = (metadataType == MetadataType.ValueType); if (isValueType || (metadataType == MetadataType.Class)) { - ImportClass(fromType, isValueType); + ImportClass(fromType, defType, isValueType); } else { @@ -143,12 +148,10 @@ namespace SpaceFlint.CilToJava - void ImportClass(TypeReference fromType, bool isValue) + void ImportClass(TypeReference fromType, TypeDefinition defType, bool isValue) { int numGeneric = ImportGenericParameters(fromType); - var defType = AsDefinition(fromType); - if (defType.HasCustomAttribute( "System.Runtime.Remoting.Contexts.SynchronizationAttribute", true)) throw CilMain.Where.Exception($"attribute [Synchronization] is not supported"); @@ -226,9 +229,7 @@ namespace SpaceFlint.CilToJava Flags |= INTERFACE; else if (IsDelegateClass(defType)) - { Flags |= DELEGATE; - } } PrimitiveType = TypeCode.Empty; @@ -434,7 +435,8 @@ namespace SpaceFlint.CilToJava for (int i = 0; i <= n; i++) { - if (SuperTypes[i].HasGenericParameters || SuperTypes[i].HasGenericSuperType) + //if (SuperTypes[i].HasGenericParameters || SuperTypes[i].HasGenericSuperType) + if (SuperTypes[i].IsGenericThisOrSuper) Flags |= HAS_GEN_SUP; } } @@ -607,21 +609,67 @@ namespace SpaceFlint.CilToJava + static Dictionary _TypeDefs = + new Dictionary(); + internal static TypeDefinition AsDefinition(TypeReference _ref) { if (_ref.IsDefinition) return _ref as TypeDefinition; - var def = _ref.Resolve(); - if (def != null) + if (_TypeDefs.TryGetValue(_ref, out var def)) return def; - if (_ref.GetElementType() is GenericParameter) - return AsDefinition(_ref.Module.TypeSystem.Object); + def = _ref.Resolve(); + if (def == null && _ref.GetElementType() is GenericParameter) + def = AsDefinition(_ref.Module.TypeSystem.Object); + if (def != null) + { + _TypeDefs.Add(_ref, def); + return def; + } throw CilMain.Where.Exception( $"could not resolve type '{_ref}' from assembly '{_ref.Scope}'"); } + static CilType[] _Primitives = new CilType[32]; + + private static CilType GetCachedPrimitive(TypeReference fromType) + { + var metadataType = fromType.MetadataType; + switch (metadataType) + { + case MetadataType.Void: + case MetadataType.Boolean: + case MetadataType.Char: + case MetadataType.SByte: + case MetadataType.Byte: + case MetadataType.Int16: + case MetadataType.UInt16: + case MetadataType.Int32: + case MetadataType.UInt32: + case MetadataType.Int64: + case MetadataType.UInt64: + case MetadataType.Single: + case MetadataType.Double: + case MetadataType.String: + case MetadataType.IntPtr: + case MetadataType.UIntPtr: + case MetadataType.Object: + + var resultType = _Primitives[(int) metadataType]; + if (resultType == null) + { + resultType = new CilType(); + resultType.Import(AsDefinition(fromType)); + _Primitives[(int) metadataType] = resultType; + } + return resultType; + } + return null; + } + + protected void SetBoxedFlags(bool clonedAtTop) => Flags |= (VALUE | BYREF) | (clonedAtTop ? CLONED_TOP : 0); diff --git a/CilToJava/src/CodeCall.cs b/CilToJava/src/CodeCall.cs index a0ff6a2..d4d22b4 100644 --- a/CilToJava/src/CodeCall.cs +++ b/CilToJava/src/CodeCall.cs @@ -525,6 +525,22 @@ namespace SpaceFlint.CilToJava int SaveMethodArguments(CilMethod callMethod) { + // pop method arguments off the stack and into temporary locals. + // this is needed when we need to manipulate the object reference, + // which was pushed before the arguments, in some way. for example, + // + // .Net has a 'newobj' which allocates the object and then calls the + // constructor, but we have to pop all arguments, insert a jvm 'new' + // instruction, then push arguments before the call to constructor. + // + // another example is a virtual call which must manipulate the pushed + // object reference in some way. see calls to this method throughout + // this source file for details. + // + // this creates noise of 'load'/'store' instructions in the generated + // code. but disassembly using 'dexdump' shows that the Android 'D8' + // compiler generally optimizes all of this away. + int localIndex = -1; int i = callMethod.Parameters.Count - (callMethod.HasDummyClassArg ? 1 : 0); while (i-- > 0) @@ -564,6 +580,8 @@ namespace SpaceFlint.CilToJava void LoadMethodArguments(CilMethod callMethod, int localIndex) { + // push methods arguments, saved by SaveMethodArguments. + int i = callMethod.Parameters.Count - (callMethod.HasDummyClassArg ? 1 : 0); while (i-- > 0) { diff --git a/CilToJava/src/CodeCompare.cs b/CilToJava/src/CodeCompare.cs index b0c5643..134206f 100644 --- a/CilToJava/src/CodeCompare.cs +++ b/CilToJava/src/CodeCompare.cs @@ -631,11 +631,7 @@ namespace SpaceFlint.CilToJava else if (! TestForBranch(code, castClass, cilInst.Next)) { ushort nextLabel = (ushort) cilInst.Next.Offset; - int localIndex = locals.GetTempIndex(stackTop); - - TestAndCast(code, castClass, stackTop, nextLabel, localIndex); - - locals.FreeTempIndex(localIndex); + TestAndCast(code, castClass, stackTop, nextLabel, locals); } } else @@ -669,14 +665,41 @@ namespace SpaceFlint.CilToJava // void TestAndCast(JavaCode code, JavaType castClass, JavaType stackTop, - ushort nextLabel, int localIndex) + ushort nextLabel, CodeLocals locals) { - code.NewInstruction(stackTop.StoreOpcode, null, (int) localIndex); + int localIndex = -1; + if (cilInst?.Previous is Mono.Cecil.Cil.Instruction prevInst) + { + (_, localIndex) = locals.GetLocalFromLoadInst( + prevInst.OpCode.Code, prevInst.Operand); + } - code.NewInstruction(0x01 /* aconst_null */, null, null); + bool usingTempIndex; + if (localIndex != -1) + { + // in the common case, the the preceding instruction loads + // a local or an argument with a known index number, so we + // don't have a allocate a temporary local variable. but + // we will have to swap the values after pushing null + usingTempIndex = false; + + code.NewInstruction(0x01 /* aconst_null */, null, null); + code.NewInstruction(0x5F /* swap */, null, null); + } + else + { + // otherwise we need to allocate a temporary variable, save + // the top of stack into it, push null, and load the temp + usingTempIndex = true; + localIndex = locals.GetTempIndex(stackTop); + + code.NewInstruction(stackTop.StoreOpcode, null, localIndex); + code.NewInstruction(0x01 /* aconst_null */, null, null); + code.NewInstruction(stackTop.LoadOpcode, null, localIndex); + } + + // the stack top has two values: null and objref code.StackMap.PushStack(castClass); - - code.NewInstruction(stackTop.LoadOpcode, null, (int) localIndex); code.StackMap.PushStack(stackTop); code.NewInstruction(0xC1 /* instanceof */, castClass, null); @@ -688,10 +711,13 @@ namespace SpaceFlint.CilToJava code.NewInstruction(0x57 /* pop */, null, null); code.StackMap.PopStack(CilMain.Where); - code.NewInstruction(stackTop.LoadOpcode, null, (int) localIndex); + code.NewInstruction(stackTop.LoadOpcode, null, localIndex); code.NewInstruction(0xC0 /* checkcast */, castClass, null); code.StackMap.PushStack(castClass); + + if (usingTempIndex) + locals.FreeTempIndex(localIndex); } } diff --git a/CilToJava/src/CodeLocals.cs b/CilToJava/src/CodeLocals.cs index 6871cfe..36587c3 100644 --- a/CilToJava/src/CodeLocals.cs +++ b/CilToJava/src/CodeLocals.cs @@ -635,8 +635,10 @@ namespace SpaceFlint.CilToJava public (CilType, int) GetLocalFromLoadInst(Code op, object data) { int localIndex; - if (op >= Code.Ldloc_0 && op <= Code.Ldloc_3) + if (op >= Code.Ldloc_0 && op <= Code.Ldloc_3) localIndex = VariableIndex(op - Code.Ldloc_0); + else if (op >= Code.Ldarg_0 && op <= Code.Ldarg_3) + localIndex = ArgumentIndex(op - Code.Ldarg_0); else if (data is ParameterDefinition dataArg) localIndex = ArgumentIndex(dataArg.Sequence); else if (data is VariableDefinition dataVar) diff --git a/JavaBinary/src/JavaCodeWriter.cs b/JavaBinary/src/JavaCodeWriter.cs index 93b08d8..00ea198 100644 --- a/JavaBinary/src/JavaCodeWriter.cs +++ b/JavaBinary/src/JavaCodeWriter.cs @@ -12,6 +12,7 @@ namespace SpaceFlint.JavaBinary { wtr.Where.Push("method body"); + PerformOptimizations(); EliminateNops(); int codeLength = FillInstructions(wtr); @@ -619,6 +620,35 @@ namespace SpaceFlint.JavaBinary + void PerformOptimizations() + { + int n = Instructions.Count; + var prevInst = Instructions[0]; + for (int i = 1; i < n; i++) + { + var currInst = Instructions[i]; + + // find occurrences of iconst_0 or iconst_1 followed by i2l + // (which must not be a branch target), and convert to lconst_xx + + if (currInst.Opcode == 0x85 /* i2l */) + { + if ( prevInst.Opcode == 0x12 /* ldc */ + && prevInst.Data is int intValue + && (intValue == 0 || intValue == 1) + && (! StackMap.HasBranchFrame(currInst.Label))) + { + // convert ldc of 0 or 1 into lconst_0 or lconst_1 + prevInst.Opcode = (byte) (intValue + 0x09); + currInst.Opcode = 0x00; // nop + } + } + prevInst = currInst; + } + } + + + void EliminateNops() { // remove any nop instructions that are not a branch target, diff --git a/README.md b/README.md index 12bbf33..9ff43ab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bluebonnet -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. +This is 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 @@ -12,16 +12,18 @@ https://www.spaceflint.com/bluebonnet - Simple interoperability with Java APIs. - Tested with C# and F# programs. -## Requirements +## Requirements for Building - Java 8 is required during building, to import Java classes from the `rt.jar` file. - Importing from Java 9 modules is not yet supported. - The translated code can run on Java 8 or later version. - Alternatively, `(ANDROID_HOME)/platforms/android-XX/android.jar` from Android SDK can be copied as `(JAVA_HOME)\jre\lib\rt.jar` file. - Android build tools with support for `D8`/`R8` desugaring. - - Tested with build tools version 30.0.2 and platform API version 28. -- .NET Framework 4.7.2 - - Tested only on Windows at this time. Not thoroughly tested with .NET Core. + - Tested with build tools version 30.0.2 and platform API version 30. +- .NET Framework (4.7 or later) + - Tested only on Windows at this time. May not necessarily build with .NET Core. + +[Recent releases](https://github.com/spaceflint7/bluebonnet/releases) include a pre-built `Android.dll` reference assembly, created from the latest Android SDK, as well as all binaries needed for running Bluebonnet. See the Usage section below. ## Building @@ -64,4 +66,4 @@ See the [BNA](https://github.com/spaceflint7/bna) and [Unjum](https://github.com ## 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. To use Bluebonnet in Android Studio, see [USAGE-ANDROID.md](USAGE-ANDROID.md). \ No newline at end of file diff --git a/USAGE-ANDROID.md b/USAGE-ANDROID.md new file mode 100644 index 0000000..5d23ef5 --- /dev/null +++ b/USAGE-ANDROID.md @@ -0,0 +1,311 @@ + +#### Goal + +- Set up development of an Android app using a .NET language + + - Either in Visual Studio or using the command line `dotnet` tool. + + +- Use Android Studio to build the app. + + - With a Gradle plugin to compile .NET code. + + - And a Gradle build task to convert the .NET code to Java compiled form. + + +- Most of development should be possible on Windows without requiring an Android device. + + - Platform-specific parts can be qualified with preprocessor constants. + +#### Environment Variables + +- Set the environment variable `MSBUILD_EXE` to point to `MSBuild.exe` program file. + - For example, `C:\Program Files (x86)\Microsoft Visual Studio\\2019\Community\MSBuild\Current\Bin\MSBuild.exe` + + +- Set the environment variable `BLUEBONNET_DIR` to point to a directory containing `Bluebonnet.exe`, `PruneMerge.exe`, `Baselib.jar` and `Android.dll`. + +- These files can be downloaded from the [Bluebonnet releases](https://github.com/spaceflint7/bluebonnet/releases) page. + +#### .NET Project Using DotNet Tool + +- Create a new directory for the project, and change to this directory. + +- Create a .NET Core project called `DotNet`: + + - `dotnet new console -n DotNet` + + - You may use some other project type or language instead of a C# console app. + + - The project must be named `DotNet`. + + +- Edit `DotNet.csproj` to add a reference to Android DLL created by Bluebonnet: + + + + $(BLUEBONNET_DIR)\Android.dll + + + +- Or, you can create the project using Visual Studio: + +#### .NET Project Using Visual Studio + +- Create a new project. + +- Select a __C# Console Application__ template (.NET Framework or .NET Core). + +- Specify `DotNet` for the project name, and check the box to __Place solution and project in the same directory__. + +- Or specify some other project name and clear that checkbox, but make sure: + + - The solution name is `DotNet`. + + - In __Project Settings__ in Visual Studio, set the __Assembly name__ to `DotNet`. + + +- Complete the creation of the project and solution. + +- Add a reference to `Android.dll` using Visual Studio, or by editing the project file as shown above. + +#### Activity Class + +- Leave the `Program.cs` file as it is. + + - Otherwise you may get a compilation error about a missing `Main` method. + + - The class in this file will be discarded from the output, unless explicitly referenced. + + +- Create a new file named `MainActivity.cs` and paste the following into it: + + #if ANDROID + + namespace com.whatever.example + { + public sealed class MainActivity : android.app.Activity + { + protected override void onCreate (android.os.Bundle savedInstanceState) + { + android.util.Log.i("EXAMPLE", ">>>>>>>> EXAMPLE ACTIVITY <<<<<<<<"); + base.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } + } + + #pragma warning disable IDE1006 // Must begin with uppercase letter + #pragma warning disable CA2211 // Non-constant fields should not be visible + + [java.attr.Discard] // discard in output + public class R + { + [java.attr.Discard] // discard in output + public class layout + { + [java.attr.RetainType] public static int activity_main; + } + } + } + + #endif + +#### Android Project in Android Studio + +- Create a new project. + +- Select the __Empty Activity__ template. + + - This generates activity definitions in the `AndroidManifest.xml` file. + + +- For this example, set __Package name__ to `com.whatever.example`. + +- Set the __Save location__ to the project root directory. + - Ignore the warning that this directory is not empty. + + +- Select __Java__ for the language and a reasonable minimum platform API version (e.g. __18__). + +- Complete the creation of the project. + +- Delete the Java class generated by Android Studio for the main activity. + + - It should be located in the project class `app/java/com.whatever.example/MainActivity` + + - Alternatively, delete the entire directory of Java source files - `app/src/main/java` + +#### Gradle Build Script + +- Open the __module__-specific build script. + + - In Android Studio, this is the `build.gradle` file annotated with __Module__ (rather than Project). + + - In the directory structure, this is `app/build.gradle` (rather than the top-level file with the same name). + + - Note that the right file begins with a `plugins` section followed by an `android` section. + + +- Optional: Between the `android` section, and the `dependencies` section, insert: + + buildDir = "${project.rootDir}/build/${project.name}" + + - This sets the output build directory just below the top-level of the project. + + - Otherwise the default is the `app/build` directory. + + +- At the top of the `dependencies` section, insert: + + implementation files("$buildDir/dotnet/dotnet.jar") + +- After the `dependencies` section, append: + + task buildDotNet { + doLast { + delete("${buildDir}/dotnet/DotNet.jar") + exec { + workingDir "${project.rootDir}" + commandLine System.env.MSBUILD_EXE ?: 'msbuild.exe', + 'DotNet', '-r', + '-p:OutputType=Library', + '-p:Configuration=Release', + '-p:DefineConstants=ANDROID', + "-p:OutputPath=$buildDir/dotnet", + "-p:IntermediateOutputPath=$buildDir/dotnet/intermediate/" + } + exec { + commandLine "${System.env.BLUEBONNET_DIR}/Bluebonnet.exe", + "${buildDir}/dotnet/DotNet.dll", + "${buildDir}/dotnet/DotNet0.jar" + } + def manifest = new XmlSlurper().parse(android.sourceSets.main.manifest.srcFile) + exec { + commandLine "${System.env.BLUEBONNET_DIR}/PruneMerge.exe", + "${buildDir}/dotnet/DotNet0.jar", + "${System.env.BLUEBONNET_DIR}/Baselib.jar", + "${buildDir}/dotnet/DotNet.jar", + ":${manifest.@package}${manifest.application.activity.'@android:name'}" + } + } + } + + preBuild.dependsOn buildDotNet + + +- Note the use of environment variables in the build script: + + - `MSBUILD_EXE` should specify the path to the `MSBuild.exe` program. + + - `BLUEBONNET_DIR` should specify the directory containing __Bluebonnet__ binaries. + + - These environment variables should be made visible to Android Studio. + +#### Gradle Build Script - Sample + +- After updating your `app/build.gradle` file, it should look (more or less) like this: + + plugins { + id 'com.android.application' + } + + android { + compileSdkVersion 30 + + defaultConfig { + applicationId "com.whatever.example" + minSdkVersion 18 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + + buildDir = "${project.rootDir}/build/${project.name}" + + dependencies { + + implementation files("$buildDir/dotnet/dotnet.jar") + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + } + + task buildDotNet { + doLast { + delete("${buildDir}/dotnet/DotNet.jar") + + exec { + workingDir "${project.rootDir}" + commandLine System.env.MSBUILD_EXE ?: 'msbuild.exe', + 'DotNet', '-r', + '-p:OutputType=Library', + '-p:Configuration=Release', + '-p:DefineConstants=ANDROID', + "-p:OutputPath=$buildDir/dotnet", + "-p:IntermediateOutputPath=$buildDir/dotnet/intermediate/" + } + exec { + commandLine "${System.env.BLUEBONNET_DIR}/Bluebonnet.exe", + "${buildDir}/dotnet/DotNet.dll", + "${buildDir}/dotnet/DotNet0.jar" + } + def manifest = new XmlSlurper().parse(android.sourceSets.main.manifest.srcFile) + exec { + commandLine "${System.env.BLUEBONNET_DIR}/PruneMerge.exe", + "${buildDir}/dotnet/DotNet0.jar", + "${System.env.BLUEBONNET_DIR}/Baselib.jar", + "${buildDir}/dotnet/DotNet.jar", + ":${manifest.@package}${manifest.application.activity.'@android:name'}" + } + } + } + + preBuild.dependsOn buildDotNet + +#### Gradle Build Script - Overview + +- In place of Java source files, a new dependency was added on a JAR file - `dotnet.jar`. + +- The `buildDotNet` task was defined to perform the following build commands: + + - Run `MSBuild` on the .NET project, in `Release` configuration, with the preprocessor define `ANDROID` + + - Run `Bluebonnet` on the resulting `dotnet.dll` to create `dotnet0.jar` + + - Extract the class name for the main activity from the file `AndroidManifest.xml` + + - Run `PruneMerge` to merge `Baselib.jar` (from Bluebonnet) and `dotnet0.jar` into the final `dotnet.jar` + + - All unreferenced classes are discard from the output. + + +- The `buildDotNet` task was set to execute before the Gradle `preBuild` task. + +#### Test It + +- Compile and run the project in Android Studio. + +- If all goes well, the example app should start in the emulator, and display Hello World! + + - This layout comes from the `activity_main.xml` layout file, generated by Android Studio. + + - `onCreate()` in the C# `MainActivity` class calls `setContentView` to inflate this layout. + + +- The __Logcat__ tab should show the debug log message printed by `MainActivity.onCreate()` diff --git a/USAGE.md b/USAGE.md index 55f3fa9..de40987 100644 --- a/USAGE.md +++ b/USAGE.md @@ -12,7 +12,7 @@ By default, all types/classes in the input are processed; one or more wildcard ` If `input_file` is a Java archive, then the output written to `output_file` is a [reference assembly](https://docs.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies) declaring all types in the input. For example, - Bluebonnet (java_home)\jre\lib\rt.jar .obj\Javalib.dll + Bluebonnet $JAVA_HOME\jre\lib\rt.jar .obj\Javalib.dll #### .NET Assembly to Java Code @@ -56,7 +56,7 @@ Bluebonnet recognizes the following attributes: - `[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). +- `[java.attr.RetainNameAttribute]` on a type or method indicates that renaming should be inhibited. For example, an interface method would be renamed to the concatenation of interface name and method name, 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.