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

Standalone objects support #221

Merged
merged 16 commits into from
Nov 26, 2015
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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: 13 additions & 1 deletion RealmNet.Shared/Attributes.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* Copyright 2015 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/

using System;
using System.Reflection;

namespace RealmNet
{
Expand Down Expand Up @@ -30,4 +31,15 @@ public MapToAttribute(string mapping)
public class WovenAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Property)]
public class WovenPropertyAttribute : Attribute
{
internal string BackingFieldName { get; private set; }

public WovenPropertyAttribute(string backingFieldName)
{
this.BackingFieldName = backingFieldName;
}
}
}
27 changes: 27 additions & 0 deletions RealmNet.Shared/Realm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,33 @@ public object CreateObject(Type objectType)
return result;
}

public void Add<T>(T obj) where T : RealmObject
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs to check if the object is already managed and throw an exception.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 On it.

{
if (obj == null)
throw new ArgumentNullException(nameof(obj));

if (obj.IsManaged)
{
// the object is already owned by this realm, so do nothing I guess
Copy link
Contributor

Choose a reason for hiding this comment

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

No, especially after some Slack discussion, the desired behaviour here is to throw an exception and I think it would be useful to have this be a unique Realm exception type. That way, as I noted further down, you can also add a unit test for it. thanks

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I saw Cocoa treat re-adding as a no-op, but I'll change it to throw as you suggested.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's done, although I think I might have gone a bit over the top with the new exception names 😁

if (obj.Realm._sharedRealmHandle == this._sharedRealmHandle)
return;

throw new RealmObjectOwnedByAnotherRealmException("Cannot add an object to a realm when it's already owned by another realm");
}


if (!IsInTransaction)
throw new RealmOutsideTransactionException("Cannot add a Realm object outside write transactions");

var tableHandle = _tableHandles[typeof(T)];

var rowPtr = NativeTable.add_empty_row(tableHandle);
var rowHandle = CreateRowHandle(rowPtr);

obj._Manage(this, rowHandle);
obj._CopyDataFromBackingFieldsToRow();
}

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private static RowHandle CreateRowHandle(IntPtr rowPtr)
{
Expand Down
1 change: 1 addition & 0 deletions RealmNet.Shared/RealmNet.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Attributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)exceptions\RealmDecryptionFailedException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)exceptions\RealmObjectOwnedByAnotherRealmException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)exceptions\RealmOutsideTransactionException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)exceptions\RealmFileExistsException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)exceptions\RealmInvalidDatabaseException.cs" />
Expand Down
28 changes: 27 additions & 1 deletion RealmNet.Shared/RealmObject.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* Copyright 2015 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;

namespace RealmNet
Expand All @@ -12,14 +15,37 @@ public class RealmObject
private Realm _realm;
private RowHandle _rowHandle;

internal Realm Realm => _realm;
internal RowHandle RowHandle => _rowHandle;

internal protected bool IsManaged => _realm != null;

internal void _Manage(Realm realm, RowHandle rowHandle)
{
_realm = realm;
_rowHandle = rowHandle;
}

internal void _CopyDataFromBackingFieldsToRow()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is great for a first version. In the longer term, we want to have this copy be as fast as possible in case people are frequently creating objects standalone and assigning via Add. Something to think about is could we use Fody to generate an optimal copy method that wouldn't need to do all this reflection, creation of wovenProperties and iteration?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. In fact, @fealebenpae wanted to do this initally but we agreed that this solution is simpler and will do for the MVP.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great minds not only think alike but can arrive at same compromise (aka Engineering Decision ) :-D

Copy link
Member Author

Choose a reason for hiding this comment

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

@kristiandupont and I had a conversation about this yesterday and I came up with the idea that we can weave a new method that does the copying so as to not incur the reflection costs. However we agreed on reflection for the time being until the performance considerations become important enough to necessitate more code weaving.

{
Debug.Assert(this.IsManaged);

var thisType = this.GetType();
var wovenProperties = from prop in thisType.GetProperties()
let backingField = prop.GetCustomAttributes(false)
.OfType<WovenPropertyAttribute>()
.Select(a => a.BackingFieldName)
.SingleOrDefault()
where backingField != null
select new { Info = prop, Field = thisType.GetField(backingField, BindingFlags.Instance | BindingFlags.NonPublic) };

foreach (var prop in wovenProperties)
{
var value = prop.Field.GetValue(this);
prop.Info.SetValue(this, value, null);
}
}

protected T GetValue<T>(string propertyName)
{
if (_realm == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* Copyright 2015 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/

namespace RealmNet
{
public class RealmObjectOwnedByAnotherRealmException : RealmException
{
public RealmObjectOwnedByAnotherRealmException(string detailMessage) : base(detailMessage)
{

}
}
}
100 changes: 86 additions & 14 deletions RealmNetWeaver/ModuleWeaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class ModuleWeaver

TypeSystem typeSystem;

MethodReference realmObjectIsManagedGetter;

// Init logging delegates to make testing easier
public ModuleWeaver()
{
Expand Down Expand Up @@ -52,6 +54,7 @@ public void Execute()
var assemblyToReference = ModuleDefinition.AssemblyResolver.Resolve("RealmNet");

var realmObjectType = assemblyToReference.MainModule.GetTypes().First(x => x.Name == "RealmObject");
realmObjectIsManagedGetter = ModuleDefinition.ImportReference(realmObjectType.Properties.Single(x => x.Name == "IsManaged").GetMethod);
var genericGetValueReference = MethodNamed(realmObjectType, "GetValue");
var genericSetValueReference = MethodNamed(realmObjectType, "SetValue");
//var getListValueReference = MethodNamed(realmObjectType, "GetListValue");
Expand All @@ -60,6 +63,11 @@ public void Execute()
var wovenAttributeClass = assemblyToReference.MainModule.GetTypes().First(x => x.Name == "WovenAttribute");
var wovenAttributeConstructor = ModuleDefinition.Import(wovenAttributeClass.GetConstructors().First());

var wovenPropertyAttributeClass = assemblyToReference.MainModule.GetTypes().First(x => x.Name == "WovenPropertyAttribute");
var wovenPropertyAttributeConstructor = ModuleDefinition.ImportReference(wovenPropertyAttributeClass.GetConstructors().First());
var corlib = ModuleDefinition.AssemblyResolver.Resolve((AssemblyNameReference)ModuleDefinition.TypeSystem.CoreLibrary);
var stringType = ModuleDefinition.ImportReference(corlib.MainModule.GetType("System.String"));

foreach (var type in GetMatchingTypes())
{
Debug.WriteLine("Weaving " + type.Name);
Expand All @@ -70,6 +78,8 @@ public void Execute()
if (mapToAttribute != null)
columnName = ((string)mapToAttribute.ConstructorArguments[0].Value);

var backingField = GetBackingField(prop);

Debug.Write(" -- Property: " + prop.Name + " (column: " + columnName + ".. ");
//TODO check if has either setter or getter and adjust accordingly - https://github.com/realm/realm-dotnet/issues/101
if (prop.PropertyType.Namespace == "RealmNet" && prop.PropertyType.Name == "RealmList`1")
Expand All @@ -89,6 +99,10 @@ public void Execute()
throw new NotSupportedException($"class '{type.Name}' field '{columnName}' is a {prop.PropertyType.Name} which is not yet supported");
}

var wovenPropertyAttribute = new CustomAttribute(wovenPropertyAttributeConstructor);
wovenPropertyAttribute.ConstructorArguments.Add(new CustomAttributeArgument(stringType, backingField.Name));
prop.CustomAttributes.Add(wovenPropertyAttribute);

Debug.WriteLine("");
}

Expand All @@ -99,18 +113,41 @@ public void Execute()
return;
}


void AddGetter(PropertyDefinition prop, string columnName, MethodReference getValueReference)
{
var specializedGetValue = new GenericInstanceMethod(getValueReference);
specializedGetValue.GenericArguments.Add(prop.PropertyType);

prop.GetMethod.Body.Instructions.Clear();
var getProcessor = prop.GetMethod.Body.GetILProcessor();
getProcessor.Emit(OpCodes.Ldarg_0);
getProcessor.Emit(OpCodes.Ldstr, columnName);
getProcessor.Emit(OpCodes.Call, specializedGetValue);
getProcessor.Emit(OpCodes.Ret);
/// A synthesized property getter looks like this:
/// 0: ldarg.0
/// 1: ldfld <backingField>
/// 2: ret
/// We want to change it so it looks like this:
/// 0: ldarg.0
/// 1: call RealmNet.RealmObject.get_IsManaged
/// 2: brfalse.s 7
/// 3: ldarg.0
/// 4: ldstr <columnName>
/// 5: call RealmNet.RealmObject.GetValue<T>
/// 6: ret
/// 7: ldarg.0
/// 8: ldfld <backingField>
/// 9: ret
/// This is roughly equivalent to:
/// if (!base.IsManaged) return this.<backingField>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, I"m not so sure about this as a philosophy because we're supposed to be moving towards optimising the data access and this is adding another couple of conditionals on every single property read and write.
I will think about it more but this is certainly good enough for the MVP and very cleanly written!

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. The alternatives, I guess, are:

  1. we update the accessor with runtime code gen
  2. we make the accessors indirect, either by introducing an interface or with delegates.

#1 would be optimal and maybe that's what we should do when we can but it's obviously not an option on iOS.
As for #2, I guess we should test it but my gut feeling is that an indirect call is more or less as expensive as one easily predicted branch. But I don't have any data to back that up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Remember we've already had discussions and planned issues about collapsing the calls further by weaving in code to directly call the wrapper code, rather than going through RealmObject.SetValue so there's a lot more to be done in this field. That's another reason I'm willing to let it go for now - we have much bigger improvements to make.

Copy link
Member Author

Choose a reason for hiding this comment

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

While I see the value in splitting GetValue/SetValue into specialized methods based on types so as to avoid the if/else cascade, I'm not sure whether further manual inlining is needed. The JIT should be smart enough to inline the check in get_IsManaged and the other thing is a branch.

I guess I can check how Xamarin's AOT will handle this, but from my experience with LLVM, indirectly calling a delegate is going to be slower than the current approach. Then again, the entirety of Cocoa is indirect calls, and that's certainly fast enough.

However, I think that we won't be able to drive down the number of operations in accessors to an absolute minimum, because at some point at least the setters would have to contain some logic that implements INotifyPropertyChanged and INotifyDataErrorInfo.

Copy link
Contributor

Choose a reason for hiding this comment

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

Your point about INotifyPropertyChanged is one reason we've not proceeded any further with the collapsing of the stack - we need to get a lot more real-world use including people wanting notifications and more reactive apps.

/// else return base.GetValue<T>(<columnName>);

var start = prop.GetMethod.Body.Instructions.First();
var il = prop.GetMethod.Body.GetILProcessor();

il.InsertBefore(start, il.Create(OpCodes.Ldarg_0));
il.InsertBefore(start, il.Create(OpCodes.Call, realmObjectIsManagedGetter));
il.InsertBefore(start, il.Create(OpCodes.Brfalse_S, start));
il.InsertBefore(start, il.Create(OpCodes.Ldarg_0));
il.InsertBefore(start, il.Create(OpCodes.Ldstr, columnName));
il.InsertBefore(start, il.Create(OpCodes.Call, specializedGetValue));
il.InsertBefore(start, il.Create(OpCodes.Ret));

Debug.Write("[get] ");
}

Expand All @@ -120,13 +157,39 @@ void AddSetter(PropertyDefinition prop, string columnName, MethodReference setVa
var specializedSetValue = new GenericInstanceMethod(setValueReference);
specializedSetValue.GenericArguments.Add(prop.PropertyType);

prop.SetMethod.Body.Instructions.Clear();
var setProcessor = prop.SetMethod.Body.GetILProcessor();
setProcessor.Emit(OpCodes.Ldarg_0);
setProcessor.Emit(OpCodes.Ldstr, columnName);
setProcessor.Emit(OpCodes.Ldarg_1);
setProcessor.Emit(OpCodes.Call, specializedSetValue);
setProcessor.Emit(OpCodes.Ret);
/// A synthesized property setter looks like this:
/// 0: ldarg.0
/// 1: ldarg.1
/// 2: stfld <backingField>
/// 3: ret
/// We want to change it so it looks like this:
/// 0: ldarg.0
/// 1: call RealmNet.RealmObject.get_IsManaged
/// 2: brfalse.s 8
/// 3: ldarg.0
/// 4: ldstr <columnName>
/// 5: ldarg.1
/// 6: call RealmNet.RealmObject.SetValue<T>
/// 7: ret
/// 8: ldarg.0
/// 9: ldarg.1
/// 10: stfld <backingField>
/// 11: ret
/// This is roughly equivalent to:
/// if (!base.IsManaged) this.<backingField> = value;
/// else base.SetValue<T>(<columnName>, value);

var start = prop.SetMethod.Body.Instructions.First();
var il = prop.SetMethod.Body.GetILProcessor();

il.InsertBefore(start, il.Create(OpCodes.Ldarg_0));
il.InsertBefore(start, il.Create(OpCodes.Call, realmObjectIsManagedGetter));
il.InsertBefore(start, il.Create(OpCodes.Brfalse_S, start));
il.InsertBefore(start, il.Create(OpCodes.Ldarg_0));
il.InsertBefore(start, il.Create(OpCodes.Ldstr, columnName));
il.InsertBefore(start, il.Create(OpCodes.Ldarg_1));
il.InsertBefore(start, il.Create(OpCodes.Call, specializedSetValue));
il.InsertBefore(start, il.Create(OpCodes.Ret));

Debug.Write("[set] ");
}
Expand All @@ -151,4 +214,13 @@ void AddHelloWorld( TypeDefinition newType)
processor.Emit(OpCodes.Ret);
newType.Methods.Add(method);
}

private static FieldReference GetBackingField(PropertyDefinition property)
{
return property.GetMethod.Body.Instructions
.Where(o => o.OpCode == OpCodes.Ldfld)
.Select(o => o.Operand)
.OfType<FieldReference>()
.SingleOrDefault();
}
}
3 changes: 3 additions & 0 deletions RealmNetWeaver/RealmNetWeaver.Fody.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,7 @@
<PostBuildEvent Condition="'Windows_NT'=='$(os)'">xcopy/Y $(TargetPath) $(SolutionDir)Tools</PostBuildEvent>
<PostBuildEvent Condition="'Unix'=='$(os)'">cp $(TargetPath) $(SolutionDir)Tools</PostBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>xcopy /Y "$(TargetPath)" "$(SolutionDir)Tools"</PostBuildEvent>
Copy link
Contributor

Choose a reason for hiding this comment

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

Has this been tested on a Mac? I don't think we have xcopy! Look at the use of the <Copy> element

Copy link
Member Author

Choose a reason for hiding this comment

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

That will most definitely fail on a Mac, but I'm hesitant to inflict MSBuild on myself just yet :D

I just quoted the path parameters as the build failed for me because I cloned the repo in a path with spaces in it.

</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Compile Include="$(MSBuildThisFileDirectory)IntegrationTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PerformanceTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Person.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StandAloneObjectTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TestHelpers.cs" />
</ItemGroup>
</Project>
29 changes: 29 additions & 0 deletions Tests/IntegrationTests.Shared/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,35 @@ public void CreateObjectOutsideTransactionShouldFail()
Assert.Throws<RealmOutsideTransactionException>(() => _realm.CreateObject<Person>());
}

[Test]
public void AddOutsideTransactionShouldFail()
{
var obj = new Person();
Assert.Throws<RealmOutsideTransactionException>(() => _realm.Add(obj));
}

[Test]
public void AddNullObjectShouldFail()
{
Assert.Throws<ArgumentNullException>(() => _realm.Add(null as Person));
}

[Test]
public void AddAnObjectFromAnotherRealmShouldFail()
{
Person p;
using (var transaction = _realm.BeginWrite())
{
p = _realm.CreateObject<Person>();
transaction.Commit();
}

using (var otherRealm = Realm.GetInstance(Path.GetTempFileName()))
{
Assert.Throws<RealmObjectOwnedByAnotherRealmException>(() => otherRealm.Add(p));
}
}

[Test]
public void SetPropertyOutsideTransactionShouldFail()
Copy link
Contributor

Choose a reason for hiding this comment

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

These are great, just need another test for AddTwiceShouldFail

{
Expand Down
Loading