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

Implement Array.Initialize in C# #77336

Merged
merged 18 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions src/coreclr/System.Private.CoreLib/src/System/Array.CoreCLR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ private unsafe bool IsValueOfElementType(object value)
// if this is an array of value classes and that value class has a default constructor
// then this calls this default constructor on every element in the value class array.
// otherwise this is a no-op. Generally this method is called automatically by the compiler
[RequiresUnreferencedCode("The parameterless constructor may be trimmed from the array element type.")]
jkotas marked this conversation as resolved.
Show resolved Hide resolved
public unsafe void Initialize()
Copy link
Member

@jkotas jkotas Oct 23, 2022

Choose a reason for hiding this comment

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

@dotnet/ilc-contrib Should Array.Initialize be marked with RequiresUnreferenceCode, or is the value type default constructor the special-cased by the trimmer and always preserved?

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 it should:

var b = new ElementType[1];
b.Initialize();

struct ElementType
{
    public ElementType()
    {
        Console.WriteLine(".ctor called");
    }
}

dotnet run prints out ".ctor called".
But trimmed the app doesn't print out anything.

Related question - what should default do in this case, for example:

var c = new ElementType[1];
c[0] = default; // Should this call the default .ctor?

Running it seems like it will NOT call the .ctor - but what does it do?

Copy link
Member

Choose a reason for hiding this comment

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

Same goes for AOT - published as Native AOT the app above also doesn't print out anything.

Copy link
Contributor

@teo-tsirpanis teo-tsirpanis Oct 24, 2022

Choose a reason for hiding this comment

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

Related question - what should default do in this case, for example:

According to the specification, default ignores the parameterless constructor and generates a zeroed instance.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, adding RequiresUnreferencedCode on Array.Initialize has a ripple effect. It introduces warnings in situations where array type is passed into DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All).

I guess it will need more careful thought.

Copy link
Member

Choose a reason for hiding this comment

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

Opened #77426

reflectronic marked this conversation as resolved.
Show resolved Hide resolved
{
MethodTable* pArrayMT = RuntimeHelpers.GetMethodTable(this);
Expand All @@ -404,32 +405,27 @@ public unsafe void Initialize()
arrayType.GenericCache = cache;
}

delegate*<ref byte, void> constructorFtn = cache.ConstructorEntrypoint;
ref byte arrayRef = ref MemoryMarshal.GetArrayDataReference(this);
nuint elementSize = pArrayMT->ComponentSize;

for (int i = 0; i < Length; i++)
{
cache.InvokeConstructor(ref arrayRef);
constructorFtn(ref arrayRef);
arrayRef = ref Unsafe.Add(ref arrayRef, elementSize);
}
}

private sealed unsafe partial class ArrayInitializeCache
{
private readonly delegate*<ref byte, void> _constructorEntrypoint;
internal readonly delegate*<ref byte, void> ConstructorEntrypoint;

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "Array_GetElementConstructorEntrypoint")]
private static partial delegate*<ref byte, void> GetElementConstructorEntrypoint(QCallTypeHandle arrayType);

public ArrayInitializeCache(RuntimeType arrayType)
reflectronic marked this conversation as resolved.
Show resolved Hide resolved
{
_constructorEntrypoint = GetElementConstructorEntrypoint(new QCallTypeHandle(ref arrayType));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InvokeConstructor(ref byte thisAddress)
{
_constructorEntrypoint(ref thisAddress);
ConstructorEntrypoint = GetElementConstructorEntrypoint(new QCallTypeHandle(ref arrayType));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,25 @@ private static void ValidateElementType(Type elementType)
throw new NotSupportedException(SR.NotSupported_OpenType);
}

public void Initialize()
[RequiresUnreferencedCode("The parameterless constructor may be trimmed from the array element type.")]
jkotas marked this conversation as resolved.
Show resolved Hide resolved
public unsafe void Initialize()
{
// This api is a nop unless the array element type is a value type with an explicit nullary constructor.
// Such a type could not be expressed in C# up until recently.
// TODO: Implement this
return;
EETypePtr pElementEEType = ElementEEType;
if (!pElementEEType.IsValueType)
return;

var constructorEntryPoint = (delegate*<ref byte, void>)RuntimeAugments.TypeLoaderCallbacks.TryGetDefaultConstructorForType(new RuntimeTypeHandle(pElementEEType));
if (constructorEntryPoint == null)
return;

ref byte arrayRef = ref MemoryMarshal.GetArrayDataReference(this);
nuint elementSize = ElementSize;

for (int i = 0; i < Length; i++)
{
constructorEntryPoint(ref arrayRef);
arrayRef = ref Unsafe.Add(ref arrayRef, elementSize);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,13 @@ private static void TestTypeCommonInvariants(this Type type)
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy;
foreach (MemberInfo mem in type.GetMember("*", MemberTypes.All, bf))
{
// Workaround: Do not try to test Array.Initialize, since one of its locals is a function pointer.
// Delete this workaround when https://github.com/dotnet/runtime/issues/69273 is addressed.
if (mem.DeclaringType == mem.DeclaringType.Assembly.GetType("System.Array") && mem.Name == "Initialize")
{
continue;
}

string s = mem.ToString();
Assert.Equal(type, mem.ReflectedType);
Type declaringType = mem.DeclaringType;
Expand Down
1 change: 0 additions & 1 deletion src/libraries/System.Runtime/tests/System/ArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4447,7 +4447,6 @@ public static void MaxSizes()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] // Array.Initialize
public static void Array_Initialize()
{
var array = new StructWithDefaultConstructor[10, 10];
Expand Down
1 change: 1 addition & 0 deletions src/mono/System.Private.CoreLib/src/System/Array.Mono.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ internal void InternalSetValue(object? value, nint index)
SetValueImpl(value, (int)index);
}

[RequiresUnreferencedCode("The parameterless constructor may be trimmed from the array element type.")]
jkotas marked this conversation as resolved.
Show resolved Hide resolved
public void Initialize()
{
object arr = this;
Expand Down