原文地址:hydro-sdk.io/blog/mangli…
原文作者:github.com/chgibb
发布时间:2021年5月20日
复原的结构化类型爱好者
将Flutter的开发时间体验与Dart编程语言脱钩。
Hydro-SDK是一个项目,有一个巨大的、雄心勃勃的目标。”成为Flutter的React Native”。
它旨在通过以下方式实现这一目标。
- 将Flutter的API表面与Dart编程语言解耦。
- 将Flutter的开发时间体验与Dart编程语言解耦。
- 为代码的空中发布提供一流的支持。
- 提供一个来自pub.dev的软件包生态系统,自动投射到支持的语言,并发布到其他软件包系统。
在这篇文章中,我想深入了解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(免费版)翻译