Skip to content

Terminal: Port to [NotNullWhen(true)] attribute#68

Merged
XuuXiaolan merged 1 commit intoTeamXiaolan:mainfrom
ratijas:work/ratijas/try-get-terminal
Feb 5, 2026
Merged

Terminal: Port to [NotNullWhen(true)] attribute#68
XuuXiaolan merged 1 commit intoTeamXiaolan:mainfrom
ratijas:work/ratijas/try-get-terminal

Conversation

@ratijas
Copy link
Contributor

@ratijas ratijas commented Feb 3, 2026

In case of public methods, a brief investigation proved that ?
nullable mark is not part of method signature when applied to reference
types; it just becomes a yet another parameter attribute in IL,
namely [NullableAttribute], so it is a backward-compatible change that
doesn't break ABI. Regarding source compatibility, API users might get
a new warning CS8600 if they are using concrete type instead of a var
keyword:

Converting null literal or possible null value to non-nullable type.

@ratijas
Copy link
Contributor Author

ratijas commented Feb 3, 2026

Actually there are more methods with an out argument:

  • TryGetKeywordInfoText
  • TryGetKeyword

and I'm not sure how to rename them!

@darmuh
Copy link
Contributor

darmuh commented Feb 3, 2026

naming do be hard, that [NotNullWhen] attribute is pretty neat 👀

@ratijas
Copy link
Contributor Author

ratijas commented Feb 3, 2026

If changing argument type from out T arg to out T? arg is NOT a signature change, then there would be no need to look for new names.

Can't say for sure yet.

@darmuh
Copy link
Contributor

darmuh commented Feb 3, 2026

If changing argument type from out T arg to out T? arg is NOT a signature change, then there would be no need to look for new names.

Can't say for sure yet.

Honestly i'm not sure anyone is using any of this (outside dawnlib itself internally) so I wouldn't worry too much about breaking changes. These were only just recently added over the last week or two and I still had adding documentation on my to-do list lol

@ratijas ratijas force-pushed the work/ratijas/try-get-terminal branch from 5e4dee9 to a18af6e Compare February 3, 2026 22:34
@ratijas ratijas changed the title TerminalExtensions: Port DawnTryResolveKeyword to [NotNullWhen] Terminal: Port to [NotNullWhen(true)] attribute Feb 3, 2026
@ratijas
Copy link
Contributor Author

ratijas commented Feb 3, 2026

I played around with attributes and signatures on a scratch project, and in Discord chat we came to a conclusion that adding ? to reference typed parameters is not an ABI breaking change.

The branch was force-pushed, the description was updated accordingly.

For the record, here is the file I compiled:

Extentions.cs
using System.Diagnostics.CodeAnalysis;

namespace DotNetAbiOutVarNullable;

public class ClassToExtend
{
}

public class ClassArg
{
    public string Keyword = "";
}

public static class ClassExtensions
{
    public static bool TryClassNullable(this ClassToExtend self, string input, [NotNullWhen(true)] out ClassArg? arg)
    {
        arg = null;
        return false;
    }

    public static bool TryClassNonNull(this ClassToExtend self, string input, out ClassArg arg)
    {
        arg = null!;
        return false;
    }
}

public struct StructArg
{
    public int Price;
}

public static class StructExtensions
{
    public static bool TryStructNullable(string input, [NotNullWhen(true)] out StructArg? word)
    {
        word = null;
        return false;
    }

    public static bool TryStructNonNull(string input, out StructArg word)
    {
        word = new StructArg()
        {
            Price = 42,
        };
        return false;
    }
}

And this is how it looks like in ILSpy with "Select language to decompile to (Alt+L)" drop-down set to IL:

ClassExtensions
.class public auto ansi abstract sealed beforefieldinit DotNetAbiOutVarNullable.ClassExtensions
	extends [netstandard]System.Object
{
	.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
		01 00 01 00 00
	)
	.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
		01 00 00 00 00
	)
	.custom instance void [netstandard]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
		01 00 00 00
	)
	// Methods
	.method public hidebysig static 
		bool TryClassNullable (
			class DotNetAbiOutVarNullable.ClassToExtend self,
			string input,
			[out] class DotNetAbiOutVarNullable.ClassArg& arg
		) cil managed 
	{
		.custom instance void [netstandard]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
			01 00 00 00
		)
		.param [3]
			.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
				01 00 02 00 00
			)
			.custom instance void [netstandard]System.Diagnostics.CodeAnalysis.NotNullWhenAttribute::.ctor(bool) = (
				01 00 01 00 00
			)
		// Method begins at RVA 0x20b0
		// Header size: 12
		// Code size: 10 (0xa)
		.maxstack 2
		.locals init (
			[0] bool
		)

		IL_0000: nop
		IL_0001: ldarg.2
		IL_0002: ldnull
		IL_0003: stind.ref
		IL_0004: ldc.i4.0
		IL_0005: stloc.0
		IL_0006: br.s IL_0008

		IL_0008: ldloc.0
		IL_0009: ret
	} // end of method ClassExtensions::TryClassNullable

	.method public hidebysig static 
		bool TryClassNonNull (
			class DotNetAbiOutVarNullable.ClassToExtend self,
			string input,
			[out] class DotNetAbiOutVarNullable.ClassArg& arg
		) cil managed 
	{
		.custom instance void [netstandard]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
			01 00 00 00
		)
		// Method begins at RVA 0x20c8
		// Header size: 12
		// Code size: 10 (0xa)
		.maxstack 2
		.locals init (
			[0] bool
		)

		IL_0000: nop
		IL_0001: ldarg.2
		IL_0002: ldnull
		IL_0003: stind.ref
		IL_0004: ldc.i4.0
		IL_0005: stloc.0
		IL_0006: br.s IL_0008

		IL_0008: ldloc.0
		IL_0009: ret
	} // end of method ClassExtensions::TryClassNonNull

} // end of class DotNetAbiOutVarNullable.ClassExtensions
StructExtensions
.class public auto ansi abstract sealed beforefieldinit DotNetAbiOutVarNullable.StructExtensions
	extends [netstandard]System.Object
{
	.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
		01 00 01 00 00
	)
	.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
		01 00 00 00 00
	)
	// Methods
	.method public hidebysig static 
		bool TryStructNullable (
			string input,
			[out] valuetype [netstandard]System.Nullable`1<valuetype DotNetAbiOutVarNullable.StructArg>& word
		) cil managed 
	{
		.param [2]
			.custom instance void [netstandard]System.Diagnostics.CodeAnalysis.NotNullWhenAttribute::.ctor(bool) = (
				01 00 01 00 00
			)
		// Method begins at RVA 0x20e0
		// Header size: 12
		// Code size: 14 (0xe)
		.maxstack 1
		.locals init (
			[0] bool
		)

		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: initobj valuetype [netstandard]System.Nullable`1<valuetype DotNetAbiOutVarNullable.StructArg>
		IL_0008: ldc.i4.0
		IL_0009: stloc.0
		IL_000a: br.s IL_000c

		IL_000c: ldloc.0
		IL_000d: ret
	} // end of method StructExtensions::TryStructNullable

	.method public hidebysig static 
		bool TryStructNonNull (
			string input,
			[out] valuetype DotNetAbiOutVarNullable.StructArg& word
		) cil managed 
	{
		// Method begins at RVA 0x20fc
		// Header size: 12
		// Code size: 31 (0x1f)
		.maxstack 3
		.locals init (
			[0] valuetype DotNetAbiOutVarNullable.StructArg,
			[1] bool
		)

		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: ldloca.s 0
		IL_0004: initobj DotNetAbiOutVarNullable.StructArg
		IL_000a: ldloca.s 0
		IL_000c: ldc.i4.s 42
		IL_000e: stfld int32 DotNetAbiOutVarNullable.StructArg::Price
		IL_0013: ldloc.0
		IL_0014: stobj DotNetAbiOutVarNullable.StructArg
		IL_0019: ldc.i4.0
		IL_001a: stloc.1
		IL_001b: br.s IL_001d

		IL_001d: ldloc.1
		IL_001e: ret
	} // end of method StructExtensions::TryStructNonNull

} // end of class DotNetAbiOutVarNullable.StructExtensions

As you can see, both methods in ClassExtensions have equivalent signatures, and one of them has an additional NullableAttribute inside.

In case of public methods, a brief investigation proved that `?`
nullable mark is not part of method signature when applied to reference
types; it just becomes a yet another parameter attribute in IL,
namely [NullableAttribute], so it is a backward-compatible change that
doesn't break ABI. Regarding source compatibility, API users might get
a new warning CS8600 if they are using concrete type instead of a `var`
keyword:

> Converting null literal or possible null value to non-nullable type.
@ratijas ratijas force-pushed the work/ratijas/try-get-terminal branch from a18af6e to 068d40c Compare February 4, 2026 22:57
@XuuXiaolan XuuXiaolan merged commit f9ac10b into TeamXiaolan:main Feb 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants