Skip to content

Commit

Permalink
[bgen] Add support for binding constructors in protocols. Fixes #14039.…
Browse files Browse the repository at this point in the history
… (#20583)

Add support for binding constructors in protocols.

Given the api definition:

```cs
[Protocol]
public interface Protocol {
    [Abstract]
    [Export ("init")]
    IntPtr Constructor ();

    [Export ("initWithValue:")]
    IntPtr Constructor (IntPtr value);

    [BindAs ("Create")]
    [Export ("initWithPlanet:")]
    IntPtr Constructor ();
}
```

we're binding it like this:

```cs
[Protocol ("Protocol")]
public interface IProtocol : INativeObject {
    [Export ("init")]
    public static T CreateInstance<T> () where T: NSObject, IProtocol { /* default implementation */ }

    [Export ("initWithValue:")]
    public static T CreateInstance<T> () where T: NSObject, IProtocol { /* default implementation */ }

    [Export ("initWithPlanet:")]
    public static T Create<T> () where T: NSObject, IProtocol { /* default implementation */ }
}
```

Also add documentation and tests.

Fixes #14039.

---------

Co-authored-by: Manuel de la Pena <[email protected]>
Co-authored-by: Alex Soto <[email protected]>
  • Loading branch information
3 people authored May 24, 2024
1 parent 536f0a1 commit f78af68
Show file tree
Hide file tree
Showing 15 changed files with 486 additions and 45 deletions.
131 changes: 131 additions & 0 deletions docs/objective-c-protocols.md
Original file line number Diff line number Diff line change
@@ -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<T> () where T: NSObject, IProtocol { /* default implementation */ }

[Export ("initWithValue:")]
public static T CreateInstance<T> () where T: NSObject, IProtocol { /* default implementation */ }

[Export ("initWithPlanet:")]
public static T Create<T> () 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;
}
}
```
86 changes: 75 additions & 11 deletions src/bgen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -2654,17 +2657,38 @@ public bool IsProtocol (Type type)
return AttributeManager.HasAttribute<ProtocolAttribute> (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) {
Expand Down Expand Up @@ -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 ("<T>");
}
}

sb.Append (" (");

bool comma = false;
Expand All @@ -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 ();
}

Expand Down Expand Up @@ -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))
Expand All @@ -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<T> ({handleName}, true);");
} else {
bool returns = mi.ReturnType != TypeCache.System_Void && mi.Name != "Constructor";
string cast_a = "", cast_b = "";
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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!;");
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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--;
Expand Down Expand Up @@ -4580,15 +4633,15 @@ group fullname by ns into g
}
}

IEnumerable<MethodInfo> SelectProtocolMethods (Type type, bool? @static = null, bool? required = null)
IEnumerable<MethodInfo> SelectProtocolMethods (Type type, bool? @static = null, bool? required = null, bool selectConstructors = false)
{
var list = type.GetMethods (BindingFlags.Public | BindingFlags.Instance);

foreach (var m in list) {
if (m.IsSpecialName)
continue;

if (m.Name == "Constructor")
if ((m.Name == "Constructor") != selectConstructors)
continue;

var attrs = AttributeManager.GetCustomAttributes<Attribute> (m);
Expand Down Expand Up @@ -4686,6 +4739,7 @@ void GenerateProtocolTypes (Type type, string class_visibility, string TypeName,
{
var allProtocolMethods = new List<MethodInfo> ();
var allProtocolProperties = new List<PropertyInfo> ();
var allProtocolConstructors = new List<MethodInfo> ();
var ifaces = (IEnumerable<Type>) type.GetInterfaces ().Concat (new Type [] { ReflectionExtensions.GetBaseType (type, this) }).OrderBy (v => v.FullName, StringComparer.Ordinal);

if (type.Namespace is not null) {
Expand All @@ -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<StaticAttribute> (v)).ToList ();
var optionalInstanceMethods = allProtocolMethods.Where ((v) => !IsRequired (v) && !AttributeManager.HasAttribute<StaticAttribute> (v));
Expand Down Expand Up @@ -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<StaticAttribute> (mi))
continue;
Expand Down
3 changes: 3 additions & 0 deletions src/bgen/Models/MemberInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions tests/bgen/bgen-tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,11 @@
<Compile Include="..\..\tools\common\SdkVersions.cs">
<Link>SdkVersions.cs</Link>
</Compile>
<Compile Include="..\generator\BGenBase.cs">
<Link>BGenBase.cs</Link>
</Compile>
<Compile Include="..\generator\ProtocolTests.cs">
<Link>ProtocolTest.cs</Link>
</Compile>
</ItemGroup>
</Project>
Loading

3 comments on commit f78af68

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.