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

[generator] Use 'var' when possible in generated code to reduce nullability annotations. #621

Merged
merged 1 commit into from
Apr 7, 2020

Conversation

jpobst
Copy link
Contributor

@jpobst jpobst commented Apr 6, 2020

Our generated code may predate var, or at least predate var becoming widely accepted. As such, we generate code like this:

java.code.MyClass __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
string key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

In an NRT world, each one of these declarations will have to be audited and optionally annotated with the nullable operator ?. Or..... we can just mark them as var and not worry about it.

var __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
var key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

Pulling this change into a separate commit in order to lower the noise in the eventual NRT PR.

@jpobst jpobst force-pushed the var-all-the-things branch 4 times, most recently from 112e771 to 56ba268 Compare April 7, 2020 15:24
@jpobst jpobst marked this pull request as ready for review April 7, 2020 15:50
@jpobst jpobst requested a review from jonpryor April 7, 2020 15:50
@jonpryor
Copy link
Member

jonpryor commented Apr 7, 2020

I feel like, as-is, this only moves the warning, it doesn't "fix" the warning.

Consider ISpannableInvoker.n_GetSpanFlags_Ljava_lang_Object_(): https://github.com/xamarin/java.interop/blob/80b4667f7085027d66592a0c36490d9488c7ce60/tests/generator-Tests/Tests-Core/expected.ji/Android.Text.ISpannable.cs#L74-L80

Updating line 76 to use var avoids a possible CS8600, but that still leaves line 78, which would instead produce a CS8602 warning:

int __ret = (int) __this.GetSpanFlags (tag);
// warning CS8602: Dereference of a possibly null reference.

Replacing CS8600 with CS8602 doesn't feel like a "win".

We could fix the CS8602 by using __this?.GetSpanFlags(tag), but that replaces the CS8602 warning with a CS0266 error:

error CS0266: Cannot implicitly convert type 'int?' to 'int'.

Which is to say, this feels like a losing game. It's simple-yet-massive change which won't really fix what it attempts to fix.

I fear that the "real" fix will be to just stick #nullable disable and #nullable enable around each method body:

		static int n_GetSpanFlags_Ljava_lang_Object_ (IntPtr jnienv, IntPtr native__this, IntPtr native_tag)
		{
#nullable disable
			Android.Text.ISpannable __this = global::Java.Lang.Object.GetObject<Android.Text.ISpannable> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
			Java.Lang.Object tag = global::Java.Lang.Object.GetObject<Java.Lang.Object> (native_tag, JniHandleOwnership.DoNotTransfer);
			int __ret = (int) __this.GetSpanFlags (tag);
			return __ret;
#nullable enable
		}

which would be an even larger change, diff wise, except we should only emit this if e.g. --lang-features=nullable-reference-types is used so that older compilers can still compile our code. (If that's even a requirement? This might be debatable.)

@jpobst
Copy link
Contributor Author

jpobst commented Apr 7, 2020

Correct, this PR does not attempt to introduce nullability annotations, or fix the right-hand side of the equals sign, it simply fixes the left-hand side.

The NRT PR will need to generate this when NRT is turned off:

var __ret = (int) __this.GetSpanFlags (tag);

and this when NRT is turned on:

var __ret = (int?) __this.GetSpanFlags (tag);

This PR makes our eventual NRT logic simpler by not needing to conditionally annotate the left-hand side.

When NRT is turned off (<nullable>disable</nullable>) the compiler will not check the annotations and the first example will not generate a warning.

These tests are the current tests and will continue testing the !NRT case, so they will not change in the NRT PR. New tests will be added for the NRT case. Thus we can move the "noise" of updating the non-NRT tests to this PR.

@jonpryor
Copy link
Member

jonpryor commented Apr 7, 2020

and this when NRT is turned on:

var __ret = (int?) __this.GetSpanFlags (tag);

I assume this should use __this?.GetSpanFlags(tag), as __this would be of type ISpannable??.

However, that won't work, because the return type of the method will still be int -- and can't change! -- resulting in the same CS0266 error.

Additionally, in this context this doesn't seem quite right? The only time that Object.GetObject<T>() should return null is if the handle parameter is IntPtr.Zero. (Or if someone passes in a JNI Weak Reference to an instance which has been collected, but that feels like a very rare corner case.) Most of the time this method will never return null.

Given how rare the return value will actually be null, should Object.GetObject<T>() return T? in the first place?

(Unfortunately we can't use NotNullIfNotNullAttribute as that doesn't "work" meaningfully with IntPtr, only things for which null can be implicitly converted.)

After discussing on #onedotnet, it feels like Object.GetObject<T>() should return T? even if it's highly unlikely, as @spouliot believes that the "iOS equivalent" in ObjCRuntime.Runtime.GetNSObject(IntPtr) will also be updated to return NSObject?.

@jpobst
Copy link
Contributor Author

jpobst commented Apr 7, 2020

I think in this specific case, the NRT PR uses the null-forgiving operator (!) to suppress the warning, as we are reasonably sure GetObject<T> will not return null due to the way our bindings are structured:

var __this = global::Java.Lang.Object.GetObject<Android.Text.ISpannable> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;

As stated in the NRT non-goals, we are not attempting to rewrite our generated code to be more provably non-null at this time. We are working under the assumption that our current generated code works and thus we are currently preventing an NRE here via other means.

However, for a user calling GetObject<T> directly, I have less faith that they have done enough due diligence to ensure they are always passing in a valid handle.

@jonpryor
Copy link
Member

jonpryor commented Apr 7, 2020

Context: https://github.com/xamarin/xamarin-android/pull/4227

Our generated code *may* predate `var`, or at least predate `var`
becoming widely accepted.  As such, we generate code like this:

	java.code.MyClass __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
	string key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

Migrate this code to `var` as it simplifies `generator` and reasoning
about behavior when "cross-compiling" code between C#7 and C#8-with-
[Nullable Reference Type][0] toolchains:

	var __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
	var key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

[0]: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references

@jonpryor jonpryor merged commit 93df5a2 into master Apr 7, 2020
@jonpryor jonpryor deleted the var-all-the-things branch April 7, 2020 19:16
@jpobst jpobst added this to the d16-7 milestone Apr 10, 2020
jonpryor pushed a commit that referenced this pull request Apr 22, 2020
Context: dotnet/android#4227

Our generated code *may* predate `var`, or at least predate `var`
becoming widely accepted.  As such, we generate code like this:

	java.code.MyClass __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
	string key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

Migrate this code to `var` as it simplifies `generator` and reasoning
about behavior when "cross-compiling" code between C#7 and C#8-with-
[Nullable Reference Type][0] toolchains:

	var __this = global::Java.Lang.Object.GetObject<java.code.MyClass> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
	var key = JNIEnv.GetString (native_key, JniHandleOwnership.DoNotTransfer);

[0]: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 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.

2 participants