Skip to content

Commit a3b7456

Browse files
jpobstjonpryor
authored andcommitted
[generator] Add support for const fields on interfaces (#439)
Add support for Java-side interface constants to be optionally bound using C# 8.0's new default interface member support. This is feature-flagged behind the `--lang-features=interface-constants` option (off by default): generator --lang-features=interface-constants ... Here is an example of what is generated from `Mono.Android.dll`: // Metadata.xml XPath interface reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']" [Register ("android/os/Parcelable", "", "Android.OS.IParcelableInvoker", ApiSince = 1)] public partial interface IParcelable : IJavaObject { + // Metadata.xml XPath field reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/field[@name='CONTENTS_FILE_DESCRIPTOR']" + [Register ("CONTENTS_FILE_DESCRIPTOR")] + public const int ContentsFileDescriptor = (int) 1; // Metadata.xml XPath method reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/method[@name='describeContents' and count(parameter)=0]" [Register ("describeContents", "()I", "GetDescribeContentsHandler:Android.OS.IParcelableInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] int DescribeContents (); // Metadata.xml XPath method reference: path="/api/package[@name='android.os']/interface[@name='Parcelable']/method[@name='writeToParcel' and count(parameter)=2 and parameter[1][@type='android.os.Parcel'] and parameter[2][@type='int']]" [Register ("writeToParcel", "(Landroid/os/Parcel;I)V", "GetWriteToParcel_Landroid_os_Parcel_IHandler:Android.OS.IParcelableInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] void WriteToParcel (Android.OS.Parcel dest, [global::Android.Runtime.GeneratedEnum] Android.OS.ParcelableWriteFlags flags); } There are several Android interfaces defined that only contain constants. Without Default-Interface-Methods, we have not been generating these interfaces because there was nothing we could generate for them. (These are referred to as `IsConstSugar`). Now we generate them, but they do not have `[Register]` on the interface and do not inherit from `IJavaObject` since they are not used for Java interop, e.g. [`android.icu.lang.UProperty.NameChoice][0]: [0]: https://developer.android.com/reference/android/icu/lang/UProperty.NameChoice // Metadata.xml XPath interface reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']" public partial interface IUPropertyNameChoice { // Metadata.xml XPath field reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']/field[@name='LONG']" [Register ("LONG")] public const int Long = (int) 1; // Metadata.xml XPath field reference: path="/api/package[@name='android.icu.lang']/interface[@name='UProperty.NameChoice']/field[@name='SHORT']" [Register ("SHORT")] public const int Short = (int) 0; } Here is the diff of this change run on `Mono.Android.dll`: https://gist.github.com/jpobst/7aa0bb1a01975a56038cb7ab12ecdb1c
1 parent 0eb3c06 commit a3b7456

File tree

16 files changed

+334
-119
lines changed

16 files changed

+334
-119
lines changed

tools/generator/CodeGenerationOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer)
7070
public bool UseShortFileNames { get; set; }
7171
public IList<GenBase> Gens {get;set;}
7272
public int ProductVersion { get; set; }
73+
public bool SupportInterfaceConstants { get; set; }
7374

7475
bool? buildingCoreAssembly;
7576
public bool BuildingCoreAssembly {

tools/generator/CodeGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
6363
UseGlobal = options.GlobalTypeNames,
6464
IgnoreNonPublicType = true,
6565
UseShortFileNames = options.UseShortFileNames,
66-
ProductVersion = options.ProductVersion
66+
ProductVersion = options.ProductVersion,
67+
SupportInterfaceConstants = options.SupportInterfaceConstants,
6768
};
6869

6970
// Load reference libraries

tools/generator/CodeGeneratorOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public CodeGeneratorOptions ()
3939
public string MappingReportFile { get; set; }
4040
public bool OnlyRunApiXmlAdjuster { get; set; }
4141
public string ApiXmlAdjusterOutput { get; set; }
42+
public bool SupportInterfaceConstants { get; set; }
4243

4344
public static CodeGeneratorOptions Parse (string[] args)
4445
{
@@ -85,6 +86,9 @@ public static CodeGeneratorOptions Parse (string[] args)
8586
{ "sdk-platform|api-level=",
8687
"SDK Platform {VERSION}/API level.",
8788
v => opts.ApiLevel = v },
89+
{ "lang-features=",
90+
"For internal use. (Flags: interface-constants)",
91+
v => opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true },
8892
{ "preserve-enums",
8993
"For internal use.",
9094
v => opts.PreserveEnums = v != null },

tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -441,61 +441,26 @@ internal virtual void WriteField (Field field, string indent, GenBase type)
441441
public void WriteInterface (InterfaceGen @interface, string indent, GenerationInfo gen_info)
442442
{
443443
opt.ContextTypes.Push (@interface);
444+
444445
// interfaces don't nest, so generate as siblings
445446
foreach (GenBase nest in @interface.NestedTypes) {
446447
WriteType (nest, indent, gen_info);
447448
writer.WriteLine ();
448449
}
449450

450-
var staticMethods = @interface.Methods.Where (m => m.IsStatic);
451-
if (@interface.Fields.Any () || staticMethods.Any ()) {
452-
string name = @interface.HasManagedName
453-
? @interface.Name.Substring (1) + "Consts"
454-
: @interface.Name.Substring (1);
455-
writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ());
456-
writer.WriteLine ("{0}public abstract class {1} : Java.Lang.Object {{", indent, name);
457-
writer.WriteLine ();
458-
writer.WriteLine ("{0}\tinternal {1} ()", indent, name);
459-
writer.WriteLine ("{0}\t{{", indent);
460-
writer.WriteLine ("{0}\t}}", indent);
461-
462-
var seen = new HashSet<string> ();
463-
bool needsClassRef = WriteFields (@interface.Fields, indent + "\t", @interface, seen) || staticMethods.Any ();
464-
foreach (var iface in @interface.GetAllImplementedInterfaces ().OfType<InterfaceGen> ()) {
465-
writer.WriteLine ();
466-
writer.WriteLine ("{0}\t// The following are fields from: {1}", indent, iface.JavaName);
467-
bool v = WriteFields (iface.Fields, indent + "\t", iface, seen);
468-
needsClassRef = needsClassRef || v;
469-
}
470-
471-
foreach (var m in @interface.Methods.Where (m => m.IsStatic))
472-
WriteMethod (m, indent + "\t", @interface, true);
473-
474-
if (needsClassRef) {
475-
writer.WriteLine ();
476-
WriteClassHandle (@interface, indent + "\t", name);
477-
}
451+
WriteInterfaceImplementedMembersAlternative (@interface, indent);
478452

479-
writer.WriteLine ("{0}}}", indent, @interface.Name);
480-
writer.WriteLine ();
453+
// If this interface is just fields and we can't generate any of them
454+
// then we don't need to write the interface
455+
if (@interface.IsConstSugar && @interface.GetGeneratableFields (opt).Count () == 0)
456+
return;
481457

482-
if (!@interface.HasManagedName) {
483-
writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ());
484-
writer.WriteLine ("{0}[global::System.Obsolete (\"Use the '{1}' type. This type will be removed in a future release.\")]", indent, name);
485-
writer.WriteLine ("{0}public abstract class {1}Consts : {1} {{", indent, name);
486-
writer.WriteLine ();
487-
writer.WriteLine ("{0}\tprivate {1}Consts ()", indent, name);
488-
writer.WriteLine ("{0}\t{{", indent);
489-
writer.WriteLine ("{0}\t}}", indent);
490-
writer.WriteLine ("{0}}}", indent);
491-
writer.WriteLine ();
492-
}
493-
}
458+
WriteInterfaceDeclaration (@interface, indent);
494459

460+
// If this interface is just constant fields we don't need to write all the invoker bits
495461
if (@interface.IsConstSugar)
496462
return;
497463

498-
WriteInterfaceDeclaration (@interface, indent);
499464
if (!@interface.AssemblyQualifiedName.Contains ('/'))
500465
WriteInterfaceExtensionsDeclaration (@interface, indent, null);
501466
WriteInterfaceInvoker (@interface, indent);
@@ -547,11 +512,15 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent)
547512

548513
if (@interface.IsDeprecated)
549514
writer.WriteLine ("{0}[ObsoleteAttribute (@\"{1}\")]", indent, @interface.DeprecatedComment);
550-
writer.WriteLine ("{0}[Register (\"{1}\", \"\", \"{2}\"{3})]", indent, @interface.RawJniName, @interface.Namespace + "." + @interface.FullName.Substring (@interface.Namespace.Length + 1).Replace ('.', '/') + "Invoker", @interface.AdditionalAttributeString ());
515+
516+
if (!@interface.IsConstSugar)
517+
writer.WriteLine ("{0}[Register (\"{1}\", \"\", \"{2}\"{3})]", indent, @interface.RawJniName, @interface.Namespace + "." + @interface.FullName.Substring (@interface.Namespace.Length + 1).Replace ('.', '/') + "Invoker", @interface.AdditionalAttributeString ());
518+
551519
if (@interface.TypeParameters != null && @interface.TypeParameters.Any ())
552520
writer.WriteLine ("{0}{1}", indent, @interface.TypeParameters.ToGeneratedAttributeString ());
553-
writer.WriteLine ("{0}{1} partial interface {2} : {3} {{", indent, @interface.Visibility, @interface.Name,
554-
@interface.Interfaces.Count == 0 || sb.Length == 0 ? "IJavaObject" : sb.ToString ());
521+
writer.WriteLine ("{0}{1} partial interface {2}{3} {{", indent, @interface.Visibility, @interface.Name,
522+
@interface.IsConstSugar ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : IJavaObject" : " : " + sb.ToString ());
523+
WriteInterfaceFields (@interface, indent + "\t");
555524
writer.WriteLine ();
556525
WriteInterfaceProperties (@interface, indent + "\t");
557526
WriteInterfaceMethods (@interface, indent + "\t");
@@ -709,6 +678,74 @@ public void WriteInterfaceExtensionsDeclaration (InterfaceGen @interface, string
709678
writer.WriteLine ();
710679
}
711680

681+
public void WriteInterfaceFields (InterfaceGen iface, string indent)
682+
{
683+
// Interface fields are only supported with DIM
684+
if (!opt.SupportInterfaceConstants)
685+
return;
686+
687+
var seen = new HashSet<string> ();
688+
var fields = iface.GetGeneratableFields (opt).ToList ();
689+
690+
WriteFields (fields, indent, iface, seen);
691+
}
692+
693+
public void WriteInterfaceImplementedMembersAlternative (InterfaceGen @interface, string indent)
694+
{
695+
// Historically .NET has not allowed interface implemented fields or constants, so we
696+
// initially worked around that by moving them to an abstract class, generally
697+
// IMyInterface -> MyInterfaceConsts
698+
// This was later expanded to accomodate static interface methods, creating a more appropriately named class
699+
// IMyInterface -> MyInterface
700+
// In this case the XXXConsts class is [Obsolete]'d and simply inherits from the newer class
701+
// in order to maintain backward compatibility.
702+
var staticMethods = @interface.Methods.Where (m => m.IsStatic);
703+
704+
if (@interface.Fields.Any () || staticMethods.Any ()) {
705+
string name = @interface.HasManagedName
706+
? @interface.Name.Substring (1) + "Consts"
707+
: @interface.Name.Substring (1);
708+
writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ());
709+
writer.WriteLine ("{0}public abstract class {1} : Java.Lang.Object {{", indent, name);
710+
writer.WriteLine ();
711+
writer.WriteLine ("{0}\tinternal {1} ()", indent, name);
712+
writer.WriteLine ("{0}\t{{", indent);
713+
writer.WriteLine ("{0}\t}}", indent);
714+
715+
var seen = new HashSet<string> ();
716+
bool needsClassRef = WriteFields (@interface.Fields, indent + "\t", @interface, seen) || staticMethods.Any ();
717+
foreach (var iface in @interface.GetAllImplementedInterfaces ().OfType<InterfaceGen> ()) {
718+
writer.WriteLine ();
719+
writer.WriteLine ("{0}\t// The following are fields from: {1}", indent, iface.JavaName);
720+
bool v = WriteFields (iface.Fields, indent + "\t", iface, seen);
721+
needsClassRef = needsClassRef || v;
722+
}
723+
724+
foreach (var m in @interface.Methods.Where (m => m.IsStatic))
725+
WriteMethod (m, indent + "\t", @interface, true);
726+
727+
if (needsClassRef) {
728+
writer.WriteLine ();
729+
WriteClassHandle (@interface, indent + "\t", name);
730+
}
731+
732+
writer.WriteLine ("{0}}}", indent, @interface.Name);
733+
writer.WriteLine ();
734+
735+
if (!@interface.HasManagedName) {
736+
writer.WriteLine ("{0}[Register (\"{1}\"{2}, DoNotGenerateAcw=true)]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ());
737+
writer.WriteLine ("{0}[global::System.Obsolete (\"Use the '{1}' type. This type will be removed in a future release.\")]", indent, name);
738+
writer.WriteLine ("{0}public abstract class {1}Consts : {1} {{", indent, name);
739+
writer.WriteLine ();
740+
writer.WriteLine ("{0}\tprivate {1}Consts ()", indent, name);
741+
writer.WriteLine ("{0}\t{{", indent);
742+
writer.WriteLine ("{0}\t}}", indent);
743+
writer.WriteLine ("{0}}}", indent);
744+
writer.WriteLine ();
745+
}
746+
}
747+
}
748+
712749
public void WriteInterfaceInvoker (InterfaceGen @interface, string indent)
713750
{
714751
writer.WriteLine ("{0}[global::Android.Runtime.Register (\"{1}\", DoNotGenerateAcw=true{2})]", indent, @interface.RawJniName, @interface.AdditionalAttributeString ());

tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.IO;
45
using System.Linq;
@@ -27,6 +28,15 @@ public override string DefaultValue {
2728

2829
public bool HasManagedName => hasManagedName;
2930

31+
// These are fields that we currently support generating on the interface with DIM
32+
public IEnumerable<Field> GetGeneratableFields (CodeGenerationOptions options)
33+
{
34+
if (!options.SupportInterfaceConstants)
35+
return Enumerable.Empty<Field> ();
36+
37+
return Fields.Where (f => !f.NeedsProperty && !(f.DeprecatedComment?.Contains ("constant will be removed") == true));
38+
}
39+
3040
public bool IsConstSugar {
3141
get {
3242
if (Methods.Count > 0 || Properties.Count > 0)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
public partial interface IMyInterface {
3+
4+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']"
5+
[Register ("MyConstantField")]
6+
public const int MyConstantField = (int) 7;
7+
8+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantStringField']"
9+
[Register ("MyConstantStringField")]
10+
public const string MyConstantStringField = (string) "hello";
11+
12+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyDeprecatedField']"
13+
[Register ("MyDeprecatedField")]
14+
[Obsolete ("")]
15+
public const int MyDeprecatedField = (int) 7;
16+
17+
}
18+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3+
public partial interface IMyInterface : IJavaObject {
4+
5+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']"
6+
[Register ("MyConstantField")]
7+
public const int MyConstantField = (int) 7;
8+
9+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
10+
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")]
11+
void DoSomething ();
12+
13+
}
14+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
public partial interface IMyInterface {
3+
4+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']"
5+
[Register ("MyConstantField")]
6+
public const int MyConstantField = (int) 7;
7+
8+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantStringField']"
9+
[Register ("MyConstantStringField")]
10+
public const string MyConstantStringField = (string) "hello";
11+
12+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyDeprecatedField']"
13+
[Register ("MyDeprecatedField")]
14+
[Obsolete ("")]
15+
public const int MyDeprecatedField = (int) 7;
16+
17+
}
18+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3+
public partial interface IMyInterface : IJavaObject {
4+
5+
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']"
6+
[Register ("MyConstantField")]
7+
public const int MyConstantField = (int) 7;
8+
9+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
10+
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")]
11+
void DoSomething ();
12+
13+
}
14+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.IO;
3+
using System.Reflection;
4+
using System.Text;
5+
using MonoDroid.Generation;
6+
using NUnit.Framework;
7+
8+
namespace generatortests.Unit_Tests
9+
{
10+
abstract class CodeGeneratorTestBase
11+
{
12+
protected CodeGenerator generator;
13+
protected StringBuilder builder;
14+
protected StringWriter writer;
15+
protected CodeGenerationOptions options;
16+
17+
[SetUp]
18+
public void SetUp ()
19+
{
20+
builder = new StringBuilder ();
21+
writer = new StringWriter (builder);
22+
options = CreateOptions ();
23+
24+
generator = options.CreateCodeGenerator (writer);
25+
}
26+
27+
[TearDown]
28+
public void TearDown ()
29+
{
30+
writer.Dispose ();
31+
}
32+
33+
protected virtual CodeGenerationOptions CreateOptions ()
34+
{
35+
return new CodeGenerationOptions {
36+
CodeGenerationTarget = Target,
37+
};
38+
}
39+
40+
protected abstract Xamarin.Android.Binder.CodeGenerationTarget Target { get; }
41+
42+
// Get the test results from "Common" for tests with the same results regardless of Target
43+
protected string GetExpected (string testName)
44+
{
45+
var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location);
46+
47+
return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", "Common", $"{testName}.txt")).NormalizeLineEndings ();
48+
}
49+
50+
// Get the test results from "JavaInterop1" or "XamarinAndroid" for tests with the different results per Target
51+
protected string GetTargetedExpected (string testName)
52+
{
53+
var target = Target.ToString ();
54+
var root = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location);
55+
56+
return File.ReadAllText (Path.Combine (root, "Unit-Tests", "CodeGeneratorExpectedResults", target, $"{testName}.txt")).NormalizeLineEndings ();
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)