Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## vNext (TBD)

### Enhancements
* Removed backing fields of generated classes' properties which should provide minor improvements to memory used by Realm Objects (Issue [#2647](https://github.com/realm/realm-dotnet/issues/2994))
* Added two extension methods on `IDictionary` to get an `IQueryable` collection wrapping the dictionary's values:
* `dictionary.AsRealmQueryable()` allows you to get a `IQueryable<T>` from `IDictionary<string, T>` that can be then treated as a regular queryable collection and filtered/ordered with LINQ or `Filter(string)`.
* `dictionary.Filter(query, arguments)` will filter the list and return a filtered collection of dictionary's values. It is roughly equivalent to `dictionary.AsRealmQueryable().Filter(query, arguments)`.
Expand All @@ -10,6 +11,7 @@
* Flexible sync will now wait for the server to have sent all pending history after a bootstrap before marking a subscription as Complete. (Core upgrade)

### Fixed
* Fixed issue where Realm parameters' initialization would get run twice, resulting in unexpected behavior.
* Prevented `IEmbeddedObject`s and `IAsymmetricObject`s from being used as `RealmValue`s when added to a realm, and displaying more meaningful error messages.
* Fix a use-after-free if the last external reference to an encrypted Realm was closed between when a client reset error was received and when the download of the new Realm began. (Core upgrade)
* Fixed an assertion failure during client reset with recovery when recovering a list operation on an embedded object that has a link column in the path prefix to the list from the top level object. (Core upgrade)
Expand Down
66 changes: 60 additions & 6 deletions Realm/Realm.Weaver/RealmWeaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,50 @@ Analytics payload
return WeaveModuleResult.Success(weaveResults);
}

private static void RemoveBackingFields(TypeDefinition type, HashSet<MetadataToken> backingFields)
{
for (var i = type.Fields.Count - 1; i >= 0; i--)
{
var field = type.Fields[i];
if (backingFields.Contains(field.MetadataToken))
{
type.Fields.RemoveAt(i);
}
}

// Iterates through all constructors' instructions from the end to start.
foreach (var constructor in type.GetConstructors())
{
// Index of the most recent "Stfld <backing_field>" instruction
var backingFieldInstructionsEnd = -1;
for (var i = constructor.Body.Instructions.Count - 1; i >= 0; i--)
{
var instruction = constructor.Body.Instructions[i];

// If it comes across "Stfld <backing_field>"
// it considers this the end index of backing field initializaion instructions.
if (instruction.OpCode == OpCodes.Stfld && instruction.Operand is FieldReference field)
{
if (backingFields.Contains(field.MetadataToken))
{
backingFieldInstructionsEnd = i;
}
}

// If it comes across "Ldarg 0",
// it considers this the start index of backing field initializaion instructions
// and removes all backing field instructions from end to start.
else if (instruction.OpCode == OpCodes.Ldarg_0)
{
for (var j = backingFieldInstructionsEnd; j >= i; j--)
{
constructor.Body.Instructions.RemoveAt(j);
}
}
}
}
}

private WeaveTypeResult WeaveGeneratedType(TypeDefinition type)
{
_logger.Debug("Weaving generated " + type.Name);
Expand All @@ -256,14 +300,21 @@ private WeaveTypeResult WeaveGeneratedType(TypeDefinition type)
var interfaceType = _moduleDefinition.GetType($"{@namespace}.Generated", interfaceName);

var persistedProperties = new List<WeavePropertyResult>();
var backingFields = new HashSet<MetadataToken>();

// We need to weave all (and only) the properties in the accessor interface
foreach (var interfaceProperty in interfaceType.Properties)
{
var prop = type.Properties.First(p => p.Name == interfaceProperty.Name);

try
{
// Stash and remove the backing field before weaving as it depends on get method.
var backingField = prop.GetBackingField();
if (backingField != null)
{
backingFields.Add(backingField.MetadataToken);
}

var weaveResult = WeaveGeneratedClassProperty(type, prop, interfaceType);
persistedProperties.Add(weaveResult);
}
Expand All @@ -278,6 +329,8 @@ private WeaveTypeResult WeaveGeneratedType(TypeDefinition type)
}
}

RemoveBackingFields(type, backingFields);

return WeaveTypeResult.Success(type.Name, persistedProperties, isGenerated: true);
}

Expand Down Expand Up @@ -310,15 +363,16 @@ private static void ReplaceGeneratedClassGetter(PropertyDefinition prop, TypeDef
//// This is equivalent to:
//// get => Accessor.Property;

var start = prop.GetMethod.Body.Instructions.First();
var il = prop.GetMethod.Body.GetILProcessor();
prop.GetMethod.Body.Instructions.Clear();
Copy link
Contributor Author

@gagik gagik Nov 11, 2022

Choose a reason for hiding this comment

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

I believe the previous implementation of replacing the getter was causing some unintended behavior, it was actually just adding instructions to the getter rather than removing the old instructions---the reason why this was not noticeable was because the old instruction became unreachable because of the ret/return statement. My ILSpy was actually giving me an error about this prior. One issue this was causing was the fact that it kept instructions that have a reference to the backing field, making it error-prone when removing them. This change makes it identical to the functionality in the ReplaceGeneratedClassSetter.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good eye! Need to take a look at this

prop.GetMethod.Body.Variables.Clear();

var propertyGetterOnAccessorReference = new MethodReference($"get_{prop.Name}", prop.PropertyType, interfaceType) { HasThis = true };

il.InsertBefore(start, il.Create(OpCodes.Ldarg_0));
il.InsertBefore(start, il.Create(OpCodes.Call, accessorGetter));
il.InsertBefore(start, il.Create(OpCodes.Callvirt, propertyGetterOnAccessorReference));
il.InsertBefore(start, il.Create(OpCodes.Ret));
il.Append(il.Create(OpCodes.Ldarg_0));
il.Append(il.Create(OpCodes.Call, accessorGetter));
il.Append(il.Create(OpCodes.Callvirt, propertyGetterOnAccessorReference));
il.Append(il.Create(OpCodes.Ret));
}

private void ReplaceGeneratedClassSetter(PropertyDefinition prop, TypeDefinition interfaceType, MethodReference accessorGetter)
Expand Down
32 changes: 32 additions & 0 deletions Tests/Realm.Tests/Database/InitializedFieldObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

namespace Realms.Tests.Database
{
public static class Generator
{
private static int _currentId;

public static int GetId() => _currentId++;
}

public partial class InitializedFieldObject : Realms.IRealmObject
{
public int Id { get; set; } = Generator.GetId();
}
}
21 changes: 21 additions & 0 deletions Tests/Realm.Tests/Database/RealmObjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,27 @@ public void RealmObject_Equals_WhenOtherIsNull_ReturnsFalse()
Assert.That(obj.Equals(null), Is.False);
}

[Test]
public void RealmObject_InitializedFields_GetCorrectValues()
{
// This test ensures we only run the initialization instructions of Realm Object fields once.
// i.e. where we have a class that has an ID field that gets incremented every time a new class
// instance is created by incrementing an external variable and assigning it to the Id field.
//
// class FieldObject { Id: Generator.Id() } where Generator.Id = () => _currentId++;
//
// It could be that because of i.e. copying over initialization commands to the accessor
// but not removing from the original constructor, the initialization of the field
// would get repeated, thus leading to the _currentId being incremented twice.
var obj0 = new InitializedFieldObject();
var obj1 = new InitializedFieldObject();
var obj2 = new InitializedFieldObject();

Assert.That(obj0.Id, Is.EqualTo(0));
Assert.That(obj1.Id, Is.EqualTo(1));
Assert.That(obj2.Id, Is.EqualTo(2));
}

[Test]
public void RealmObject_EqualsInvalidObject_WhenValid_ReturnsFalse()
{
Expand Down
Loading