diff --git a/src/ILCompiler.WebAssembly/src/CodeGen/ILToWebAssemblyImporter.cs b/src/ILCompiler.WebAssembly/src/CodeGen/ILToWebAssemblyImporter.cs index 48d6974320c..59daf5f9e30 100644 --- a/src/ILCompiler.WebAssembly/src/CodeGen/ILToWebAssemblyImporter.cs +++ b/src/ILCompiler.WebAssembly/src/CodeGen/ILToWebAssemblyImporter.cs @@ -1072,7 +1072,7 @@ private void ImportStoreVar(int index, bool argument) TypeDesc varType; StackEntry toStore = _stack.Pop(); LLVMValueRef varAddress = LoadVarAddress(index, argument ? LocalVarKind.Argument : LocalVarKind.Local, out varType); - CastingStore(varAddress, toStore, varType, $"Variable{index}_"); + CastingStore(varAddress, toStore, varType, false, $"Variable{index}_"); } private void ImportStoreHelper(LLVMValueRef toStore, LLVMTypeRef valueType, LLVMValueRef basePtr, uint offset, string name = null, LLVMBuilderRef builder = default(LLVMBuilderRef)) @@ -1104,10 +1104,75 @@ private LLVMValueRef CastToPointerToTypeDesc(LLVMValueRef source, TypeDesc type, return CastIfNecessary(source, LLVMTypeRef.CreatePointer(GetLLVMTypeForTypeDesc(type), 0), (name ?? "") + type.ToString()); } - private void CastingStore(LLVMValueRef address, StackEntry value, TypeDesc targetType, string targetName = null) + private void CastingStore(LLVMValueRef address, StackEntry value, TypeDesc targetType, bool withBarrier, string targetName = null) { - var typedStoreLocation = CastToPointerToTypeDesc(address, targetType, targetName); - _builder.BuildStore(value.ValueAsType(targetType, _builder), typedStoreLocation); + if (withBarrier && targetType.IsGCPointer) + { + CallRuntime(_method.Context, "InternalCalls", "RhpAssignRef", new StackEntry[] + { + new ExpressionEntry(StackValueKind.Int32, "address", address), value + }); + } + else + { + var typedStoreLocation = CastToPointerToTypeDesc(address, targetType, targetName); + var llvmValue = value.ValueAsType(targetType, _builder); + if (withBarrier && IsStruct(targetType)) + { + StoreStruct(address, llvmValue, targetType, typedStoreLocation); + } + else + { + _builder.BuildStore(llvmValue, typedStoreLocation); + } + } + } + + private static bool IsStruct(TypeDesc typeDesc) + { + return typeDesc.IsValueType && !typeDesc.IsPrimitive && !typeDesc.IsEnum; + } + + private void StoreStruct(LLVMValueRef address, LLVMValueRef llvmValue, TypeDesc targetType, LLVMValueRef typedStoreLocation, bool childStruct = false) + { + foreach (FieldDesc f in targetType.GetFields()) + { + if (f.IsStatic) continue; + if (IsStruct(f.FieldType) && llvmValue.TypeOf.IsPackedStruct) + { + LLVMValueRef targetAddress = _builder.BuildGEP(address, new[] { BuildConstInt32(f.Offset.AsInt) }); + uint index = LLVMSharpInterop.ElementAtOffset(_compilation.TargetData, llvmValue.TypeOf, (ulong)f.Offset.AsInt); + LLVMValueRef fieldValue = _builder.BuildExtractValue(llvmValue, index); + //recurse into struct + StoreStruct(targetAddress, fieldValue, f.FieldType, CastToPointerToTypeDesc(targetAddress, f.FieldType), true); + } + else if (f.FieldType.IsGCPointer) + { + LLVMValueRef targetAddress = _builder.BuildGEP(address, new[] {BuildConstInt32(f.Offset.AsInt)}); + LLVMValueRef fieldValue; + if (llvmValue.TypeOf.IsPackedStruct) + { + uint index = LLVMSharpInterop.ElementAtOffset(_compilation.TargetData, llvmValue.TypeOf, (ulong) f.Offset.AsInt); + fieldValue = _builder.BuildExtractValue(llvmValue, index); + Debug.Assert(fieldValue.TypeOf.Kind == LLVMTypeKind.LLVMPointerTypeKind, "expected an LLVM pointer type"); + } + else + { + // single field IL structs are not LLVM structs + fieldValue = llvmValue; + } + CallRuntime(_method.Context, "InternalCalls", "RhpAssignRef", + new StackEntry[] + { + new ExpressionEntry(StackValueKind.Int32, "targetAddress", targetAddress), + new ExpressionEntry(StackValueKind.ObjRef, "sourceAddress", fieldValue) + }); + } + } + if (!childStruct) + { + _builder.BuildStore(llvmValue, typedStoreLocation); // just copy all the fields again for simplicity, if all the fields where set using RhpAssignRef then a possible optimisation would be to skip this line + } } private LLVMValueRef CastIfNecessary(LLVMValueRef source, LLVMTypeRef valueType, string name = null, bool unsigned = false) @@ -3578,7 +3643,6 @@ private void ImportLoadIndirect(TypeDesc type) { var pointer = _stack.Pop(); Debug.Assert(pointer is ExpressionEntry || pointer is ConstantEntry); - var expressionPointer = pointer as ExpressionEntry; if (type == null) { type = GetWellKnownType(WellKnownType.Object); @@ -3600,19 +3664,36 @@ private void ImportStoreIndirect(TypeDesc type) StackEntry destinationPointer = _stack.Pop(); LLVMValueRef typedValue; LLVMValueRef typedPointer; + bool requireWriteBarrier; if (type != null) { - typedValue = value.ValueAsType(type, _builder); typedPointer = destinationPointer.ValueAsType(type.MakePointerType(), _builder); + typedValue = value.ValueAsType(type, _builder); + if (IsStruct(type)) + { + StoreStruct(typedPointer, typedValue, type, typedPointer); + return; + } + requireWriteBarrier = type.IsGCPointer; } else { typedPointer = destinationPointer.ValueAsType(LLVMTypeRef.CreatePointer(LLVMTypeRef.Int32, 0), _builder); typedValue = value.ValueAsInt32(_builder, false); + requireWriteBarrier = (value is ExpressionEntry) && !((ExpressionEntry)value).RawLLVMValue.IsNull && value.Type.IsGCPointer; + } + if (requireWriteBarrier) + { + CallRuntime(_method.Context, "InternalCalls", "RhpAssignRef", new StackEntry[] + { + new ExpressionEntry(StackValueKind.Int32, "typedPointer", typedPointer), value + }); + } + else + { + _builder.BuildStore(typedValue, typedPointer); } - - _builder.BuildStore(typedValue, typedPointer); } private void ImportBinaryOperation(ILOpcode opcode) @@ -4720,7 +4801,7 @@ private void ImportStoreField(int token, bool isStatic) StackEntry valueEntry = _stack.Pop(); LLVMValueRef fieldAddress = GetFieldAddress(runtimeDeterminedField, field, isStatic); - CastingStore(fieldAddress, valueEntry, field.FieldType); + CastingStore(fieldAddress, valueEntry, field.FieldType, true); } // Loads symbol address. Address is represented as a i32* @@ -4936,7 +5017,7 @@ private void ImportStoreElement(TypeDesc elementType) StackEntry arrayReference = _stack.Pop(); var nullSafeElementType = elementType ?? GetWellKnownType(WellKnownType.Object); LLVMValueRef elementAddress = GetElementAddress(index.ValueAsInt32(_builder, true), arrayReference.ValueAsType(LLVMTypeRef.CreatePointer(LLVMTypeRef.Int8, 0), _builder), nullSafeElementType); - CastingStore(elementAddress, value, nullSafeElementType); + CastingStore(elementAddress, value, nullSafeElementType, true); } private void ImportLoadLength() diff --git a/src/ILCompiler.WebAssembly/src/CodeGen/LLVMSharpInterop.cs b/src/ILCompiler.WebAssembly/src/CodeGen/LLVMSharpInterop.cs new file mode 100644 index 00000000000..e4a2fb9c1b0 --- /dev/null +++ b/src/ILCompiler.WebAssembly/src/CodeGen/LLVMSharpInterop.cs @@ -0,0 +1,15 @@ +using LLVMSharp.Interop; + +namespace Internal.IL +{ + /// + /// Workaround while waiting for https://github.com/microsoft/LLVMSharp/pull/141 + /// + internal class LLVMSharpInterop + { + internal static unsafe uint ElementAtOffset(LLVMTargetDataRef targetDataRef, LLVMTypeRef structTypeRef, ulong offset) + { + return LLVM.ElementAtOffset(targetDataRef, structTypeRef, offset); + } + } +} diff --git a/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs b/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs index 0fcb2982101..ae2075f7e8e 100644 --- a/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs +++ b/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs @@ -17,6 +17,7 @@ public sealed class WebAssemblyCodegenCompilation : Compilation { internal WebAssemblyCodegenConfigProvider Options { get; } internal LLVMModuleRef Module { get; } + internal LLVMTargetDataRef TargetData { get; } public new WebAssemblyCodegenNodeFactory NodeFactory { get; } internal LLVMDIBuilderRef DIBuilder { get; } internal Dictionary DebugMetadataMap { get; } @@ -32,7 +33,7 @@ internal WebAssemblyCodegenCompilation( { NodeFactory = nodeFactory; LLVMModuleRef m = LLVMModuleRef.CreateWithName("netscripten"); - m.Target = "wasm32-unknown-unknown-wasm"; + m.Target = "wasm32-unknown-emscripten"; // https://llvm.org/docs/LangRef.html#langref-datalayout // e litte endian, mangled names // m:e ELF mangling @@ -41,8 +42,8 @@ internal WebAssemblyCodegenCompilation( // n:32:64 native widths // S128 natural alignment of stack m.DataLayout = "e-m:e-p:32:32-i64:64-n32:64-S128"; - Module = m; - + Module = m; + TargetData = m.CreateExecutionEngine().TargetData; Options = options; DIBuilder = Module.CreateDIBuilder(); DebugMetadataMap = new Dictionary(); diff --git a/src/ILCompiler.WebAssembly/src/ILCompiler.WebAssembly.csproj b/src/ILCompiler.WebAssembly/src/ILCompiler.WebAssembly.csproj index 63257e819bd..401bf46abb2 100644 --- a/src/ILCompiler.WebAssembly/src/ILCompiler.WebAssembly.csproj +++ b/src/ILCompiler.WebAssembly/src/ILCompiler.WebAssembly.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Native/gc/gc.cpp b/src/Native/gc/gc.cpp index 3aae8dd4f3f..35eabd18244 100644 --- a/src/Native/gc/gc.cpp +++ b/src/Native/gc/gc.cpp @@ -17835,7 +17835,7 @@ uint8_t* gc_heap::find_object (uint8_t* interior) heap_segment* seg = find_segment (interior, FALSE); if (seg #ifdef FEATURE_CONSERVATIVE_GC - && (GCConfig::GetConservativeGC() || interior <= heap_segment_allocated(seg)) + && (!GCConfig::GetConservativeGC() || interior <= heap_segment_allocated(seg)) #endif ) { diff --git a/tests/src/Simple/HelloWasm/Program.cs b/tests/src/Simple/HelloWasm/Program.cs index 5c428ce1e60..9c2bc5f96cf 100644 --- a/tests/src/Simple/HelloWasm/Program.cs +++ b/tests/src/Simple/HelloWasm/Program.cs @@ -21,6 +21,11 @@ internal static class Program internal static bool Success; private static unsafe int Main(string[] args) { + var x = new StructWithObjRefs + { + C1 = null, + C2 = null, + }; Success = true; PrintLine("Starting " + 1); @@ -393,15 +398,235 @@ private static void TestGC() PrintString("GC Collection Count " + i.ToString() + " "); PrintLine(GC.CollectionCount(i).ToString()); } - if(!TestObjectRefInUncoveredShadowStackSlot()) + + if (!TestObjectRefInUncoveredShadowStackSlot()) { FailTest("struct Child1 alive unexpectedly"); - } - EndTest(true); + + if (!TestRhpAssignRefWithClassInStructGC()) + { + FailTest(); + return; + } + + EndTest(TestGeneration2Rooting()); } + private static Parent aParent; + private static ParentOfStructWithObjRefs aParentOfStructWithObjRefs; private static WeakReference childRef; + + private static unsafe bool TestRhpAssignRefWithClassInStructGC() + { + bool result = true; + + var parentRef = CreateParentWithStruct(); + result &= BumpToGen(parentRef, 1); + result &= BumpToGen(parentRef, 2); + + StoreChildInC1(); + GC.Collect(1); + PrintLine("GC finished"); + + if (!childRef.IsAlive) + { + PrintLine("Child died unexpectedly"); + result = false; + } + + KillParentWithStruct(); + GC.Collect(); + if (childRef.IsAlive) + { + PrintLine("Child alive unexpectedly"); + result = false; + } + if (parentRef.IsAlive) + { + PrintLine("Parent of struct Child1 alive unexpectedly"); + result = false; + } + return result; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool BumpToGen(WeakReference reference, int expectedGeneration) + { + GC.Collect(); + var target = reference.Target; + if (target == null) + { + PrintLine("WeakReference died unexpectedly"); + return false; + } + if (GC.GetGeneration(target) is { } actualGeneration && actualGeneration != expectedGeneration) + { + PrintLine("WeakReference is in gen " + actualGeneration + " instead of " + expectedGeneration); + return false; + } + return true; + } + + private static bool TestGeneration2Rooting() + { + var parent = CreateParent(); + GC.Collect(); // parent moves to gen1 + GC.Collect(); // parent moves to gen2 + if (!CheckParentGeneration()) return false; + + // store our children in the gen2 object + var child1 = StoreProperty(); + var child2 = StoreField(); + + KillParent(); // even though we kill the parent, it should survive as we do not collect gen2 + GC.Collect(1); + + // the parent should have kept the children alive + bool parentAlive = parent.IsAlive; + bool child1Alive = child1.IsAlive; + bool child2Alive = child2.IsAlive; + if (!parentAlive) + { + PrintLine("Parent died unexpectedly"); + return false; + } + + if (!child1Alive) + { + PrintLine("Child1 died unexpectedly"); + return false; + } + + if (!child2Alive) + { + PrintLine("Child2 died unexpectedly"); + return false; + } + + // Test struct assignment keeps fields alive + var parentRef = CreateParentWithStruct(); + GC.Collect(); // move parent to gen1 + GC.Collect(); // move parent to gen2 + StoreChildInC1(); // store ephemeral object in gen 2 object via struct assignment + KillParentWithStruct(); + GC.Collect(1); + + if (childRef.IsAlive) + { + PrintLine("Child1 gen:" + GC.GetGeneration(childRef.Target)); + } + + if (!childRef.IsAlive) + { + PrintLine("struct Child1 died unexpectedly"); + return false; + } + if (!parentRef.IsAlive) + { + PrintLine("parent of struct Child1 died unexpectedly"); + return false; + } + + return true; + } + + class ParentOfStructWithObjRefs + { + internal StructWithObjRefs StructWithObjRefs; + } + + struct StructWithObjRefs + { + internal Child C1; + internal Child C2; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static WeakReference CreateParent() + { + var parent = new Parent(); + aParent = parent; + return new WeakReference(parent); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static WeakReference CreateStruct() + { + var parent = new Parent(); + aParent = parent; + return new WeakReference(parent); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void KillParent() + { + aParent = null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool CheckParentGeneration() + { + int actualGen = GC.GetGeneration(aParent); + if (actualGen != 2) + { + PrintLine("Parent Object is not in expected generation 2 but in " + actualGen); + return false; + } + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static WeakReference StoreProperty() + { + var child = new Child(); + aParent.Child1 = child; + return new WeakReference(child); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static WeakReference StoreField() + { + var child = new Child(); + aParent.Child2 = child; + return new WeakReference(child); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + unsafe static WeakReference CreateParentWithStruct() + { + var parent = new ParentOfStructWithObjRefs(); + aParentOfStructWithObjRefs = parent; + return new WeakReference(parent); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void KillParentWithStruct() + { + aParentOfStructWithObjRefs = null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static unsafe void StoreChildInC1() + { + var child = new Child(); + aParentOfStructWithObjRefs.StructWithObjRefs = new StructWithObjRefs + { + C1 = child, + }; + childRef = new WeakReference(child); + } + + public class Parent + { + public Child Child1 { get; set; } + public Child Child2; + } + + public class Child + { + } + // This test is to catch where slots are allocated on the shadow stack uncovering object references that were there previously. // If this happens in the call to GC.Collect, which at the time of writing allocate 12 bytes in the call, 3 slots, then any objects that were in those // 3 slots will not be collected as they will now be (back) in the range of bottom of stack -> top of stack. @@ -421,10 +646,6 @@ static unsafe void CreateObjectRefsInShadowStack() childRef = new WeakReference(child); } - public class Child - { - } - private static unsafe void TestBoxUnboxDifferentSizes() { StartTest("Box/Unbox different sizes");