[Flutter翻譯]糾結動態語言符號,獨特解決熱負荷的原子問題

Sunbreak發表於2021-05-31

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

相關文章