-
Notifications
You must be signed in to change notification settings - Fork 63
[generator] Encapsulate and parallelize enum generation. #442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using static MonoDroid.Generation.EnumMappings; | ||
|
|
||
| namespace MonoDroid.Generation | ||
| { | ||
| class EnumGenerator | ||
| { | ||
| protected TextWriter sw; | ||
|
|
||
| public EnumGenerator (TextWriter writer) | ||
| { | ||
| sw = writer; | ||
| } | ||
|
|
||
| public void WriteEnumeration (KeyValuePair<string, EnumDescription> enu, GenBase [] gens) | ||
| { | ||
| string ns = enu.Key.Substring (0, enu.Key.LastIndexOf ('.')).Trim (); | ||
| string enoom = enu.Key.Substring (enu.Key.LastIndexOf ('.') + 1).Trim (); | ||
|
|
||
| sw.WriteLine ("namespace {0} {{", ns); | ||
| if (enu.Value.BitField) | ||
| sw.WriteLine (" [System.Flags]"); | ||
| sw.WriteLine (" public enum {0} {{", enoom); | ||
|
|
||
| foreach (var member in enu.Value.Members) { | ||
| var managedMember = FindManagedMember (enu.Value, member.Key, gens); | ||
| sw.WriteLine (" [global::Android.Runtime.IntDefinition (" + (managedMember != null ? "\"" + managedMember + "\"" : "null") + ", JniField = \"" + StripExtraInterfaceSpec (enu.Value.JniNames [member.Key]) + "\")]"); | ||
| sw.WriteLine (" {0} = {1},", member.Key.Trim (), member.Value.Trim ()); | ||
| } | ||
| sw.WriteLine (" }"); | ||
| sw.WriteLine ("}"); | ||
| } | ||
|
|
||
| string FindManagedMember (EnumDescription desc, string enumFieldName, IEnumerable<GenBase> gens) | ||
| { | ||
| if (desc.FieldsRemoved) | ||
| return null; | ||
|
|
||
| var jniMember = desc.JniNames [enumFieldName]; | ||
| if (string.IsNullOrWhiteSpace (jniMember)) { | ||
| // enum values like "None" falls here. | ||
| return null; | ||
| } | ||
| return FindManagedMember (jniMember, gens); | ||
| } | ||
|
|
||
| WeakReference cache_found_class; | ||
|
|
||
| string FindManagedMember (string jniMember, IEnumerable<GenBase> gens) | ||
| { | ||
| string package, type, member; | ||
| ParseJniMember (jniMember, out package, out type, out member); | ||
| var fullJavaType = (string.IsNullOrEmpty (package) ? "" : package + ".") + type; | ||
|
|
||
| var cls = cache_found_class != null ? cache_found_class.Target as GenBase : null; | ||
| if (cls == null || cls.JniName != fullJavaType) { | ||
| cls = gens.FirstOrDefault (g => g.JavaName == fullJavaType); | ||
| if (cls == null) { | ||
| // The class was not found e.g. removed by metadata fixup. | ||
| return null; | ||
| } | ||
| cache_found_class = new WeakReference (cls); | ||
| } | ||
| var fld = cls.Fields.FirstOrDefault (f => f.JavaName == member); | ||
| if (fld == null) { | ||
| // The field was not found e.g. removed by metadata fixup. | ||
| return null; | ||
| } | ||
| return cls.FullName + "." + fld.Name; | ||
| } | ||
|
|
||
| internal void ParseJniMember (string jniMember, out string package, out string type, out string member) | ||
| { | ||
| try { | ||
| DoParseJniMember (jniMember, out package, out type, out member); | ||
| } catch (Exception ex) { | ||
| throw new Exception (string.Format ("failed to parse enum mapping: JNI member: {0}", jniMember, ex)); | ||
| } | ||
| } | ||
|
|
||
| static void DoParseJniMember (string jniMember, out string package, out string type, out string member) | ||
| { | ||
| int endPackage = jniMember.LastIndexOf ('/'); | ||
| int endClass = jniMember.LastIndexOf ('.'); | ||
|
|
||
| package = jniMember.Substring (0, endPackage).Replace ('/', '.'); | ||
| if (package.StartsWith ("I:")) | ||
| package = package.Substring (2); | ||
|
|
||
| if (endClass >= 0) { | ||
| type = jniMember.Substring (endPackage + 1, endClass - endPackage - 1).Replace ('$', '.'); | ||
| member = jniMember.Substring (endClass + 1); | ||
| } else { | ||
| type = jniMember.Substring (endPackage + 1).Replace ('$', '.'); | ||
| member = ""; | ||
| } | ||
| } | ||
|
|
||
| string StripExtraInterfaceSpec (string jniFieldSpec) | ||
| { | ||
| return jniFieldSpec.StartsWith ("I:", StringComparison.Ordinal) ? jniFieldSpec.Substring (2) : jniFieldSpec; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| using System; | ||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
| using System.Xml; | ||
| using System.Xml.Linq; | ||
| using System.Xml.XPath; | ||
|
|
@@ -139,75 +141,22 @@ internal List<string> WriteEnumerations (string output_dir, Dictionary<string, E | |
| if (!Directory.Exists (output_dir)) | ||
| Directory.CreateDirectory (output_dir); | ||
|
|
||
| var files = new List<string> (); | ||
| foreach (var enu in enums) { | ||
| var path = Path.Combine (output_dir, GetFileName (enu.Key, useShortFileNames) + ".cs"); | ||
| files.Add (path); | ||
| using (StreamWriter sw = new StreamWriter (path, append: false)) { | ||
| string ns = enu.Key.Substring (0, enu.Key.LastIndexOf ('.')).Trim (); | ||
| string enoom = enu.Key.Substring (enu.Key.LastIndexOf ('.') + 1).Trim (); | ||
|
|
||
| sw.WriteLine ("namespace {0} {{", ns); | ||
| if (enu.Value.BitField) | ||
| sw.WriteLine (" [System.Flags]"); | ||
| sw.WriteLine (" public enum {0} {{", enoom); | ||
|
|
||
| foreach (var member in enu.Value.Members) { | ||
| var managedMember = FindManagedMember (ns, enoom, enu.Value, member.Key, gens); | ||
| sw.WriteLine (" [global::Android.Runtime.IntDefinition (" + (managedMember != null? "\"" + managedMember + "\"" : "null") + ", JniField = \"" + StripExtraInterfaceSpec (enu.Value.JniNames [member.Key]) + "\")]"); | ||
| sw.WriteLine (" {0} = {1},", member.Key.Trim (), member.Value.Trim ()); | ||
| } | ||
| sw.WriteLine (" }"); | ||
| sw.WriteLine ("}"); | ||
| } | ||
| } | ||
| return files; | ||
| } | ||
|
|
||
| string StripExtraInterfaceSpec (string jniFieldSpec) | ||
| { | ||
| return jniFieldSpec.StartsWith ("I:", StringComparison.Ordinal) ? jniFieldSpec.Substring (2) : jniFieldSpec; | ||
| } | ||
|
|
||
| string FindManagedMember (string ns, string enumName, EnumDescription desc, string enumFieldName, IEnumerable<GenBase> gens) | ||
| { | ||
| if (desc.FieldsRemoved) | ||
| return null; | ||
|
|
||
| var jniMember = desc.JniNames [enumFieldName]; | ||
| if (string.IsNullOrWhiteSpace (jniMember)) { | ||
| // enum values like "None" falls here. | ||
| return null; | ||
| } | ||
| return FindManagedMember (jniMember, gens); | ||
| } | ||
| var files = new ConcurrentBag<string> (); | ||
|
|
||
| WeakReference cache_found_class; | ||
| Parallel.ForEach (enums, enu => { | ||
| var path = Path.Combine (output_dir, GetFileName (enu.Key, useShortFileNames) + ".cs"); | ||
| files.Add (path); | ||
|
|
||
| string FindManagedMember (string jniMember, IEnumerable<GenBase> gens) | ||
| { | ||
| string package, type, member; | ||
| ParseJniMember (jniMember, out package, out type, out member); | ||
| var fullJavaType = (string.IsNullOrEmpty (package) ? "" : package + ".") + type; | ||
|
|
||
| var cls = cache_found_class != null ? cache_found_class.Target as GenBase : null; | ||
| if (cls == null || cls.JniName != fullJavaType) { | ||
| cls = gens.FirstOrDefault (g => g.JavaName == fullJavaType); | ||
| if (cls == null) { | ||
| // The class was not found e.g. removed by metadata fixup. | ||
| return null; | ||
| using (var sw = File.CreateText (path)) { | ||
| var generator = new EnumGenerator (sw); | ||
| generator.WriteEnumeration (enu, gens); | ||
| } | ||
| cache_found_class = new WeakReference (cls); | ||
| } | ||
| var fld = cls.Fields.FirstOrDefault (f => f.JavaName == member); | ||
| if (fld == null) { | ||
| // The field was not found e.g. removed by metadata fixup. | ||
| return null; | ||
| } | ||
| return cls.FullName + "." + fld.Name; | ||
| }); | ||
|
|
||
| return files.ToList (); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that this should be Rationale: C# deterministic builds. If, for whatever reason, The order of generated files appears to be used in the // app1.cs
class App1 {
public static void Main ()
{
App2 a = new App2 ();
}
}// app2.cs
class App2 {
}$ csc /deterministic /out:upwards.exe app1.cs app2.cs
$ csc /deterministic /out:downwards.exe app2.cs app1.cs
$ md5 downwards.exe upwards.exe
MD5 (downwards.exe) = cb7690e4878515303bde517447130b28
MD5 (upwards.exe) = b258bd49fe38709db5191f1adef9afc9"Simply" changing the order of command-line arguments results in a different assembly.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do the sort when we write the project file:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I missed that. Thanks! |
||
| } | ||
|
|
||
| Dictionary<string,string> file_name_map = new Dictionary<string, string> (); | ||
| ConcurrentDictionary<string,string> file_name_map = new ConcurrentDictionary<string, string> (); | ||
jpobst marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| string GetFileName (string file, bool useShortFileNames) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| namespace Android.App { | ||
| public enum RecentTaskFlags { | ||
| [global::Android.Runtime.IntDefinition (null, JniField = "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE")] | ||
| WithExcluded = 1, | ||
| [global::Android.Runtime.IntDefinition (null, JniField = "android/app/ActivityManager.RECENT_WITH_EXCLUDED")] | ||
| IgnoreUnavailable = 2, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| namespace Android.App { | ||
| public enum RecentTaskFlags { | ||
| [global::Android.Runtime.IntDefinition ("android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE", JniField = "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE")] | ||
| WithExcluded = 1, | ||
| [global::Android.Runtime.IntDefinition (null, JniField = "android/app/ActivityManager.RECENT_WITH_EXCLUDED")] | ||
| IgnoreUnavailable = 2, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| namespace Android.App { | ||
| [System.Flags] | ||
| public enum RecentTaskFlags { | ||
| [global::Android.Runtime.IntDefinition (null, JniField = "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE")] | ||
| WithExcluded = 1, | ||
| [global::Android.Runtime.IntDefinition (null, JniField = "android/app/ActivityManager.RECENT_WITH_EXCLUDED")] | ||
| IgnoreUnavailable = 2, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Reflection; | ||
| using System.Text; | ||
| using MonoDroid.Generation; | ||
| using NUnit.Framework; | ||
| using NUnit.Framework.Internal; | ||
|
|
||
| namespace generatortests.Unit_Tests | ||
| { | ||
| [TestFixture] | ||
| class EnumGeneratorTests | ||
| { | ||
| protected EnumGenerator generator; | ||
| protected StringBuilder builder; | ||
| protected StringWriter writer; | ||
|
|
||
| [SetUp] | ||
| public void SetUp () | ||
| { | ||
| builder = new StringBuilder (); | ||
| writer = new StringWriter (builder); | ||
|
|
||
| generator = new EnumGenerator (writer); | ||
| } | ||
|
|
||
| [Test] | ||
| public void WriteBasicEnum () | ||
| { | ||
| var enu = CreateEnum (); | ||
| enu.Value.FieldsRemoved = true; | ||
|
|
||
| generator.WriteEnumeration (enu, null); | ||
|
|
||
| Assert.AreEqual (GetExpected (nameof (WriteBasicEnum)), writer.ToString ().NormalizeLineEndings ()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void WriteFlagsEnum () | ||
| { | ||
| var enu = CreateEnum (); | ||
| enu.Value.BitField = true; | ||
| enu.Value.FieldsRemoved = true; | ||
|
|
||
| generator.WriteEnumeration (enu, null); | ||
|
|
||
| Assert.AreEqual (GetExpected (nameof (WriteFlagsEnum)), writer.ToString ().NormalizeLineEndings ()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void WriteEnumWithGens () | ||
| { | ||
| var enu = CreateEnum (); | ||
| var gens = CreateGens (); | ||
|
|
||
| generator.WriteEnumeration (enu, gens); | ||
|
|
||
| Assert.AreEqual (GetExpected (nameof (WriteEnumWithGens)), writer.ToString ().NormalizeLineEndings ()); | ||
| } | ||
|
|
||
| protected string GetExpected (string testName) | ||
| { | ||
| var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); | ||
|
|
||
| return File.ReadAllText (Path.Combine (root, "Unit-Tests", "EnumGeneratorExpectedResults", $"{testName}.txt")).NormalizeLineEndings (); | ||
| } | ||
|
|
||
| KeyValuePair<string, EnumMappings.EnumDescription> CreateEnum () | ||
| { | ||
| var enu = new EnumMappings.EnumDescription { | ||
| Members = new Dictionary<string, string> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Entirely unrelated, this (This doesn't need to be fixed here; it's simply that I noticed this now.) |
||
| { "WithExcluded", "1" }, | ||
| { "IgnoreUnavailable", "2" } | ||
| }, | ||
| JniNames = new Dictionary<string, string> { | ||
| { "WithExcluded", "android/app/ActivityManager.RECENT_IGNORE_UNAVAILABLE" }, | ||
| { "IgnoreUnavailable", "android/app/ActivityManager.RECENT_WITH_EXCLUDED" } | ||
| }, | ||
| BitField = false, | ||
| FieldsRemoved = false | ||
| }; | ||
|
|
||
| return new KeyValuePair<string, EnumMappings.EnumDescription> ("Android.App.RecentTaskFlags", enu); | ||
| } | ||
|
|
||
| GenBase[] CreateGens () | ||
| { | ||
| var klass = new TestClass (string.Empty, "android.app.ActivityManager"); | ||
|
|
||
| klass.Fields.Add (new TestField ("int", "RECENT_IGNORE_UNAVAILABLE")); | ||
|
|
||
| return new [] { klass }; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.