Skip to content

[generator] Encapsulate and parallelize enum generation. #442

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Jul 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ("\t[System.Flags]");
sw.WriteLine ("\tpublic enum {0} {{", enoom);

foreach (var member in enu.Value.Members) {
var managedMember = FindManagedMember (enu.Value, member.Key, gens);
sw.WriteLine ("\t\t[global::Android.Runtime.IntDefinition (" + (managedMember != null ? "\"" + managedMember + "\"" : "null") + ", JniField = \"" + StripExtraInterfaceSpec (enu.Value.JniNames [member.Key]) + "\")]");
sw.WriteLine ("\t\t{0} = {1},", member.Key.Trim (), member.Value.Trim ());
}
sw.WriteLine ("\t}");
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;
Expand Down Expand Up @@ -139,85 +141,36 @@ 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 files = new ConcurrentBag<string> ();

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;
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 ();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that this should be files.OrderBy (f => f).ToList (), or some variation thereof.

Rationale: C# deterministic builds. If, for whatever reason, generator is re-executed during a build, it would be nice if, when deterministic builds are enabled, the resulting assembly did not change unless the generated code actually changed.

The order of generated files appears to be used in the csc /deterministic algorithm, and thus altering the order of files may result in the creation of a different assembly. For example:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The 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> ();
readonly Dictionary<string,string> file_name_map = new Dictionary<string, string> ();

string GetFileName (string file, bool useShortFileNames)
{
if (!useShortFileNames)
return file;
string s;
if (file_name_map.TryGetValue (file, out s))
return s;
s = file_name_map.Count.ToString ();
file_name_map [file] = s;

lock (file_name_map) {
if (file_name_map.TryGetValue (file, out s))
return s;
s = file_name_map.Count.ToString ();
file_name_map [file] = s;
}

return s;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Android.Text {
public enum SpanTypes {
[global::Android.Runtime.IntDefinition (null, JniField = "android/text/Spanned.SPAN_COMPOSING")]
Composing = 256,
}
public enum SpanTypes {
[global::Android.Runtime.IntDefinition (null, JniField = "android/text/Spanned.SPAN_COMPOSING")]
Composing = 256,
}
}
8 changes: 4 additions & 4 deletions tools/generator/Tests-Core/expected/Android.Text.SpanTypes.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Android.Text {
public enum SpanTypes {
[global::Android.Runtime.IntDefinition (null, JniField = "android/text/Spanned.SPAN_COMPOSING")]
Composing = 256,
}
public enum SpanTypes {
[global::Android.Runtime.IntDefinition (null, JniField = "android/text/Spanned.SPAN_COMPOSING")]
Composing = 256,
}
}
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,
}
}
96 changes: 96 additions & 0 deletions tools/generator/Tests/Unit-Tests/EnumGeneratorTests.cs
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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entirely unrelated, this Members and JniNames APIs are ugly; they need to be consistent with each other, but they're entirely separate properties, and thus need not actually be consistent. :-(

(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 };
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace Xamarin.Test {
public enum SomeValues {
[global::Android.Runtime.IntDefinition (null, JniField = "xamarin/test/SomeObject.SOME_VALUE")]
SomeValue = 0,
[global::Android.Runtime.IntDefinition (null, JniField = "xamarin/test/SomeObject.SOME_VALUE2")]
SomeValue2 = 1,
}
public enum SomeValues {
[global::Android.Runtime.IntDefinition (null, JniField = "xamarin/test/SomeObject.SOME_VALUE")]
SomeValue = 0,
[global::Android.Runtime.IntDefinition (null, JniField = "xamarin/test/SomeObject.SOME_VALUE2")]
SomeValue2 = 1,
}
}
Loading