Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java.interop] address some "easy" trimmer warnings #1184

Merged
merged 8 commits into from
Feb 13, 2024

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented Feb 5, 2024

Context: #1157

On the path to enabling IsAotCompatible, we can enable some settings
to get started:

<IsTrimmable>true</IsTrimmable>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>

This opts into the analyzers without declaring the assembly is fully AOT-compatible.

Starting out, I got 33 warnings: this is an attempt to address the ones that don't require too much thinking. Unfortunately, solving 1 warning likely will create dozens more -- as you have to update all callers.

This results in 24 warnings remaining. Since Release builds have $(TreatWarningsAsErrors), I will wait to enable the analyzers until all warnings are addressed.

Example Warnings

System.Linq.Expression usage:

src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(672,58):
warning IL2026: Using member 'System.Linq.Expressions.Expression.Property(Expression, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.

I decorated this one with:

[RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]

Type.GetNestedType() usage:

src\Java.Interop\Java.Interop\JniRuntime.JniTypeManager.cs(447,28):
warning IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.NonPublicNestedTypes' in call to 'System.Type.GetNestedType(String, BindingFlags)'. The parameter 'type' of method 'Java.Interop.JniRuntime.JniTypeManager.TryLoadJniMarshalMethods(JniType, Type, String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Added:

[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.NonPublicNestedTypes)]

Activator.CreateInstance() usage:

src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(542,33): warning IL2072: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The return value of method 'Java.Interop.JniValueMarshalerAttribute.MarshalerType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Added:

[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]

Code that Actually Changed

As I enabled more attributes, these snowball into more and more warnings! I eventually had to make GetPeerType() look like:

[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
static Type GetPeerType ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)

The analyzer was not able to understand code like:

static  readonly    KeyValuePair<Type, Type>[]      PeerTypeMappings = new []{
    new KeyValuePair<Type, Type>(typeof (object),           typeof (JavaObject)),
    new KeyValuePair<Type, Type>(typeof (IJavaPeerable),    typeof (JavaObject)),
    new KeyValuePair<Type, Type>(typeof (Exception),        typeof (JavaException)),
};
//...
foreach (var m in PeerTypeMappings) {
    if (m.Key == type)
        return m.Value;
}

Simply removing the PeerTypeMappings array and using if statements solved the warnings. This may be the only real code change if any.

@jonathanpeppers
Copy link
Member Author

Hmm, on CI some warnings I introduced with analyzers are errors:

src\Java.Interop\Java.Interop\ManagedPeer.cs(237,19): Error IL2072: 'targetType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors'

Will look into why.

@jonathanpeppers
Copy link
Member Author

Ok, building with -c Release these become errors???

@jpobst
Copy link
Contributor

jpobst commented Feb 6, 2024

Yes, we use <TreatWarningsAsErrors> in JI for Release builds:

https://github.com/xamarin/java.interop/blob/07c73009571d3b5d36ae79d0d4d69a02062dd6e8/Directory.Build.props#L6

@jonpryor
Copy link
Member

jonpryor commented Feb 7, 2024

I have a random unpublished diff which has an alternate take at "fixing" these Expression.Property() warnings by moving those callsites into Java.Interop.Export.dll. It's incomplete, and I need to turn it into a proper branch -- it's currently based atop #1153 -- but it comes to mind as "maybe cleaner", if also more complicated. Incomplete, untested, needs much more work, but it's an idea

https://gist.github.com/jonpryor/417294863fc9ff17c0521c04a607829e

@jonathanpeppers
Copy link
Member Author

I have a random unpublished diff which has an alternate take at "fixing" these Expression.Property() warnings by moving those callsites into Java.Interop.Export.dll. It's incomplete, and I need to turn it into a proper branch -- it's currently based atop #1153 -- but it comes to mind as "maybe cleaner", if also more complicated. Incomplete, untested, needs much more work, but it's an idea

https://gist.github.com/jonpryor/417294863fc9ff17c0521c04a607829e

We discussed this idea, and the problems were:

  • We don't ship Java.Interop.Export.dll yet
  • If we did, we'd have to mark it trimmable and fix warnings
  • [RequiresUnreferencedCode] would still bubble up to all callers

I have a good idea on what's left here, maybe I can finish it today/tomorrow.

@jonathanpeppers jonathanpeppers marked this pull request as draft February 8, 2024 19:05
@jonathanpeppers jonathanpeppers force-pushed the EnableAotAnalyzer branch 2 times, most recently from 246b40f to 4a456bc Compare February 8, 2024 20:39
Context: dotnet#1157

On the path to enabling `IsAotCompatible`, we can enable some settings
to get started:

    <IsTrimmable>true</IsTrimmable>
    <EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
    <EnableAotAnalyzer>true</EnableAotAnalyzer>

This opts into the analyzers without declaring the assembly is fully
AOT-compatible.

Starting out, I got 33 warnings: this is an attempt to address the
ones that don't require too much thinking. Unfortunately, solving 1
warning likely will create dozens more -- as you have to update all
callers.

This results in 24 warnings remaining. Since `Release` builds have
`$(TreatWarningsAsErrors)`, I will wait to enable the analyzers until
all warnings are addressed.

~~ Example Warnings ~~

`System.Linq.Expression` usage:

    src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(672,58):
    warning IL2026: Using member 'System.Linq.Expressions.Expression.Property(Expression, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.

I decorated this one with:

    [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]

`Type.GetNestedType()` usage:

    src\Java.Interop\Java.Interop\JniRuntime.JniTypeManager.cs(447,28):
    warning IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.NonPublicNestedTypes' in call to 'System.Type.GetNestedType(String, BindingFlags)'. The parameter 'type' of method 'Java.Interop.JniRuntime.JniTypeManager.TryLoadJniMarshalMethods(JniType, Type, String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Added:

    [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.NonPublicNestedTypes)]

`Activator.CreateInstance()` usage:

    src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(542,33): warning IL2072: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The return value of method 'Java.Interop.JniValueMarshalerAttribute.MarshalerType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Added:

    [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]

~~ Code that Actually Changed ~~

As I enabled more attributes, these snowball into more and more
warnings! I eventually had to make `GetPeerType()` look like:

    [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
    static Type GetPeerType ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)

The analyzer was not able to understand code like:

    static  readonly    KeyValuePair<Type, Type>[]      PeerTypeMappings = new []{
        new KeyValuePair<Type, Type>(typeof (object),           typeof (JavaObject)),
        new KeyValuePair<Type, Type>(typeof (IJavaPeerable),    typeof (JavaObject)),
        new KeyValuePair<Type, Type>(typeof (Exception),        typeof (JavaException)),
    };
    //...
    foreach (var m in PeerTypeMappings) {
        if (m.Key == type)
            return m.Value;
    }

Simply removing the `PeerTypeMappings` array and using `if` statements
solved the warnings. This may be the only real code change if any.
@jonathanpeppers jonathanpeppers marked this pull request as ready for review February 8, 2024 20:47
@@ -362,7 +364,7 @@ public void Dispose ()
}
}

public abstract class JavaPrimitiveArray<T> : JavaArray<T> {
public abstract class JavaPrimitiveArray<[DynamicallyAccessedMembers (ConstructorsAndInterfaces)] T> : JavaArray<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent lines from looking like line noise, we should start requiring that all custom attributes take at least two lines: one (or more) for the custom attribute(s), and one for the thing being attributed.

The downside with that idea is the location of < and > for generics, and I'm just gonna decree:

  1. < is on the line of the containing declaration
  2. Contents should be indented an additional tab stop
  3. > is on a new line vertically aligned with the start of the declaration.

And we should start consistently having { begin on a newline for class declarations. (We frequently did this anyway, just not consistently.)

Thus:

public abstract class JavaPrimitiveArray<
        [DynamicallyAccessedMembers (ConstructorsAndInterfaces)] // two tab stops
        T   // two tab stops
> // vertically aligned with start of declaration
    : JavaArray<T> // one tab stop
{
    // … one tab stop
}

If we assume/imagine that JavaArray<T>.ArrayCreator needed custom attributes, I'd want:

internal delegate TArray ArrayCreator<
        [DynamicallyAccessedMembers ()] // 2 tab stops
        TArray
> (
        ref JniObjectReference reference,  // 2 tab stops
        JniObjectReferenceOptions transfer)
    where TArray : JavaArray<T>; // 1 tab stop

The > (, while fugly, provides a visual barrier between the end of the type parameters and the beginning of the parameter list.

This only needs to happen when custom attributes are used on type parameters. If custom attributes aren't required, this is quite compact and readable:

internal TArray CreateArray<TArray> (
        ref JniObjectReference reference, // 2 tab stops
        JniObjectReferenceOptions transfer) // 2 tab stops
    where TArray : JavaArray<T> // 1 tab stop
{
    // … 1 tab stop
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I've found & reformatted these: used VS's regex search to find them all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we found a deficiency in VS's regex search: it doesn't appear to have found or fully fixed the .tt files, meaning if (when) we regenerate the .cs files form the .tt templates, changes are lost.

I think we've fixed all "mismatches" now.

We should probably consider a pre-build step which explicitly regenerates the .cs files from the .tt files, to ensure/require that the templates are the real source of truth. (Though I'm not quite sure how to make this work; the "obvious" route would be to use Target Inputs & Outputs, but if the .cs is manually changed w/o a corresponding .tt change, the Inputs & Outputs won't cause regeneration…)

@jonpryor
Copy link
Member

Context: https://github.com/xamarin/java.interop/issues/1157

We want to enable `$(IsAotCompatible)`=true, so we can identify and
fix trimmer warnings within `Java.Interop.dll`.  (Then later work our
way "up the stack", fixing trimmer warnings within `Mono.Android.dll`
and `Microsoft.Maui.dll` and…)

On the path to enabling `$(IsAotCompatible)`=true, we can enable some
settings to get started:

	<IsTrimmable>true</IsTrimmable>
	<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
	<EnableAotAnalyzer>true</EnableAotAnalyzer>

This opts into the analyzers without declaring that the assembly is
fully AOT-compatible.

Starting out, I got 33 warnings: this is an attempt to address the
ones that don't require too much thinking.  Unfortunately, solving
one warning likely will create dozens more -- as you have to update
all callers.

This results in 24 warnings remaining.  Since `Release` builds have
`$(TreatWarningsAsErrors)`, I will wait to enable the analyzers until
all warnings are addressed.

~~ Example Warnings ~~

**`System.Linq.Expression` usage:**
`JniRuntime.JniValueManager.CreateParameterFromManagedExpression()`

	/* 638 */ partial class JavaPeerableValueMarshaler {
	/* 667 */     public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize)
	/* 668 */     {
	/* 669 */         var r = CreateIntermediaryExpressionFromManagedExpression (context, sourceValue);
	/* 670 */         var h = Expression.Variable (typeof (IntPtr), sourceValue.Name + "_handle");
	/* 671 */         context.LocalVariables.Add (h);
	/* 672 */         context.CreationStatements.Add (Expression.Assign (h, Expression.Property (r, "Handle")));
	/* 674 */         return h;
	/* 675 */     }
	/* 710 */ }

emits an IL2026:

	src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(672,58):
	warning IL2026: Using member 'System.Linq.Expressions.Expression.Property(Expression, String)'
	which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.
	Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.

I updated this with:

	partial class JniValueMarshaler {
	    internal const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code.";

	    [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]
	    public virtual Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize) => …
	}
	partial class JavaPeerableValueMarshaler /* indirectly inherits JniValueMarshaler */ {
	    [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]
	    public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize) => …
	}

**`Type.GetNestedType()` usage:**
`JniRuntime.JniTypeManager.TryLoadJniMarshalMethods()`:

	/*  82 */ partial class JniRuntime.JniTypeManager {
	/* 445 */     bool TryLoadJniMarshalMethods (JniType nativeClass, Type type, string? methods)
	/* 446 */     {
	/* 447 */         var marshalType = type?.GetNestedType ("__<$>_jni_marshal_methods", BindingFlags.NonPublic);

emits an IL2070 warning:

	src\Java.Interop\Java.Interop\JniRuntime.JniTypeManager.cs(447,28):
	warning IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.NonPublicNestedTypes' in call to
	'System.Type.GetNestedType(String, BindingFlags)'.
	The parameter 'type' of method 'Java.Interop.JniRuntime.JniTypeManager.TryLoadJniMarshalMethods(JniType, Type, String)'
	does not have matching annotations.
	The source value must declare at least the same requirements as those declared on the target location it is assigned to.

I updated this with:

	partial class JniRuntime.JniTypeManager {
	    bool TryLoadJniMarshalMethods (
	            JniType nativeClass,
	            [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.NonPublicNestedTypes)]
	            Type type,
	            string? methods) => …

**`Activator.CreateInstance()` usage:**
`JniRuntime.JniValueManager.GetValueMarshaler()`:

	/*  50 */ partial class JniRuntime.JniValueManager {
	/* 530 */     public JniValueMarshaler GetValueMarshaler (Type type)
	/* 531 */     {
	/* 541 */         if (marshalerAttr != null)
	/* 542 */             return (JniValueMarshaler) Activator.CreateInstance (marshalerAttr.MarshalerType)!;
	/* 591 */     }
	/* 612 */ }

emits an IL2072 warning:

	src\Java.Interop\Java.Interop\JniRuntime.JniValueManager.cs(542,33): warning IL2072:
	'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor'
	in call to 'System.Activator.CreateInstance(Type)'.
	The return value of method 'Java.Interop.JniValueMarshalerAttribute.MarshalerType.get' does not have matching annotations.
	The source value must declare at least the same requirements as those declared on the target location it is assigned to.

I updated this with:

	partial class JniRuntime.JniValueManager {
	    public JniValueMarshaler GetValueMarshaler (
	            [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.Interfaces)]
	            Type type)
	        => …
	}
	partial class JniValueMarshalerAttribute {
	    public Type MarshalerType {
	        [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
	        get;
	    }
	}


~~ Code that Actually Changed ~~

As I added more attributes, these snowballed into more and more
warnings!  I eventually had to make
`JniRuntime.JniValueManager.GetPeerType()` look like:

	partial class JniRuntime.JniValueManager {
	    internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

	    [return: DynamicallyAccessedMembers (Constructors)]
	    static Type GetPeerType ([DynamicallyAccessedMembers Constructors)] Type type) => …
	}

The analyzer was not able to understand code like:

	partial class JniRuntime.JniValueManager {
	    static  readonly    KeyValuePair<Type, Type>[]      PeerTypeMappings = new []{
	        new KeyValuePair<Type, Type>(typeof (object),           typeof (JavaObject)),
	        new KeyValuePair<Type, Type>(typeof (IJavaPeerable),    typeof (JavaObject)),
	        new KeyValuePair<Type, Type>(typeof (Exception),        typeof (JavaException)),
	    };
	    static Type GetPeerType (Type type)
	    {
	        foreach (var m in PeerTypeMappings) {
	            if (m.Key == type)
	                return m.Value;
	        }
	    }
	}

Simply removing the `PeerTypeMappings` array and using `if` statements
solved the warnings.  This may be the only real code change if any.

@jonpryor jonpryor merged commit b8f6f88 into dotnet:main Feb 13, 2024
4 checks passed
@@ -31,6 +31,7 @@ partial class CreationOptions {
#if NET
[DynamicDependency (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, "Java.Interop.MarshalMemberBuilder", "Java.Interop.Export")]
[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "DynamicDependency should preserve the constructor.")]
[UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "DynamicDependency should preserve the constructor.")]
[UnconditionalSuppressMessage ("Trimming", "IL2035", Justification = "Java.Interop.Export.dll is not always present.")]
#endif
partial void SetMarshalMemberBuilder (CreationOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this can't use Type.GetType("Java.Interop.MarshalMemberBuilder, Java.Interop.Export").
That is something both trimmer and AOT understand and will handle without warnings and correctly preserve the .ctor in the call to Activator.CreateInstance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of an odd codepath. We have a Java.Interop.Export.dll in this repo, but we don't actually ship it in the Android workload. Some of the projects in here could be used for Java interop on desktop, etc., but there isn't a real product using them.

Let me check if it's worth refactoring this yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a scenario this is not new - there are many places even in the core BCL where something similar happens. In fact there are a couple of places even in CoreLib where we use this to break circular references between assemblies. Meaning it's a pattern which trimming/AOT has to support well.

@vitek-karas
Copy link
Member

/cc @simonrozsival as FYI

@jonathanpeppers jonathanpeppers deleted the EnableAotAnalyzer branch February 13, 2024 14:17
jonpryor pushed a commit to dotnet/android that referenced this pull request Feb 14, 2024
Changes: dotnet/java-interop@dfcbd67...b8f6f88

  * dotnet/java-interop@b8f6f888: [Java.Interop] address some "easy" trimmer warnings (dotnet/java-interop#1184)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
jonathanpeppers added a commit to jonathanpeppers/java.interop that referenced this pull request Feb 15, 2024
…ilder`

Context: dotnet#1184 (comment)

Previously we were using `Assembly.Load()` and `Assembly.GetType()`,
which require us to ignore various trimmer warnings.

If we use `Type.GetType()` with a constant string instead, the trimmer
knows how to handle this properly. (The code is also simpler.)
jonpryor pushed a commit that referenced this pull request Feb 15, 2024
…1193)

Context: #1184 (comment)

Previously we were using `Assembly.Load()` and `Assembly.GetType()`,
which require us to ignore various trimmer warnings.

During code review, @vitek-karas asked:

> Is there a reason
> [`JniRuntime.JniMarshalMemberBuilder.SetMarshalMemberBuilder()`]
> can't use
> `Type.GetType("Java.Interop.MarshalMemberBuilder, Java.Interop.Export")`.
> That is something both trimmer and AOT understand and will handle
> without warnings and correctly preserve the .ctor in the call to
> `Activator.CreateInstance`.

Indeed, there isn't a reason to avoid `Type.GetType()` in
`JniRuntime.JniMarshalMemberBuilder.SetMarshalMemberBuilder()`.

If we use `Type.GetType()` with a constant string instead, the
trimmer knows how to handle this properly.
(The code is also simpler.)
@github-actions github-actions bot locked and limited conversation to collaborators Apr 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants