原文地址: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(免費版)翻譯