Adventures in automatically binding Dart to TypeScript.
This was originally posted here: https://chgibb.github.io/expressing-inexpressible-constant-values/
Hydro-SDK is a project with one large, ambitious goal. "Become React Native for Flutter".
It aims to do that by:
- Decoupling the API surface of Flutter from the Dart programming language.
- Decoupling the development time experience of Flutter from the Dart programming language.
- Providing first-class support for over-the-air distribution of code.
- Providing an ecosystem of packages from
pub.dev, automatically projected to supported languages and published to other package systems.
Structured Wrapper and Interface generator for Dart (SWID) is a component of Hydro-SDK supporting goals 1 and 4. SWID takes as input a Dart package (like
package:flutter) and produces as output TypeScript files representing the input Dart package's public API and Dart files to allow for gluing TypeScript code written against that API together into a host app that can be run on Hydro-SDK's Common Flutter Runtime (CFR). This process is referred to as "language projection".
Representing Dart constructs in TypeScript has come with many challenges. One recently encountered is how to express constant values which are composed of private symbols.
Every Flutter programmer should be familiar with the concept of
const in Dart. It’s used perhaps most prolifically in Flutter’s Material and Cupertino design icons APIs. For instance, the
Icons class in Flutter’s
material library contains thousands of static fields that appear like the following which represents the "directions boat" material design icon:
Describing how Flutter
IconData instances, font ligatures and font loading work is out of scope of this article.
When performing language projection of
package:flutter, SWID will emit the above declaration as the following:
SWID will perform a simple syntax transform for the
Icons class and
directions_boat field while making sure the translation unit (
flutter/material/icons.ts) imports required symbols (in this case, simply
The only area of language projection that SWID aims to describe one-to-one between Dart and TypeScript is
const values. This is done to free host applications from having to compile every possible constant value that guest code might want to access. This approach works great for constant values that consist simply of public symbols (like
IconData above), and primitive values.
This becomes trickier however when fields consist of private symbols. For instance, the
Rect class from
dart:ui has some static constant fields which are declared as the following:
zero field references a public static method, so it is fine to perform a simple syntax transform. At first glance, the
largest field appears impossible to express in TypeScript. There is no way to expose the
_giantScalar symbol in a way that it can be accessed from TypeScript.
SWID has enough semantic understanding to understand that the reference to
largest is not only a field on
Rect but that it's also just a primitive. Therefore, both are safe to emit.
SWID's semantic understanding of the Dart code it is projecting allow it to categorize constant values into two categories: "expressible" and "inexpressible". Constant values which can be decomposed into references to primitives or public symbols are considered to be "expressible". In the
Rect example above,
_giantScalar, despite being private, decomposes into a reference to a primitive which allows
largest to decompose into primitive references.
_giantScalar to be emitted in the final translation unit.
Endian class from
little both include references to a private constructor. This can't be further decomposed into an expressible form and so both fields are considered to be "inexpressible". SWID expresses "inexpressible" constant values by emitting code like the following:
declare block is what SWID calls a "virtual machine declaration". This is a typed description of the environment that the code in the given translation unit expects. "virtual machine declarations" in SWID's TypeScript backend and their associated "namespace symbol declarations" that fulfill their expectations in SWID's Dart backend are how SWID expresses API bindings not just for "inexpressible" constant values but for all methods and fields. This binding system will be the subject of a future blog post.
As seen above,
little are expressed as calls to their associated declarations. Unfortunately, this scheme prevents the host application from ever being able to tree-shake away the definitions of
little but still allows guest code access to these fields.
Hopefully this problem and its solution was as fun to read about as it was to discover and solve. Hydro-SDK is an endless fractal of problems like this. Hopefully I’ll be able to make “Adventures in automatically binding Dart to TypeScript” a continuing series of posts.