diff --git a/docs/objective-c-protocols.md b/docs/objective-c-protocols.md new file mode 100644 index 000000000000..3e4618295443 --- /dev/null +++ b/docs/objective-c-protocols.md @@ -0,0 +1,131 @@ +# Objective-C protocols + +This document describes how we bind Objective-C protocols in C#, and in +particular improvements we've done in .NET 9. + +## Can Objective-C protocols be modeled as C# interfaces? + +Objective-C protocols are quite similar to C# interfaces, except when they're +not, and that makes binding them somewhat complicated. + +### Optional/required members + +Objective-C protocols can have both optional and required members. It's always +been possible to represent required members in a C# interface (any interface +member would be required), but optional members were not possible until C# +added support for default interface members in C# 8. + +We represent optional members in two ways: + +* As an extension method on the interface (useful when calling the optional member). +* As an IDE feature that would show any optional members from an interface by + typing 'override ...' in the text editor (useful when implementing an optional member). + +This has a few drawbacks: + +* There are no extension properties, so optional properties would have to be + bound as a pair of GetProperty/SetProperty methods. + +* The IDE feature was obscure, few people knew about it, it broke on pretty + much every major release of Visual Studio for Mac, and it was never + implemented for Visual Studio on Windows. This made it quite hard to + implement optional members in a managed class extending an Objective-C + protocol, since developers would have to figure out the correct Export + attribute as well as the signature (which is quite complicated for more + complex signatures, especially if blocks are involved). + +### Changing requiredness + +It's entirely possible to change a member from being required to being optional +in Objective-C. Technically it's also a breaking change to do the opposite (make +an optional member required), but Apple does it all the time. + +We've handled this by just not updating the binding until we're able to do +breaking changes (which happens very rarely). + +### Static members + +Objective-C protocols can have static members. C# didn't allow for static +members in interfaces until C# 11, so until recently there hasn't been any +good way to bind static protocol members on a protocol. + +Our workaround is to manually inline every static member in all classes that +implemented a given protocol. + +### Initializers + +Objective-C protocols can have initializers (constructors). C# still doesn't +allow for constructors in interfaces. + +In the past we haven't bound any protocol initializer at all, we've completely +ignored them. + +## Binding in C# + +### Initializers + +Given the following API definition: + +```cs +[Protocol] +public interface Protocol { + [Abstract] + [Export ("init")] + IntPtr Constructor (); + + [Export ("initWithValue:")] + IntPtr Constructor (IntPtr value); + + [Bind ("Create")] + [Export ("initWithPlanet:")] + IntPtr Constructor (); +} +``` + +we're binding it like this: + +```cs +[Protocol ("Protocol")] +public interface IProtocol : INativeObject { + [Export ("init")] + public static T CreateInstance () where T: NSObject, IProtocol { /* default implementation */ } + + [Export ("initWithValue:")] + public static T CreateInstance () where T: NSObject, IProtocol { /* default implementation */ } + + [Export ("initWithPlanet:")] + public static T Create () where T: NSObject, IProtocol { /* default implementation */ } +} +``` + +In other words: we bind initializers as a static C# factory method that takes +a generic type argument specifying the type to instantiate. + +Notes: + +1. Constructors are currently not inlined in any implementing classes, like + other members are. This is something we could look into if there's enough + interest. +2. If a managed class implements a protocol with a constructor, the class has + to implement the constructor manually using the `[Export]` attribute in + order to conform to the protocol: + +```cs +[Protocol] +interface IMyProtocol { + [Export ("initWithValue:")] + IntPtr Constructor (string value); +} +``` + +```cs +class MyClass : NSObject, IMyProtocol { + public string Value { get; private set; } + + [Export ("initWithValue:")] + public MyClass (string value) + { + this.Value = value; + } +} +``` diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index f532c9061d72..e9d6ad11de94 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -2543,11 +2543,14 @@ public void PrintPlatformAttributesNoDuplicates (MemberInfo generatedType, Membe } } - public string SelectorField (string s, bool ignore_inline_directive = false) + public string SelectorField (string s, bool ignore_inline_directive = false, bool force_gethandle = false) { string name; if (InlineSelectors && !ignore_inline_directive) + force_gethandle = true; + + if (force_gethandle) return "Selector.GetHandle (\"" + s + "\")"; if (selector_names.TryGetValue (s, out name)) @@ -2654,17 +2657,38 @@ public bool IsProtocol (Type type) return AttributeManager.HasAttribute (type); } + public string GetMethodName (MemberInformation minfo, bool is_async) + { + var mi = minfo.Method; + string name; + if (minfo.is_ctor) { + if (minfo.is_protocol_member) { + var bindAttribute = GetBindAttribute (mi); + name = bindAttribute?.Selector ?? "CreateInstance"; + } else { + name = Nomenclator.GetGeneratedTypeName (mi.DeclaringType); + } + } else if (is_async) { + name = GetAsyncName (mi); + } else { + name = mi.Name; + } + return name; + } + public string MakeSignature (MemberInformation minfo, bool is_async, ParameterInfo [] parameters, string extra = "", bool alreadyPreserved = false) { var mi = minfo.Method; var category_class = minfo.category_extension_type; StringBuilder sb = new StringBuilder (); - string name = minfo.is_ctor ? Nomenclator.GetGeneratedTypeName (mi.DeclaringType) : is_async ? GetAsyncName (mi) : mi.Name; + string name = GetMethodName (minfo, is_async); // Some codepaths already write preservation info PrintAttributes (minfo.mi, preserve: !alreadyPreserved, advice: true, bindAs: true, requiresSuper: true); - if (!minfo.is_ctor && !is_async) { + if (minfo.is_ctor && minfo.is_protocol_member) { + sb.Append ("T? "); + } else if (!minfo.is_ctor && !is_async) { var prefix = ""; if (!BindThirdPartyLibrary) { if (minfo.Method.ReturnType.IsArray) { @@ -2700,6 +2724,13 @@ public string MakeSignature (MemberInformation minfo, bool is_async, ParameterIn name = "Set" + name.Substring (4); } sb.Append (name); + + if (minfo.is_protocol_member) { + if (minfo.is_ctor || minfo.is_static) { + sb.Append (""); + } + } + sb.Append (" ("); bool comma = false; @@ -2718,6 +2749,14 @@ public string MakeSignature (MemberInformation minfo, bool is_async, ParameterIn MakeSignatureFromParameterInfo (comma, sb, mi, minfo.type, parameters); sb.Append (extra); sb.Append (")"); + + if (minfo.is_protocol_member) { + if (minfo.is_static || minfo.is_ctor) { + sb.Append (" where T: NSObject, "); + sb.Append ("I").Append (minfo.Method.DeclaringType.Name); + } + } + return sb.ToString (); } @@ -2929,10 +2968,10 @@ void GenerateInvoke (bool stret, bool supercall, MethodInfo mi, MemberInformatio if (minfo.is_interface_impl || minfo.is_extension_method) { var tmp = InlineSelectors; InlineSelectors = true; - selector_field = SelectorField (selector); + selector_field = SelectorField (selector, force_gethandle: minfo.is_protocol_member); InlineSelectors = tmp; } else { - selector_field = SelectorField (selector); + selector_field = SelectorField (selector, force_gethandle: minfo.is_protocol_member); } if (ShouldMarshalNativeExceptions (mi)) @@ -2946,6 +2985,12 @@ void GenerateInvoke (bool stret, bool supercall, MethodInfo mi, MemberInformatio print ("{0} ({5}, {1}{2}, {3}{4});", sig, target_name, handle, selector_field, args, ret_val); print ("aligned_assigned = true;"); + } else if (minfo.is_protocol_member && mi.Name == "Constructor") { + const string handleName = "__handle__"; + print ($"IntPtr {handleName};"); + print ($"{handleName} = global::{NamespaceCache.Messaging}.IntPtr_objc_msgSend (Class.GetHandle (typeof (T)), Selector.GetHandle (\"alloc\"));"); + print ($"{handleName} = {sig} ({handleName}, {selector_field}{args});"); + print ($"{(assign_to_temp ? "ret = " : "return ")} global::ObjCRuntime.Runtime.GetINativeObject ({handleName}, true);"); } else { bool returns = mi.ReturnType != TypeCache.System_Void && mi.Name != "Constructor"; string cast_a = "", cast_b = ""; @@ -3510,7 +3555,8 @@ public void GenerateMethodBody (MemberInformation minfo, MethodInfo mi, string s (IsNativeEnum (mi.ReturnType)) || (mi.ReturnType == TypeCache.System_Boolean) || (mi.ReturnType == TypeCache.System_Char) || - (mi.Name != "Constructor" && by_ref_processing.Length > 0 && mi.ReturnType != TypeCache.System_Void); + minfo.is_protocol_member && disposes.Length > 0 && mi.Name == "Constructor" || + ((mi.Name != "Constructor" || minfo.is_protocol_member) && by_ref_processing.Length > 0 && mi.ReturnType != TypeCache.System_Void); if (use_temp_return) { // for properties we (most often) put the attribute on the property itself, not the getter/setter methods @@ -3530,6 +3576,8 @@ public void GenerateMethodBody (MemberInformation minfo, MethodInfo mi, string s print ("byte ret;"); } else if (mi.ReturnType == TypeCache.System_Char) { print ("ushort ret;"); + } else if (minfo.is_ctor && minfo.is_protocol_member) { + print ($"T? ret;"); } else { var isClassType = mi.ReturnType.IsClass || mi.ReturnType.IsInterface; var nullableReturn = isClassType ? "?" : string.Empty; @@ -3540,7 +3588,7 @@ public void GenerateMethodBody (MemberInformation minfo, MethodInfo mi, string s bool needs_temp = use_temp_return || disposes.Length > 0; if (minfo.is_virtual_method || mi.Name == "Constructor") { //print ("if (this.GetType () == TypeManager.{0}) {{", type.Name); - if (external || minfo.is_interface_impl || minfo.is_extension_method) { + if (external || minfo.is_interface_impl || minfo.is_extension_method || minfo.is_protocol_member) { GenerateNewStyleInvoke (false, mi, minfo, sel, argsArray, needs_temp, category_type); } else { var may_throw = shouldMarshalNativeExceptions; @@ -3655,6 +3703,8 @@ public void GenerateMethodBody (MemberInformation minfo, MethodInfo mi, string s print ("return ret != 0;"); } else if (mi.ReturnType == TypeCache.System_Char) { print ("return (char) ret;"); + } else if (minfo.is_ctor && minfo.is_protocol_member) { + print ("return ret;"); } else { // we can't be 100% confident that the ObjC API annotations are correct so we always null check inside generated code print ("return ret!;"); @@ -4363,6 +4413,9 @@ void PrintBlockProxy (Type type) void PrintExport (MemberInformation minfo) { + if (minfo.is_ctor && minfo.is_protocol_member) + return; + if (minfo.is_export) print ("[Export (\"{0}\"{1})]", minfo.selector, minfo.is_variadic ? ", IsVariadic = true" : string.Empty); } @@ -4440,7 +4493,7 @@ void GenerateMethod (MemberInformation minfo) #if NET var is_abstract = false; - var do_not_call_base = minfo.is_abstract || minfo.is_model; + var do_not_call_base = (minfo.is_abstract || minfo.is_model) && !(minfo.is_ctor && minfo.is_protocol_member); #else var is_abstract = minfo.is_abstract; var do_not_call_base = minfo.is_model; @@ -4454,7 +4507,7 @@ void GenerateMethod (MemberInformation minfo) if (!is_abstract) { - if (minfo.is_ctor) { + if (minfo.is_ctor && !minfo.is_protocol_member) { indent++; print (": {0}", minfo.wrap_method is null ? "base (NSObjectFlag.Empty)" : minfo.wrap_method); indent--; @@ -4580,7 +4633,7 @@ group fullname by ns into g } } - IEnumerable SelectProtocolMethods (Type type, bool? @static = null, bool? required = null) + IEnumerable SelectProtocolMethods (Type type, bool? @static = null, bool? required = null, bool selectConstructors = false) { var list = type.GetMethods (BindingFlags.Public | BindingFlags.Instance); @@ -4588,7 +4641,7 @@ IEnumerable SelectProtocolMethods (Type type, bool? @static = null, if (m.IsSpecialName) continue; - if (m.Name == "Constructor") + if ((m.Name == "Constructor") != selectConstructors) continue; var attrs = AttributeManager.GetCustomAttributes (m); @@ -4686,6 +4739,7 @@ void GenerateProtocolTypes (Type type, string class_visibility, string TypeName, { var allProtocolMethods = new List (); var allProtocolProperties = new List (); + var allProtocolConstructors = new List (); var ifaces = (IEnumerable) type.GetInterfaces ().Concat (new Type [] { ReflectionExtensions.GetBaseType (type, this) }).OrderBy (v => v.FullName, StringComparer.Ordinal); if (type.Namespace is not null) { @@ -4697,6 +4751,7 @@ void GenerateProtocolTypes (Type type, string class_visibility, string TypeName, allProtocolMethods.AddRange (SelectProtocolMethods (type)); allProtocolProperties.AddRange (SelectProtocolProperties (type)); + allProtocolConstructors.AddRange (SelectProtocolMethods (type, selectConstructors: true)); var requiredInstanceMethods = allProtocolMethods.Where ((v) => IsRequired (v) && !AttributeManager.HasAttribute (v)).ToList (); var optionalInstanceMethods = allProtocolMethods.Where ((v) => !IsRequired (v) && !AttributeManager.HasAttribute (v)); @@ -4838,6 +4893,15 @@ void GenerateProtocolTypes (Type type, string class_visibility, string TypeName, print ("{"); indent++; + +#if NET + foreach (var ctor in allProtocolConstructors) { + var minfo = new MemberInformation (this, this, ctor, type, null); + minfo.is_protocol_member = true; + GenerateMethod (minfo); + print (""); + } +#endif foreach (var mi in requiredInstanceMethods) { if (AttributeManager.HasAttribute (mi)) continue; diff --git a/src/bgen/Models/MemberInformation.cs b/src/bgen/Models/MemberInformation.cs index e11919c0358f..d8d2b7fe3fad 100644 --- a/src/bgen/Models/MemberInformation.cs +++ b/src/bgen/Models/MemberInformation.cs @@ -33,6 +33,7 @@ public class MemberInformation { public bool is_variadic; public bool is_interface_impl; public bool is_extension_method; + public bool is_protocol_member; public bool is_appearance; public bool is_model; public bool is_ctor; @@ -215,6 +216,8 @@ public string GetModifiers () if (is_sealed) { mods += ""; + } else if (is_ctor && is_protocol_member) { + mods += "unsafe static "; } else if (is_static || is_category_extension || is_extension_method) { mods += "static "; } else if (is_abstract) { diff --git a/tests/bgen/bgen-tests.csproj b/tests/bgen/bgen-tests.csproj index c283fb0844a6..e4a9f3007008 100644 --- a/tests/bgen/bgen-tests.csproj +++ b/tests/bgen/bgen-tests.csproj @@ -84,5 +84,11 @@ SdkVersions.cs + + BGenBase.cs + + + ProtocolTest.cs + diff --git a/tests/bindings-test/ApiDefinition.cs b/tests/bindings-test/ApiDefinition.cs index 9b844e3a59a4..b2854cd6d8a1 100644 --- a/tests/bindings-test/ApiDefinition.cs +++ b/tests/bindings-test/ApiDefinition.cs @@ -429,6 +429,34 @@ interface ObjCBlockTester { delegate void InnerBlock (int magic_number); delegate void OuterBlock ([BlockCallback] InnerBlock callback); +#if NET + [Protocol] + interface ConstructorProtocol { + [Abstract] + [Export ("initRequired:")] + IntPtr Constructor (string p0); + + [Export ("initOptional:")] + IntPtr Constructor (NSDate p0); + } + + [BaseType (typeof (NSObject))] + [DisableDefaultCtor] + interface TypeProvidingProtocolConstructors : ConstructorProtocol { + [Export ("initRequired:")] + new IntPtr Constructor (string p0); + + [Export ("initOptional:")] + new IntPtr Constructor (NSDate p0); + + [Export ("stringValue")] + string StringValue { get; set; } + + [Export ("dateValue")] + NSDate DateValue { get; set; } + } +#endif + [BaseType (typeof (NSObject))] interface EvilDeallocator { [Export ("evilCallback")] diff --git a/tests/bindings-test/ProtocolTest.cs b/tests/bindings-test/ProtocolTest.cs index 3b898eed978e..c8a68a24f156 100644 --- a/tests/bindings-test/ProtocolTest.cs +++ b/tests/bindings-test/ProtocolTest.cs @@ -9,6 +9,8 @@ using NUnit.Framework; +using Bindings.Test; + namespace Xamarin.BindingTests { [TestFixture] [Preserve (AllMembers = true)] @@ -29,6 +31,77 @@ bool HasProtocolAttributes { } } +#if NET + [Test] + public void Constructors () + { + using var dateNow = (NSDate) DateTime.Now; + + using (var obj = IConstructorProtocol.CreateInstance ("Hello world")) { + Assert.AreEqual ("Hello world", obj.StringValue, "A StringValue"); + Assert.IsNull (obj.DateValue, "A DateValue"); + } + + using (var obj = IConstructorProtocol.CreateInstance (dateNow)) { + Assert.IsNull (obj.StringValue, "B StringValue"); + Assert.AreEqual (dateNow, obj.DateValue, "B DateValue"); + } + + using (var obj = IConstructorProtocol.CreateInstance ("Hello Subclassed")) { + Assert.AreEqual ("Hello Subclassed", obj.StringValue, "C1 StringValue"); + Assert.IsNull (obj.DateValue, "C1 DateValue"); + } + + using (var obj = IConstructorProtocol.CreateInstance (dateNow)) { + Assert.IsNull (obj.StringValue, "C2 StringValue"); + Assert.AreEqual (dateNow, obj.DateValue, "C2 DateValue"); + } + + if (global::XamarinTests.ObjCRuntime.Registrar.IsDynamicRegistrar) { + Assert.Throws (() => { + IConstructorProtocol.CreateInstance ("Hello Subclassed 2"); + }, "D1 Exception"); + } else { + using (var obj = IConstructorProtocol.CreateInstance ("Hello Subclassed 2")) { + Assert.AreEqual ("Managed interceptor! Hello Subclassed 2", obj.StringValue, "D1 StringValue"); + Assert.IsNull (obj.DateValue, "D1 DateValue"); + } + } + + if (XamarinTests.ObjCRuntime.Registrar.IsDynamicRegistrar) { + Assert.Throws (() => { + IConstructorProtocol.CreateInstance (dateNow); + }, "D2 Exception"); + } else { + using (var obj = IConstructorProtocol.CreateInstance (dateNow)) { + Assert.IsNull (obj.StringValue, "D2 StringValue"); + Assert.AreEqual (dateNow.AddSeconds (42), obj.DateValue, "D2 DateValue"); + } + } + } + + class SubclassedTypeProvidingProtocolConstructors : TypeProvidingProtocolConstructors { + SubclassedTypeProvidingProtocolConstructors (NativeHandle handle) : base (handle) {} + + } + + class SubclassedTypeProvidingProtocolConstructors2 : TypeProvidingProtocolConstructors { + SubclassedTypeProvidingProtocolConstructors2 (NativeHandle handle) : base (handle) {} + + [Export ("initRequired:")] + public SubclassedTypeProvidingProtocolConstructors2 (string value) + : base ($"Managed interceptor! " + value) + { + } + + [Export ("initOptional:")] + public SubclassedTypeProvidingProtocolConstructors2 (NSDate value) + : base (value.AddSeconds (42)) + { + } + } +#endif + [Test] #if NET [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This test verifies trimmer behavior, and as such must do trimmer-unsafe stuff.")] diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index 78d4d2e47428..43367bb8a9b0 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -30278,6 +30278,7 @@ M:CoreML.IMLBatchProvider.GetFeatures(System.IntPtr) M:CoreML.IMLCustomLayer.EvaluateOnCpu(CoreML.MLMultiArray[],CoreML.MLMultiArray[],Foundation.NSError@) M:CoreML.IMLCustomLayer.GetOutputShapes(Foundation.NSArray[],Foundation.NSError@) M:CoreML.IMLCustomLayer.SetWeightData(Foundation.NSData[],Foundation.NSError@) +M:CoreML.IMLCustomModel.CreateInstance``1(CoreML.MLModelDescription,Foundation.NSDictionary{Foundation.NSString,Foundation.NSObject},Foundation.NSError@) M:CoreML.IMLCustomModel.GetPrediction(CoreML.IMLFeatureProvider,CoreML.MLPredictionOptions,Foundation.NSError@) M:CoreML.IMLFeatureProvider.GetFeatureValue(System.String) M:CoreML.IMLWritable.Write(Foundation.NSUrl,Foundation.NSError@) @@ -31568,6 +31569,7 @@ M:Foundation.ExportAttribute.ToGetter(System.Reflection.PropertyInfo) M:Foundation.ExportAttribute.ToSetter(System.Reflection.PropertyInfo) M:Foundation.FieldAttribute.#ctor(System.String,System.String) M:Foundation.FieldAttribute.#ctor(System.String) +M:Foundation.INSCoding.CreateInstance``1(Foundation.NSCoder) M:Foundation.INSCoding.EncodeTo(Foundation.NSCoder) M:Foundation.INSCopying.Copy(Foundation.NSZone) M:Foundation.INSDiscardableContent.BeginContentAccess @@ -36496,9 +36498,11 @@ M:MetalKit.MTKView.Dispose(System.Boolean) M:MetalKit.MTKView.MTKViewAppearance.#ctor(System.IntPtr) M:MetalKit.MTKViewDelegate.Draw(MetalKit.MTKView) M:MetalKit.MTKViewDelegate.DrawableSizeWillChange(MetalKit.MTKView,CoreGraphics.CGSize) +M:MetalPerformanceShaders.IMPSCnnBatchNormalizationDataSource.CreateInstance``1(Foundation.NSCoder) M:MetalPerformanceShaders.IMPSCnnBatchNormalizationDataSource.Purge M:MetalPerformanceShaders.IMPSCnnConvolutionDataSource.Load M:MetalPerformanceShaders.IMPSCnnConvolutionDataSource.Purge +M:MetalPerformanceShaders.IMPSCnnInstanceNormalizationDataSource.CreateInstance``1(Foundation.NSCoder) M:MetalPerformanceShaders.IMPSDeviceProvider.GetMTLDevice M:MetalPerformanceShaders.IMPSHeapProvider.GetNewHeap(Metal.MTLHeapDescriptor) M:MetalPerformanceShaders.IMPSImageAllocator.GetImage(Metal.IMTLCommandBuffer,MetalPerformanceShaders.MPSImageDescriptor,MetalPerformanceShaders.MPSKernel) diff --git a/tests/generator/BGenBase.cs b/tests/generator/BGenBase.cs new file mode 100644 index 000000000000..09994ea321bd --- /dev/null +++ b/tests/generator/BGenBase.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using System.IO; + +using NUnit.Framework; + +using Xamarin.Tests; + +namespace GeneratorTests { + public class BGenBase { + internal BGenTool BuildFile (Profile profile, params string [] filenames) + { + return BuildFile (profile, true, false, filenames); + } + + internal BGenTool BuildFile (Profile profile, bool nowarnings, params string [] filenames) + { + return BuildFile (profile, nowarnings, false, filenames); + } + + internal BGenTool BuildFile (Profile profile, bool nowarnings, bool processEnums, params string [] filenames) + { + return BuildFile (profile, nowarnings, processEnums, Enumerable.Empty (), filenames); + } + + internal BGenTool BuildFile (Profile profile, bool nowarnings, bool processEnums, IEnumerable references, params string [] filenames) + { + Configuration.IgnoreIfIgnoredPlatform (profile.AsPlatform ()); + var bgen = new BGenTool (); + bgen.Profile = profile; + bgen.ProcessEnums = processEnums; + bgen.Defines = BGenTool.GetDefaultDefines (bgen.Profile); + bgen.References = references.ToList (); + TestContext.Out.WriteLine (TestContext.CurrentContext.Test.FullName); + foreach (var filename in filenames) + TestContext.Out.WriteLine ($"\t{filename}"); + bgen.CreateTemporaryBinding (filenames.Select ((filename) => File.ReadAllText (Path.Combine (Configuration.SourceRoot, "tests", "generator", filename))).ToArray ()); + bgen.AssertExecute ("build"); + if (nowarnings) + bgen.AssertNoWarnings (); + return bgen; + } + } +} diff --git a/tests/generator/BGenTests.cs b/tests/generator/BGenTests.cs index b1b60b6dde19..fee5ec360887 100644 --- a/tests/generator/BGenTests.cs +++ b/tests/generator/BGenTests.cs @@ -15,7 +15,7 @@ namespace GeneratorTests { [TestFixture ()] [Parallelizable (ParallelScope.All)] - public class BGenTests { + public class BGenTests : BGenBase { // Removing the following variable might make running the unit tests in VSMac fail. static Type variable_to_keep_reference_to_system_runtime_compilerservices_unsafe_assembly = typeof (System.Runtime.CompilerServices.Unsafe); @@ -1556,38 +1556,5 @@ public void Issue19612 () bgen.AssertNoWarnings (); } #endif - - BGenTool BuildFile (Profile profile, params string [] filenames) - { - return BuildFile (profile, true, false, filenames); - } - - BGenTool BuildFile (Profile profile, bool nowarnings, params string [] filenames) - { - return BuildFile (profile, nowarnings, false, filenames); - } - - BGenTool BuildFile (Profile profile, bool nowarnings, bool processEnums, params string [] filenames) - { - return BuildFile (profile, nowarnings, processEnums, Enumerable.Empty (), filenames); - } - - BGenTool BuildFile (Profile profile, bool nowarnings, bool processEnums, IEnumerable references, params string [] filenames) - { - Configuration.IgnoreIfIgnoredPlatform (profile.AsPlatform ()); - var bgen = new BGenTool (); - bgen.Profile = profile; - bgen.ProcessEnums = processEnums; - bgen.Defines = BGenTool.GetDefaultDefines (bgen.Profile); - bgen.References = references.ToList (); - TestContext.Out.WriteLine (TestContext.CurrentContext.Test.FullName); - foreach (var filename in filenames) - TestContext.Out.WriteLine ($"\t{filename}"); - bgen.CreateTemporaryBinding (filenames.Select ((filename) => File.ReadAllText (Path.Combine (Configuration.SourceRoot, "tests", "generator", filename))).ToArray ()); - bgen.AssertExecute ("build"); - if (nowarnings) - bgen.AssertNoWarnings (); - return bgen; - } } } diff --git a/tests/generator/ProtocolTests.cs b/tests/generator/ProtocolTests.cs new file mode 100644 index 000000000000..7a115fc066ec --- /dev/null +++ b/tests/generator/ProtocolTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; + +using NUnit.Framework; +using Xamarin.Tests; + +namespace GeneratorTests { + [TestFixture ()] + [Parallelizable (ParallelScope.All)] + public class ProtocolTests : BGenBase { +#if !NET + [Ignore ("This only applies to .NET")] +#endif + [TestCase (Profile.MacCatalyst)] + [TestCase (Profile.iOS)] + public void Members (Profile profile) + { + var bgen = BuildFile (profile, "tests/protocols.cs"); + + var allTypeDefinitions = bgen.ApiAssembly.MainModule.GetTypes ().ToArray (); + var allTypes = allTypeDefinitions.Select (v => v.FullName).ToArray (); + + var allTypeNames = allTypes.OrderBy (v => v).ToArray (); + var expectedTypes = new string [] { + "", + "api0.Messaging", + "Protocols.IProtocolWithConstructors", + "Protocols.ProtocolWithConstructorsWrapper", + }; + CollectionAssert.AreEqual (expectedTypes, allTypeNames, "Types"); + + var allMethods = allTypeDefinitions.SelectMany (v => v.Methods).Select (v => v.ToString ()).OrderBy (v => v).ToArray (); + var expectedMethods = new string [] { + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSend_NativeHandle(System.IntPtr,System.IntPtr,ObjCRuntime.NativeHandle)", + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSend_ref_NativeHandle(System.IntPtr,System.IntPtr,ObjCRuntime.NativeHandle*)", + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSend(System.IntPtr,System.IntPtr)", + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSendSuper_NativeHandle(System.IntPtr,System.IntPtr,ObjCRuntime.NativeHandle)", + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSendSuper_ref_NativeHandle(System.IntPtr,System.IntPtr,ObjCRuntime.NativeHandle*)", + "ObjCRuntime.NativeHandle api0.Messaging::NativeHandle_objc_msgSendSuper(System.IntPtr,System.IntPtr)", + "System.IntPtr api0.Messaging::IntPtr_objc_msgSend_IntPtr(System.IntPtr,System.IntPtr,System.IntPtr)", + "System.IntPtr api0.Messaging::IntPtr_objc_msgSend(System.IntPtr,System.IntPtr)", + "System.IntPtr api0.Messaging::IntPtr_objc_msgSendSuper_IntPtr(System.IntPtr,System.IntPtr,System.IntPtr)", + "System.IntPtr api0.Messaging::IntPtr_objc_msgSendSuper(System.IntPtr,System.IntPtr)", + "System.Void api0.Messaging::.cctor()", + "System.Void Protocols.ProtocolWithConstructorsWrapper::.ctor(ObjCRuntime.NativeHandle,System.Boolean)", + "T Protocols.IProtocolWithConstructors::Create(Foundation.NSDate)", + "T Protocols.IProtocolWithConstructors::CreateInstance()", + "T Protocols.IProtocolWithConstructors::CreateInstance(Foundation.NSError&)", + "T Protocols.IProtocolWithConstructors::CreateInstance(System.String)", + }; + + CollectionAssert.AreEqual (expectedMethods, allMethods, "Types"); + } + } +} + diff --git a/tests/generator/generator-tests.csproj b/tests/generator/generator-tests.csproj index 9e55a14219dd..396804421594 100644 --- a/tests/generator/generator-tests.csproj +++ b/tests/generator/generator-tests.csproj @@ -67,6 +67,8 @@ + + Profile.cs diff --git a/tests/generator/tests/protocols.cs b/tests/generator/tests/protocols.cs new file mode 100644 index 000000000000..67bffa2393ab --- /dev/null +++ b/tests/generator/tests/protocols.cs @@ -0,0 +1,22 @@ +using System; +using Foundation; +using ObjCRuntime; + +namespace Protocols { + [Protocol] + interface ProtocolWithConstructors { + [Abstract] + [Export ("init")] + NativeHandle Constructor (); + + [Export ("initWithValue:")] + NativeHandle Constructor (string p0); + + [Export ("initWithError:")] + NativeHandle Constructor (out NSError error); + + [Bind ("Create")] + [Export ("initWithCustomName:")] + NativeHandle Constructor (NSDate error); + } +} diff --git a/tests/test-libraries/libtest.h b/tests/test-libraries/libtest.h index 7f5a03495b58..e481dab7ec6d 100644 --- a/tests/test-libraries/libtest.h +++ b/tests/test-libraries/libtest.h @@ -277,6 +277,23 @@ typedef void (^outerBlock) (innerBlock callback); // And Apple does (see UIAppearance appearanceWhenContainedInInstancesOfClasses for an example). @end +@protocol ConstructorProtocol +@required + -(id) initRequired: (NSString *) p0; +@optional + -(id) initOptional: (NSDate *) p0; +@end + +@interface TypeProvidingProtocolConstructors : NSObject { +} + -(id) initRequired: (NSString *) p0; + -(id) initOptional: (NSDate *) p0; + +@property (copy) NSString* stringValue; +@property (copy) NSDate* dateValue; + +@end + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/tests/test-libraries/libtest.m b/tests/test-libraries/libtest.m index ce08ffacead5..01dfb8bc991c 100644 --- a/tests/test-libraries/libtest.m +++ b/tests/test-libraries/libtest.m @@ -1346,4 +1346,26 @@ -(void) testClassArray: (int) action a:(NSArray **) refValue b:(NSArray **) outV } } @end + +@implementation TypeProvidingProtocolConstructors +-(id) initRequired: (NSString *) p0 +{ + self = [super init]; + if (self) { + _stringValue = [p0 copy]; + } + return self; +} + +-(id) initOptional: (NSDate *) p0 +{ + self = [super init]; + if (self) { + _dateValue = [p0 copy]; + } + return self; +} + +@end + #include "libtest.decompile.m" diff --git a/tests/test-libraries/rename.h b/tests/test-libraries/rename.h index c11892b03da7..9ba236efdf0e 100644 --- a/tests/test-libraries/rename.h +++ b/tests/test-libraries/rename.h @@ -11,6 +11,7 @@ #define UltimateMachine object_UltimateMachine #define FrameworkTest object_FrameworkTest #define RefOutParameters object_RefOutParameters + #define TypeProvidingProtocolConstructors objecct_TypeProvidingProtocolConstructors #define Sc object_Sc #define Scc object_Scc #define Sccc object_Sccc @@ -85,6 +86,7 @@ #define UltimateMachine ar_UltimateMachine #define FrameworkTest ar_FrameworkTest #define RefOutParameters ar_RefOutParameters + #define TypeProvidingProtocolConstructors ar_TypeProvidingProtocolConstructors #define Sc ar_Sc #define Scc ar_Scc #define Sccc ar_Sccc