阅读过《三体》的同学肯定知道“降维打击”,从更高维度看问题,直接将对手KO。今天我们闲聊一下编程界的“二向箔”——元编程。

1. 什么是元编程

我们听过了太多太多的名词,耳朵似乎都有点名词麻痹症了。比如,有些名词为了装x(比如笔者的文章标题...)或者其本身的意义难以定义,就会加上一些似乎闪烁着光芒的前缀——如meta。计算机软件这行业就有meta data, meta model, meta programming。
今天我们装x的主角就是meta programming——元编程。
其实网络上也能搜出很多相关的文章,对于该词的定义参考wikipedia的一句话:

简而言之,就是将程序作为数据,可以用于操作程序或者自身,而实现某些能力,比如将运行时工作移到编译时。
按照编译器发展的进程,元编程可实现如代码替换(通过宏实现),泛型编程(从宏演变而来,脱离类型,高度抽象逻辑,可减少代码量),或者在更高级的语言中,运行时通过内省/反射机制来操作代码逻辑,或者随着编译过程的解耦和开放,可以实现在中间语言阶段(AST,IL),操作语法树和中间语言,实现更可扩展性的能力。

编程界的“二向箔”——Dart元编程-LMLPHP

Dart做为一门现代高级语言,除了模板化能力,也能基于中间语言来操作代码。本篇文章主要讨论如何基于其中间语言(dill),通过AST树的操作来进行元编程,并实现一些现有dart语法本身实现不了的能力。并且这种实现在编译时,对于程序在运行时的性能几乎没有影响。

2. Dart中的元编程简介

2.1 背景知识

我们知道,几乎任何语言中,代码在 "编译"(解释型语言在运行时也有编译的过程) 的过程中,都会生成一种树状的中间状态,这就是 AST(抽象语法树)。AST 描述了每个表达式/语句中的子语句的执行顺序和执行逻辑,因而它可以被很方便地翻译成目标代码 。基于这种抽象,能合理的将编译器拆分为三阶段:FrontEnd,Optimizer, Backend,从而实现能兼容各种语法形式的语言,更易于迁移并兼容不同架构的cpu。见下图:

 

编程界的“二向箔”——Dart元编程-LMLPHP

这三个阶段围绕这IL(intermediate language)进行。IL语言隔离了语法(能轻易适配各种新的语种),平台架构等的差异性。

 

编程界的“二向箔”——Dart元编程-LMLPHP

2.2 Dart的编译流程

Dart的设计也类似,其中间语言就是Dill。不同的是,这里的Dill不像java的IL或者DotNet的IL那样开放出来可以直接编写,而是通过程序的方式操作实现。

编程界的“二向箔”——Dart元编程-LMLPHP

这种方式其实就是基于AST库对Dill进行manipulation。

编程界的“二向箔”——Dart元编程-LMLPHP

这个库内的组件包含了所有AST树涉及到的节点的定义和访问,将类型,函数,语句,声明,表达式等编程基本概念抽象成了对象。基于这些对象我们可以遍历整个AST树, 或者生成新的类型和函数,插入代码语句,实现新的逻辑。

2.3 几个栗子

入门其实很简单,看一下例子代码就可以啦。

2.3.1. 比如以下语句定义了一个新的Map变量,并且调用了它的构造函数:

//组装参数
Arguments mapFromArgs = Arguments.empty();
mapFromArgs.positional.add(MapLiteral([], keyType:keyInterType));
//调用from构造函数
StaticInvocation mapConstructor = StaticInvocation(MapFromFactoryProc, mapFromArgs);
//声明一个名字为jsonMap的Map类型变量
VariableDeclaration mapInstDecl = VariableDeclaration("jsonMap", type:mapInterType);
//相当于var jsonMap = new Map();
VariableSet set_mymap = VariableSet(mapInstDecl, mapConstructor);

2.3.2. 创建函数体

函数体其实就是Block。

Block bodyStatements = Block(List<Statement>());
bodyStatements.addStatement(mapInstDecl);
bodyStatements.addStatement(ExpressionStatement(inst));

2.3.3 创建函数

这个例子是参考某个函数的声明形式来创建新函数,篇幅所限,一些参数从略。

static Procedure createProcedure(Procedure referProcedure ,Statement bodyStatements, DartType returnType) {
      FunctionNode functionNode = new FunctionNode(bodyStatements,
          //...参数从略
      );
      Procedure procedure = new Procedure(
        Name(referProcedure.canonicalName.name, referProcedure.name.library),ProcedureKind.Method, functionNode,
        //...参数从略
      );
      return procedure;
  }
//调用函数创建,并添加到类定义中
Procedure overridedToJsonFunc = createProcedure(jsonTypes.StaticBaseToJsonProc, bodyStatements, InterfaceType(mapClass));
youClazz.addMember(overridedToJsonFunc);

2.3.4 其他

基于AST还可以创建复杂的表达式和语句,如ForInStatement(for...in循环)等,语句和表达式还可以通过ExpressionStatement和BlockExpression互相转化。更多的技巧可参考AST的定义。

2.4 如何调试

编辑好的Dill似乎是个黑盒,除了看日志或者看异常堆栈,并不能进行单步调试,给开发带来了一些困难。但Dart提供了已将将kernel dill转成可阅读的文本的工具,方便调试:

$DartHome/dart ../../pkg/vm/bin/dump_kernel.dart /your/dill/file/path /output/dill/text/file.text

打开的text文件是类似于这样的:

    static method  __from_Json1(core::Map<dynamic, dynamic> m) → aop2::UserDataT {
      aop2::UserDataT inst;
      inst = new aop2::UserDataT::•();
      inst.name = m.[]("name") is core::String ?{core::String} m.[]("name") : null;
      inst.city = m.[]("city") is core::String ?{core::String} m.[]("city") : null;
      inst.age = m.[]("age") is core::int ?{core::int} m.[]("age") : null;
      inst.squres = m.[]("squres") is core::double ?{core::double} m.[]("squres") : null;
      inst.sex = m.[]("sex") is core::bool ?{core::bool} m.[]("sex") : null;
      inst.houses = m.[]("houses") is core::Map<dynamic, dynamic> ?{core::Map<dynamic, dynamic>} block {
        core::Map<core::String, core::String> mymap;
        mymap = col::LinkedHashMap::from<core::String, core::String>(<core::String, core::String>{});
        for (core::String item in (m.[]("houses") as core::Map<dynamic, dynamic>).keys) {
          mymap.[]=(item, (m.[]("houses") as core::Map<dynamic, dynamic>).[](item) is core::String ?{core::String} (m.[]("houses") as core::Map<dynamic, dynamic>).[](item) : null);
        }
      } =>mymap : null;
      return inst;
    }

3. 应用畅想

基于Dill的Manipulation,我们可以实现往代码中注入新的逻辑。比如闲鱼科技之前开源的AOP库AspectD的原理就是通过加载dill文件生成AST,然后遍历AST,寻找到已经annotation到的函数或语句,在dill层面操作后又生成dill参加到编译器后续的流程,最终实现了AOP。

类似的,我们知道Dart对于Json解析操作不是很方便,jsonDecode不能直接生成业务对象,而是Map或者List之类的集合,还需要用户自己手动代码遍历这些集合并装载对象。虽然官方开源了一个基于Source_gen的方案,但使用上也不友好(还有其他一些方案如Dason等,但依赖于Mirror,详见这里的比较)。其实遍历Map或者List并装配对象这样的操作逻辑很简单,我们也可以通过Dill Manipulation来做。

其使用方式简便,举例如下:

@JsonModel()
class UserData {
  String name;
  String city;
  UserData son;
}

void main(){
    var str = '''
    {
      "name": "Jim",
      "city": "hangzhou",
      "son":{
        "name": "Kong",
        "city":"Hangzhou"
      }
    }
    ''';

    UserData userData = JsonDrill.fromJson<UserData>(jsonDecode(str));
    var map = JsonDrill.toJson<UserData>(userData);
    print("$map");
  }

更深入的思考一下,Dart现有的mirror能力至今未推荐使用(原因分析可参考这篇文章),那我们是否可以基于Dill Manipulation实现一个简单轻量的LiteMirror库呢?并基于这个LiteMirror库实现更上层的Json解析和AOP甚至Hook能力?

当然,聪明的你或许已经发现,Dill Manipulation不可避免的要对编译流程进行定制,这就要求比如在Flutter环境中,需要对Flutter Tool进行定制,以加入Dill再编辑的环节。剧透一下,闲鱼科技目前就已经实现了Json解析器,正在准备开源中,敬请期待


原文链接

08-30 20:43