Skip to content

Commit

Permalink
[bgen] Add support for marking API bindings as preview APIs using the…
Browse files Browse the repository at this point in the history
… Experimental attribute. (#20591)
  • Loading branch information
rolfbjarne authored May 9, 2024
1 parent 4ba38c7 commit c5f93a2
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 1 deletion.
72 changes: 72 additions & 0 deletions docs/preview-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Preview APIs

The APIs listed here are currently marked as preview APIs, and as such may
change in the future (we don't guarante binary or source compatibility between
releases for these APIs).

We've marked these APIs using the [Experimental][1] attribute, which means
that compilation error will be shown if they're used:

> error APL0001: 'PreviewAPI' is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
This means that it's not possible to use these preview APIs by accident, the diagnostic has to be explicitly ignored.

Example program consuming preview API:

```cs
using System.Diagnostics.CodeAnalysis;

class App
{
public static void Main ()
{
Do.Something ();
}
}

[Experimental ("APL0001")]
class Do {
public static void Something () {}
}
```

this will show:

> Program.cs(8,9): error APL0001: 'Do' is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Then ignore the warning in order to make the code compile:

```cs
using System.Diagnostics.CodeAnalysis;

class App
{
public unsafe static void Main ()
{
#pragma warning disable APL0001
Do.Something ();
#pragma warning restore APL0001
}
}

[Experimental ("APL0001")]
class Do {
public static void Something () {}
}

```

Our diagnostic IDs will be of the format `APL####` - for instance `APL0001` -
where the number is just monotonically increasing since the previous number,
without any specific meaning.

References:

* https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0
* https://learn.microsoft.com/en-us/dotnet/fundamentals/apicompat/preview-apis#experimentalattribute

## Placeholder header for APL####

Coming soon!

[1]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0
5 changes: 5 additions & 0 deletions src/bgen/AttributeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ Type LookupReflectionType (string fullname, ICustomAttributeProvider provider)
case "WrapAttribute":
return typeof (WrapAttribute);
#if NET
case "System.Diagnostics.CodeAnalysis.ExperimentalAttribute":
return typeof (System.Diagnostics.CodeAnalysis.ExperimentalAttribute);
case "System.Runtime.Versioning.SupportedOSPlatformAttribute":
return typeof (System.Runtime.Versioning.SupportedOSPlatformAttribute);
case "System.Runtime.Versioning.UnsupportedOSPlatformAttribute":
Expand Down Expand Up @@ -335,6 +337,9 @@ Type ConvertTypeToMeta (System.Type type)
case "AvailabilityAttribute":
return AttributeConversionManager.ConvertAvailability (attribute);
#if NET
case "ExperimentalAttribute":
var earg = attribute.ConstructorArguments [0].Value as string;
return new System.Diagnostics.CodeAnalysis.ExperimentalAttribute (earg).Yield ();
case "SupportedOSPlatformAttribute":
var sarg = attribute.ConstructorArguments [0].Value as string;
(var sp, var sv) = ParseOSPlatformAttribute (sarg);
Expand Down
2 changes: 2 additions & 0 deletions src/bgen/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ void GenerateEnum (Type type)
WriteDocumentation (f);
PrintPlatformAttributes (f);
PrintObsoleteAttributes (f);
PrintExperimentalAttribute (f);
print ("{0} = {1},", f.Name, f.GetRawConstantValue ());
var fa = AttributeManager.GetCustomAttribute<FieldAttribute> (f);
if (fa is null)
Expand Down Expand Up @@ -139,6 +140,7 @@ void GenerateEnum (Type type)
}
// the *Extensions has the same version requirement as the enum itself
PrintPlatformAttributes (type);
PrintExperimentalAttribute (type);
print_generated_code ();
print ("static {1} partial class {0}Extensions {{", type.Name, visibility);
indent++;
Expand Down
39 changes: 38 additions & 1 deletion src/bgen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -304,6 +305,17 @@ bool IsProtocolInterface (Type type, bool checkPrefix, out Type protocol)
return AttributeManager.HasAttribute<ProtocolAttribute> (protocol);
}

#if NET
public ExperimentalAttribute GetExperimentalAttribute (ICustomAttributeProvider cu)
{
ExperimentalAttribute rv;
if (cu is not null && (rv = AttributeManager.GetCustomAttribute<ExperimentalAttribute> (cu)) is not null)
return rv;

return null;
}
#endif

public BindAsAttribute GetBindAsAttribute (ICustomAttributeProvider cu)
{
BindAsAttribute rv;
Expand Down Expand Up @@ -1423,6 +1435,10 @@ public void Go ()
continue;
else if (attr is NoMethodAttribute)
continue;
#if NET
else if (attr is ExperimentalAttribute)
continue;
#endif
else {
switch (attr.GetType ().Name) {
case "PreserveAttribute":
Expand Down Expand Up @@ -1555,11 +1571,13 @@ void GenerateTrampolinesForQueue (TrampolineInfo [] queue)
var parameters = mi.GetParameters ();

print ("");
PrintExperimentalAttribute (ti.Type);
print ("[UnmanagedFunctionPointerAttribute (CallingConvention.Cdecl)]");
print ("[UserDelegateType (typeof ({0}))]", ti.UserDelegate);
print ("unsafe internal delegate {0} {1} ({2});", ti.ReturnType, ti.DelegateName, ti.Parameters);
print ("");
print ("//\n// This class bridges native block invocations that call into C#\n//");
PrintExperimentalAttribute (ti.Type);
print ("static internal class {0} {{", ti.StaticName); indent++;
// it can't be conditional without fixing https://github.com/mono/linker/issues/516
// but we have a workaround in place because we can't fix old, binary bindings so...
Expand Down Expand Up @@ -1640,6 +1658,7 @@ void GenerateTrampolinesForQueue (TrampolineInfo [] queue)
// Now generate the class that allows us to invoke a Objective-C block from C#
//
print ("");
PrintExperimentalAttribute (ti.Type);
print ($"internal sealed class {ti.NativeInvokerName} : TrampolineBlockBase {{");
indent++;
print ("{0} invoker;", ti.DelegateName);
Expand Down Expand Up @@ -4542,6 +4561,7 @@ group fullname by ns into g

var del = mi.DeclaringType;

PrintExperimentalAttribute (mi.DeclaringType);
if (AttributeManager.HasAttribute (mi.DeclaringType, "MonoNativeFunctionWrapper"))
print ("[MonoNativeFunctionWrapper]\n");

Expand Down Expand Up @@ -4937,9 +4957,11 @@ void GenerateProtocolTypes (Type type, string class_visibility, string TypeName,
}

PrintPreserveAttribute (type);
PrintExperimentalAttribute (type);
print ("internal unsafe sealed class {0}Wrapper : BaseWrapper, I{0} {{", TypeName);
indent++;
// ctor (IntPtr, bool)
PrintExperimentalAttribute (type);
print ("[Preserve (Conditional = true)]");
print ("public {0}Wrapper ({1} handle, bool owns)", TypeName, NativeHandleType);
print ("\t: base (handle, owns)");
Expand Down Expand Up @@ -5208,7 +5230,10 @@ public void PrintBindAsAttribute (ICustomAttributeProvider mi, StringBuilder sb
print (attribstr);
}

public void PrintAttributes (ICustomAttributeProvider mi, bool platform = false, bool preserve = false, bool advice = false, bool notImplemented = false, bool bindAs = false, bool requiresSuper = false, Type inlinedType = null)
// Not adding the experimental attribute is bad (it would mean that an API
// we meant to be experimental ended up being released as stable), so it's
// opt-out instead of opt-in.
public void PrintAttributes (ICustomAttributeProvider mi, bool platform = false, bool preserve = false, bool advice = false, bool notImplemented = false, bool bindAs = false, bool requiresSuper = false, Type inlinedType = null, bool experimental = true)
{
if (platform)
PrintPlatformAttributes (mi as MemberInfo, inlinedType);
Expand All @@ -5222,6 +5247,18 @@ public void PrintAttributes (ICustomAttributeProvider mi, bool platform = false,
PrintBindAsAttribute (mi);
if (requiresSuper)
PrintRequiresSuperAttribute (mi);
if (experimental)
PrintExperimentalAttribute (mi);
}

public void PrintExperimentalAttribute (ICustomAttributeProvider mi)
{
#if NET
var e = GetExperimentalAttribute (mi);
if (e is null)
return;
print ($"[Experimental (\"{e.DiagnosticId}\")]");
#endif
}

void WriteDocumentation (MemberInfo info)
Expand Down
28 changes: 28 additions & 0 deletions tests/generator/BGenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,34 @@ public void XmlDocs (Profile profile)
}
}

[Test]
#if !NET
[Ignore ("This only applies to .NET")]
#endif
[TestCase (Profile.iOS)]
[TestCase (Profile.MacCatalyst)]
[TestCase (Profile.macOSMobile)]
[TestCase (Profile.tvOS)]
public void PreviewAPIs (Profile profile)
{
var bgen = BuildFile (profile, false, true, "tests/preview.cs");

// Each Experimental attribute in the api definition has its own diagnostic ID (with an incremental number)
// Here we collect all diagnostic IDS for all the Experimental attributes in the compiled assembly,
// and assert that they're all present at least once.
var module = bgen.ApiAssembly.MainModule;
var allExperimentalAttributes = module.GetCustomAttributes ().Where (v => v.AttributeType.Name == "ExperimentalAttribute");
var allExperimentalDiagnosticIds = allExperimentalAttributes.Select (v => (string) v.ConstructorArguments [0].Value).ToHashSet ();
var previewApiCount = 32;
var expectedDiagnosticIds = Enumerable.Range (1, previewApiCount).Select (v => $"BGEN{v:0000}").ToHashSet ();

var unexpectedDiagnosticIds = allExperimentalDiagnosticIds.Except (expectedDiagnosticIds).OrderBy (v => v);
var missingDiagnosticIds = expectedDiagnosticIds.Except (allExperimentalDiagnosticIds).OrderBy (v => v);

Assert.That (unexpectedDiagnosticIds, Is.Empty, "No unexpected diagnostic IDs"); // you probably need to increase the previewApiCount variable above (if you added more definitions to the tests/preview.cs file).
Assert.That (missingDiagnosticIds, Is.Empty, "No missing diagnostic IDs");
}

[Test]
public void DelegateParameterAttributes ()
{
Expand Down
133 changes: 133 additions & 0 deletions tests/generator/tests/preview.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System;
using System.Diagnostics.CodeAnalysis;

using Foundation;

namespace Experimental {
[Experimental ("BGEN0001")]
[BaseType (typeof (NSObject))]
interface T1 : P1 {
[Experimental ("BGEN0002")]
[Export ("initWithString:")]
IntPtr Constructor (string p);

[Experimental ("BGEN0003")]
[Export ("method")]
int Method ();

[Experimental ("BGEN0004")]
[Export ("property")]
D Property {
[Experimental ("BGEN0005")]
get;
[Experimental ("BGEN0006")]
set;
}
}

[Experimental ("BGEN0007")]
delegate void D ();


[Experimental ("BGEN0008")]
[Protocol]
interface P1 {
[Experimental ("BGEN0009")]
[Export ("method")]
int PMethod ();

[Experimental ("BGEN0010")]
[Export ("property")]
int PProperty { get; set; }

[Experimental ("BGEN0011")]
[Abstract]
[Export ("methodRequired")]
int PAMethod ();

[Experimental ("BGEN0012")]
[Abstract]
[Export ("propertyRequired")]
int PAProperty { get; set; }
}

[Experimental ("BGEN0013")]
[BaseType (typeof (NSObject))]
interface TG1<T, U>
where T : NSObject
where U : NSObject {

[Experimental ("BGEN0014")]
[Export ("method")]
int TGMethod ();

[Experimental ("BGEN0015")]
[Export ("property")]
int TGProperty { get; set; }

[Experimental ("BGEN0016")]
[Export ("method2:")]
void TGMethod2 (TG1<T, U> value);
}

[Experimental ("BGEN0017")]
public enum E1 {
[Experimental ("BGEN0018")]
Value1,
}

[Experimental ("BGEN0019")]
[BaseType (typeof (NSObject))]
interface Notification1 {
[Experimental ("BGEN0020")]
[Notification]
[Field ("NSANotification", LibraryName = "__Internal")]
NSString ANotification { get; }
}

[Experimental ("BGEN0021")]
enum E2 {
[Experimental ("BGEN0022")]
[Field ("E2A", LibraryName = "__Internal")]
A,
}

[Experimental ("BGEN0023")]
[ErrorDomain ("E3Domain", LibraryName = "__Internal")]
enum E3 {
[Experimental ("BGEN0024")]
ErrorA,
}

[Experimental ("BGEN0025")]
[Flags]
enum E4 {
[Experimental ("BGEN0026")]
Bit1 = 1,
[Experimental ("BGEN0027")]
Bit3 = 4,
}

[Experimental ("BGEN0028")]
[Protocol, Model]
interface PM1 {
[Experimental ("BGEN0029")]
[Export ("method")]
int PMethod ();

[Experimental ("BGEN0030")]
[Export ("property")]
int PProperty { get; set; }

[Experimental ("BGEN0031")]
[Abstract]
[Export ("methodRequired")]
int PAMethod ();

[Experimental ("BGEN0032")]
[Abstract]
[Export ("propertyRequired")]
int PAProperty { get; set; }
}

}

7 comments on commit c5f93a2

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

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

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.