Skip to content

Commit ba23fc7

Browse files
[RGen] Add code in the protocol emiter for the interface definition. (#23423)
1 parent 8bbf5b8 commit ba23fc7

File tree

12 files changed

+304
-24
lines changed

12 files changed

+304
-24
lines changed

src/rgen/Microsoft.Macios.Generator/Emitters/EmitterFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ static class EmitterFactory {
1414
static readonly Dictionary<BindingType, ICodeEmitter> emitters = new () {
1515
{ BindingType.Class, new ClassEmitter () },
1616
{ BindingType.SmartEnum, new EnumEmitter () },
17-
{ BindingType.Protocol, new InterfaceEmitter () },
17+
{ BindingType.Protocol, new ProtocolEmitter () },
1818
{ BindingType.Category, new CategoryEmitter () },
1919
{ BindingType.StrongDictionary, new StrongDictionaryEmitter () },
2020
{ BindingType.StrongDictionaryKeys, new StrongDictionaryKeysEmitter () }

src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using System.Collections.Generic;
4+
using System.Collections.Immutable;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.IO;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.Macios.Generator.Attributes;
10+
using Microsoft.Macios.Generator.Context;
11+
using Microsoft.Macios.Generator.DataModel;
12+
using Microsoft.Macios.Generator.IO;
13+
using ObjCBindings;
14+
using static Microsoft.Macios.Generator.Emitters.BindingSyntaxFactory;
15+
using Property = Microsoft.Macios.Generator.DataModel.Property;
16+
using Method = Microsoft.Macios.Generator.DataModel.Method;
17+
18+
namespace Microsoft.Macios.Generator.Emitters;
19+
20+
/// <summary>
21+
/// Emitter responsible for generating protocol interfaces.
22+
/// Generates C# interfaces that represent Objective-C protocols with proper protocol member attributes.
23+
/// </summary>
24+
class ProtocolEmitter : ICodeEmitter {
25+
/// <inheritdoc />
26+
public string GetSymbolName (in Binding binding) => binding.Name;
27+
28+
/// <inheritdoc />
29+
public IEnumerable<string> UsingStatements => [];
30+
31+
/// <summary>
32+
/// Emits the default constructors and dynamic dependency attributes for the protocol interface.
33+
/// This includes DynamicDependencyAttribute for each property and a static constructor with GC.KeepAlive.
34+
/// </summary>
35+
/// <param name="bindingContext">The binding context containing protocol information.</param>
36+
/// <param name="interfaceBlock">The writer for the interface block.</param>
37+
void EmitDefaultConstructors (in BindingContext bindingContext, TabbedWriter<StringWriter> interfaceBlock)
38+
{
39+
// emit the DynamicDependencyAttribute per property
40+
foreach (var property in bindingContext.Changes.Properties.OrderBy (p => p.Name)) {
41+
interfaceBlock.AppendDynamicDependencyAttribute (property.Name);
42+
}
43+
interfaceBlock.AppendGeneratedCodeAttribute ();
44+
interfaceBlock.WriteRaw (
45+
$@"static {bindingContext.Changes.Name} ()
46+
{{
47+
{GC}.KeepAlive (null);
48+
}}
49+
");
50+
}
51+
52+
/// <summary>
53+
/// Gets the properties from the binding context and their corresponding extension methods.
54+
/// Returns a collection of tuples containing the property and its optional getter/setter methods.
55+
/// </summary>
56+
/// <param name="bindingContext">The binding context containing the properties.</param>
57+
/// <returns>An immutable array of tuples containing properties and their extension methods.</returns>
58+
static ImmutableArray<(Property Property, Method? Getter, Method? Setter)> GetProperties (in BindingContext bindingContext)
59+
{
60+
// collect all properties and generate the extension methods, this will be used to generate the protocol
61+
// member data and later the extension methods.
62+
var propertiesBucket = ImmutableArray.CreateBuilder<(Property Property, Method? Getter, Method? Setter)> (bindingContext.Changes.Properties.Length);
63+
foreach (var property in bindingContext.Changes.Properties.OrderBy (p => p.Name)) {
64+
var (getter, setter) = property.ToExtensionMethods (new (bindingContext.Changes.Name, SpecialType.None));
65+
propertiesBucket.Add ((property, getter, setter));
66+
}
67+
var properties = propertiesBucket.ToImmutable ();
68+
return properties;
69+
}
70+
71+
/// <inheritdoc />
72+
public bool TryEmit (in BindingContext bindingContext, [NotNullWhen (false)] out ImmutableArray<Diagnostic>? diagnostics)
73+
{
74+
diagnostics = null;
75+
if (bindingContext.Changes.BindingType != BindingType.Protocol) {
76+
diagnostics = [Diagnostic.Create (
77+
Diagnostics
78+
.RBI0000, // An unexpected error occurred while processing '{0}'. Please fill a bug report at https://github.com/dotnet/macios/issues/new.
79+
null,
80+
bindingContext.Changes.FullyQualifiedSymbol)];
81+
return false;
82+
}
83+
84+
var bindingData = (BindingTypeData<Protocol>) bindingContext.Changes.BindingInfo;
85+
// namespace declaration
86+
this.EmitNamespace (bindingContext);
87+
88+
using (var _ = this.EmitOuterClasses (bindingContext, out var builder)) {
89+
// append the class availability, this will add the necessary attributes to the class
90+
builder.AppendMemberAvailability (bindingContext.Changes.SymbolAvailability);
91+
92+
// Protocol registration
93+
var protocolName = bindingData.Name ?? bindingContext.Changes.Name [1..];
94+
builder.AppendProtocolAttribute (protocolName, Nomenclator.GetProtocolWrapperName (protocolName));
95+
96+
// we need to collect the properties extension methods, we do that with a helper method
97+
// that will return the properties and their getters/setters.
98+
var properties = GetProperties (bindingContext);
99+
100+
// append the properties to the protocol member data
101+
foreach (var (property, getter, setter) in properties) {
102+
var protocolMember = new ProtocolMemberData (property, getter, setter);
103+
builder.AppendProtocolMemberData (protocolMember);
104+
}
105+
106+
var modifiers = $"{string.Join (' ', bindingContext.Changes.Modifiers)} ";
107+
// class declaration, the analyzer should ensure that the class is static, otherwise it will fail to compile with an error.
108+
using (var interfaceBlock = builder.CreateBlock (
109+
$"{(string.IsNullOrWhiteSpace (modifiers) ? string.Empty : modifiers)}interface {bindingContext.Changes.Name} : INativeObject, IDisposable",
110+
true)) {
111+
// space for readability
112+
interfaceBlock.WriteLine ();
113+
114+
// emit static constructor
115+
EmitDefaultConstructors (in bindingContext, interfaceBlock);
116+
}
117+
}
118+
return true;
119+
}
120+
}

src/rgen/Microsoft.Macios.Generator/IO/TabbedStringBuilderExtensions.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public static TabbedWriter<StringWriter> AppendDynamicDependencyAttribute (this
146146
string member)
147147
{
148148

149-
self.WriteLine ($"[DynamicDependency ({member})]");
149+
self.WriteLine ($"[DynamicDependency (\"{member}\")]");
150150
return self;
151151
}
152152

@@ -206,4 +206,19 @@ string Quoted (string? str)
206206
return str is null ? "null" : $"\"{str}\"";
207207
}
208208
}
209+
210+
/// <summary>
211+
/// Appends a `[Protocol]` attribute to the current writer.
212+
/// This attribute is used to mark an interface as representing an Objective-C protocol.
213+
/// </summary>
214+
/// <param name="self">A tabbed string writer.</param>
215+
/// <param name="name">The name of the Objective-C protocol.</param>
216+
/// <param name="wrapperName">The name of the wrapper type for the protocol.</param>
217+
/// <returns>The current writer.</returns>
218+
public static TabbedWriter<StringWriter> AppendProtocolAttribute (this TabbedWriter<StringWriter> self,
219+
string name, string wrapperName)
220+
{
221+
self.WriteLine ($"[Protocol (Name = \"{name}\", WrapperType = typeof ({wrapperName}))]");
222+
return self;
223+
}
209224
}

src/rgen/Microsoft.Macios.Generator/Nomenclator.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,18 @@ public static string GetTaskCallbackParameterName (string parameterName)
247247
/// </summary>
248248
/// <returns>The name of the TaskCompletionSource variable.</returns>
249249
public static string GetTaskCompletionSourceName () => "_tcs";
250+
251+
/// <summary>
252+
/// Generates the name for a protocol wrapper class.
253+
/// The wrapper name is created by appending 'Wrapper' to the protocol name,
254+
/// with dots replaced by underscores to ensure valid identifier names for nested classes.
255+
/// </summary>
256+
/// <param name="protocolName">The name of the protocol.</param>
257+
/// <returns>The name of the protocol wrapper class.</returns>
258+
public static string GetProtocolWrapperName (string protocolName)
259+
{
260+
// we will use the name of the protocol with a 'Wrapper' suffix
261+
// we will replace any . with _ to ensure that the name is valid when working with nested classes.
262+
return $"{protocolName.Replace ('.', '_')}Wrapper";
263+
}
250264
}

tests/rgen/Microsoft.Macios.Generator.Tests/Emitters/EmitterFactoryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public interface ITestInterface {
145145
";
146146
var changes = CreateSymbol<InterfaceDeclarationSyntax> (platform, inputText);
147147
Assert.True (EmitterFactory.TryCreate (changes, out var emitter));
148-
Assert.IsType<InterfaceEmitter> (emitter);
148+
Assert.IsType<ProtocolEmitter> (emitter);
149149
Assert.True (EmitterFactory.TryCreate (changes, out var secondEmitter));
150150
Assert.Same (emitter, secondEmitter);
151151
}

tests/rgen/Microsoft.Macios.Generator.Tests/IO/TabbedStringBuilderTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public void AppendPreserveAttribute (bool conditional, int tabCount, string expe
310310
[InlineData ("nameof(MyProperty)", 5, "\t\t\t\t\t")]
311311
public void AppendDynamicDependencyAttributeTests (string member, int tabCount, string expectedTabs)
312312
{
313-
var expected = $"{expectedTabs}[DynamicDependency ({member})]\n";
313+
var expected = $"{expectedTabs}[DynamicDependency (\"{member}\")]\n";
314314
string result;
315315
using (var block = new TabbedStringBuilder (sb, tabCount)) {
316316
block.AppendDynamicDependencyAttribute (member);
@@ -320,6 +320,22 @@ public void AppendDynamicDependencyAttributeTests (string member, int tabCount,
320320
Assert.Equal (expected, result);
321321
}
322322

323+
[Theory]
324+
[InlineData ("MyProtocol", "MyProtocolWrapper", 0, "")]
325+
[InlineData ("AnotherProtocol", "AnotherProtocolWrapper", 1, "\t")]
326+
[InlineData ("ThirdProtocol", "ThirdProtocolWrapper", 5, "\t\t\t\t\t")]
327+
public void AppendProtocolAttributeTests (string name, string wrapperName, int tabCount, string expectedTabs)
328+
{
329+
var expected = $"{expectedTabs}[Protocol (Name = \"{name}\", WrapperType = typeof ({wrapperName}))]\n";
330+
string result;
331+
using (var block = new TabbedStringBuilder (sb, tabCount)) {
332+
block.AppendProtocolAttribute (name, wrapperName);
333+
result = block.ToCode ();
334+
}
335+
336+
Assert.Equal (expected, result);
337+
}
338+
323339
[Theory]
324340
[InlineData (0, "")]
325341
[InlineData (1, "\t")]

tests/rgen/Microsoft.Macios.Generator.Tests/Microsoft.Macios.Generator.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@
6363
<Compile Remove="Classes\Data\*.cs" />
6464
<Compile Remove="Classes\Data\Trampolines\*.cs" />
6565
<Compile Remove="Categories\Data\*.cs" />
66+
<Compile Remove="Protocols\Data\*.cs" />
6667
<Compile Remove="StrongDictionaries\Data\*.cs" />
6768

6869
<None Include="SmartEnum\Data\*.cs" />
6970
<None Include="Classes\Data\*.cs" />
7071
<None Include="Classes\Data\Trampolines\*.cs" />
7172
<None Include="Categories\Data\*.cs" />
73+
<None Include="Protocols\Data\*.cs" />
7274
<None Include="StrongDictionaries\Data\*.cs" />
7375
</ItemGroup>
7476

tests/rgen/Microsoft.Macios.Generator.Tests/NomenclatorTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,10 @@ public void GetPropertyBackingFieldNameTests (string propertyName, bool isStatic
210210
[InlineData ("error", "_cberror")]
211211
public void GetTaskCallbackParameterNameTests (string parameterName, string expectedName)
212212
=> Assert.Equal (expectedName, Nomenclator.GetTaskCallbackParameterName (parameterName));
213+
214+
[Theory]
215+
[InlineData ("MyProtocol", "MyProtocolWrapper")]
216+
[InlineData ("My.Nested.Protocol", "My_Nested_ProtocolWrapper")]
217+
public void GetProtocolWrapperNameTests (string protocolName, string expectedName)
218+
=> Assert.Equal (expectedName, Nomenclator.GetProtocolWrapperName (protocolName));
213219
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// <auto-generated />
2+
3+
#nullable enable
4+
5+
using AVFoundation;
6+
using Foundation;
7+
using ObjCBindings;
8+
using ObjCRuntime;
9+
using System.Runtime.Versioning;
10+
11+
namespace Microsoft.Macios.Generator.Tests.Protocols.Data;
12+
13+
[SupportedOSPlatform ("macos")]
14+
[SupportedOSPlatform ("ios")]
15+
[SupportedOSPlatform ("tvos")]
16+
[SupportedOSPlatform ("maccatalyst13.1")]
17+
[Protocol (Name = "AVAudio3DMixing", WrapperType = typeof (AVAudio3DMixingWrapper))]
18+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "Obstruction", Selector = "obstruction", PropertyType = typeof (float), GetterSelector = "obstruction", SetterSelector = "setObstruction:", ArgumentSemantic = ArgumentSemantic.None)]
19+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "Occlusion", Selector = "occlusion", PropertyType = typeof (float), GetterSelector = "occlusion", SetterSelector = "setOcclusion:", ArgumentSemantic = ArgumentSemantic.None)]
20+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "PointSourceInHeadMode", Selector = "pointSourceInHeadMode", PropertyType = typeof (global::AVFoundation.AVAudio3DMixingPointSourceInHeadMode), GetterSelector = "pointSourceInHeadMode", SetterSelector = "setPointSourceInHeadMode:", ArgumentSemantic = ArgumentSemantic.Assign)]
21+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "Rate", Selector = "rate", PropertyType = typeof (float), GetterSelector = "rate", SetterSelector = "setRate:", ArgumentSemantic = ArgumentSemantic.None)]
22+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "RenderingAlgorithm", Selector = "renderingAlgorithm", PropertyType = typeof (global::AVFoundation.AVAudio3DMixingRenderingAlgorithm), GetterSelector = "renderingAlgorithm", SetterSelector = "setRenderingAlgorithm:", ArgumentSemantic = ArgumentSemantic.None)]
23+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "ReverbBlend", Selector = "reverbBlend", PropertyType = typeof (float), GetterSelector = "reverbBlend", SetterSelector = "setReverbBlend:", ArgumentSemantic = ArgumentSemantic.None)]
24+
[ProtocolMember (IsRequired = true, IsProperty = true, IsStatic = false, Name = "SourceMode", Selector = "sourceMode", PropertyType = typeof (global::AVFoundation.AVAudio3DMixingSourceMode), GetterSelector = "sourceMode", SetterSelector = "setSourceMode:", ArgumentSemantic = ArgumentSemantic.Assign)]
25+
public interface IAVAudio3DMixing : INativeObject, IDisposable
26+
{
27+
28+
[DynamicDependency ("Obstruction")]
29+
[DynamicDependency ("Occlusion")]
30+
[DynamicDependency ("PointSourceInHeadMode")]
31+
[DynamicDependency ("Rate")]
32+
[DynamicDependency ("RenderingAlgorithm")]
33+
[DynamicDependency ("ReverbBlend")]
34+
[DynamicDependency ("SourceMode")]
35+
[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]
36+
static IAVAudio3DMixing ()
37+
{
38+
global::System.GC.KeepAlive (null);
39+
}
40+
}

0 commit comments

Comments
 (0)