X Tutup
Skip to content

Fix array span constructors in C##105950

Merged
Repiteo merged 1 commit intogodotengine:masterfrom
Joy-less:Fix-array-span-constructors
Sep 17, 2025
Merged

Fix array span constructors in C##105950
Repiteo merged 1 commit intogodotengine:masterfrom
Joy-less:Fix-array-span-constructors

Conversation

@Joy-less
Copy link
Contributor

Fixes the Span/ReadOnlySpan constructors for Godot.Collections.Array in C#.

The following code:

Godot.Collections.Array Array = new(new Span<StringName>());
GD.Print(Array);

Throws:

E 0:00:03:219   Array.cs:92 @ Godot.Collections.Array..ctor(System.Span`1[Godot.StringName]): System.ArgumentNullException: Value cannot be null. (Parameter 'array')
  <C# Error>    System.ArgumentNullException
  <C# Source>   /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs:92 @ Godot.Collections.Array..ctor(System.Span`1[Godot.StringName])
  <Stack Trace> Array.cs:92 @ Godot.Collections.Array..ctor(System.Span`1[Godot.StringName])
                Mirror.cs:5 @ void Mirror._Ready()
                Node.cs:2546 @ bool Godot.Node.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
                Node3D.cs:1101 @ bool Godot.Node3D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
                Mirror_ScriptMethods.generated.cs:48 @ bool Mirror.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
                CSharpInstanceBridge.cs:24 @ Godot.NativeInterop.godot_bool Godot.Bridge.CSharpInstanceBridge.Call(nint, Godot.NativeInterop.godot_string_name*, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant_call_error*, Godot.NativeInterop.godot_variant*)

The reason is that the constructors erroneously assume that Span/ReadOnlySpan are nullable, and that null checks are necessary. In fact, they are not only not nullable, making these checks redundant, but there is an implicit cast from arrays to spans, meaning casting null to a Span/ReadOnlySpan actually creates an empty span. So span == null is the same as span.IsEmpty.

I also took the opportunity to normalise the use of ReadOnlySpan rather than Span to allow read-only spans, add scoped to make it clear that stackalloc is allowed, and renamed the parameter for clarity.

@Joy-less Joy-less requested a review from a team as a code owner April 30, 2025 14:55
@raulsntos
Copy link
Member

There is a difference between a Span created from a null array and an empty array: SharpLab

int[]? array1 = null;
int[]  array2 = [];

Span<int> span1 = array1;
Span<int> span2 = array2;

Console.WriteLine(span1 == null); // Prints 'True'
Console.WriteLine(span2 == null); // Prints 'False'

So the null check is not redundant and was added on purpose to catch cases where the user passes a null array.

Also, changing parameter types breaks compatibility.

@Joy-less
Copy link
Contributor Author

Joy-less commented Apr 30, 2025

So the null check is not redundant and was added on purpose to catch cases where the user passes a null array.

I didn't know this.

@Joy-less Joy-less closed this Apr 30, 2025
@Joy-less
Copy link
Contributor Author

Joy-less commented Apr 30, 2025

Nevermind @raulsntos I closed this too early. I think you have gotten confused here.

using System;

Span<int> span = [];

Console.WriteLine(span == null); // Prints 'True'

SharpLab

In C#, converting a null array to a span always creates an empty but usable span. See the code:
image

@Joy-less Joy-less reopened this Apr 30, 2025
@Chaosus Chaosus added this to the 4.x milestone May 2, 2025
@paulloz
Copy link
Member

paulloz commented May 12, 2025

Hi. You're absolutely right that spans created from null arrays are functionally identical to spans created from empty arrays.

I imagine what raulsntos was going for is that they can still be differentiated, and thus, we might want to purposefully design the API to not accept spans created from null arrays as a valid argument here.

For me, it's a matter of looking at how the BCL handles this kind of cases, and stick to it as long as it makes sense. I personally can't think of a method anywhere that'd treat both cases differently. Even this atrocity is valid C# code:

byte[] array = ((Span<byte>)null).ToArray();

And Span<T>.ToArray() is probably the closest thing we'll find to the methods we're discussing.

So, long story short, I think we can merge this PR.

I'm not entirely sure how breaking this is in terms of compatibility, since any previous call to Array(Span<T> array) should still be valid for Array(ReadOnlySpan<T> span).

@raulsntos
Copy link
Member

I'm not entirely sure how breaking this is in terms of compatibility, since any previous call to Array(Span<T> array) should still be valid for Array(ReadOnlySpan<T> span).

Changing the parameter type from Span<T> to ReadOnlySpan<T> maintains source compatibility since Span<T> is implicitly convertible to ReadOnlySpan<T>. But it breaks binary compatibility.

Changing the name of the parameter from array to span breaks source compatibility because the callsite could be using named parameters (e.g.: new Array<int>(array: [1, 2, 3])) but we normally don't care too much about this.

We have to keep the Array(Span<T> array) constructors to avoid breaking binary compatibility. Breaking source compatibility is less disruptive.

@Joy-less
Copy link
Contributor Author

We have to keep the Array(Span<T> array) constructors to avoid breaking binary compatibility. Breaking source compatibility is less disruptive.

@raulsntos I added back the old constructors as deprecated and hidden, to be removed in the next major version of Godot. Let me know if you want something else.

@raulsntos
Copy link
Member

I don't think there's a reason to deprecate them, it will produce warnings when constructing from T[] or Span<T> which just adds noise.

@Joy-less
Copy link
Contributor Author

I don't think there's a reason to deprecate them, it will produce warnings when constructing from T[] or Span<T> which just adds noise.

Ideally this would be a good case for OverloadResolutionPriorityAttribute but GodotSharp is targeting .NET 8.0. Removing the deprecations.

@Joy-less Joy-less force-pushed the Fix-array-span-constructors branch from 780334b to 14783cd Compare May 13, 2025 12:28
Copy link
Member

@raulsntos raulsntos left a comment

Choose a reason for hiding this comment

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

The .NET team discussed this PR and we think it's fine now, but we want to make sure it gets enough testing.

@raulsntos raulsntos modified the milestones: 4.x, 4.6 Jun 18, 2025
@Repiteo Repiteo merged commit 8b4b93a into godotengine:master Sep 17, 2025
20 checks passed
@Repiteo
Copy link
Contributor

Repiteo commented Sep 17, 2025

Thanks!

@Joy-less Joy-less deleted the Fix-array-span-constructors branch September 17, 2025 17:03
ProfessorOctopus pushed a commit to ProfessorOctopus/godot that referenced this pull request Oct 5, 2025
…nstructors

Fix array span constructors in C#
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

X Tutup