Hydro-sdk. IO /blog/mangli…

Original author: github.com/chgibb

Published: May 20, 2021

Recovered structured type enthusiasts delink the Flutter development time experience from the Dart programming language.

“Hydro-sdk is a project with a big, ambitious goal.” Become the React Native of Flutter “. It aims to achieve this in the following ways.

  1. Decouple the API surface of Flutter from the Dart programming language.
  2. Decouple the development time experience of Flutter from the Dart programming language.
  3. Provide first-class support for airborne distribution of code.
  4. Provides a package ecosystem from pub.dev, automatically projecting to supported languages and publishing to other package systems.

I’ve written about hydro-SDK’s past and future here before.

In this article, I want to dive into some of the details of how hydro-SDK compiles, manages, and hot-loads Typescript code.

Ts2hc is a command line program distributed as part of each HYDRO-SDK release. Ts2hc’s job is to turn Typescript code into bytecode and debug symbols. Users generally do not interact with it directly, but indirectly through commands such as hydroc build and hydroc run.

Build time#Github.com/TypeScriptT…

Typescript is reduced to Lua by taking advantage of the excellent Typescript to Lua (TSTL) library. Take a look at this excerpt from the Counter App display area below.

//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(a); }public build(): Widget {
        return new MaterialApp({
            title: "Counter App".initialRoute: "/".home: new MyHomePage("Counter App Home Page")}); }}Copy the code

Ts2hc will reduce the counter – app/ota/lib/counterApp ts, and all of its dependencies to separate the Lua module. These Lua modules are then bundled together and the result looks something like this.

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("[email protected]") -... local StatelessWidget = ____index.StatelessWidget -- ... local ____index =require("[email protected]")
    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)
Copy the code

Lua, reduced and bundled, is still somewhat similar to Typescript for input. Typescript ES6 modules are wrapped as function expressions (IIFE) that Lua calls immediately, assigned string keys in package.preload map, and made available by requireing for their exits. This pattern should be familiar to anyone who has ever hacked a Javascript bundler/module parser like Browserify or Rollup.

Lua lacks built-in object Oriented programming (OOP) facilities (prototype or otherwise). Typescript’s language features are not exactly one-to-one with Lua, using the __TS_* function to adjust through the Lualib_bundle module (injected by TS2HC at bundle time). Above, the CounterApp class is reduced to a series of calls to __TS__Class and __TS__ClassExtends, and then puts its declared methods on its prototypes.

The Lua bundle output by TS2HC is eventually converted into bytecode by the Pu-Rio Lua 5.2 compiler, and hydro-SDK is released as LuAC52. The build method for the CounterApp class above compiles to the following.

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
Copy the code

Lua bytecode is beyond the scope of this article, but is mentioned here for completeness.

Entanglement #

In addition to lowering, TS2HC also analyzes each output Lua module to discover its function.

From the above example, TS2HC will log the following functions.

CounterApp.prototype.____constructor
CounterApp.prototype.build
Copy the code

Declarations of constructors and build methods corresponding to the original CounterApp class. These names are clearly captured from the names of the original declaration. For cases like our example, where the explicit function name is given, this is good enough.

However, TS2HC cannot rely on programmers to give it explicit and understandable function names. Ts2hc takes the original symbolic names from the output Lua modules and handles the names. The purpose of name processing is to uniquely identify a given function, no matter where or how it is declared. Ts2hc’s name handling is largely inspired by the ia-64 Itanium C++ ABI as well as Rust’s name handling. Both methods rely heavily on type information to produce their messy names, and Lua, being a dynamic, typeless language, offers no such convenience.

CounterApp.prototype.____constructor::self
CounterApp.prototype.build::self
Copy the code

Ts2hc further considers the hash value of Typescript file names and an ambiguous index suffix to resolve name conflicts due to declaration order, resulting in the following.

_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.____constructor::self::0
_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.build::self::0    
Copy the code

This form is perfect for functions named by the programmer, such as class methods or free functions. But consider if the Build method of the CounterApp class was written as an anonymous closure.

public build(): Widget {
        return new MaterialApp({
            title: "Counter App".initialRoute: "/".home: (() = > new MyHomePage("Counter App Home Page")) ()}); }Copy the code

For anonymous closures, TS2HC simply names them “anonymouscloSure”. In order to uniquely identify anonymous closure (or any nested function declarations), declaration of each function order [dominator analysis] (en.wikipedia.org/wiki/Domina… . The dominant boundary of the root function (in our case, Counterapp.build) forms a directed acyclic graph. A transitive reduction walk along the force boundary from the root function to a given subfunction defines the order of confounding names that the subfunction needs to include in order to be unique.

For the anonymous closure in Counterapp.build above, this produces the following results.

_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::CounterApp.prototype.build::self::0::anonymous_closu re::0Copy the code

Ts2hc connects the name of each function to a debug symbol with the line/column number in the original Typescript file, the line/column number in the Lua module that the original Typescript file was sent to, and the line/column number in the Lua bundle that the function ended up in. These debug symbols power function diagrams, providing readable stack traces, and hot overloading.

Running time #

The Common Flutter Runtime (CFR) is a general term that refers to the Lua 5.2 virtual machine, binding system, and other libraries that are the core of the HYDRO-SDK Runtime environment. Users typically do not interact directly with the CFR, but through widgets such as RunComponent and RunComponentFromFile.

Ts2hc and CFR are at the heart of hydro-SDK’s developer experience and runtime system. They work together to support goal 2 above by providing killer development-time features similar to Flutter; Thermal overload.

Thermal overload # struts

In Flutter, thermal overloading is provided by the Dart VM. The thermal overloading of the Dart VM is based on several pillars.

Delayed binding is ubiquitous

  • The program behaves as if a method lookup occurs every time it is called

Immutable method

  • Overloaded atoms are methods. The method will never be changed. A change to the method declaration creates a new method that changes the method dictionary of the class or library. If the old method is captured by a closure or stack frame, it may still exist.
  • Closures capture their functionality at creation time. A closure always has the same functionality before and after changes, and all calls to a particular closure run the same functionality.

State is preserved

  • Hot overloading does not reset fields, either instance fields or class or library fields.

The THERMAL overloading of the CFR is inspired by (and largely adhered to) these pillars. However, the CFR differs from the Dart VM in the “immutable method” pillar. In CFR, closures (and their scope) are flushed before each call. This means that old functions cannot be called after hot overloading, whether or not they are caught by closures. The only exception is if an old function is a stack frame. CFR uniquely resolves function problems using the messy names of debug symbols provided to it by TS2HC, allowing it to do just-in-time method lookup and late binding in a manner similar to the Dart VM.

Consider the way the MyHomePageState class is built in the Counter-App presentation.

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,
            }),
        });
    }
Copy the code

Simple additions and deletions, such as changing the string “you’ve pressed the button so many times” to something else, or adding more text widgets, result in hot overloading without changing the state of the application.

Consider changing the original build method to the following.

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,
            }),
        });
    }
Copy the code

The above changes will result in errors like the following.

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 - the nightly. 231 / ts2hc / 40 bd309e7516dae86ac3d02346f6d3a9b20fa010a9da4e6e3a65a33420bb9d32 / index. The ts: 11563 (_Lae3eafcf842016833530caebe7755167b0866b5ac96416b45848c6fc6d65c58f::MyHomePageState.prototype.build::self_context::0) Dart StackTrace follows: #0 context. tableIndex package:hydro_sdk/... / vm/context. The dart: 135 # 1 gettable package: hydro_sdk /... / instructions/gettable. Dart: 12 # 2 Frame. Cont package: hydro_sdk /... / vm/frame. The dart: 228 # 3 Closure. _dispatch package: hydro_sdk /... / vm/closure. Dart: 96 # 4 closure. Dispatch package: hydro_sdk /... /vm/closure.dart:69 ...Copy the code

This error is the result of colors.blue.swatch not being initialized. Review the example of Lua module output above. The imported symbol is assigned to the value of the call to require. The build method now closes uninitialized symbols (newly imported symbols). Unfortunately, this is an artifact of how Typescript modules behave when degraded. As a result, referencing a newly imported symbol in a hot-overloaded function usually raises an exception.

Thermal overloading in hydro-SDK is implemented purely in Lua. Hot overloading support for other programming languages such as Haxe and C# in hydro-sdk should not be subject to this limitation (although it may have its own challenges and limitations).


Translation via www.DeepL.com/Translator (free version)