[Flutter翻译]纠结动态语言符号,独特解决热负荷的原子问题

原文地址:hydro-sdk.io/blog/mangli…

原文作者:github.com/chgibb

发布时间:2021年5月20日

复原的结构化类型爱好者
将Flutter的开发时间体验与Dart编程语言脱钩。

Hydro-SDK是一个项目,有一个巨大的、雄心勃勃的目标。”成为Flutter的React Native”。
它旨在通过以下方式实现这一目标。

  1. 将Flutter的API表面与Dart编程语言解耦。
  2. 将Flutter的开发时间体验与Dart编程语言解耦。
  3. 为代码的空中发布提供一流的支持。
  4. 提供一个来自pub.dev的软件包生态系统,自动投射到支持的语言,并发布到其他软件包系统。

我之前在这里写过关于Hydro-SDK的过去和未来。

在这篇文章中,我想深入了解Hydro-SDK如何编译、管理和热重载Typescript代码的一些细节。

ts2hc是一个命令行程序,作为每个Hydro-SDK版本的一部分分发。ts2hc的工作是把Typescript代码变成字节码和调试符号。用户一般不与它直接互动,而是通过hydroc build和hydroc run等命令间接互动。

构建时间#github.com/TypeScriptT…

通过利用优秀的Typescript to Lua(TSTL)库,Typescript被降低到Lua中。请看下面这个来自Counter-App展示区的摘录。

//counter-app/ota/lib/counterApp.ts
import {
    //...
    StatelessWidget,
} from "@hydro-sdk/hydro-sdk/runtime/flutter/widgets/index";
import { 
    //...
    MaterialApp, 
} from "@hydro-sdk/hydro-sdk/runtime/flutter/material/index";
import { Widget } from "@hydro-sdk/hydro-sdk/runtime/flutter/widget";

export class CounterApp extends StatelessWidget {
    public constructor() {
        super();
    }

    public build(): Widget {
        return new MaterialApp({
            title: "Counter App",
            initialRoute: "/",
            home: new MyHomePage("Counter App Home Page"),
        });
    }
}
复制代码

ts2hc将降低counter-app/ota/lib/counterApp.ts,以及它所有的依赖关系到单独的Lua模块。然后这些Lua模块被捆绑起来,其结果看起来像下面这样。

local package = {preload={}, loaded={}}
local function require(file)
    local loadedModule = package.loaded[file]
    if loadedModule == nil
    then
        loadedModule = package.preload[file]()
        package.loaded[file] = loadedModule
        return loadedModule
    end
    return loadedModule
end
-- ...
package.preload["ota.lib.counterApp"] = (function (...)
    require("lualib_bundle");
    local ____exports = {}
    local ____index = require("ota.@hydro-sdk.hydro-sdk.runtime.flutter.widgets.index")
    -- ...
    local StatelessWidget = ____index.StatelessWidget

    -- ...
    local ____index = require("ota.@hydro-sdk.hydro-sdk.runtime.flutter.material.index")
    local MaterialApp = ____index.MaterialApp

    ____exports.CounterApp = __TS__Class()

    local CounterApp = ____exports.CounterApp

    CounterApp.name = "CounterApp"

    __TS__ClassExtends(CounterApp, StatelessWidget)

    function CounterApp.prototype.____constructor(self)
        StatelessWidget.prototype.____constructor(self)
    end

    function CounterApp.prototype.build(self)
        return __TS__New(
            MaterialApp,
            {
                title = "Counter App",
                initialRoute = "/",
                home = __TS__New(MyHomePage, "Counter App Home Page")
            }
        )
    end

    return ____exports

end)
复制代码

被降低和捆绑的Lua仍然有点类似于输入的Typescript。Typescript ES6模块被包装成Lua立即调用的函数表达式(IIFE),在package.preload map中被分配了字符串键,并通过requireing使它们的出口可用。这种模式对于任何在Javascript捆绑器/模块解析器(如Browserify或Rollup)上黑过的人来说应该是很熟悉的。

Lua缺乏内置的面向对象编程(OOP)设施(无论是原型还是其他)。Typescript的语言特性与Lua不完全一一对应,使用__TS_*函数通过lualib_bundle模块(ts2hc在捆绑时注入)进行调整。上面,CounterApp类被降低到一系列对__TS__Class__TS__ClassExtends的调用,然后将其声明的方法放在其原型上。

ts2hc输出的Lua bundle最终会被PUC-RIO Lua 5.2编译器转化为字节码,Hydro-SDK以luac52的名义发布。上面的CounterApp类的构建方法将编译成如下内容。

1   GETTABUP    1 0 -1  
2   GETUPVAL    2 1 
3   NEWTABLE    3 0 1   
4   GETTABUP    4 0 -1
5   GETUPVAL    5 2 
6   CALL        4 2 2   
7   SETTABLE    3 -2 4
8   TAILCALL    1 3 0   
9   RETURN      1 0 
10  RETURN      0 1
复制代码

Lua字节码不在本文的范围内,不过为了完整起见,在此提及。

纠缠#

除了降低,ts2hc还对每个输出的Lua模块进行分析,以发现其功能。

从上面的例子中,ts2hc将记录以下函数。

CounterApp.prototype.____constructor
CounterApp.prototype.build
复制代码

对应于原始CounterApp类的构造函数和构建方法的声明。这些名字显然是从原始声明的名字中捕捉到的。对于像我们的例子这样的情况(给出了明确的函数名),这就足够好用了。

然而,ts2hc不能依靠程序员给它明确和可理解的函数名,ts2hc从输出的Lua模块中获取原始的符号名,并进行名称处理。名称处理的目的是唯一地识别一个给定的函数,无论它在哪里或如何声明。 ts2hc的名称处理方法在很大程度上受到IA-64 Itanium C++ ABI以及Rust的名称处理方法的启发。这两种方法在很大程度上依赖类型信息来产生它们的杂乱的名字,而Lua是一种动态的、没有类型的语言,没有提供这样的便利。

CounterApp.prototype.____constructor::self
CounterApp.prototype.build::self
复制代码

ts2hc进一步考虑Typescript文件名的哈希值,以及一个歧义索引后缀,以解决声明顺序带来的名称冲突,结果如下。

_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.____constructor::self::0
_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.build::self::0    
复制代码

这种形式对于那些由程序员命名的函数,如类方法或自由函数,是完美的。但是请考虑一下,如果CounterApp类的build方法被写成匿名闭包的话。

public build(): Widget {
        return new MaterialApp({
            title: "Counter App",
            initialRoute: "/",
            home: (() => new MyHomePage("Counter App Home Page"))(),
        });
    }
复制代码

对于匿名闭包,ts2hc简单地将其命名为 “anonymousclosure”。为了唯一地识别匿名闭包(或任何嵌套的函数声明),对每个函数的声明顺序进行[dominator analysis](en.wikipedia.org/wiki/Domina… 。根函数(在我们的例子中,CounterApp.build)的支配边界形成一个有向无环图。从根函数到一个给定的子函数,沿着支配力边界的传递性还原行走,定义了该子函数需要包括的混杂名称的顺序,以便成为唯一的。

对于上面CounterApp.build中的匿名闭包,这产生了以下结果。

_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.build::self::0::anonymous_closure::0
复制代码

ts2hc将把每个函数的名字与原始Typescript文件中的行/列号、原始Typescript文件被下放到Lua模块中的行/列号、函数最终在Lua捆绑包中的行/列号等信息连接到一个调试符号。这些调试符号为函数图提供了动力,提供了可读的堆栈跟踪以及热重载。

运行时间#

Common Flutter Runtime (CFR)是一个统称,指的是Lua 5.2虚拟机、绑定系统和其他库,是Hydro-SDK运行环境的核心。用户一般不与CFR直接交互,而是通过RunComponent和RunComponentFromFile这样的部件。

ts2hc和CFR是Hydro-SDK的开发者体验和运行时系统的核心。它们一起工作,通过提供类似Flutter的杀手级开发时功能来支持上述目标2;热重载。

热重载#的支柱

在Flutter中,热重载是由Dart VM提供的。Dart VM的热重载是基于几个支柱的。

无处不在的延迟绑定

  • 程序的行为就像每次调用都会发生方法查找一样

不可变的方法

  • 重载的 “原子 “是方法。方法永远不会被改变。对方法声明的改变会创建一个新的方法,使类或库的方法字典发生变化。如果旧的方法被一个闭包或堆栈框架捕获,那么它可能仍然存在。
  • 闭包在创建时捕获其功能。一个闭包在改变之前和之后总是有相同的功能,并且一个特定闭包的所有调用都运行相同的功能。

状态被保留

  • 热重载不会重置字段,无论是实例的字段还是类或库的字段。

CFR的热重载是受这些支柱的启发(并基本遵守)。然而,CFR与Dart VM在 “不可改变的方法 “这一支柱上有所不同。在CFR中,闭包(和它们的作用域)在每次调用之前都会被刷新。这意味着旧的函数在热重载后不能被调用,无论它们是否被闭包所捕获。唯一的例外是,如果一个旧函数是一个堆栈框架。CFR使用ts2hc提供给它的调试符号的杂乱名称来唯一地解决函数问题,允许它以类似于Dart VM的方式进行及时的方法查找和后期绑定。

考虑一下Counter-App展示中的MyHomePageState类的构建方法。

import {
    Text,
    Center,
    StatefulWidget,
    State,
    Column,
    MainAxisAlignment,
    Icon,
} from "@hydro-sdk/hydro-sdk/runtime/flutter/widgets/index";
import { AppBar, FloatingActionButton, Icons, Scaffold, Theme } from "@hydro-sdk/hydro-sdk/runtime/flutter/material/index";
import { Widget } from "@hydro-sdk/hydro-sdk/runtime/flutter/widget";
import { BuildContext } from "@hydro-sdk/hydro-sdk/runtime/flutter/buildContext";
import { Key } from "@hydro-sdk/hydro-sdk/runtime/flutter/foundation/key";
//...
public build(context: BuildContext): Widget {
        return new Scaffold({
            appBar: new AppBar({
                title: new Text(this.title),
            }),
            body: new Center({
                child: new Column({
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                        new Text("You have pushed the button this many times"),
                        new Text(this.counter.toString(), {
                            key: new Key("counter"),
                            style: Theme.of(context).textTheme.display1,
                        }),
                    ],
                }),
            }),
            floatingActionButton: new FloatingActionButton({
                key: new Key("increment"),
                child: new Icon(Icons.add),
                onPressed: this.incrementCounter,
            }),
        });
    }
复制代码

做一些简单的增删,比如把 “你已经按了这么多次按钮 “这个字符串改成其他内容,或者添加更多的文本小部件,结果是热重载成功,而应用程序的状态没有变化。

考虑将原来的构建方法修改为以下内容。

import {
    Text,
    Center,
    StatefulWidget,
    State,
    Column,
    MainAxisAlignment,
    Icon,
    Container,
    MediaQuery
} from "@hydro-sdk/hydro-sdk/runtime/flutter/widgets/index";
import { Colors, FloatingActionButton, Icons, MaterialApp, Scaffold, Theme } from "@hydro-sdk/hydro-sdk/runtime/flutter/material/index";
import { Widget } from "@hydro-sdk/hydro-sdk/runtime/flutter/widget";
import { BuildContext } from "@hydro-sdk/hydro-sdk/runtime/flutter/buildContext";
import { Key } from "@hydro-sdk/hydro-sdk/runtime/flutter/foundation/key";
//...
public build(context: BuildContext): Widget {
        return new Scaffold({
            appBar: new AppBar({
                title: new Text(this.title),
            }),
            body: new Center({
                child: new Column({
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                        //Add a thin blue box on top of the counter text
                        new Container({
                            color: Colors.blue.swatch[100],
                            height: 25,
                            width: MediaQuery.of(context).size.getWidth(),
                        }),
                        new Text("You have pushed the button this many times"),
                        new Text(this.counter.toString(), {
                            key: new Key("counter"),
                            style: Theme.of(context).textTheme.display1,
                        }),
                    ],
                }),
            }),
            floatingActionButton: new FloatingActionButton({
                key: new Key("increment"),
                child: new Icon(Icons.add),
                onPressed: this.incrementCounter,
            }),
        });
    }
复制代码

上述改变将导致类似以下的错误。

attempt to index a nil value null swatch
Error raised in: 
  MyHomePageState.prototype.build
     defined in counter-app/ota/lib/counterApp.ts:63
VM stacktrace follows:
@.hydroc/0.0.1-nightly.231/ts2hc/40bd309e7516dae86ac3d02346f6d3a9b20fa010a9da4e6e3a65a33420bb9d32/index.ts:11563
  (_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::MyHomePageState.prototype.build::self_context::0)
Dart stacktrace follows:
#0      Context.tableIndex package:hydro_sdk/…/vm/context.dart:135
#1      gettable           package:hydro_sdk/…/instructions/gettable.dart:12
#2      Frame.cont         package:hydro_sdk/…/vm/frame.dart:228
#3      Closure._dispatch  package:hydro_sdk/…/vm/closure.dart:96
#4      Closure.dispatch   package:hydro_sdk/…/vm/closure.dart:69
...
复制代码

这个错误是Colors.blue.swatch未被初始化的结果。回顾上面的Lua模块输出的例子。导入的符号被分配到对 require 的调用值中。现在构建方法关闭了未初始化的符号(新导入的符号)。不幸的是,这是Typescript模块在降低时的表现方式的一个工件。其结果是,在热重载函数中引用新导入的符号通常会触发一个异常。

Hydro-SDK中的热重载纯粹是在Lua方面实现的。在Hydro-SDK中对其他编程语言(如Haxe和C#)的热重载支持应该不会受到这种限制(尽管可能会有自己的挑战和限制)。


通过www.DeepL.com/Translator(免费版)翻译

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享