Look! Idle Fish Open Source Another Flutter Development Tool

Ali Mei Guide: With the rapid development of Flutter framework, more and more businesses begin to use Flutter to reconstruct or build new products. But in our practice, we found that on the one hand, Flutter has high development efficiency, excellent performance and good cross-platform performance, on the other hand, Flutter is also facing plug-ins, basic capabilities, lack or imperfection of the underlying framework. Today, the real thing of the idle fish team takes us to solve a problem: how to solve AOP for Flutter?

Background of the problem

In the process of implementing an automated recording and playback system, we found that we need to modify the code of the Flutter framework (Dart level) to meet the requirements, which will make the framework intrusive. In order to solve this intrusive problem and reduce the maintenance cost in the iteration process better, the primary scheme we consider is Face-Oriented Programming.

So how to solve the problem of AOP for Flutter? This article focuses on AspectD, an AOP programming framework for Dart developed by an idle fish technology team.

AspectD: Dart-oriented AOP framework

Whether AOP capabilities are run-time or compile-time support depends on the characteristics of the language itself. In iOS, for example, Objective C itself provides powerful runtime and dynamic capabilities that make runtime AOP easy to use. Under Android, Java language features not only compile-time static proxy based on bytecode modification such as AspectJ, but also run-time dynamic proxy based on runtime enhancement such as Spring AOP. What about Dart? First, Dart's reflection support is very weak. It only supports Introspection and does not support Modification. Second, Flutter prohibits reflection for package size, robustness and other reasons.

Therefore, we design and implement AspectD, an AOP scheme based on compile-time modification.

1. Design Details

2. Typical AOP scenarios

The following AspectD code illustrates a typical AOP usage scenario:

aop.dart


import 'package:example/main.dart' as app;
import 'aop_impl.dart';


void main()=> app.main();
aop_impl.dart


import '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!');
}
}

3. Developer-oriented API design

Design of PointCut

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

PointCut needs to fully characterize in what way (Call/Execute, etc.), to which Library, which class (this is empty when Library Method) and which method to add AOP logic. Data structure of PointCut:

@pragma('vm:entry-point')
class PointCut {
final Map<dynamic, 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 source code information (such as library name, file name, line number, etc.), method call object, function name, parameter information, etc. Note the @pragma('vm:entry-point') annotation here, whose core logic is Tree-Shaking. Under AOT(ahead of time) compilation, if it cannot be eventually tuned by the main application entry, it will be discarded as useless code. AOP code is obviously not called by main because of its non-intrusive injection logic, so this annotation is needed to tell the compiler not to discard this logic. The proceed method here, similar to the ProceedingJoinPoint.proceed() method in AspectJ, calls the pointcut.proceed() method to implement the call to the original logic. The proceed method body in the original definition is just an empty shell, and its content will be dynamically generated at run time.

Design of Advice

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

The @pragma("vm:entry-point") effect here is the same as described in a. The pointCut object is passed into the AOP method as a parameter, so that the developer can obtain the relevant information of the source code invocation information, realize his own logic or invoke the original logic through pointcut.proceed().

Aspect Design

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

Aspect annotations can make AOP implementation classes such as ExecuteDemo easy to identify and extract, and can also act as switches, that is, if you want to disable this section of AOP logic, remove it. @Aspect Annotations are enough.

4. Compiling AOP Code

The main entrance containing the original project

As you can see above, aop.dart introduces import'package:example/main.dart'as app; this makes it possible to compile aop.dart with all the code of the entire example project.

Compilation in Debug Mode

import'aop_impl.dart'is introduced into aop.dart, which enables content in aop_impl.dart to be compiled in Debug mode even if it is not explicitly dependent on aop.dart.

Compilation in Release Mode

In AOT compilation (Release mode), Tree-Shaking logic prevents content from compiling into dill when the content in aop_impl.dart is not called by main in aop. The effect can be avoided by adding @pragma("vm:entry-point").

When we write AOP code with AspectD and compile aop.dart to generate intermediate products, so that dill contains both original project code and AOP code, we need to consider how to modify it. In AspectJ, modification is achieved by manipulating lass files. In AspectD, we manipulate dill files.

5. Dill operation

The dill file, also known as Dart Intermediate Language, is a concept in Dart language compilation. Whether it is Script Snapshot or AOT compilation, dill is needed as an intermediate product.

Structure of Dill

We can print out the internal structure of dill through dump_kernel.dart provided by vm package in dart sdk

Dill transformation

dart provides a way of Kernel to Kernel Transform. It can transform dill by recursive AST traversal of dill files.

Based on the AspectD annotations written by developers, the transformation part of AspectD can extract which libraries/classes/methods need to add what kind of AOP code, and then in the process of AST recursion through the operation of the target class, realize the functions of Call/Execute.

A typical part of the Transform logic is as follows:

@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 AST objects in dill (visitMethodInvocation function here) and combining the developer's ASED annotations (aspectdInfoMap and aspectdItemInfo here), the original AST objects (methodInvocation here) can be transformed to change the original code logic, that is, the Transform process. .

6. AspectD-supported grammar

Unlike the three BeforeAround After previews provided in AspectJ, there is only one unified abstraction in AspectD, namely Around. In terms of whether to modify the original method internally, there are Call and Execute, the former PointCut is the call point, and the latter PointCut 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

Supporting Call and Execute alone is obviously thin for Flutter(Dart). On the one hand, Flutter forbids reflection, and on the other hand, even if Flutter turns on reflection support, it is still weak enough to meet the demand. For a typical scenario, if class y of the x.dart file defines a private method m or member variable p in the dart code that needs to be injected, there is no way to access it in aop_impl.dart, let alone acquire multiple consecutive private variable attributes. On the other hand, it may not be enough to operate on the whole method. We may need to insert processing logic in the middle of the method. To solve this problem, AspectD designed a syntax Inject. See the following example: the flutter library contains this gesture-related code:

Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> 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 instance and context after onTapCancel, Call and Execute are not feasible, but with Inject, it only needs a few simple sentences to solve:



@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');
}
}

With the above processing logic, the GestureDetector.build method in the compiled and constructed dill is as follows:

In addition, the input parameter of Inject has an additional lineNum naming parameter relative to Call/Execute, which can be used to specify the specific line number of the insertion logic.

7. Building Process Support

Although AOP can be achieved by compiling aop.dart to compile both original engineering code and AspectD code to dill file, and then by transforming the dill layer to achieve AOP, standard flutter builds (i.e. flutter tools) do not support this process, so it is necessary to make minor modifications to the construction process. In AspectJ, this process is implemented by Ajc, a non-standard Java compiler. In AspectD, the application of Patch on fluttertools can support AspectD.

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...

Practical combat and reflection

Based on AspectD, we have successfully removed all the intrusive codes for Flutter framework in practice, realized the same functions as intrusive codes, supported the recording and playback of hundreds of scripts and automated regression stable and reliable operation.

From AspectD's point of view, Call/Execute can help us easily implement such functions as performance burying point (the length of key method invocation), log enhancement (getting detailed information about where a method is invoked), and Doom recording and playback (such as generating records and playback of random number sequences). Inject grammar is more powerful. It can inject logic freely in a way similar to source code. It can support complex scenarios such as App recording and automatic regression (such as recording and playback of user touch events).

Furthermore, AspectD's principle is based on Dill transformation. With Dill operation, developers can freely manipulate Dart compiled products. Moreover, this transformation is oriented to AST objects near source code level, which is not only powerful but also reliable. Whether it is doing some logical substitution or Json <--> model transformation, it provides a new perspective and possibility.


Links to the original text
This article is the original content of Yunqi Community, which can not be reproduced without permission.

Tags: Programming calculator Java iOS

Posted on Mon, 05 Aug 2019 20:54:22 -0700 by daydreamer