private void RaiseAndSetIfChanged(MethodReference raiseAndSetIfChangedMethod, TypeDefinition targetType, FieldDefinition field, ILProcessor il, string propertyName) { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldflda, field.BindDefinition(targetType)); // pop -> ref this.$PropertyName il.Emit(OpCodes.Ldarg_1); // value il.Emit(OpCodes.Ldstr, propertyName); // "PropertyName" il.Emit(OpCodes.Call, raiseAndSetIfChangedMethod); // pop * 4 -> this.RaiseAndSetIfChanged(this.$PropertyName, value, "PropertyName") il.Emit(OpCodes.Pop); // We don't care about the result of RaiseAndSetIfChanged, so pop it off the stack (stack is now empty) il.Emit(OpCodes.Ret); // Return out of the function }
/// <summary> /// Executes this property weaver. /// </summary> public void Execute() { if (ModuleDefinition is null) { LogInfo?.Invoke("The module definition has not been defined."); return; } var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); if (reactiveUI is null) { LogInfo?.Invoke("Could not find assembly: ReactiveUI (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); return; } LogInfo?.Invoke($"{reactiveUI.Name} {reactiveUI.Version}"); var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); if (helpers is null) { LogInfo?.Invoke("Could not find assembly: ReactiveUI.Fody.Helpers (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); return; } LogInfo?.Invoke($"{helpers.Name} {helpers.Version}"); var exceptionName = typeof(Exception).FullName; if (exceptionName is null) { LogInfo?.Invoke("Could not find the full name for System.Exception"); return; } var reactiveObject = ModuleDefinition.FindType("ReactiveUI", "ReactiveObject", reactiveUI); // The types we will scan are subclasses of ReactiveObject var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType is not null && reactiveObject.IsAssignableFrom(x.BaseType)); var observableAsPropertyHelper = ModuleDefinition.FindType("ReactiveUI", "ObservableAsPropertyHelper`1", reactiveUI, "T"); var observableAsPropertyAttribute = ModuleDefinition.FindType("ReactiveUI.Fody.Helpers", "ObservableAsPropertyAttribute", helpers); var observableAsPropertyHelperGetValue = ModuleDefinition.ImportReference(observableAsPropertyHelper.Resolve().Properties.Single(x => x.Name == "Value").GetMethod); var exceptionDefinition = FindType?.Invoke(exceptionName); var constructorDefinition = exceptionDefinition.GetConstructors().Single(x => x.Parameters.Count == 1); var exceptionConstructor = ModuleDefinition.ImportReference(constructorDefinition); foreach (var targetType in targetTypes) { foreach (var property in targetType.Properties.Where(x => x.IsDefined(observableAsPropertyAttribute) || (x.GetMethod?.IsDefined(observableAsPropertyAttribute) ?? false)).ToArray()) { var genericObservableAsPropertyHelper = observableAsPropertyHelper.MakeGenericInstanceType(property.PropertyType); var genericObservableAsPropertyHelperGetValue = observableAsPropertyHelperGetValue.Bind(genericObservableAsPropertyHelper); ModuleDefinition.ImportReference(genericObservableAsPropertyHelperGetValue); // Declare a field to store the property value var field = new FieldDefinition("$" + property.Name, FieldAttributes.Private, genericObservableAsPropertyHelper); targetType.Fields.Add(field); // It's an auto-property, so remove the generated field if (property.SetMethod is not null && property.SetMethod.HasBody) { // Remove old field (the generated backing field for the auto property) var oldField = (FieldReference)property.GetMethod.Body.Instructions.Single(x => x.Operand is FieldReference).Operand; var oldFieldDefinition = oldField.Resolve(); targetType.Fields.Remove(oldFieldDefinition); // Re-implement setter to throw an exception property.SetMethod.Body = new MethodBody(property.SetMethod); property.SetMethod.Body.Emit(il => { il.Emit(OpCodes.Ldstr, "Never call the setter of an ObservableAsPropertyHelper property."); il.Emit(OpCodes.Newobj, exceptionConstructor); il.Emit(OpCodes.Throw); il.Emit(OpCodes.Ret); }); } property.GetMethod.Body = new MethodBody(property.GetMethod); property.GetMethod.Body.Emit(il => { var isValid = il.Create(OpCodes.Nop); il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldfld, field.BindDefinition(targetType)); // pop -> this.$PropertyName il.Emit(OpCodes.Dup); // Put an extra copy of this.$PropertyName onto the stack il.Emit(OpCodes.Brtrue, isValid); // If the helper is null, return the default value for the property il.Emit(OpCodes.Pop); // Drop this.$PropertyName EmitDefaultValue(property.GetMethod.Body, il, property.PropertyType); // Put the default value onto the stack il.Emit(OpCodes.Ret); // Return that default value il.Append(isValid); // Add a marker for if the helper is not null il.Emit(OpCodes.Callvirt, genericObservableAsPropertyHelperGetValue); // pop -> this.$PropertyName.Value il.Emit(OpCodes.Ret); // Return the value that is on the stack }); } } }
public void Execute() { var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); if (reactiveUI == null) { LogInfo("Could not find assembly: ReactiveUI (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); return; } LogInfo($"{reactiveUI.Name} {reactiveUI.Version}"); var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); if (helpers == null) { LogInfo("Could not find assembly: ReactiveUI.Fody.Helpers (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); return; } LogInfo($"{helpers.Name} {helpers.Version}"); var reactiveObject = new TypeReference("ReactiveUI", "IReactiveObject", ModuleDefinition, reactiveUI); var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType != null && reactiveObject.IsAssignableFrom(x.BaseType)).ToArray(); var reactiveObjectExtensions = new TypeReference("ReactiveUI", "IReactiveObjectExtensions", ModuleDefinition, reactiveUI).Resolve(); if (reactiveObjectExtensions == null) { throw new Exception("reactiveObjectExtensions is null"); } var raiseAndSetIfChangedMethod = ModuleDefinition.Import(reactiveObjectExtensions.Methods.Single(x => x.Name == "RaiseAndSetIfChanged")); if (raiseAndSetIfChangedMethod == null) { throw new Exception("raiseAndSetIfChangedMethod is null"); } var reactiveAttribute = ModuleDefinition.FindType("ReactiveUI.Fody.Helpers", "ReactiveAttribute", helpers); if (reactiveAttribute == null) { throw new Exception("reactiveAttribute is null"); } foreach (var targetType in targetTypes) { foreach (var property in targetType.Properties.Where(x => x.IsDefined(reactiveAttribute)).ToArray()) { if (property.SetMethod == null) { LogError($"Property {property.DeclaringType.FullName}.{property.Name} has no setter, therefore it is not possible for the property to change, and thus should not be marked with [Reactive]"); continue; } // Declare a field to store the property value var field = new FieldDefinition("$" + property.Name, FieldAttributes.Private, property.PropertyType); targetType.Fields.Add(field); // Remove old field (the generated backing field for the auto property) var oldField = (FieldReference)property.GetMethod.Body.Instructions.Where(x => x.Operand is FieldReference).Single().Operand; var oldFieldDefinition = oldField.Resolve(); targetType.Fields.Remove(oldFieldDefinition); // See if there exists an initializer for the auto-property var constructors = targetType.Methods.Where(x => x.IsConstructor); foreach (var constructor in constructors) { var fieldAssignment = constructor.Body.Instructions.SingleOrDefault(x => Equals(x.Operand, oldFieldDefinition) || Equals(x.Operand, oldField)); if (fieldAssignment != null) { // Replace field assignment with a property set (the stack semantics are the same for both, // so happily we don't have to manipulate the bytecode any further.) var setterCall = constructor.Body.GetILProcessor().Create(property.SetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, property.SetMethod); constructor.Body.GetILProcessor().Replace(fieldAssignment, setterCall); } } // Build out the getter which simply returns the value of the generated field property.GetMethod.Body = new MethodBody(property.GetMethod); property.GetMethod.Body.Emit(il => { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldfld, field.BindDefinition(targetType)); // pop -> this.$PropertyName il.Emit(OpCodes.Ret); // Return the field value that is lying on the stack }); TypeReference genericTargetType = targetType; if (targetType.HasGenericParameters) { var genericDeclaration = new GenericInstanceType(targetType); foreach (var parameter in targetType.GenericParameters) { genericDeclaration.GenericArguments.Add(parameter); } genericTargetType = genericDeclaration; } var methodReference = raiseAndSetIfChangedMethod.MakeGenericMethod(genericTargetType, property.PropertyType); // Build out the setter which fires the RaiseAndSetIfChanged method if (property.SetMethod == null) { throw new Exception("[Reactive] is decorating " + property.DeclaringType.FullName + "." + property.Name + ", but the property has no setter so there would be nothing to react to. Consider removing the attribute."); } property.SetMethod.Body = new MethodBody(property.SetMethod); property.SetMethod.Body.Emit(il => { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldflda, field.BindDefinition(targetType)); // pop -> this.$PropertyName il.Emit(OpCodes.Ldarg_1); // value il.Emit(OpCodes.Ldstr, property.Name); // "PropertyName" il.Emit(OpCodes.Call, methodReference); // pop * 4 -> this.RaiseAndSetIfChanged(this.$PropertyName, value, "PropertyName") il.Emit(OpCodes.Pop); // We don't care about the result of RaiseAndSetIfChanged, so pop it off the stack (stack is now empty) il.Emit(OpCodes.Ret); // Return out of the function }); } } }
public void Execute() { LogInfo("Obleak weaving"); // Validate we have the dependencies we need var rxCore = ModuleDefinition.FindAssembly("System.Reactive.Core", LogError); if (rxCore == null) { return; } var mscorlib = ModuleDefinition.FindAssembly("mscorlib", LogError); if (mscorlib == null) { return; } var obleakCore = ModuleDefinition.FindAssembly("Obleak.Fody.Core", LogError); if (obleakCore == null) { return; } // Get the IDisposable type var disposableType = new TypeReference("System", "IDisposable", ModuleDefinition, mscorlib); // Get the CompositeDisposable type and required methods var compositeDisposableType = new TypeReference("System.Reactive.Disposables", "CompositeDisposable", ModuleDefinition, rxCore); var compositeDisposableTypeResolved = compositeDisposableType.Resolve(); if (compositeDisposableTypeResolved == null) { throw new Exception("compositeDisposableTypeResolved is null"); } var compositeDisposableCtor = ModuleDefinition.Import(compositeDisposableTypeResolved.Methods.Single(m => m.IsConstructor && !m.HasParameters)); var compositeDisposableDisposeMethod = ModuleDefinition.Import(compositeDisposableTypeResolved.Methods.Single(m => m.Name == "Dispose")); var obleakAttribute = ModuleDefinition.FindType("Obleak.Fody.Core", "ObleakSubscriptionAttribute", obleakCore); if (obleakAttribute == null) { throw new Exception("obleakAttribute is null"); } var disposableExtensions = new TypeReference("Obleak.Fody.Core", "Extensions", ModuleDefinition, obleakCore).Resolve(); if (disposableExtensions == null) { throw new Exception("disposableExtensions is null"); } var handleWithExtensionMethod = ModuleDefinition.Import(disposableExtensions.Methods.Single(x => x.Name == "HandleWith")); if (handleWithExtensionMethod == null) { throw new Exception("handleWithExtensionMethod is null"); } // Any class where the Obleak attribute appears on the class, a constructor or a method var targets = ModuleDefinition.GetAllTypes() .Where( x => x.IsDefined(obleakAttribute) || x.GetConstructors().Any(y => y.IsDefined(obleakAttribute)) || x.GetMethods().Any(y => y.IsDefined(obleakAttribute))); targets.ForEach(target => { LogInfo($"Weaving target: {target.FullName}"); // We can only weave classes which are disposable if (!target.IsDisposable()) { LogError($"Target class {target.FullName} is not disposable and therefore cannot be weaved with Obleak"); return; } // If this is class wide process every method, else only tackle what has been attributed. If the class + specific methods / constructors // have the attribute, still process everything // Note: we only need one composite disposable per target type var isClassWide = target.IsDefined(obleakAttribute); // The methods we're going to weave var methods = target.Methods.Where(x => isClassWide || x.IsDefined(obleakAttribute)); // Declare a field for the composite disposable var compositeDisposableField = new FieldDefinition(COMPOSITE_DISPOSABLE_FIELD_NAME + target.Name, FieldAttributes.Private, compositeDisposableType); target.Fields.Add(compositeDisposableField); // Initialise this in all of the constructors in this class (not it's inheritance hierarchy) -- hence target.Methods usage target.Methods.Where(m => m.IsConstructor).ForEach(constructor => { constructor.Body.Emit(il => { var first = constructor.Body.Instructions[0]; // first instruction // Instructions equivalent of: this.$ObleakCompositeDisposable = new CompositeDisposable(); // But as it's done as the first instruction set when decompiled this is actually // private CompositeDisposable $ObleakCompositeDisposable = new CompositeDisposable(); il.InsertBefore(first, il.Create(OpCodes.Ldarg_0)); // this il.InsertBefore(first, il.Create(OpCodes.Newobj, compositeDisposableCtor)); // new CompositeDisposable from ctor il.InsertBefore(first, il.Create(OpCodes.Stfld, compositeDisposableField.BindDefinition(target))); // store new obj in fld }); }); // For every .Subscribe() which returns a disposable call the .Add method on the compositeDisposableField methods.Where(m => m.HasBody && m.Body.Instructions.Any(i => _isExpectedMethodCall(i, disposableType, "Subscribe"))).ForEach(method => { method.Body.Emit(il => { var subscribes = method.Body.Instructions.Where(i => _isExpectedMethodCall(i, disposableType, "Subscribe")).ToArray(); subscribes.ForEach(i => { var next = i.Next; il.InsertBefore(next, il.Create(OpCodes.Ldarg_0)); // this il.InsertBefore(next, il.Create(OpCodes.Ldfld, compositeDisposableField.BindDefinition(target))); il.InsertBefore(next, il.Create(OpCodes.Call, handleWithExtensionMethod)); // Call .HandleWith($ObleakCompositeDisposable) }); }); }); // Does the target itself have a dispose method or is it inheriting the implementation? // If doesn't have one of it's own we need to add one and call the base.Dispose() as we need to clean up this // new local composite disposable. var hasDisposeMethod = target.HasDisposeMethod(); if (!hasDisposeMethod && target.HasGenericParameters) { LogError($"Automatically generating a Dispose method for {target.Name} is not supported due to generics in it's inheritance hierarchy. " + $"You need to create an empty parameterless Dispose() and re-build to use the Obleak weavers"); return; } if (!hasDisposeMethod) { target.CreateDisposeMethod(); } // Find the dispose method and append the instructions at the end to clean up the composite disposable var dispose = target.GetDisposeMethod(); dispose.Body.Emit(il => { var last = dispose.Body.Instructions.Last(i => i.OpCode == OpCodes.Ret && (i.Next == null || dispose.Body.Instructions.Last() == i)); il.InsertBefore(last, il.Create(OpCodes.Ldarg_0)); // this il.InsertBefore(last, il.Create(OpCodes.Ldfld, compositeDisposableField.BindDefinition(target))); il.InsertBefore(last, il.Create(OpCodes.Callvirt, compositeDisposableDisposeMethod)); // call $ObleakCompositeDisposable.Dispose(); }); LogInfo($"Completed weaving target: {target.FullName}"); }); }
public void Execute() { var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "Reactive.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); var reactiveObject = new TypeReference("ReactiveUI", "IReactiveObject", ModuleDefinition, reactiveUI); var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType != null && reactiveObject.IsAssignableFrom(x.BaseType)).ToArray(); var reactivePropertyExtensions = ModuleDefinition.FindType("Reactive.Fody.Helpers", "ReactivePropertyExtensions", helpers).Resolve(); if (reactivePropertyExtensions == null) { throw new Exception("reactivePropertyExtensions is null"); } var raiseAndSetIfChangedMethod = ModuleDefinition.ImportReference(reactivePropertyExtensions.Methods.Single(x => x.Name == "RaiseAndSetIfChanged")); if (raiseAndSetIfChangedMethod == null) { throw new Exception("raiseAndSetIfChangedMethod is null"); } var reactiveAttribute = ModuleDefinition.FindType("Reactive.Fody.Helpers", "ReactiveAttribute", helpers); if (reactiveAttribute == null) { throw new Exception("reactiveAttribute is null"); } var reactiveObjectExtensions = new TypeReference("ReactiveUI", "IReactiveObjectExtensions", ModuleDefinition, reactiveUI).Resolve(); if (reactiveObjectExtensions == null) { throw new Exception("reactiveObjectExtensions is null"); } var raisePropertyChangedMethod = ModuleDefinition.ImportReference(reactiveObjectExtensions.Methods.Single(x => x.Name == "RaisePropertyChanged")); if (raisePropertyChangedMethod == null) { throw new Exception("raisePropertyChangedMethod is null"); } foreach (var targetType in targetTypes) { var setMethodByGetMethods = targetType.Properties .Where(x => x.SetMethod != null && x.GetMethod != null && x.IsDefined(reactiveAttribute)) .ToDictionary(x => x.GetMethod, x => x.SetMethod); foreach (var property in targetType.Properties.Where(x => x.IsDefined(reactiveAttribute)).ToArray()) { TypeReference genericTargetType = targetType; if (targetType.HasGenericParameters) { var genericDeclaration = new GenericInstanceType(targetType); foreach (var parameter in targetType.GenericParameters) { genericDeclaration.GenericArguments.Add(parameter); } genericTargetType = genericDeclaration; } MethodDefinition[] getMethods; if (property.SetMethod == null && property.GetMethod.TryGetMethodDependencies(out getMethods)) { var setMethodsForGetInstructions = getMethods .Where(x => setMethodByGetMethods.ContainsKey(x)) .Select(x => setMethodByGetMethods[x]) .ToArray(); if (!setMethodsForGetInstructions.Any()) { LogError($"Get only Property {property.DeclaringType.FullName}.{property.Name} has no supported dependent properties. " + $"Only dependent auto properties decorated with the [Reactive] attribute can be weaved to raise property change on {property.Name}"); } var raisePropertyChangedMethodReference = raisePropertyChangedMethod.MakeGenericMethod(genericTargetType); foreach (var method in setMethodsForGetInstructions) { method.Body.Emit(il => { var last = method.Body.Instructions.Last(i => i.OpCode == OpCodes.Ret); il.InsertBefore(last, il.Create(OpCodes.Ldarg_0)); il.InsertBefore(last, il.Create(OpCodes.Ldstr, property.Name)); il.InsertBefore(last, il.Create(OpCodes.Call, raisePropertyChangedMethodReference)); }); } // Move on to next property for the target type continue; } if (property.SetMethod == null) { LogError($"Property {property.DeclaringType.FullName}.{property.Name} has no setter, therefore it is not possible for the property to change, and thus should not be marked with [Reactive]"); continue; } // Declare a field to store the property value var field = new FieldDefinition("$" + property.Name, FieldAttributes.Private, property.PropertyType); targetType.Fields.Add(field); // Remove old field (the generated backing field for the auto property) var oldField = (FieldReference)property.GetMethod.Body.Instructions.Where(x => x.Operand is FieldReference).Single().Operand; var oldFieldDefinition = oldField.Resolve(); targetType.Fields.Remove(oldFieldDefinition); // See if there exists an initializer for the auto-property var constructors = targetType.Methods.Where(x => x.IsConstructor); foreach (var constructor in constructors) { var fieldAssignment = constructor.Body.Instructions.SingleOrDefault(x => Equals(x.Operand, oldFieldDefinition) || Equals(x.Operand, oldField)); if (fieldAssignment != null) { // Replace field assignment with a property set (the stack semantics are the same for both, // so happily we don't have to manipulate the bytecode any further.) var setterCall = constructor.Body.GetILProcessor().Create(property.SetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, property.SetMethod); constructor.Body.GetILProcessor().Replace(fieldAssignment, setterCall); } } // Build out the getter which simply returns the value of the generated field property.GetMethod.Body = new MethodBody(property.GetMethod); property.GetMethod.Body.Emit(il => { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldfld, field.BindDefinition(targetType)); // pop -> this.$PropertyName il.Emit(OpCodes.Ret); // Return the field value that is lying on the stack }); var methodReference = raiseAndSetIfChangedMethod.MakeGenericMethod(genericTargetType, property.PropertyType); // Build out the setter which fires the RaiseAndSetIfChanged method if (property.SetMethod == null) { throw new Exception("[Reactive] is decorating " + property.DeclaringType.FullName + "." + property.Name + ", but the property has no setter so there would be nothing to react to. Consider removing the attribute."); } property.SetMethod.Body = new MethodBody(property.SetMethod); property.SetMethod.Body.Emit(il => { RaiseAndSetIfChanged(methodReference, targetType, field, il, property.Name); }); } } }