AspectD: An Effective AOP Solution for Flutter — Open Sourced by Alibaba Xianyu Tech Team

By Alibaba Tech

Github. AspectD for Flutter

Background

For example, in the process of implementing an automated recording and playback, we have found that the code of the Flutter framework (Dart level) needs to be modified to meet the requirements during automatic recording playback. This leads to the risk of the framework becoming vulnerable to intrusion. To solve this problem and reduce the maintenance cost in the iteration process, the first solution we consider is Aspect-Oriented Programming.

Aspect-Oriented Programming (AOP) can dynamically insert the code into a specific method and position of the class at compile time (or runtime), thereby adding features to existing code dynamically and uniformly, without modifying the source code.

But the question is, how can we implement AOP for Flutter? This article focuses on AspectD, a Dart-oriented AOP programming framework developed by the XianYu technical team.

AspectD: Dart-Oriented AOP Framework

What about Dart? Firstly, the reflection support of Dart is poor. Only Introspection is supported, while Modification is not supported. Secondly, Flutter disables reflection for packet size and robustness.

Therefore, we have designed and implemented an AOP solution based on compile-time modification, AspectD.

Design Details

Typical AOP Scenarios

aop.dartimport 'package:example/main.dart' as app;
import 'aop_impl.dart';
void main()=> app.main();aop_impl.dartimport 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();
@Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter")
@pragma("vm:entry-point")
void _incrementCounter(PointCut pointcut) {
pointcut.proceed();
print('KWLM called!') ;
}
}

Developer-Oriented API design

Design of PointCut

@Call("package:app/calculator.dart","Calculator","-getCurTime")

PointCut needs to be fully characterized how to add the AOP logic, for example in what way (Call/Execute), and to which library, which class (this item is empty in the case of Library Method), and which method.
Data structure of PointCut:

@pragma('vm:entry-point')
class PointCut {
final Map<dynamic> sourceInfos;
final Object target;
final String function;
final String stubId;
final List<dynamic> positionalParams;
final Map<dynamic, dynamic> namedParams;
@pragma('vm:entry-point')
PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams);
@pragma('vm:entry-point')
Object proceed(){
return null;
}
}

It contains the source code information (such as the library name, file name, and row number), method call object, function name, and parameter information.

Please note the @pragma('vm:entry-point') annotation here. Its core logic is Tree-Shaking. In AOT (Ahead of Time) compilation, if it cannot be called by the Main entry of the application, it will be discarded as useless code. The injection logic of the AOP code is non-invasive, so obviously it will not be called by the Main entry. Therefore, this annotation is required to tell the compiler not to discard this logic.

The proceed method here is similar to the ProceedingJoinPoint.proceed() method in AspectJ, and the original logic can be called by calling pointcut.proceed() method. The proceed method body in the original definition is empty, and its content will be dynamically generated at runtime.

Design of Advice

@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
...
return result;
}

The effect of @pragma("vm:entry-point") here is the same as described above. The pointCut object is passed into the AOP method as a parameter, so that developers can obtain relevant information about the source code call to implement its own logic or call the original logic through pointcut.proceed().

Design of Aspect

@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();
...
}

The Aspect annotation can enable the AOP implementation class, such as ExecuteDemo, to be easily identified and extracted, and can also be used as a switch. That is, if we want to disable this AOP logic, just remove the @Aspect annotation.

Compilation of AOP code

Contain the Main Entry in the Original Project

Compilation in Debug Mode

Compilation in Release Mode

When we use AspectD to write the AOP code, and generate intermediate products by compiling aop.dart, so that Dill contains both original project code and the AOP code, then we need to consider how to modify it. In AspectJ, modifications are implemented through operations on the Class file. In AspectD, it is implemented by operating the Dill file.

Dill Operation

Structure of Dill

dart bin/dump_kernel.dart /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill.txt

Transformation of Dill

Based on the AspectD annotation compiled by developers, the libraries, classes, and methods that need to be added with the specific AOP code can be extracted from the transformation part of AspectD, and then features, such as Call/Execute, can be implemented through operations on target classes during AST recursion.

The following is part of a typical Transform logic:

@override
MethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) {
methodInvocation.transformChildren(this);
Node node = methodInvocation.interfaceTargetReference?.node;
String uniqueKeyForMethod = null;
if (node is Procedure) {
Procedure procedure = node;
Class cls = procedure.parent as Class;
String procedureImportUri = cls.reference.canonicalName.parent.name;
uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(
procedureImportUri, cls.name, methodInvocation.name.name, false, null);
}
else if(node == null) {
String importUri = methodInvocation?.interfaceTargetReference?.canonicalName?.reference?.canonicalName?.nonRootTop?.name;
String clsName = methodInvocation?.interfaceTargetReference?.canonicalName?.parent?.parent?.name;
String methodName = methodInvocation?.interfaceTargetReference?.canonicalName?.name;
uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(
importUri, clsName, methodName, false, null);
}
if(uniqueKeyForMethod ! = null) {
AspectdItemInfo aspectdItemInfo = _aspectdInfoMap[uniqueKeyForMethod];
if (aspectdItemInfo?.mode == AspectdMode.Call &&
! _transformedInvocationSet.contains(methodInvocation) && AspectdUtils.checkIfSkipAOP(aspectdItemInfo, _curLibrary) == false) {
return transformInstanceMethodInvocation(
methodInvocation, aspectdItemInfo);
}
}
return methodInvocation;
}

By traversing the AST object in Dill (the visitMethodInvocation function here), combined with the AspectD annotation written by developers (the aspectdInfoMap and aspectdItemInfo here), we can transform the original AST object (the methodInvocation here) to change the original code logic, namely the Transform process.

Syntax Supported by AspectD

In terms of whether to modify the original method internally, two types, Call and Execute, are available. The PointCut of the former is the call point, and the PointCut of the latter is the execution point.

Call

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class CallDemo{
@Call("package:app/calculator.dart","Calculator","-getCurTime")
@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
print('Aspectd:KWLM02');
print('${pointcut.sourceInfos.toString()}');
Future<String> result = pointcut.proceed();
String test = await result;
print('Aspectd:KWLM03');
print('${test}');
return result;
}
}

Execute

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo{
@Execute("package:app/calculator.dart","Calculator","-getCurTime")
@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
print('Aspectd:KWLM12');
print('${pointcut.sourceInfos.toString()}');
Future<String> result = pointcut.proceed();
String test = await result;
print('Aspectd:KWLM13');
print('${test}');
return result;
}

Inject

For a typical scenario, if class “y” in the x.dart file defines a private method “m” or a member variable “p” in the dart code to be injected, then it cannot be accessed in aop_impl.dart, not to mention obtaining multiple continuous private variable properties. On the other hand, it may not be enough to just operate the entire method. We may need to insert the processing logic in the middle of the method.

To solve this problem, a syntax, Inject, is designed in AspectD. See the following example:

The Flutter library contains the following gesture-related code:

@override
Widget build(BuildContext context) {
final Map<TapGestureRecognizer> gestures = <Type, GestureRecognizerFactory>{};
if (onTapDown ! = null || onTapUp ! = null || onTap ! = null || onTapCancel ! = null) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel;
},
);
}

If we want to add a processing logic for the instance and context after onTapCancel, Call and Execute are not feasible. However, with Inject, only a few simple statements are needed to solve the problem:

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class InjectDemo{
@Inject("package:flutter/src/widgets/gesture_detector.dart","GestureDetector","-build", lineNum:452)
@pragma("vm:entry-point")
static void onTapBuild() {
Object instance; //Aspectd Ignore
Object context; //Aspectd Ignore
print(instance);
print(context);
print('Aspectd:KWLM25');
}
}

Through the above processing logic, the GestureDetector.build method in Dill after compilation is as follows:

In addition, compared with Call/Execute, the input parameter of Inject has an additional lineNum named parameter, which can be used to specify the specific row number of the insert logic.

Build Process Support

In AspectJ, this process is implemented by Ajc of the non-standard Java compiler. In AspectD, support for AspectD can be implemented by appending the Patch to flutter_tools.

kylewong@KyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch
kylewong@KyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp
kylewong@KyleWongdeMacBook-Pro fluttermaster % flutter doctor -v
Building flutter tool...

Practice and Consideration

From the perspective of AspectD, Call/Execute can help us easily implement features, such as performance tracking (call duration of key methods), log enhancement (obtaining detailed information about the place where a method is specifically called), and Doom recording and playback (such as the build record and playback of random number sequences). The Inject syntax is more powerful. It can implement the free injection of logic by means similar to the source code, and support complex scenarios, such as app recording and automatic regression (for example, recording and playback of user touch events).

Furthermore, the AspectD principle is based on the Dill transformation. With the power of Dill, developers can freely operate on Dart compiled products. In addition, this transformation targets AST objects, which are close to the source code level, and are not only powerful but also reliable. Whether it is the logical replacement or the Json←> model transformation, it provides a new perspective and possibility.

Conclusion

If you have any questions or suggestions during usage, please feel free to submit an issue or a pull request.

Original Source

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.