-
Notifications
You must be signed in to change notification settings - Fork 167
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
Changes from 3 commits
3112d6c
8460deb
b11075b
4c5d3cc
2b8707d
79262c2
5e1a598
df30bc2
1b5db28
236f45e
01eb1e5
ad78b2a
cb88595
01f44dd
b823492
c411051
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,6 +148,33 @@ public object CreateObject(Type objectType) | |
return result; | ||
} | ||
|
||
public void Add<T>(T obj) where T : RealmObject | ||
{ | ||
if (obj == null) | ||
throw new ArgumentNullException(nameof(obj)); | ||
|
||
if (obj.IsManaged) | ||
{ | ||
// the object is already owned by this realm, so do nothing I guess | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
{ | ||
|
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 | ||
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
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) | ||
{ | ||
|
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,8 @@ public class ModuleWeaver | |
|
||
TypeSystem typeSystem; | ||
|
||
MethodReference realmObjectIsManagedGetter; | ||
|
||
// Init logging delegates to make testing easier | ||
public ModuleWeaver() | ||
{ | ||
|
@@ -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"); | ||
|
@@ -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); | ||
|
@@ -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") | ||
|
@@ -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(""); | ||
} | ||
|
||
|
@@ -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>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. The alternatives, I guess, are:
#1 would be optimal and maybe that's what we should do when we can but it's obviously not an option on iOS. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your point about |
||
/// 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] "); | ||
} | ||
|
||
|
@@ -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] "); | ||
} | ||
|
@@ -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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are great, just need another test for |
||
{ | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 On it.